对象
花括号 {…}
来创建对象。一个属性就是一个键值对(“key: value”)。其中键(key
)是一个字符串(也叫做属性名),值(value
)可以是任何值。
- 文本和属性
- 移除属性:
delete
user.
age;
- 移除属性:
- 方括号
- 对于多词属性,点操作就不能用了,例如:user["likes birds"] = true;
- 计算属性
- 当创建一个对象时,我们可以在对象字面量中使用方括号。这叫做 计算属性。
JavaScript
let fruit = prompt("Which fruit to buy?", "apple");
let bag = {
[fruit]: 5, // 属性名是从 fruit 变量中得到的
};
alert( bag.apple ); // 5 如果 fruit="apple"
- 属性值简写
JavaScript
function makeUser(name, age) {
return {
name, // 与 name: name 相同
age, // 与 age: age 相同
// ...
};
}
- 属性名称限制
- 变量名不能是编程语言的某个保留字,如 “for”、“let”、“return” 等……,但对象的属性名并不受此限制
- 一个名为
__proto__
的属性。我们不能将它设置为一个非对象的
- 属性存在性测试,“in” 操作符
- 检查这个属性是否存在对象内
"key"
in
objectin
的左边必须是 属性名。
- "for..in" 循环
- 为了遍历一个对象的所有键(key),可以使用一个特殊形式的循环:
for..in
。这跟我们在前面学到的for(;;)
循环是完全不一样的东西。
- 为了遍历一个对象的所有键(key),可以使用一个特殊形式的循环:
JavaScript
for (key in object) {
// 对此对象属性中的每个键执行的代码
}
- 像对象一样排序
- 对象属性有特别的顺序。
对象引用和复制
对象与原始类型的根本区别之一是,对象是“通过引用”存储和复制的,而原始类型:字符串、数字、布尔值等 —— 总是“作为一个整体”复制。 赋值了对象的变量存储的不是对象本身,而是该对象“在内存中的地址” —— 换句话说就是对该对象的“引用”。当一个对象变量被复制 —— 引用被复制,而该对象自身并没有被复制。 例如:let
admin =
user;
// 复制引用
- 通过引用来比较
- 两个对象引用相同才相等,不然对象内容相同,两个对象也不相等。
- 克隆与合并,Object.assign
JavaScript
let user = {
name: "John",
age: 30
};
let clone = {}; // 新的空对象
// 将 user 中所有的属性拷贝到其中
for (let key in user) {
clone[key] = user[key];
}
// 现在 clone 是带有相同内容的完全独立的对象
clone.name = "Pete"; // 改变了其中的数据
alert( user.name ); // 原来的对象中的 name 属性依然是 John
Object.assign 方法:
Object.assign(dest, [src1, src2, src3...])
1. 第一个参数 dest 是指目标对象。
2. 更后面的参数 src1, ..., srcN(可按需传递多个参数)是源对象。
3. 该方法将所有源对象的属性拷贝到目标对象 dest 中。换句话说,从第二个开始的所有 参数的属性都被拷贝到第一个参数的对象中。
4. 调用结果返回 dest。
合并多个对象:
let user = { name: "John" };
let permissions1 = { canView: true };
let permissions2 = { canEdit: true };
// 将 permissions1 和 permissions2 中的所有属性都拷贝到 user 中
Object.assign(user, permissions1, permissions2);
// 现在 user = { name: "John", canView: true, canEdit: true }
1. 拷贝的属性的属性名已经存在,那么它会被覆盖
2. 可以用 Object.assign 代替 for..in 循环来进行简单克隆
3. 还有其他克隆对象的方法,例如使用 spread 语法 clone = {...user}
- 深层克隆
- 假设 user 的所有属性均为原始类型。但属性可以是对其他对象的引用。
JavaScript
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
alert( user.sizes.height ); // 182
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
let clone = Object.assign({}, user);
alert( user.sizes === clone.sizes ); // true,同一个对象
// user 和 clone 分享同一个 sizes
user.sizes.width++; // 通过其中一个改变属性值
alert(clone.sizes.width); // 51,能从另外一个获取到变更后的结果
让 user 和 clone 成为两个真正独立的对象,我们应该使用一个拷贝循环来检查 user[key] 的每个值,如果它是一个对象,那么也复制它的结构。这就是所谓的“深拷贝”。
可以使用递归来实现它。或者为了不重复造轮子,采用现有的实现,例如 lodash 库的 _.cloneDeep(obj)。
使用 const 声明的对象也是可以被修改的
只有当我们尝试将 user=... 作为一个整体进行赋值时,const user 才会报错。
垃圾回收
我们创建的原始值、对象、函数……这一切都会占用内存。
- 可达性(Reachability)
- “可达”值是那些以某种方式可访问或可用的值。它们被存储在内存中
- 固有的可达值的基本集合,这些值明显不能被释放。这些值被称作 根(roots)。
- 当前执行的函数,它的局部变量和参数。
- 当前嵌套调用链上的其他函数、它们的局部变量和参数。
- 全局变量
- (还有一些其他的,内部实现)
- 如果一个值可以从根通过引用或者引用链进行访问,则认为该值是可达的。,如果全局变量中有一个对象,并且该对象有一个属性引用了另一个对象,则 该 对象被认为是可达的。而且它引用的内容也是可达的。
- 例子
- 如果 全局变量
user
的值(初始值:a)被重写了,这个引用就没了。那之前全局变量user初始化的值a就变得不可达了,引用没了,就不能访问到a了, - 垃圾回收器会认为它是垃圾数据并进行回收,然后释放内存。
- 如果 全局变量
- 两个引用
- 如果全局变量user引用复制给了admin,重写了user里面属性的值,但是之前的属性值可以通过admin访问到。如果我们又重写了
admin
,对象就会被删除。
- 如果全局变量user引用复制给了admin,重写了user里面属性的值,但是之前的属性值可以通过admin访问到。如果我们又重写了
- 相互关联的对象
JavaScript
function marry(man, woman) {
woman.husband = man;
man.wife = woman;
return {
father: man,
mother: woman
}
}
let family = marry({
name: "John"
}, {
name: "Ann"
});
现在所有对象都可达
//移除两个引用
delete family.father;
delete family.mother.husband;
John现在不可达
- 无法到达的岛屿
- 几个对象相互引用,但外部没有对其任意对象的引用,这些对象也可能是不可达的,并被从内存中删除。
- 内部算法
- 垃圾回收的基本算法被称为 “mark-and-sweep”。
- 垃圾回收步骤:
- 垃圾收集器找到所有的根,并“标记”(记住)它们。
- 然后它遍历并“标记”来自它们的所有引用。
- 然后它遍历标记的对象并标记 它们的 引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。
- ……如此操作,直到所有可达的(从根部)引用都被访问到。
- 没有被标记的对象都会被删除。
- 分代收集(Generational collection)
- 新对象(工作完就是去意义的对象)内存就会把它们及时清理,长期存在的对象检查的频率就会变小
- 增量收集(Incremental collection)
- 引擎将现有的整个对象集拆分为多个部分,这样会带来许多微小的延迟而不是一个大的延迟。
- 闲时收集(Idle-time collection)
- 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。
- 总结
- 垃圾回收是自动完成的,我们不能强制执行或是阻止执行。
- 当对象是可达状态时,它一定是存在于内存中的。
- 被引用与可访问(从一个根)不同:一组相互连接的对象可能整体都不可达。
- V8 之旅:垃圾回收。
对象方法,’this‘
- 方法示例
- 作为对象属性的函数被称为 方法。
JavaScript
let user = {
name: "John",
age: 30
};
user.sayHi = function() {
alert("Hello!");
};
user.sayHi(); // Hello! 调用方法
- 面向对象编程
- 当我们在代码中用对象表示实体时,就是所谓的 面向对象编程,简称为 “OOP”。
- 方法简写
JavaScript
// 这些对象作用一样
user = {
sayHi: function() {
alert("Hello");
}
};
// 方法简写看起来更好,对吧?
let user = {
sayHi() { // 与 "sayHi: function(){...}" 一样
alert("Hello");
}
};
两种方式继承方面有差别
- 方法中的 “this”
- 为了访问该对象,方法中可以使用
this
关键字。
- 为了访问该对象,方法中可以使用
JavaScript
let user = {
name: "John",
age: 30,
sayHi() {
// "this" 指的是“当前的对象”
alert(this.name);
}
};
user.sayHi(); // John
- “this” 不受限制
JavaScript
let user = { name: "John" };
let admin = { name: "Admin" };
function sayHi() {
alert( this.name );
}
// 在两个对象中使用相同的函数
user.f = sayHi;
admin.f = sayHi;
// 这两个调用有不同的 this 值
// 函数内部的 "this" 是“点符号前面”的那个对象
user.f(); // John(this == user)
admin.f(); // Admin(this == admin)
admin['f'](); // Admin(使用点符号或方括号语法来访问这个方法,都没有关系。)
- 在没有对象的情况下调用:
this == undefined
- 解除
this
绑定的后果,在 JavaScript 中,this
是“自由”的,它的值是在调用时计算出来的,它的值并不取决于方法声明的位置,而是取决于在“点符号前”的是什么对象。
- 解除
- 箭头函数没有自己的 “this”
构造器和操作符 ’new‘
常规的 {...}
语法允许创建一个对象。但是我们经常需要创建很多类似的对象,例如多个用户或菜单项等。这可以使用构造函数和 "new"
操作符来实现。
- 构造函数 2. 它们的命名以大写字母开头。 3. 它们只能由
"new"
操作符来执行。
JavaScript
function User(name) {
this.name = name;
this.isAdmin = false;
}
let user = new User("Jack");
alert(user.name); // Jack
alert(user.isAdmin); // false
1. new对象的步骤:
2. 一个新的空对象被创建并分配给 `this`。
3. 函数体执行。通常它会修改 `this`,为其添加新的属性。
4. 返回 `this` 的值。
- 构造器的主要目的 —— 实现可重用的对象创建代码。
- 有许多行用于创建单个复杂对象的代码,我们可以将它们封装在一个立即调用的构造函数中:
JavaScript
// 创建一个函数并立即使用 new 调用它
let user = new function() {
this.name = "John";
this.isAdmin = false;
// ……用于用户创建的其他代码
// 也许是复杂的逻辑和语句
// 局部变量等
};
这个构造函数不能被再次调用,因为它不保存在任何地方,只是被创建和调用。
- 构造器的 return
- ,构造器没有
return
语句。它们的任务是将所有必要的东西写入this
,并自动转换为结果。 - 如果有return语句
- 如果
return
返回的是一个对象,则返回这个对象,而不是this
。
- 如果
- 如果
return
返回的是一个原始类型,则忽略。
- 如果
- 带有对象的
return
返回该对象,在所有其他情况下返回this
。
- ,构造器没有
- 省略括号
- 如果没有参数,我们可以省略
new
后的括号 - 例如:
let
user=
new
User``;
// <-- 没有参数
等同于 let user = new User(); 但是不建议这样写
- 如果没有参数,我们可以省略
- 构造器中的方法
- 使用构造函数来创建对象会带来很大的灵活性。构造函数可能有一些参数,这些参数定义了如何构造对象以及要放入什么。当然,我们不仅可以将属性添加到
this
中,还可以添加方法。
- 使用构造函数来创建对象会带来很大的灵活性。构造函数可能有一些参数,这些参数定义了如何构造对象以及要放入什么。当然,我们不仅可以将属性添加到
可选链’?‘
这是一个最近添加到 JavaScript 的特性。 旧式浏览器可能需要 polyfills. 可选链 ?.
是一种访问嵌套对象属性的安全的方式。即使中间的属性不存在,也不会出现错误。
- “不存在的属性”的问题
- 如果一个实例对象的某个属性值没有,但是又不想程序报错,可以:user
.
address&&
user.
address.
street&&
user.
address.
street.
name
- 如果一个实例对象的某个属性值没有,但是又不想程序报错,可以:user
- 可选链
- 为了简明起见,在本文接下来的内容中,我们会说如果一个属性既不是
null
也不是undefined
,那么它就“存在”。 - user
?.
address?.
street
- 为了简明起见,在本文接下来的内容中,我们会说如果一个属性既不是
- 不要过度使用可选链
- 我们应该只将
?.
使用在一些东西可以不存在的地方。
- 我们应该只将
?.
前的变量必须已声明- 短路效应
- 正如前面所说的,如果
?.
左边部分不存在,就会立即停止运算(“短路效应”)。
- 正如前面所说的,如果
- 其它变体:?.(),?.[]
- 可选链
?.
不是一个运算符,而是一个特殊的语法结构。它还可以与函数和方括号一起使用。- 将
?.()
用于调用一个可能不存在的函数。
- 将
- 我们可以使用 ?. 来安全地读取或删除,但不能写入
- 可选链
?.
不能用在赋值语句的左侧。
symbol 类型
只有两种原始类型可以用作对象属性键:字符串类型、symbol 类型 如果使用另一种类型,例如数字,它会被自动转换为字符串, obj[1]
与 obj["1"]
相同,而 obj[true]
与 obj["true"]
相同。
- symbol
- “symbol” 值表示唯一的标识符。
- 可以使用
Symbol()
来创建这种类型的值: - 我们可以给 symbol 一个描述(也称为 symbol 名),这在代码调试时非常有用
JavaScript
let id1 = Symbol("id");
let id2 = Symbol("id");
alert(id1 == id2); // false
4. symbol 是带有可选描述的“原始唯一值”
5. symbol 不会被自动转换为字符串
6. 获取 `symbol.description` 属性,只显示描述(description)
- “隐藏”属性
- 如果我们使用的是属于第三方代码的
user
对象,我们想要给它们添加一些标识符
- 如果我们使用的是属于第三方代码的
JavaScript
let user = { // 属于另一个代码
name: "John"
};
let id = Symbol("id");
user[id] = 1;
alert( user[id] ); // 我们可以使用 symbol 作为键来访问数据
- 对象字面量中的 symbol
- 如果我们要在对象字面量 {...} 中使用 symbol,则需要使用方括号把它括起来。
JavaScript
let id = Symbol("id");
let user = {
name: "John",
[id]: 123 // 而不是 "id":123
};
- symbol 在 for…in 中会被跳过
- symbol 属性不参与
for..in
循环。
- symbol 属性不参与
- 全局 symbol
- 全局 symbol 注册表。
- 从注册表中读取(不存在则创建)symbol,请使用
Symbol.for(key)
。 - 注册表内的 symbol 被称为 全局 symbol。
- 可以在代码中随处访问 —— 这就是它们的用途。
- Symbol.keyFor
Symbol.for(key)
按名字返回一个 symbol。相反,通过全局 symbol 返回一个名字,我们可以使用Symbol.keyFor(sym)
- 如果 symbol 不是全局的,它将无法找到它并返回
undefined
。
- 系统 symbol
- 众所周知的 symbol 表的规范中
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.toPrimitive
- 众所周知的 symbol 表的规范中
对象 —— 原始值转换
- 转换规则
- 对象原始值转换
- 没有转换为布尔值。所有的对象在布尔上下文(context)中均为
true
,就这么简单。只有字符串和数字转换。 - 1数字转换发生在对象相减或应用数学函数时。例如,
Date
对象(将在 日期和时间 一章中介绍)可以相减,date1 - date2
的结果是两个日期之间的差值。 - 至于字符串转换 —— 通常发生在我们像
alert(obj)
这样输出一个对象和类似的上下文中。
- 没有转换为布尔值。所有的对象在布尔上下文(context)中均为
- 对象原始值转换
- hint
- 类型转换在各种情况下有三种变体。它们被称为 “hint”
- "string"、"number" (数学运算(除了二元加法))、"default"
- 为了进行转换,JavaScript 尝试查找并调用三个对象方法:
- 调用
obj[Symbol.toPrimitive](hint)
—— 带有 symbol 键Symbol.toPrimitive
(系统 symbol)的方法,如果这个方法存在的话,
- 调用
- 否则,如果 hint 是
"string"
—— 尝试调用obj.toString()
或obj.valueOf()
,无论哪个存在 - 否则,如果 hint 是
"number"
或"default"
—— 尝试调用obj.valueOf()
或obj.toString()
,无论哪个存在
- Symbol.toPrimitive
- 有一个名为
Symbol.toPrimitive
的内建 symbol,它被用来给转换方法命名
- 有一个名为
JavaScript
obj[Symbol.toPrimitive] = function(hint) {
// 这里是将此对象转换为原始值的代码
// 它必须返回一个原始值
// hint = "string"、"number" 或 "default" 中的一个
}
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
- 对于
"string"
hint:调用toString
方法,如果它不存在,则调用valueOf
方法(因此,对于字符串转换,优先调用toString
)。
- 对于
- 对于其他 hint:调用
valueOf
方法,如果它不存在,则调用toString
方法(因此,对于数学运算,优先调用valueOf
方法)。
- 对于其他 hint:调用
toString
方法返回一个字符串"[object Object]"
。
valueOf
方法返回对象自身。
- 转换可以返回任何原始类型
- 所有原始转换方法,有一个重要的点需要知道,就是它们不一定会返回 “hint” 的原始值。
- 这些方法必须返回一个原始值,而不是对象。
- 进一步的转换
- 如果我们将对象作为参数传递
- 对象被转换为原始值(通过前面我们描述的规则)。
- 如果还需要进一步计算,则生成的原始值会被进一步转换。
- 如果我们将对象作为参数传递
JavaScript
let obj = {
// toString 在没有其他方法的情况下处理所有转换
toString() {
return "2";
}
};
alert(obj * 2); // 4,对象被转换为原始值字符串 "2",之后它被乘法转换为数字 2。