学习来源:尚硅谷JavaScript基础&实战丨JS入门到精通全套完整版
笔记来源:在这位大佬的基础上添加了一些东西,欢迎大家支持原创,大佬太棒了:JavaScript |(二)JavaScript自定义对象及函数 | 尚硅谷JavaScript基础&实战
JavaScript |(二)JavaScript自定义对象及函数 | 尚硅谷JavaScript基础&实战
- 🔴 自定义对象
- 🟡 对象的分类
- 1️⃣ 内建对象(Built-in Objects)
- 2️⃣ 宿主对象(Host Objects)
- 3️⃣ 自定义对象(User-defined Objects)
- 🟡 对象基本操作
- 🟡 对象的属性
- 1️⃣ 属性名(Property Name)
- 📌 示例 1:标准属性名
- 📌 示例 2:特殊字符的属性名
- 📌 示例 3:动态属性名
- 2️⃣ 属性值(Property Value)
- 📌 示例 4:属性值是不同的数据类型
- 🟡 基本和引用数据类型
- 🟡 对象字面量
- 🔴 函数(function)
- 🟡 函数创建及调用
- 1️⃣ 形参(参数)
- 📌 示例 1:函数定义时声明形参
- 2️⃣ 实参(传入的值)
- 📌 示例 2:调用函数时传入实参
- 3️⃣ 实参的类型不会被检查
- 📌 示例 3:传入错误类型的参数
- 4️⃣ 函数的实参可以是任意数据类型
- 📌 示例 4:传递不同类型的实参
- 5️⃣ 多余的实参不会被赋值
- 📌 示例 5:多余的实参
- 6️⃣ 少于形参的数量,未传入的参数是 `undefined`
- 📌 示例 6:少于形参数量
- 🟡 函数的返回值
- 🟡 立即执行函数
- 🟡 函数——对象的方法
- 🟡 变量提升(Hoisting)
- 🎯 变量的声明提前(Hoisting for Variables)
- 📌 示例 1:`var` 声明变量会提升
- 📌 示例 2:不使用 `var` 的变量不会被提升
- 📌 示例 3:`let` 和 `const` 不会被初始化
- 🎯 函数的声明提前(Hoisting for Functions)
- ✅ 规则:
- 📌 示例 4:函数声明会被提升
- 📌 示例 5:函数表达式不会被提升
- 🟡 作用域
- 🎯 全局作用域
- 🎯 函数作用域(Function Scope)
- 📌 示例 1:函数作用域中的变量访问
- 📌 示例 2:作用域链
- 📌 示例 3:函数中的变量声明提升
- 📌 示例 4:函数中不使用 `var` 声明的变量会变成全局变量
- 📌 示例 5:使用 `window` 访问全局变量
- 🟡 this
- 🎯 `this` 解析:不同调用方式的 `this` 指向
- ✅ 1. 以「普通函数」调用,`this` 指向 `window`
- ✅ 2. 以「方法」调用,`this` 指向调用方法的对象
- ✅ 3. 赋值后调用(丢失 `this` 绑定)
- ✅ 4. `this` 在构造函数中指向新对象
- ✅ 5. `this` 在箭头函数中继承外层作用域
- ✅ 6. `call()`、`apply()` 和 `bind()` 改变 `this`
- 🎯 `this` 绑定规则总结
- 🟡 工厂动态创对象
- 🟡 构造函数
- 🟡 原型函数
- 🟡 toString()方法
- 1️⃣ 默认情况下,`toString()` 方法
- 2️⃣ 覆盖 `toString()` 方法
- 3️⃣ 在类的 `prototype` 中添加 `toString()`
- 4️⃣ `Object.prototype.toString.call()` 识别数据类型
- 🟡 垃圾回收(GC)
- 🔄 JavaScript 垃圾回收(GC)机制
- 1️⃣ JavaScript 内存管理
- 2️⃣ JavaScript 的垃圾回收算法
- 🔹(1)标记清除(Mark-and-Sweep)
- 🔹(2)引用计数(Reference Counting)
- 3️⃣ 如何优化垃圾回收?
- ✅ 1. 手动释放对象
- ✅ 2. 避免全局变量
- ✅ 3. 使用 `WeakMap` / `WeakSet`
- ✅ 4. 避免 DOM 泄漏
- 💡 JavaScript GC 优化技巧
🔴 自定义对象
在JS中来表示一个人的信息(name gender age):
var name = "孙悟空";
var gender = "男";
var age = 18;
如果使用基本数据类型的数据,我们所创建的变量都是独立,不能成为一个整体。
对象属于一种复合的数据类型,在对象中可以保存多个不同数据类型的属性。
🟡 对象的分类
在 JavaScript 中,对象可以分为 三大类,它们的作用和来源各不相同:
1️⃣ 内建对象(Built-in Objects)
这些对象是 ECMAScript(ES)标准 定义的,在任何 JavaScript 运行环境中都可以使用。它们是语言本身提供的工具,帮助开发者处理各种数据类型和功能。
🔹 常见的内建对象
- 数学和数值处理:
Math
、Number
、BigInt
- 字符串操作:
String
- 布尔值:
Boolean
- 函数相关:
Function
- 对象相关:
Object
、Array
、Set
、Map
- 日期时间:
Date
- 正则表达式:
RegExp
- 错误处理:
Error
、SyntaxError
- JSON 处理:
JSON
📌 示例
console.log(Math.PI); // 3.141592653589793
console.log(Number.isInteger(10.5)); // false
console.log("hello".toUpperCase()); // "HELLO"
2️⃣ 宿主对象(Host Objects)
宿主对象是 JavaScript 运行环境(例如 浏览器 或 Node.js)提供的对象,它们不属于 JavaScript 语言本身,而是由宿主环境定义的。
🔹 在浏览器中的宿主对象
- BOM(浏览器对象模型)
window
(全局对象)document
(DOM 的入口)navigator
(浏览器信息)location
(地址栏信息)history
(浏览记录)
- DOM(文档对象模型)
document.getElementById()
document.querySelector()
document.createElement()
📌 示例
console.log(window.innerWidth); // 浏览器窗口的宽度
console.log(document.title); // 获取网页标题
console.log(navigator.userAgent); // 获取浏览器的用户代理信息
3️⃣ 自定义对象(User-defined Objects)
自定义对象是 开发人员自己创建的,可以根据需求定义属性和方法。
🔹 创建自定义对象的方式
- 对象字面量
- 构造函数
- ES6 类(class)
📌 示例
// 方式 1:对象字面量
let person = {name: "Alice",age: 25,sayHello: function() {console.log(`Hello, my name is ${this.name}`);}
};
person.sayHello(); // 输出:Hello, my name is Alice// 方式 2:构造函数
function Car(brand, model) {this.brand = brand;this.model = model;
}
let myCar = new Car("Toyota", "Corolla");
console.log(myCar.brand); // "Toyota"// 方式 3:ES6 类
class Animal {constructor(name) {this.name = name;}speak() {console.log(`${this.name} makes a sound.`);}
}
let dog = new Animal("Dog");
dog.speak(); // "Dog makes a sound."
类型 | 由谁提供? | 作用 | 示例 |
---|---|---|---|
内建对象 | JavaScript 语言本身 | 提供基础的数据处理功能 | Math 、String 、Array |
宿主对象 | 运行环境(浏览器/Node.js) | 提供与环境相关的功能 | window 、document 、console |
自定义对象 | 开发者 | 根据需求创建对象 | let obj = {} ,class Person {} |
📝 记住:内建对象是标准的,宿主对象因环境而异,自定义对象由开发者自由创建! 🚀
🟡 对象基本操作
-
创建对象:
var obj = new Object();
-
添加属性:
对象.属性名 = 属性值
-
读取属性:
对象.属性名
-
修改属性:
对象.属性名 = 新值
-
删除属性:
delete 对象.属性名
<!DOCTYPE html> <html><head><meta charset="UTF-8"><title></title><script type="text/javascript">//创建对象/** 使用new关键字调用的函数,是构造函数constructor* 构造函数是专门用来创建对象的函数* 使用typeof检查一个对象时,会返回object*/var obj = new Object();/** 在对象中保存的值称为属性* 向对象添加属性* 语法:对象.属性名 = 属性值;*///向obj中添加一个name属性obj.name = "孙悟空";//向obj中添加一个gender属性obj.gender = "男";//向obj中添加一个age属性obj.age = 18;/** 读取对象中的属性* 语法:对象.属性名* 如果读取对象中没有的属性,不会报错而是会返回undefined*/console.log(obj.gender);console.log(obj.hello);/** 修改对象的属性值* 语法:对象.属性名 = 新值*/obj.name = "tom";/** 删除对象的属性* 语法:delete 对象.属性名*/delete obj.name;</script></head><body></body> </html>
🟡 对象的属性
在 JavaScript 中,对象的 属性名 和 属性值 具有很大的灵活性,具体如下:
1️⃣ 属性名(Property Name)
JavaScript 对象的 属性名 不强制遵守标识符命名规范,可以使用任意字符串(包括特殊字符、数字等),但推荐遵守标识符规范(如 camelCase
命名法)。
- 使用
.
语法(仅适用于符合变量命名规范的属性) - 使用
[]
语法(适用于所有属性名,尤其是特殊字符或动态属性)
📌 示例 1:标准属性名
let person = {name: "Alice",age: 25
};console.log(person.name); // "Alice"
console.log(person.age); // 25
console.log(person["name"]); // 也可以使用 [] 访问
📌 示例 2:特殊字符的属性名
let obj = {"first-name": "John","123": "Number as key","@key": "Special character"
};// console.log(obj.first-name); // ❌ 报错,不能使用 . 访问
console.log(obj["first-name"]); // ✅ "John"
console.log(obj["123"]); // ✅ "Number as key"
console.log(obj["@key"]); // ✅ "Special character"
📌 示例 3:动态属性名
let prop = "dynamicKey";
let myObj = {[prop]: "Hello World"
};
console.log(myObj.dynamicKey); // "Hello World"
console.log(myObj["dynamicKey"]); // "Hello World"
✅ 解释:
- 在
[]
里可以放一个 变量,变量的值会作为属性名。
2️⃣ 属性值(Property Value)
JavaScript 对象的 属性值 可以是 任意数据类型,包括:
- 基本类型(
string
、number
、boolean
等) - 对象(嵌套对象)
- 数组
- 函数(方法)
📌 示例 4:属性值是不同的数据类型
let person = {name: "Alice", // 字符串age: 25, // 数字isStudent: false, // 布尔值scores: [90, 85, 88], // 数组address: { city: "New York", zip: "10001" }, // 对象greet: function() { // 方法console.log("Hello, my name is " + this.name);}
};console.log(person.scores[1]); // 85
console.log(person.address.city); // "New York"
person.greet(); // "Hello, my name is Alice"
特点 | 解释 | 示例 |
---|---|---|
属性名 | 可以是任意字符串 | obj["first-name"] = "John"; |
. 访问 | 只能用于 符合标识符规则 的属性名 | obj.name ✅,obj["first-name"] ✅,obj.first-name ❌ |
[] 访问 | 可用于任何属性名,支持动态属性 | obj["@key"] ✅,obj[variable] ✅ |
属性值 | 可以是 任何数据类型 | obj.age = 30; obj.list = [1, 2, 3]; obj.child = {} |
✅ 最佳实践
- 属性名遵循命名规范(避免使用特殊字符)。
- 使用
[]
访问特殊或动态属性。 - 属性值可以是任意类型,包括 数组、对象、方法,灵活使用。
in
运算符:通过该运算符可以检查一个对象中是否含有指定的属性。如果有则返回true,没有则返回false。- 语法:
"属性名" in 对象
- 语法:
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">var obj = new Object();//向对象中添加属性obj.name = "喜羊羊";// 使用特殊的属性名obj["123"] = 789;obj["nihao"] = "你好";var n = "nihao";console.log(obj["123"]);//输出789console.log(n);//输出你好obj.test = true;obj.test = null;obj.test = undefined;//创建一个对象var obj2 = new Object();obj2.name = "美羊羊";//将obj2设置为obj的属性obj.test = obj2;console.log(obj.test.name);//输出美羊羊console.log("name" in obj);//输出true</script></head><body></body>
</html>
🟡 基本和引用数据类型
- 基本数据类型:
String
Number
Boolean
Null
Undefined
- 引用数据类型:
Object
- JS中的变量都是保存到栈内存中的,基本数据类型的值直接在栈内存中存储,值与值之间是独立存在,修改一个变量不会影响其他的变量。
- 对象是保存到堆内存中的,每创建一个新的对象,就会在堆内存中开辟出一个新的空间,而变量保存的是对象的内存地址(对象的引用),如果两个变量保存的是同一个对象引用,当一个通过一个变量修改属性时,另一个也会受到影响。
- 当比较两个基本数据类型的值时,就是比较值。而比较两个引用数据类型时,它是比较的对象的内存地址,
如果两个对象是一摸一样的,但是地址不同,它也会返回false
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">//基本数据类型的值直接在栈内存中存储,值与值之间是独立存在//修改一个变量不会影响其他的变量。var a = 123;var b = a;a++;console.log("a = "+a);//a=124console.log("b = "+b);//b=123//对象是保存到堆内存中的//如果两个变量保存的是同一个对象引用,当一个通过一个变量修改属性时,另一个也会受到影响。var obj = new Object();obj.name = "喜羊羊";var obj2 = obj;//修改obj的name属性obj.name = "美羊羊";console.log(obj.name);//美羊羊console.log(obj2.name);//美羊羊//设置obj2为nullobj2 = null;console.log(obj);//objectconsole.log(obj2);//null//比较基本数据类型的值,就是比较值var c = 10;var d = 10;console.log(c == d);//true//两个引用数据类型时,它是比较的对象的内存地址//如果两个对象是一摸一样的,但是地址不同,它也会返回falsevar obj3 = new Object();var obj4 = new Object();obj3.name = "懒羊羊";obj4.name = "懒羊羊";console.log(obj3 == obj4);//false</script></head><body></body>
</html>
🟡 对象字面量
- 使用对象字面量,可以在创建对象时,直接指定对象中的属性
- 语法:
{属性名:属性值,属性名:属性值....}
- 对象字面量的属性名可以加引号也可以不加,建议不加,如果要使用一些特殊的名字,则必须加引号
- 属性名和属性值是一组一组的名值对结构,
- 名和值之间使用
:
连接,多个名值对之间使用,
隔开。如果一个属性之后没有其他的属性了,就不要写,
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">//使用对象字面量来创建一个对象var obj = {name:"猪八戒",age:13,gender:"男",test:{name:"沙僧"}};console.log(obj.test);</script></head><body></body>
</html>
🔴 函数(function)
- 函数也是一个对象
- 函数中可以封装一些功能(代码),在需要时可以执行这些功能(代码)
- 函数中可以保存一些代码在需要的时候调用
- 使用
typeof
检查一个函数对象时,会返回function
🟡 函数创建及调用
在 JavaScript 中,函数的形参(参数) 和 实参(传入的值) 具有很大的灵活性,具体规则如下:
1️⃣ 形参(参数)
- 在 函数定义 时,可以在
()
内指定一个或多个形参(形式参数)。 - 多个形参 之间使用
,
隔开。 - 形参在函数内部相当于局部变量,但默认值是
undefined
,直到调用函数时赋值。
📌 示例 1:函数定义时声明形参
function greet(name, age) {console.log("Hello, " + name + "! You are " + age + " years old.");
}
2️⃣ 实参(传入的值)
- 调用函数 时,可以在
()
指定实参(实际参数)。 - 实参将按照顺序赋值 给对应的形参。
📌 示例 2:调用函数时传入实参
greet("Alice", 25);
// 输出:Hello, Alice! You are 25 years old.
3️⃣ 实参的类型不会被检查
- JavaScript 不会 在调用函数时检查实参的数据类型,所以要注意可能会传入非法参数。
📌 示例 3:传入错误类型的参数
greet(123, "Hello");
// 输出:Hello, 123! You are Hello years old. ❌(可能导致逻辑错误)
✅ 解决方案:可以手动检查类型
function greet(name, age) {if (typeof name !== "string" || typeof age !== "number") {console.log("Invalid input!");return;}console.log("Hello, " + name + "! You are " + age + " years old.");
}
greet(123, "Hello"); // 输出:Invalid input!
4️⃣ 函数的实参可以是任意数据类型
- 实参可以是 字符串、数字、对象、数组、函数等。
- 甚至可以传递函数作为参数(回调函数)。
📌 示例 4:传递不同类型的实参
function printData(data) {console.log("Received:", data);
}printData(42); // 传递数字
printData("Hello"); // 传递字符串
printData([1, 2, 3]); // 传递数组
printData({ key: "value" }); // 传递对象
5️⃣ 多余的实参不会被赋值
- 如果传递的实参 超过 形参数量,多出的不会被赋值,但也不会报错。
📌 示例 5:多余的实参
function show(a, b) {console.log(a, b);
}show(1, 2, 3, 4); // 1 2 (3 和 4 被忽略)
6️⃣ 少于形参的数量,未传入的参数是 undefined
- 如果传递的实参 少于 形参数量,未被赋值的形参默认为
undefined
。
📌 示例 6:少于形参数量
function show(a, b, c) {console.log(a, b, c);
}show(1, 2); // 1 2 undefined (c 没有赋值)
✅ 解决方案:可以使用默认参数
function show(a, b, c = "default value") {console.log(a, b, c);
}show(1, 2); // 1 2 "default value"
规则 | 说明 | 示例 |
---|---|---|
形参在函数定义时声明 | 形参是函数内部的局部变量 | function sum(a, b) {} |
调用函数时传递实参 | 实参赋值给形参 | sum(3, 5); |
不会检查实参类型 | 可能会导致错误 | sum("hello", 10); |
实参可以是任何类型 | 包括数组、对象、函数 | sum([1,2,3], {x:10}); |
多余的实参会被忽略 | 解析器不会报错 | sum(1, 2, 3, 4); |
少于形参的数量时,缺失的参数是 undefined | 可能导致 NaN | sum(1); |
可以使用默认参数 | 避免 undefined 问题 | function sum(a, b=10) {} |
✅ 最佳实践
- 使用默认参数 避免
undefined
。 - 检查参数类型,确保输入合法。
- 尽量匹配实参与形参的数量,避免逻辑错误。
🟡 函数的返回值
- 可以使用 return 来设置函数的返回值,语法:
return 值
- return后的值将会作为函数的执行结果返回,可以定义一个变量,来接收该结果。
- 在函数中return后的语句都不会执行。
- 如果return语句后不跟任何值就相当于返回一个undefined,如果函数中不写return,则也会返回undefined。
- return后可以跟任意类型的值。
<!DOCTYPE html>
<html><head><meta charset="utf-8" /><title></title><script type="text/javascript">// 创建一个函数,用来计算三个数的和function sum(a , b , c){var d = a + b + c;return d;}//调用函数//变量result的值就是函数的执行结果//函数返回什么result的值就是什么var result = sum(4,7,8);console.log("result = "+result);</script></head><body></body>
</html>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">/** 返回值可以是任意的数据类型* 也可以是一个对象,也可以是一个函数*/function fun1(){//返回一个对象return {name:"沙和尚"};}var a = fun1();console.log("a = "+a);function fun2(){//在函数内部再声明一个函数function fun3(){alert("我是fun3");}//将fun3函数对象作为返回值返回return fun3;}a = fun2();console.log(a);a();</script></head><body></body>
</html>
在 JavaScript 中,当你执行:
console.log("a=" + a);
它等价于:
console.log("a=" + a.toString());
🔍 为什么 a
变成了 [object Object]
?
a
是一个对象,console.log(a)
直接打印a
,它会以对象的格式显示{name: "沙和尚"}
。- 字符串拼接时,
a
需要转换为字符串,此时 JavaScript 调用了a.toString()
方法。 - 默认情况下,对象的
toString()
方法返回"[object Object]"
,这是Object.prototype.toString()
的默认行为。
✅ 如何让 a
显示 {name: '沙和尚'}
?
如果你想在字符串拼接时正确显示对象内容,可以使用 JSON.stringify(a)
:
console.log("a=" + JSON.stringify(a));
✅ 输出:
a={"name":"沙和尚"}
🛠 解决方案
方法 1️⃣:使用 JSON.stringify()
console.log("a=" + JSON.stringify(a)); // a={"name":"沙和尚"}
方法 2️⃣:手动定义 toString()
方法
var obj = {name: "沙和尚",toString: function() {return `{name: '${this.name}'}`;}
};
console.log("obj=" + obj); // obj={name: '沙和尚'}
- 默认情况下,对象在字符串拼接时会调用
toString()
,返回[object Object]
。 - 如果想让对象以 JSON 格式显示,可以用
JSON.stringify()
。 - 可以自定义
toString()
方法来控制对象的输出格式。
这样就可以更好地控制对象的输出格式啦!🚀
🟡 立即执行函数
立即执行函数:函数定义完,立即被调用,这种函数叫做立即执行函数。立即执行函数往往只会执行一次
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">(function(){alert("我是一个匿名函数~~~");})();(function(a,b){console.log("a = "+a);console.log("b = "+b);})(123,456); </script></head><body></body>
</html>
🟡 函数——对象的方法
- 函数也可以称为对象的属性,如果一个函数作为一个对象的属性保存,那么我们称这个函数是这个对象的方法。
- 调用这个函数就说调用对象的方法(method)。
- 但是它只是名称上的区别没有其他的区别。
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">//创建一个对象var obj = new Object();//向对象中添加属性obj.name = "喜羊羊";obj.age = 18;//对象的属性值可以是任何的数据类型,也可以是个函数obj.sayName = function(){console.log(obj.name);};function fun(){console.log(obj.name);};//调用方法obj.sayName();//调用函数fun();/** 函数也可以称为对象的属性,* 如果一个函数作为一个对象的属性保存,* 那么我们称这个函数时这个对象的方法* 调用这个函数就说调用对象的方法(method)*/var obj2 = {name:"美羊羊",age:18,sayName:function(){console.log(obj2.name);}};obj2.sayName();</script></head><body></body>
</html>
枚举对象中的属性
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">var obj = {name:"喜羊羊",age:18,gender:"男",address:"青青草原"}; //枚举对象中的属性,使用for ... in 语句/** 语法:* for(var 变量 in 对象){* * }* * for...in语句 对象中有几个属性,循环体就会执行几次* 每次执行时,会将对象中的一个属性的名字赋值给变量*/for(var n in obj){console.log("属性名:"+n);console.log("属性值:"+obj[n]);}</script></head><body></body>
</html>
🟡 变量提升(Hoisting)
在 JavaScript 中,变量提升(Hoisting) 是一个重要的概念,它会影响变量和函数的执行顺序。以下是详细解析:
🎯 变量的声明提前(Hoisting for Variables)
✅ 规则:
- 使用
var
关键字声明的变量,会在代码执行前提升到作用域的顶部,但不会赋值。 - 不使用
var
、let
、const
声明的变量(即隐式全局变量)不会被提升。 let
和const
声明的变量 也会被提升(但不会初始化,访问会报错)。
📌 示例 1:var
声明变量会提升
console.log(a); // 输出:undefined (因为 a 被提升,但未赋值)
var a = 10;
console.log(a); // 输出:10
等价于
var a; // 变量提升,但未赋值,默认是 undefined
console.log(a);
a = 10;
console.log(a);
📌 示例 2:不使用 var
的变量不会被提升
console.log(b); // ❌ ReferenceError: b is not defined
b = 20;
解释:b
直接赋值,没有使用 var
,不会提升。
📌 示例 3:let
和 const
不会被初始化
console.log(c); // ❌ ReferenceError: Cannot access 'c' before initialization
let c = 30;
解释:c
虽然被提升,但不会自动初始化,访问时会报错。
🎯 函数的声明提前(Hoisting for Functions)
✅ 规则:
- 函数声明
function myFunc() {}
—— 会被提升,所以可以在声明前调用。 - 函数表达式
var myFunc = function() {}
—— 不会被提升,所以不能在声明前调用。
📌 示例 4:函数声明会被提升
sayHello(); // ✅ 可以调用,输出 "Hello, world!"function sayHello() {console.log("Hello, world!");
}
解释:sayHello()
作为函数声明,会被提升到作用域顶部。
📌 示例 5:函数表达式不会被提升
sayHi(); // ❌ TypeError: sayHi is not a functionvar sayHi = function() {console.log("Hi!");
};
等价于
var sayHi;
sayHi(); // ❌ sayHi 变量已提升,但值是 undefined,调用 undefined 会报错
sayHi = function() {console.log("Hi!");
};
🎯 总结
提升规则 | 是否提升? | 是否初始化? | 是否能在声明前调用? |
---|---|---|---|
var 变量 | ✅ 变量名被提升 | ❌ 不会初始化(默认 undefined ) | ❌ 访问时是 undefined |
let / const 变量 | ✅ 变量名被提升 | ❌ 不会初始化(访问会报错) | ❌ 访问时报 ReferenceError |
函数声明 function fn() {} | ✅ 提升整个函数 | ✅ 会初始化 | ✅ 可以在声明前调用 |
函数表达式 var fn = function() {} | ✅ 变量名被提升 | ❌ 不会初始化(默认 undefined ) | ❌ 访问时报 TypeError |
🎯 最佳实践
✅ 推荐使用 let
或 const
,避免 var
变量提升带来的 undefined
问题。
✅ 尽量使用函数声明,如果用函数表达式,要确保先声明再调用。
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">console.log("a = "+a);var a = 123;fun();//函数声明,会被提前创建function fun(){console.log("我是一个fun函数");}//函数表达式,不会被提前创建var fun2 = function(){console.log("我是fun2函数");};fun2(); </script></head><body></body>
</html>
🟡 作用域
作用域:指一个变量的作用的范围。在JS中一共有两种作用域:
🎯 全局作用域
直接编写在script标签中的JS代码
,都在全局作用域- 全局作用域在页面打开时创建,在页面关闭时销毁
- 在全局作用域中有一个全局对象window,它代表的是一个浏览器的窗口,它由浏览器创建我们可以直接使用。
- 在全局作用域中:
- 创建的变量都会作为window对象的属性保存
- 创建的函数都会作为window对象的方法保存
- 全局作用域中的变量都是全局变量,在页面的任意的部分都可以访问的到。
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">var a = 10;var b = 20;var c = "hello";console.log(window.c);function fun(){console.log("我是fun函数");}window.fun();</script></head><body></body>
</html>
🎯 函数作用域(Function Scope)
在 JavaScript 中,函数作用域 是一个重要的概念,它决定了变量的可见性和生命周期。以下是详细解析:
✅ 作用域规则:
- 每调用一次函数,就会创建一个新的函数作用域,执行完毕后,作用域销毁。
- 函数内部可以访问全局变量,但全局作用域无法访问函数内部的变量。
- 作用域链(Scope Chain):在函数内部使用变量时,会先从当前作用域寻找,如果找不到,就逐层向上查找,直到全局作用域。
- 变量声明提升(Hoisting) 也适用于函数作用域,
var
声明的变量会被提升,但不会初始化。 - 如果在函数内部未使用
var
、let
、const
声明变量,该变量会变成全局变量。 - 函数的形参(参数)相当于在函数作用域内声明了变量。
📌 示例 1:函数作用域中的变量访问
var globalVar = "I am global"; // 全局变量function myFunction() {var localVar = "I am local"; // 函数作用域变量console.log(globalVar); // ✅ 可以访问全局变量console.log(localVar); // ✅ 可以访问函数内部变量
}myFunction();console.log(globalVar); // ✅ 可以访问全局变量
console.log(localVar); // ❌ ReferenceError: localVar is not defined(函数作用域变量不能被外部访问)
📌 示例 2:作用域链
var name = "Global";function outer() {var name = "Outer";function inner() {var name = "Inner";console.log(name); // 输出:Inner(先在自身作用域找)}inner();console.log(name); // 输出:Outer(自身作用域有 name)
}outer();
console.log(name); // 输出:Global(访问全局变量)
解释:
inner()
内部访问name
,它先查找自身作用域,找到"Inner"
,所以打印"Inner"
。outer()
内部访问name
,找到"Outer"
,所以打印"Outer"
。- 全局访问
name
,找到"Global"
,所以打印"Global"
。
📌 示例 3:函数中的变量声明提升
function test() {console.log(a); // 输出:undefined(变量声明提升,但未赋值)var a = 10;console.log(a); // 输出:10
}test();
等价于
function test() {var a; // 变量声明被提升console.log(a); // undefineda = 10;console.log(a); // 10
}
var a
在函数执行前被提升,但 不会赋值,所以console.log(a)
输出undefined
。
📌 示例 4:函数中不使用 var
声明的变量会变成全局变量
function myFunc() {x = 100; // 没有使用 var/let/const 声明,会变成全局变量!
}myFunc();
console.log(x); // 输出:100(x 变成了全局变量)
解释:
x = 100;
没有var
、let
、const
,所以 JavaScript 自动创建了全局变量x
。- 这是一种不推荐的做法,因为它会污染全局作用域。
✅ 最佳实践:始终使用 var
、let
或 const
来声明变量,避免意外创建全局变量。
📌 示例 5:使用 window
访问全局变量
var globalVar = "Hello, World!";function myFunction() {console.log(window.globalVar); // ✅ 可以通过 window 访问全局变量
}myFunction();
解释:
- 在浏览器环境下,全局变量会成为
window
对象的属性。 - 所以可以用
window.globalVar
来访问。
🎯 总结
作用域规则 | 解释 |
---|---|
函数执行时创建作用域,结束后销毁 | 每次调用函数都会生成新的作用域,不会共享 |
函数内部可以访问全局变量,全局不能访问局部变量 | 局部变量只在函数内部可见 |
作用域链 | 变量在自身作用域找不到,会向上查找 |
变量声明提升 | var 声明的变量会被提升,但不会初始化 |
未声明变量会变成全局变量 | x = 100; 会创建全局变量(⚠️ 不推荐) |
形参也是局部变量 | 函数参数等同于在函数内部声明了变量 |
可以用 window 访问全局变量 | window.globalVar 访问全局变量 |
✅ 最佳实践
- 避免在函数中不使用
var
、let
、const
声明变量,防止污染全局作用域。 - 使用
let
或const
代替var
,避免变量提升带来的undefined
问题。 - 尽量避免全局变量,尽量将变量限制在函数作用域内。
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">//创建一个变量var a = 10;function fun(){var a = "我是fun函数中的变量a";var b = 20;console.log("a = "+a);function fun2(){console.log("a = "+window.a);}fun2(); }fun();// 没有全局变量的b,然后就报错了,后边就不运行了// console.log("b = "+b);function fun3(){//声明提前console.log(a);var a = 35;fun4();function fun4(){alert("I'm fun4");}}fun3();//在函数中,不适用var声明的变量都会成为全局变量function fun5(){//d没有使用var关键字,则会设置为全局变量d = 100;}fun5();//在全局输出dconsole.log("d = "+d);//定义形参就相当于在函数作用域中声明了变量function fun6(e=10){alert(e);}fun6(); </script></head><body></body>
</html>
🟡 this
在 JavaScript 中,this
是一个动态绑定的关键字,它的指向取决于函数的调用方式,而不是定义位置。让我们来详细解析:
🎯 this
解析:不同调用方式的 this
指向
✅ 1. 以「普通函数」调用,this
指向 window
function sayHello() {console.log(this); // 指向 window(在严格模式下是 undefined)
}sayHello(); // 浏览器环境下:window
解析:
sayHello()
直接调用,属于普通函数调用。- 在非严格模式下,
this
指向window
。 - 在严格模式(
"use strict"
)下,this
变为undefined
。
✅ 2. 以「方法」调用,this
指向调用方法的对象
var person = {name: "Alice",greet: function () {console.log(this.name);}
};person.greet(); // 输出:Alice(this 指向 person)
解析:
person.greet()
是方法调用,所以this
指向person
对象。
✅ 3. 赋值后调用(丢失 this
绑定)
var obj = {name: "Bob",say: function () {console.log(this.name);}
};var func = obj.say; // 赋值给变量
func(); // 输出:undefined(在非严格模式下是 window)
解析:
func()
作为普通函数调用,this
指向window
(或undefined
)。- 解决方案可以用
bind()
,如下👇:
var boundFunc = obj.say.bind(obj);
boundFunc(); // 输出:Bob
🔹 bind(obj)
可以永久绑定 this
。
✅ 4. this
在构造函数中指向新对象
function Person(name) {this.name = name;
}var p1 = new Person("Charlie");
console.log(p1.name); // 输出:Charlie
解析:
- 使用
new
关键字,this
指向新创建的对象。
✅ 5. this
在箭头函数中继承外层作用域
var obj = {name: "David",say: function () {var inner = () => {console.log(this.name);};inner();}
};obj.say(); // 输出:David
解析:
- 箭头函数不会创建自己的
this
,它继承外层作用域(即say
方法中的this
)。 - 因为
say
是obj
调用的,所以this
指向obj
。
✅ 6. call()
、apply()
和 bind()
改变 this
var obj1 = { name: "Eve" };
var obj2 = { name: "Frank" };function sayHello() {console.log(this.name);
}sayHello.call(obj1); // Eve
sayHello.apply(obj2); // Frank
解析:
call(obj)
和apply(obj)
强制把this
绑定到obj
。bind(obj)
返回一个新的函数,可以永久绑定this
:
var boundFunc = sayHello.bind(obj1);
boundFunc(); // Eve
🎯 this
绑定规则总结
调用方式 | this 指向 |
---|---|
普通函数调用 | window (严格模式下 undefined ) |
方法调用 | 该方法所属的对象 |
构造函数调用 | 新创建的对象 |
箭头函数 | 继承外层作用域的 this |
call(obj) / apply(obj) | 指定的 obj |
bind(obj) | 绑定后 this 永远指向 obj |
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript"> function fun(){console.log(this.name);}var obj = {name:"喜羊羊",sayName:fun};var obj2 = {name:"美羊羊",sayName:fun};var name = "全局的name属性";//以函数形式调用,this是windowfun();//以方法的形式调用,this是调用方法的对象obj.sayName();obj2.sayName();</script></head><body></body>
</html>
🟡 工厂动态创对象
- 使用工厂方法创建对象,通过该方法可以大批量的创建对象。
- 使用工厂方法创建的对象,使用的构造函数都是Object,所以创建的对象都是Object这个类型,就导致我们无法区分出多种不同类型的对象。
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">//创建一个对象var obj = {name:"喜羊羊",age:18,gender:"男",sayName:function(){alert(this.name);}};//使用工厂方法创建对象,通过该方法可以大批量的创建对象function createPerson(name , age ,gender){//创建一个新的对象 var obj = new Object();//向对象中添加属性obj.name = name;obj.age = age;obj.gender = gender;obj.sayName = function(){alert(this.name);};//将新的对象返回return obj;}//用来创建狗的对象function createDog(name , age){var obj = new Object();obj.name = name;obj.age = age;obj.sayHello = function(){alert("汪汪~~");};return obj;}var obj2 = createPerson("美羊羊",16,"女");var obj3 = createPerson("懒羊羊",6,"男");var obj4 = createPerson("沸羊羊",18,"男");//创建一个狗的对象var dog = createDog("旺财",3);console.log(dog);console.log(obj4);</script></head><body></body>
</html>
🟡 构造函数
- 构造函数就是一个普通的函数,创建方式和普通函数没有区别,不同的是构造函数
习惯上首字母大写
。 - 构造函数和普通函数的区别就是
调用方式的不同
,普通函数是直接调用,而构造函数需要使用new关键字来调用。 - 构造函数的执行流程:
- 1.立刻创建一个新的对象
- 2.将新建的
对象设置为函数中this
,在构造函数中可以使用this来引用新建的对象。 - 3.逐行执行函数中的代码。
- 4.将新建的对象作为返回值返回。
- 使用同一个构造函数创建的对象,我们称为一类对象,也将一个构造函数称为一个类。
- 我们将通过一个构造函数创建的对象,称为是该类的实例。
- this的情况:
- 1.当以函数的形式调用时,this是window
- 2.当以方法的形式调用时,谁调用方法this就是谁
- 3.当以构造函数的形式调用时,this就是新创建的那个对象
- 使用
instanceof
可以检查一个对象是否是一个类的实例:语法:对象 instanceof 构造函数
。如果是,则返回true,否则返回false。 - 所有的对象都是Object的后代,所以任何对象和Object左instanceof检查时都会返回true。
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">//创建一个构造函数,专门用来创建Person对象的function Person(name , age , gender){this.name = name;this.age = age;this.gender = gender;this.sayName = function(){alert(this.name);};}function Dog(name , age){var obj = new Object();obj.name = name;obj.age = age;obj.sayHello = function(){alert("汪汪~~");};return obj;}var per = new Person("喜羊羊",18,"男");var per2 = new Person("美羊羊",16,"女");var per3 = new Person("慢羊羊",58,"男");var dog = new Dog();console.log(per instanceof Person);//trueconsole.log(dog instanceof Person);//false//所有的对象都是Object的后代,所以任何对象和Object左instanceof检查时都会返回trueconsole.log(dog instanceof Object);//true</script></head><body></body>
</html>
在Person构造函数中,
为每一个对象都添加了一个sayName方法
- 目前我们的方法是在构造函数内部创建的,也就是构造函数每执行一次就会创建一个新的sayName方法,也是所有实例的sayName都是唯一的。
- 这样就导致了构造函数执行一次就会创建一个新的方法,执行10000次就会创建10000个新的方法,而10000个方法都是一模一样的,这是完全没有必要,完全可以使所有的对象共享同一个方法
解决方法一:
将sayName方法在全局作用域中定义
- 将函数定义在全局作用域,污染了全局作用域的命名空间。
- 而且定义在全局作用域中也很不安全。
解决方法二(good):向原型中添加sayName方法
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">//创建一个Person构造函数function Person(name , age , gender){this.name = name;this.age = age;this.gender = gender;//向对象中添加一个方法//this.sayName = fun;}//将sayName方法在全局作用域中定义//将函数定义在全局作用域,污染了全局作用域的命名空间//而且定义在全局作用域中也很不安全/*function fun(){alert("Hello大家好,我是:"+this.name);};*///向原型中添加sayName方法Person.prototype.sayName = function(){alert("Hello大家好,我是:"+this.name);};//创建一个Person的实例var per = new Person("喜羊羊",18,"男");var per2 = new Person("蕉太狼",28,"男");per.sayName();per2.sayName();console.log(per.sayName == per2.sayName);//true</script></head><body></body>
</html>
🟡 原型函数
原型prototype
:我们所创建的每一个函数,解析器都会向函数中添加一个属性prototype,这个属性对应着一个对象,这个对象就是我们所谓的原型对象。- 如果函数作为普通函数调用prototype没有任何作用。
- 当函数以构造函数的形式调用时,它所创建的对象中都会有一个隐含的属性,指向该构造函数的原型对象,我们可以通过
__proto__
来访问该属性。 - 原型对象(
prototype
)相当于一个公共区域,所有同一个类的实例都可以访问到这个原型对象,我们可以将对象中共有的内容,统一到原型对象中。 - 当我们访问对象的一个属性或方法时,它会
先在对象自身中寻找
,如果有则直接使用,如果没有则会去原型对象中寻找,如找到则直接使用。 - 以后我们创建构造函数时,可以将这些对象共有的属性和方法,统一添加到构造函数的原型对象中,这样不用分别为每一个对象添加,也不会影响到全局作用域,就可以使每个对象都具有这些属性和方法了。
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">function MyClass(){}//向MyClass的原型中添加属性aMyClass.prototype.a = 123;//向MyClass的原型中添加一个方法MyClass.prototype.sayHello = function(){alert("hello");};var mc = new MyClass();var mc2 = new MyClass();console.log(MyClass.prototype);console.log(mc2.__proto__ == MyClass.prototype);//向mc中添加a属性mc.a = "我是mc中的a";console.log(mc.a);console.log(mc2.a);mc.sayHello();</script></head><body></body>
</html>
- 使用
in
检查对象中是否含有某个属性时,如果对象中没有但是原型中有,也会返回true
。 - 可以使用对象的
hasOwnProperty()
来检查对象自身中是否含有该属性,使用该方法只有当对象自身中含有属性时,才会返回true。 - 原型对象也是对象,所以它也有原型,当我们使用一个对象的属性或方法时:
- 先在
自身
中寻找,自身中如果有,则直接使用。 - 如果没有则去
原型对象
中寻找,如果原型对象中有,则使用。 - 如果没有则去
原型的原型
中寻找,直到找到Object对象的原型。 - Object对象的原型没有原型,如果在Object原型中依然没有找到,则返回
undefined
。
- 先在
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">//创建一个构造函数function MyClass(){}//向MyClass的原型中添加一个name属性MyClass.prototype.name = "我是原型中的名字";var mc = new MyClass();mc.age = 18;console.log(mc.name);//使用in检查对象中是否含有某个属性时,如果对象中没有但是原型中有,也会返回trueconsole.log("name" in mc);//可以使用对象的hasOwnProperty()来检查对象自身中是否含有该属性//使用该方法只有当对象自身中含有属性时,才会返回trueconsole.log(mc.hasOwnProperty("age"));console.log(mc.hasOwnProperty("hasOwnProperty"));/** 原型对象也是对象,所以它也有原型,* 当我们使用一个对象的属性或方法时,会现在自身中寻找,* 自身中如果有,则直接使用,* 如果没有则去原型对象中寻找,如果原型对象中有,则使用,* 如果没有则去原型的原型中寻找,直到找到Object对象的原型,* Object对象的原型没有原型,如果在Object原型中依然没有找到,则返回undefined*/console.log(mc.__proto__.hasOwnProperty("hasOwnProperty"));console.log(mc.__proto__.__proto__.hasOwnProperty("hasOwnProperty"));// Object对象的原型没有原型console.log(mc.__proto__.__proto__.__proto__);</script></head><body></body>
</html>
🟡 toString()方法
在 JavaScript 中,toString()
是所有对象都继承自 Object.prototype
的一个方法,它的作用是将对象转换为字符串。
1️⃣ 默认情况下,toString()
方法
如果我们直接打印一个对象,默认会调用 toString()
方法:
var obj = {};
console.log(obj.toString()); // "[object Object]"
解析:
Object.prototype.toString()
默认返回[object Object]
,表示这是一个普通对象。
2️⃣ 覆盖 toString()
方法
如果我们希望自定义 toString()
的返回值,可以在类的原型上重写:
var person = {name: "Alice",age: 25,toString: function () {return `姓名:${this.name}, 年龄:${this.age}`;}
};console.log(person.toString()); // "姓名:Alice, 年龄:25"
解析:
- 由于
toString()
被重写,现在console.log(person)
输出的是更具可读性的字符串。
3️⃣ 在类的 prototype
中添加 toString()
在 ES6 类中,可以这样重写 toString()
:
class Person {constructor(name, age) {this.name = name;this.age = age;}toString() {return `姓名:${this.name}, 年龄:${this.age}`;}
}let p1 = new Person("Bob", 30);
console.log(p1.toString()); // "姓名:Bob, 年龄:30"
解析:
- 这里
Person
类内部重写了toString()
,实例p1
调用toString()
后返回自定义的字符串。
4️⃣ Object.prototype.toString.call()
识别数据类型
Object.prototype.toString.call(value)
是判断数据类型的标准方法:
console.log(Object.prototype.toString.call(123)); // "[object Number]"
console.log(Object.prototype.toString.call("hello")); // "[object String]"
console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Object.prototype.toString.call({})); // "[object Object]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
console.log(Object.prototype.toString.call(function(){})); // "[object Function]"
解析:
- 这个方法比
typeof
更精确,可以区分Array
和Object
,还能正确识别null
和undefined
。
✅ 总结
用法 | 说明 |
---|---|
obj.toString() | 默认返回 [object Object] |
obj.toString = function() {...} | 自定义对象的 toString() |
class 内定义 toString() | 让 console.log(obj) 输出更友好的内容 |
Object.prototype.toString.call(value) | 用于判断数据类型 |
这样你就掌握了 toString()
的用法!🚀
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">function Person(name , age , gender){this.name = name;this.age = age;this.gender = gender;}//当我们直接在页面中打印一个对象时,事件上是输出的对象的toString()方法的返回值//如果我们希望在输出对象时不输出[object Object],可以为对象添加一个toString()方法//修改Person原型的toStringPerson.prototype.toString = function(){return "Person[name="+this.name+",age="+this.age+",gender="+this.gender+"]";};//创建一个Person实例var per = new Person("孙悟空",18,"男");var per2 = new Person("猪八戒",28,"男");var result = per.toString();console.log("result = " + result);console.log(per.__proto__.__proto__.hasOwnProperty("toString"));</script></head><body></body>
</html>
🟡 垃圾回收(GC)
- 就像人生活的时间长了会产生垃圾一样,程序运行过程中也会产生垃圾,这些垃圾积攒过多以后,会导致程序运行的速度过慢,所以我们需要一个垃圾回收的机制,来处理程序运行过程中产生垃圾。
- 当一个对象没有任何的变量或属性对它进行引用,此时我们将永远无法操作该对象,此时这种对象就是一个垃圾,这种对象过多会占用大量的内存空间,导致程序运行变慢,所以这种垃圾必须进行清理。
- 在JS中拥有自动的垃圾回收机制,会自动将这些垃圾对象从内存中销毁,我们不需要也不能进行垃圾回收的操作,我们需要做的只是要将不再使用的对象设置
null
即可。
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title><script type="text/javascript">var obj = new Object();//对对象进行各种操作。。。。obj = null;</script></head><body></body>
</html>
🔄 JavaScript 垃圾回收(GC)机制
JavaScript 是一门自动管理内存的语言,这意味着程序员不需要手动分配和释放内存。JavaScript 引擎会自动回收不再使用的对象,释放它们占用的内存,这个过程叫做垃圾回收(Garbage Collection, GC)。
1️⃣ JavaScript 内存管理
在 JavaScript 中,内存管理的过程可以分为三个阶段:
- 内存分配(Allocation):创建变量、对象、函数时,JS 引擎会自动为其分配内存。
- 内存使用(使用变量、调用函数):程序运行时,访问和修改变量、操作 DOM、执行函数等都会占用内存。
- 内存回收(Garbage Collection):当某些对象不再被使用时,JS 引擎会回收这些对象的内存。
2️⃣ JavaScript 的垃圾回收算法
目前主流的 JavaScript 引擎(如 V8)主要使用**标记清除(Mark-and-Sweep)**算法进行垃圾回收。
🔹(1)标记清除(Mark-and-Sweep)
原理:
- 从根对象(Global Object)开始标记所有可达对象(即仍然可以访问的对象)。
- 扫描内存中的所有对象,如果某个对象没有被标记,就认为它是不可达的,进行回收。
- 释放这些不可达对象占用的内存。
示例:
function demo() {let obj = { name: "Alice" }; // obj 被创建console.log(obj.name);
} // 函数执行完毕,obj 没有被引用,GC 会回收它
demo();
分析:
obj
在demo()
函数执行时被创建。- 当
demo()
结束后,obj
变量被销毁,没有其他地方引用它。 - GC 发现
obj
无法再被访问,就会回收其内存。
🔹(2)引用计数(Reference Counting)
原理:
- 每个对象都有一个引用计数,记录被多少个变量或属性引用。
- 如果对象的引用计数变成
0
,则表示它不再被使用,可以被回收。
示例:
let obj1 = { name: "Bob" };
let obj2 = obj1; // obj1 和 obj2 都引用这个对象
obj1 = null; // obj2 仍然引用这个对象,无法回收
obj2 = null; // 现在对象引用数为 0,GC 进行回收
⚠️ 问题:
- 循环引用可能导致对象无法被回收,从而引发内存泄漏。
循环引用示例(可能导致内存泄漏):
function circularReference() {let objA = {};let objB = {};objA.ref = objB; // objA 引用 objBobjB.ref = objA; // objB 引用 objAreturn "done";
}
circularReference(); // 由于相互引用,GC 可能无法回收 objA 和 objB
解决方案:
- 使用
WeakMap
或WeakSet
,弱引用不会阻止对象被回收:
let weakMap = new WeakMap();
let obj = { name: "Charlie" };
weakMap.set(obj, "some value"); // obj 仍可被垃圾回收
obj = null; // GC 会回收 obj
3️⃣ 如何优化垃圾回收?
✅ 1. 手动释放对象
- 如果对象不再使用,将其设为
null
,帮助 GC 回收:
let user = { name: "David" };
user = null; // 释放 user 变量的引用
✅ 2. 避免全局变量
- 全局变量不会自动回收,应尽量减少:
function foo() {let temp = "Hello"; // temp 在函数结束后被回收
}
foo();
✅ 3. 使用 WeakMap
/ WeakSet
- 适用于存储可能随时消失的对象:
let weakMap = new WeakMap();
let obj = { name: "Eve" };
weakMap.set(obj, "value");
obj = null; // obj 失去引用后,会被 GC 回收
✅ 4. 避免 DOM 泄漏
- 移除不再使用的 DOM 事件监听:
let btn = document.getElementById("myButton");
function handleClick() {console.log("Clicked");
}
btn.addEventListener("click", handleClick);
btn.removeEventListener("click", handleClick); // 及时移除
总结
GC 算法 | 说明 |
---|---|
标记清除 | 主流垃圾回收机制,标记不可达对象并回收 |
引用计数 | 维护引用次数,引用为 0 时回收(可能有循环引用问题) |
💡 JavaScript GC 优化技巧
- 减少全局变量(局部变量更容易被回收)
- 手动释放不再使用的对象(赋值
null
) - 使用
WeakMap
/WeakSet
(避免循环引用) - 及时清理 DOM 事件监听(避免 DOM 泄漏)