垃圾回收
- JavaScript 中主要的内存管理概念是 可达性。
- 列出固有的可达值的基本集合,这些值明显不能被释放。这些值被称作 根(roots)。
- 当前函数的局部变量和参数。
- 嵌套调用时,当前调用链上所有函数的变量与参数。
- 全局变量。
- (还有一些内部的)
- 如果一个值可以通过引用或引用链从根访问任何其他值,则认为该值是可达的。
- 在 JavaScript 引擎中有一个被称作 垃圾回收器 的东西在后台执行。它监控着所有对象的状态,并删除掉那些已经不可达的。
1 2 3 4 |
// user 具有对这个对象的引用 let user = { name: "John" }; |
- 如果
user
的值被重写了,这个引用就没了
1 |
user = null; |
- 现在 这个对象变成不可达的了。因为没有引用了,就不能访问到它了。垃圾回收器会认为它是垃圾数据并进行回收,然后释放内存。
内部算法
- 垃圾回收的基本算法被称为 “mark-and-sweep”。
- 步骤
- 垃圾收集器找到所有的根,并“标记”(记住)它们。
- 然后它遍历并“标记”来自它们的所有引用。
- 然后它遍历标记的对象并标记 它们的 引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。
- ……如此操作,直到所有可达的(从根部)引用都被访问到。
- 没有被标记的对象都会被删除。
this
- 为了访问该对象,方法中可以使用
this
关键字。
1 2 3 4 5 6 7 8 9 10 11 12 |
let user = { name: "John", age: 30, sayHi() { // "this" 指的是“当前的对象” alert(this.name); } }; user.sayHi(); // John |
- javaScript 中的
this
可以用于任何函数,即使它不是对象的方法。this
的值是在代码运行时计算出来的,它取决于代码上下文。
1 2 3 |
function sayHi() { alert( this.name ); } |
- 箭头函数有些特别:它们没有自己的
this
。如果我们在这样的函数中引用this
,this
值取决于外部“正常的”函数。
构造函数
- 构造函数在技术上是常规函数。不过有两个约定:
- 它们的命名以大写字母开头。
- 它们只能由
"new"
操作符来执行。
1 2 3 4 5 6 7 8 9 |
function User(name) { this.name = name; this.isAdmin = false; } let user = new User("Jack"); alert(user.name); // Jack alert(user.isAdmin); // false |
1 2 3 4 5 6 7 8 9 |
function User(name) { // this = {};(隐式创建) // 添加属性到 this this.name = name; this.isAdmin = false; // return this;(隐式返回) } |
构造模式测试:new.target
- 在一个函数内部,我们可以使用
new.target
属性来检查它是否被使用new
进行调用了。
1 2 3 4 5 6 7 8 9 |
function User() { alert(new.target); } // 不带 "new": User(); // undefined // 带 "new": new User(); // function User { ... } |
构造器的return
- 通常,构造器没有
return
语句。它们的任务是将所有必要的东西写入this
,并自动转换为结果。 - 但是,如果这有一个
return
语句,那么规则就简单了:- 如果
return
返回的是一个对象,则返回这个对象,而不是this
。 - 如果
return
返回的是一个原始类型,则忽略。
- 如果
- 换句话说,带有对象的
return
返回该对象,在所有其他情况下返回this
。
1 2 3 4 5 6 7 8 |
function BigUser() { this.name = "John"; return { name: "Godzilla" }; // <-- 返回这个对象 } alert( new BigUser().name ); // Godzilla,得到了那个对象 |
1 2 3 4 5 6 7 8 |
function SmallUser() { this.name = "John"; return; // <-- 返回 this } alert( new SmallUser().name ); // John |
构造器中的方法
- 使用构造函数来创建对象会带来很大的灵活性。
- 构造函数可能有一些参数,这些参数定义了如何构造对象以及要放入什么。
- 我们不仅可以将属性添加到
this
中,还可以添加方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function User(name) { this.name = name; this.sayHi = function() { alert( "My name is: " + this.name ); }; } let john = new User("John"); john.sayHi(); // My name is: John /* john = { name: "John", sayHi: function() { ... } } */ |
可选链"?."
- 可选链
?.
是一种访问嵌套对象属性的安全的方式。即使中间的属性不存在,也不会出现错误。
1 2 3 4 5 6 7 8 9 |
let obj = { foo: { bar: { baz: 42 } } }; let value = obj.foo.bar.baz; // 如果 obj.foo 或 obj.foo.bar 为 null 或 undefined,将会抛出错误 |
1 2 3 4 5 6 7 8 9 |
let obj = { foo: { bar: { baz: 42 } } }; let value = obj.foo?.bar?.baz; // 安全地访问 obj.foo、obj.foo.bar 和 obj.foo.bar.baz,如果其中任何一个为 null 或 undefined,则 value 将为 undefined |
- 用于函数调用
1 2 3 4 5 6 7 8 9 |
let obj = { foo: { bar: function() { return 'Hello, world!'; } } }; let result = obj.foo?.bar(); // 安全地调用 obj.foo.bar(),如果 obj.foo 或 obj.foo.bar 为 null 或 undefined,result 将为 undefined |
"不存在的属性"
value?.prop
:- 如果
value
存在,则结果与value.prop
相同 - 否则(当
value
为undefined/null
时)则返回undefined
- 如果
- 如果
?.
左边部分不存在,就会立即停止运算(“短路效应”)
1 2 3 |
let user = {}; // user 没有 address 属性 alert( user?.address?.street ); // undefined(不报错) |
- 其他变体
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 将 ?.() 用于调用一个可能不存在的函数。 let userAdmin = { admin() { alert("I am admin"); } }; let userGuest = {}; userAdmin.admin?.(); // I am admin userGuest.admin?.(); // 啥都没有(没有这样的方法) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 语法 ?.[]允许从一个可能不存在的对象上安全地读取属性。 let user1 = { firstName: "John" }; let user2 = null; // 假设,我们不能授权此用户 let key = "firstName"; alert( user1?.[key] ); // John alert( user2?.[key] ); // undefined alert( user1?.[key]?.something?.not?.existing); // undefined |
Symbol类型
- 根据规范,对象的属性键只能是字符串类型或者 Symbol 类型。
- “Symbol” 值表示唯一的标识符。
1 2 |
// id 是 symbol 的一个实例化对象 let id = Symbol(); |
- 创建时,我们可以给 Symbol 一个描述(也称为 Symbol 名),这在代码调试时非常有用:
1 2 |
// id 是描述为 "id" 的 Symbol let id = Symbol("id"); |
- Symbol 保证是唯一的。即使我们创建了许多具有相同描述的 Symbol,它们的值也是不同。描述只是一个标签,不影响任何东西。
1 2 3 4 |
let id1 = Symbol("id"); let id2 = Symbol("id"); alert(id1 == id2); // false |
- 注意
- JavaScript 中的大多数值都支持字符串的隐式转换。但Symbol 不会被自动转换为字符串。
1 2 3 4 5 6 7 8 |
let id = Symbol("id"); alert(id); // 类型错误:无法将 Symbol 值转换为字符串。 let id = Symbol("id"); alert(id.toString()); // Symbol(id),现在它有效了 let id = Symbol("id"); alert(id.description); // id |
"隐藏"属性
- Symbol 允许我们创建对象的“隐藏”属性,代码的任何其他部分都不能意外访问或重写这些属性。
1 2 3 4 5 6 7 8 9 |
let user = { // 属于另一个代码 name: "John" }; let id = Symbol("id"); user[id] = 1; alert( user[id] ); // 我们可以使用 Symbol 作为键来访问数据 |
- 对象字面量中的Symbol
1 2 3 4 5 6 |
let id = Symbol("id"); let user = { name: "John", [id]: 123 // 而不是 "id":123 }; |
- Symbol 属性不参与
for..in
循环。
1 2 3 4 5 6 7 8 9 10 11 |
let id = Symbol("id"); let user = { name: "John", age: 30, [id]: 123 }; for (let key in user) alert(key); // name, age (no symbols) // 使用 Symbol 任务直接访问 alert( "Direct: " + user[id] ); |
全局Symbol
- 但有时我们想要名字相同的 Symbol 具有相同的实体。为了实现这一点,这里有一个 全局 Symbol 注册表。
- 我们可以在其中创建 Symbol 并在稍后访问它们,它可以确保每次访问相同名字的 Symbol 时,返回的都是相同的 Symbol。
- 要从注册表中读取(不存在则创建)Symbol,请使用
Symbol.for(key)
。 - 该调用会检查全局注册表,如果有一个描述为
key
的 Symbol,则返回该 Symbol,否则将创建一个新 Symbol(Symbol(key)
),并通过给定的key
将其存储在注册表中。
1 2 3 4 5 6 7 8 |
// 从全局注册表中读取 let id = Symbol.for("id"); // 如果该 Symbol 不存在,则创建它 // 再次读取(可能是在代码中的另一个位置) let idAgain = Symbol.for("id"); // 相同的 Symbol alert( id === idAgain ); // true |
- Symbol.keyFor:对于全局 Symbol,不仅有
Symbol.for(key)
按名字返回一个 Symbol,还有一个反向调用:Symbol.keyFor(sym)
,它的作用完全反过来:通过全局 Symbol 返回一个名字。
1 2 3 4 5 6 7 |
// 通过 name 获取 Symbol let sym = Symbol.for("name"); let sym2 = Symbol.for("id"); // 通过 Symbol 获取 name alert( Symbol.keyFor(sym) ); // name alert( Symbol.keyFor(sym2) ); // id |
系统Symbol
- JavaScript 内部有很多“系统” Symbol,我们可以使用它们来微调对象的各个方面。
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.toPrimitive
- ……等等。
对象-原始值转换
- 所有的对象在布尔上下文(context)中均为
true
。所以对于对象,不存在 to-boolean 转换,只有字符串和数值转换。 - 数值转换发生在对象相减或应用数学函数时。
- 至于字符串转换 —— 通常发生在我们像
alert(obj)
这样输出一个对象和类似的上下文中。 - ToPrimitive
- string
1 2 3 4 5 |
// 输出 alert(obj); // 将对象作为属性键 anotherObj[obj] = 123; |
- number
1 2 3 4 5 6 7 8 9 |
// 显式转换 let num = Number(obj); // 数学运算(除了二进制加法) let n = +obj; // 一元加法 let delta = date1 - date2; // 小于/大于的比较 let greater = user1 > user2; |
- default
1 2 3 4 5 |
// 二元加法使用默认 hint let total = obj1 + obj2; // obj == number 使用默认 hint if (user == 1) { ... }; |
Symbol.toPrimitive
- 有一个名为
Symbol.toPrimitive
的内建 symbol,它被用来给转换方法命名
1 2 3 4 |
obj[Symbol.toPrimitive] = function(hint) { // 返回一个原始值 // hint = "string"、"number" 和 "default" 中的一个 } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let user = { name: "John", money: 1000, [Symbol.toPrimitive](hint) { alert(`hint: ${hint}`); return hint == "string" ? `{name: "${this.name}"}` : this.money; } }; // 转换演示: alert(user); // hint: string -> {name: "John"} alert(+user); // hint: number -> 1000 alert(user + 500); // hint: default -> 1500 |
toString/valueOf
- 方法
toString
和valueOf
来自上古时代。 toString
方法返回一个字符串"[object Object]"
。valueOf
方法返回对象自身。
1 2 3 4 |
let user = {name: "John"}; alert(user); // [object Object] alert(user.valueOf() === user); // true |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
let user = { name: "John", money: 1000, // 对于 hint="string" toString() { return `{name: "${this.name}"}`; }, // 对于 hint="number" 或 "default" valueOf() { return this.money; } }; alert(user); // toString -> {name: "John"} alert(+user); // valueOf -> 1000 alert(user + 500); // valueOf -> 1500 |
1 2 3 4 5 6 7 8 9 10 |
let user = { name: "John", toString() { return this.name; } }; alert(user); // toString -> John alert(user + 500); // toString -> John500 |
返回类型
- 关于所有原始转换方法,有一个重要的点需要知道,就是它们不一定会返回 “hint” 的原始值。
- 没有限制
toString()
是否返回字符串,或Symbol.toPrimitive
方法是否为 hint “number” 返回数字。 - 唯一强制性的事情是:这些方法必须返回一个原始值,而不是对象。
进一步转换
- 如果我们将对象作为参数传递,则会出现两个阶段:
- 对象被转换为原始值(通过前面我们描述的规则)。
- 如果生成的原始值的类型不正确,则继续进行转换。
1 2 3 4 5 6 7 8 |
let obj = { // toString 在没有其他方法的情况下处理所有转换 toString() { return "2"; } }; alert(obj * 2); // 4,对象被转换为原始值字符串 "2",之后它被乘法转换为数字 2。 |
1 2 3 4 5 6 7 |
let obj = { toString() { return "2"; } }; alert(obj + 2); // 22("2" + 2)被转换为原始值字符串 => 级联 |
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ HTTP协议相关学习一03/22
- ♥ 【Javascript】第二部分05/14
- ♥ WebSocket协议相关学习一03/24
- ♥ 【Javascript】第一部分05/10
- ♥ 【LeetCode-30 天 JavaScript 挑战】07/23
- ♥ 【Javascript】对象引用复制05/18