Object对象
# 4.1 对象基础
# 4.1.1 创建对象
我们可以用下面两种语法,构造函数或字面量中的任一种来创建一个空的对象
let user = new Object(); // “构造函数” 的语法
let user = {}; // “字面量” 的语法
2
# 4.1.2 键值对
我们可以在创建对象的时候,立即将一些属性以键值对的形式放到 {...}
中
let user = { // 一个对象
name: "John", // 键 "name",值 "John"
age: 30 // 键 "age",值 30
};
2
3
4
属性有键(或者也可以叫做“名字”或“标识符”),位于冒号 ":"
的前面,值在冒号的右边
可以随时添加、删除和读取属性值
# 点符号访问属性值
// 读取文件的属性:
alert( user.name ); // John
alert( user.age ); // 30
2
3
# delete
操作符移除属性
delete user.age;
# 多词属性
多字词语来作为属性名,但必须给它们加上引号
let user = {
name: "John",
age: 30,
"likes birds": true // 多词属性名必须加引号
};
2
3
4
5
# 4.1.3 []
# 多词属性
多词属性,点操作就不能用
// 这将提示有语法错误
user.likes birds = true
2
点符号要求 key
是有效的变量标识符。这意味着:不包含空格,不以数字开头,也不包含特殊字符(允许使用 $
和 _
)
let user = {};
// 设置
user["likes birds"] = true;
// 读取
alert(user["likes birds"]); // true
// 删除
delete user["likes birds"];
2
3
4
5
6
7
请注意方括号中的字符串要放在引号中,单引号或双引号都可以
# 变量 key
由表达式得到
let user = {
name: "John",
age: 30
};
let key = prompt("What do you want to know about the user?", "name");
// 访问变量
alert( user[key] ); // John(如果输入 "name")
let key = "name";
alert( user.key ) // undefined
2
3
4
5
6
7
8
9
# 4.1.4 计算属性
当创建一个对象时,我们可以在对象字面量中使用方括号。这叫做 计算属性
let fruit = prompt("Which fruit to buy?", "apple");
let bag = {
[fruit]: 5, // 属性名是从 fruit 变量中得到的
};
alert( bag.apple ); // 5 如果 fruit="apple"
2
3
4
5
# 4.1.5 属性值简写
属性名跟变量名一样。这种通过变量生成属性的应用场景很常见,在这有一种特殊的 属性值缩写 方法,使属性名变得更短
function makeUser(name, age) {
return {
name, // 与 name: name 相同
age, // 与 age: age 相同
// ...
};
}
2
3
4
5
6
7
# 4.1.6 属性名称限制
变量名不能是编程语言的某个保留字,如 "for"、"let"、"return" 等,但对象的属性名并不受此限制
属性名可以是任何字符串或者 symbol。
其他类型会被自动地转换为字符串
# 4.1.7 属性存在性测试,"in" 操作符
JavaScript 的对象有一个需要注意的特性:能够被访问任何属性。即使属性不存在也不会报错!
读取不存在的属性只会得到 undefined
检查属性是否存在的操作符 "in"
"key" in object
注意
请注意,in
的左边必须是 属性名。通常是一个带引号的字符串
如果我们省略引号,就意味着左边是一个变量,它应该包含要判断的实际属性名
属性存在,但存储的值是 undefined
的时候,使用undefined
判断属性存在与否会有问题:
let obj = {
test: undefined
};
alert( obj.test ); // 显示 undefined,所以属性不存在?
alert( "test" in obj ); // true,属性存在!
2
3
4
5
# 4.1.8 for..in
为了遍历一个对象的所有键(key),可以使用一个特殊形式的循环:for..in
for (key in object) {
// 对此对象属性中的每个键执行的代码
}
2
3
# 4.2 对象引用和复制
对象与原始类型的根本区别之一是,对象是“通过引用”存储和复制的,而原始类型:字符串、数字、布尔值等 —— 总是“作为一个整体”复制
赋值了对象的变量存储的不是对象本身,而是该对象“在内存中的地址” —— 换句话说就是对该对象的“引用”。
当一个对象变量被复制 —— 引用被复制,而该对象自身并没有被复制。
# 4.2.1 对象通过引用比较
仅当两个对象为同一对象时,两者才相等
let a = {};
let b = a; // 复制引用
alert( a == b ); // true,都引用同一对象
alert( a === b ); // true
let c = {};
let d = {}; // 两个独立的对象
alert( c == d ); // false
2
3
4
5
6
7
8
# 4.2.2 Object.assign,克隆与合并
拷贝一个对象变量会又创建一个对相同对象的引用
要复制一个对象需要创建一个新对象,并通过遍历现有属性的结构,在原始类型值的层面,将其复制到新对象,以复制已有对象的结构
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
2
3
4
5
6
7
8
9
10
11
12
我们也可以使用 Object.assign (opens new window) 方法来达成同样的效果
Object.assign(dest, [src1, src2, src3...])
- 第一个参数
dest
是指目标对象。 - 更后面的参数
src1, ..., srcN
(可按需传递多个参数)是源对象。 - 该方法将所有源对象的属性拷贝到目标对象
dest
中。换句话说,从第二个开始的所有参数的属性都被拷贝到第一个参数的对象中。 - 调用结果返回
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 }
2
3
4
5
6
如果被拷贝的属性的属性名已经存在,那么它会被覆盖:
let user = { name: "John" };
Object.assign(user, { name: "Pete" });
alert(user.name); // 现在 user = { name: "Pete" }
2
3
# 4.2.3 深层克隆
如果对象中的属性存在对象,那么该属性会被以引用形式拷贝
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,能从另外一个看到变更的结果
2
3
4
5
6
7
8
9
10
11
12
为了解决这个问题,我们应该使用一个拷贝循环来检查 user[key]
的每个值,如果它是一个对象,那么也复制它的结构。这就是所谓的深拷贝。
const 声明的对象是可以被修改的
通过引用对对象进行存储的一个重要的副作用是声明为 const
的对象 可以 被修改。
例如:
const user = {
name: "John"
};
*!*
user.name = "Pete"; // (*)
*/!*
alert(user.name); // Pete
2
3
4
5
6
7
看起来 (*)
行的代码会触发一个错误,但实际并没有。user
的值是一个常量,它必须始终引用同一个对象,但该对象的属性可以被自由修改
# 4.3 垃圾回收
对于开发者来说,JavaScript 的内存管理是自动的、无形的
# 4.3.1 可达性(Reachability)
JavaScript 中主要的内存管理概念是 可达性 “可达”值是那些以某种方式可访问或可用的值。它们一定是存储在内存中的
这里列出固有的可达值的基本集合,这些值明显不能被释放。
比方说:
- 当前执行的函数,它的局部变量和参数。
- 当前嵌套调用链上的其他函数、它们的局部变量和参数。
- 全局变量。
- (还有一些内部的)
这些值被称作 根(roots)。
如果一个值可以通过引用链从根访问任何其他值,则认为该值是可达的
# 4.3.2 简单的例子
// user 具有对这个对象的引用
let user = {
name: "John"
};
user = null;
2
3
4
5
全局变量 "user"
引用了对象 {name:"John"}
(为简洁起见,我们称它为 John)。John 的 "name"
属性存储一个原始值,所以它被写在对象内部。
如果 user
的值被重写了,这个引用就没了。John 变成不可达的了。因为没有引用了,就不能访问到它了。垃圾回收器会认为它是垃圾数据并进行回收,然后释放内存。
# 4.3.3 两个引用
我们把 user
的引用复制给 admin
:
// user 具有对这个对象的引用
let user = {
name: "John"
};
let admin = user;
user = null;
2
3
4
5
6
对象仍然可以被通过 admin
这个全局变量访问到,所以对象还在内存中。如果我们又重写了 admin
,对象就会被删除
# 4.3.4 相互关联的对象
function marry(man, woman) {
woman.husband = man;
man.wife = woman;
return {
father: man,
mother: woman
}
}
let family = marry({
name: "John"
}, {
name: "Ann"
});
2
3
4
5
6
7
8
9
10
11
12
13
marry
函数通过让两个对象相互引用使它们“结婚”了,并返回了一个包含这两个对象的新对象。
由此产生的内存结构
现在让我们移除两个引用:
delete family.father;
delete family.mother.husband;
2
对外引用不重要,只有传入引用才可以使对象可达。所以,John 现在是不可达的,并且将被从内存中删除,同时 John 的所有数据也将变得不可达
# 4.3.5 无法到达的岛屿
几个对象相互引用,但外部没有对其任意对象的引用,这些对象也可能是不可达的,并被从内存中删除
family = null;
# 4.3.6 垃圾回收内部算法
垃圾回收的基本算法被称为 "mark-and-sweep"。
定期执行以下“垃圾回收”步骤:
- 垃圾收集器找到所有的根,并“标记”(记住)它们。
- 然后它遍历并“标记”来自它们的所有引用。
- 然后它遍历标记的对象并标记 它们的 引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。
- ……如此操作,直到所有可达的(从根部)引用都被访问到。
- 没有被标记的对象都会被删除。
这是垃圾收集工作的概念。JavaScript 引擎做了许多优化,使垃圾回收运行速度更快,并且不影响正常代码运行。
一些优化建议:
- 分代收集(Generational collection)—— 对象被分成两组:“新的”和“旧的”。许多对象出现,完成它们的工作并很快死去,它们可以很快被清理。那些长期存活的对象会变得“老旧”,而且被检查的频次也会减少。
- 增量收集(Incremental collection)—— 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟。所以引擎试图将垃圾收集工作分成几部分来做。然后将这几部分会逐一进行处理。这需要它们之间有额外的标记来追踪变化,但是这样会有许多微小的延迟而不是一个大的延迟。
- 闲时收集(Idle-time collection)—— 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。
# 4.4 对象方法,this
在 JavaScript 中,行为(action)由属性中的函数来表示
# 4.4.1 对象方法示例
作为对象属性的函数被称为 方法
let user = {
name: "John",
age: 30
};
user.sayHi = function() {
alert("Hello!");
};
user.sayHi(); // Hello!
2
3
4
5
6
7
8
使用函数表达式创建了一个函数,并将其指定给对象的 user.sayHi
属性
也可以使用预先声明的函数作为方法
let user = {
// ...
};
// 首先,声明函数
function sayHi() {
alert("Hello!");
}
// 然后将其作为一个方法添加
user.sayHi = sayHi;
user.sayHi(); // Hello!
2
3
4
5
6
7
8
9
10
# 方法简写
在对象字面量中,有一种更短的(声明)方法的语法
user = {
sayHi: function() {
alert("Hello");
}
};
// 方法简写看起来更好,对吧?
let user = {
sayHi() { // 与 "sayHi: function(){...}" 一样
alert("Hello");
}
};
2
3
4
5
6
7
8
9
10
11
# 4.4.2 方法中的this
为了访问该对象,方法中可以使用 this
关键字。 this
的值就是在点之前的这个对象,即调用该方法的对象
let user = {
name: "John",
age: 30,
sayHi() {
// "this" 指的是“当前的对象”
alert(this.name);
}
};
user.sayHi(); // John
2
3
4
5
6
7
8
9
技术上讲,也可以在不使用 this
的情况下,通过外部变量名来引用它。但这样的代码是不可靠的
let user = {
name: "John",
age: 30,
sayHi() {
alert( user.name ); // 导致错误
}
};
let admin = user;
user = null; // 重写让其更明显
admin.sayHi(); // TypeError: Cannot read property 'name' of null
2
3
4
5
6
7
8
9
10
# 4.4.3 "this" 不受限制
JavaScript 中的 this
可以用于任何函数,即使它不是对象的方法
this
的值是在代码运行时计算出来的,它取决于代码上下文。
例如,这里相同的函数被分配给两个不同的对象,在调用中有着不同的 "this" 值:
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(使用点符号或方括号语法来访问这个方法,都没有关系。)
2
3
4
5
6
7
8
9
10
11
12
13
如果 obj.f()
被调用了,则 this
在 f
函数调用期间是 obj
。所以在上面的例子中 this 先是 user
,之后是 admin
在没有对象的情况下调用:`this == undefined`
function sayHi() {
alert(this);
}
sayHi(); // undefined
2
3
4
在这种情况下,严格模式下的 this
值为 undefined
。如果我们尝试访问 this.name
,将会报错。
在非严格模式的情况下,this
将会是 全局对象(浏览器中的 window
)。这是一个历史行为,"use strict"
已经将其修复了
# 4.4.4 箭头函数没有自己的 "this"
箭头函数有些特别:它们没有自己的 this
。如果我们在这样的函数中引用 this
,this
值取决于外部“正常的”函数。
举个例子,这里的 arrow()
使用的 this
来自于外部的 user.sayHi()
方法:
let user = {
firstName: "Ilya",
sayHi() {
let arrow = () => alert(this.firstName);
arrow();
}
};
user.sayHi(); // Ilya
2
3
4
5
6
7
8
这是箭头函数的一个特性,当我们并不想要一个独立的 this
,反而想从外部上下文中获取时,它很有用
# 4.5 构造器与new
# 4.5.1 构造函数
构造函数在技术上是常规函数。不过有两个约定:
- 它们的命名以大写字母开头。
- 它们只能由
"new"
操作符来执行
function User(name) {
this.name = name;
this.isAdmin = false;
}
let user = new User("Jack");
alert(user.name); // Jack
alert(user.isAdmin); // false
2
3
4
5
6
7
当一个函数被使用 new
操作符执行时,它按照以下步骤:
- 一个新的空对象被创建并分配给
this
。 - 函数体执行。通常它会修改
this
,为其添加新的属性。 - 返回
this
的值
function User(name) {
// this = {};(隐式创建)
// 添加属性到 this
this.name = name;
this.isAdmin = false;
// return this;(隐式返回)
}
2
3
4
5
6
7
# 4.5.2 构造器模式测试:new.target
在一个函数内部,我们可以使用 new.target
属性来检查它是否被使用 new
进行调用了。
对于常规调用,它为 undefined,对于使用 new
的调用,则等于该函数:
function User() {
alert(new.target);
}
// 不带 "new":
User(); // undefined
// 带 "new":
new User(); // function User { ... }
2
3
4
5
6
7
我们也可以让 new
调用和常规调用做相同的工作,像这样:
function User(name) {
if (!new.target) { // 如果你没有通过 new 运行我
return new User(name); // ……我会给你添加 new
}
this.name = name;
}
let john = User("John"); // 将调用重定向到新用户
alert(john.name); // John
2
3
4
5
6
7
8
# 4.5.3 构造器的 return
通常,构造器没有 return
语句。它们的任务是将所有必要的东西写入 this
,并自动转换为结果。
但是,如果这有一个 return
语句,那么规则就简单了:
- 如果
return
返回的是一个对象,则返回这个对象,而不是this
。 - 如果
return
返回的是一个原始类型,则忽略
function BigUser() {
this.name = "John";
return { name: "Godzilla" }; // <-- 返回这个对象
}
alert( new BigUser().name ); // Godzilla,得到了那个对象
function SmallUser() {
this.name = "John";
return; // <-- 返回 this
}
alert( new SmallUser().name ); // John
2
3
4
5
6
7
8
9
10
11
省略括号
如果没有参数,我们可以省略 new
后的括号:
let user = new User; // <-- 没有参数
// 等同于
let user = new User();
2
3
# 4.5.4 构造器中的方法
我们不仅可以将属性添加到 this
中,还可以添加方法
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() { ... }
}
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
# 4.6 可选链 ?.
可选链 ?.
是一种访问嵌套对象属性的安全的方式。即使中间的属性不存在,也不会出现错误
# 4.6.1 可选链语法
如果可选链 ?.
前面的值为 undefined
或者 null
,它会停止运算并返回 undefined
例如 value?.prop
:
- 如果
value
存在,则结果与value.prop
相同, - 否则(当
value
为undefined/null
时)则返回undefined
请注意:?.
语法使其前面的值成为可选值,但不会对其后面的起作用
`?.` 前的变量必须已声明
如果未声明变量 user
,那么 user?.anything
会触发一个错误:
// ReferenceError: user is not defined
user?.address;
2
?.
前的变量必须已声明(例如 let/const/var user
或作为一个函数参数)。可选链仅适用于已声明的变量
# 4.6.2 短路效应
如果 ?.
左边部分不存在,就会立即停止运算(“短路效应”)。
因此,如果在 ?.
的右侧有任何进一步的函数调用或操作,它们均不会执行。
let user = null;
let x = 0;
user?.sayHi(x++); // 没有 "user",因此代码执行没有到达 sayHi 调用和 x++
alert(x); // 0,值没有增加
2
3
4
# 4.6.3 其它变体:?.(),?.[]
可选链 ?.
不是一个运算符,而是一个特殊的语法结构。它还可以与函数和方括号一起使用
let userAdmin = {
admin() {
alert("I am admin");
}
};
let userGuest = {};
userAdmin.admin?.(); // I am admin
userGuest.admin?.(); // 啥都没发生(没有这样的方法)
2
3
4
5
6
7
8
此外,我们还可以将 ?.
跟 delete
一起使用:
delete user?.name; // 如果 user 存在,则删除 user.name
我们可以使用 `?.` 来安全地读取或删除,但不能写入
可选链 ?.
不能用在赋值语句的左侧。
let user = null;
user?.name = "John"; // Error,不起作用
// 因为它在计算的是:undefined = "John"
2
3
# 4.7 symbol 类型
根据规范,只有两种原始类型可以用作对象属性键:
- 字符串类型
- symbol 类型
# 4.7.1 symbol
"symbol" 值表示唯一的标识符。
可以使用 Symbol()
来创建这种类型的值:
let id = Symbol();
创建时,我们可以给 symbol 一个描述(也称为 symbol 名),这在代码调试时非常有用:
// id 是描述为 "id" 的 symbol
let id = Symbol("id");
2
symbol 保证是唯一的。即使我们创建了许多具有相同描述的 symbol,它们的值也是不同。描述只是一个标签,不影响任何东西 两个描述相同的 symbol —— 它们不相等:
let id1 = Symbol("id");
let id2 = Symbol("id");
alert(id1 == id2); // false
2
3
symbol 不会被自动转换为字符串
JavaScript 中的大多数值都支持字符串的隐式转换
let id = Symbol("id");
alert(id); // 类型错误:无法将 symbol 值转换为字符串。
2
这是一种防止混乱的“语言保护”,因为字符串和 symbol 有本质上的不同,不应该意外地将它们转换成另一个
如果我们真的想显示一个 symbol,我们需要在它上面调用 .toString()
,如下所示:
let id = Symbol("id");
alert(id.toString()); // Symbol(id),现在它有效了
2
或者获取 symbol.description
属性,只显示描述(description):
let id = Symbol("id");
alert(id.description); // id
2
# 4.7.2 “隐藏”属性
symbol 允许我们创建对象的“隐藏”属性,代码的任何其他部分都不能意外访问或重写这些属性
let user = { // 属于另一个代码
name: "John"
};
let id = Symbol("id");
user[id] = 1;
alert( user[id] ); // 我们可以使用 symbol 作为键来访问数据
2
3
4
5
6
# 对象字面量中的 symbol
如果我们要在对象字面量 {...}
中使用 symbol,则需要使用方括号把它括起来。
就像这样:
let id = Symbol("id");
let user = {
name: "John",
[id]: 123 // 而不是 "id":123
};
2
3
4
5
这是因为我们需要变量 id
的值作为键,而不是字符串 "id"
# symbol 在 for..in 中会被跳过
symbol 属性不参与 for..in
循环
let id = Symbol("id");
let user = {
name: "John",
age: 30,
[id]: 123
};
for (let key in user) alert(key); // name, age(没有 symbol)
// 使用 symbol 任务直接访问
alert( "Direct: " + user[id] );
2
3
4
5
6
7
8
9
Object.keys(user) (opens new window) 也会忽略它们。这是一般“隐藏符号属性”原则的一部分 相反,Object.assign 会同时复制字符串和 symbol 属性
# 4.7.3 全局 symbol
有一个 全局 symbol 注册表。我们可以在其中创建 symbol 并在稍后访问它们,它可以确保每次访问相同名字的 symbol 时,返回的都是相同的 symbol
要从注册表中读取(不存在则创建)symbol,请使用 Symbol.for(key)
。
该调用会检查全局注册表,如果有一个描述为 key
的 symbol,则返回该 symbol,否则将创建一个新 symbol(Symbol(key)
),并通过给定的 key
将其存储在注册表中
// 从全局注册表中读取
let id = Symbol.for("id"); // 如果该 symbol 不存在,则创建它
// 再次读取(可能是在代码中的另一个位置)
let idAgain = Symbol.for("id");
// 相同的 symbol
alert( id === idAgain ); // true
2
3
4
5
6
# Symbol.keyFor
对于全局 symbol,不仅有 Symbol.for(key)
按名字返回一个 symbol,还有一个反向调用:Symbol.keyFor(sym)
,它的作用完全反过来:通过全局 symbol 返回一个名字
// 通过 name 获取 symbol
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");
// 通过 symbol 获取 name
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id
2
3
4
5
6
Symbol.keyFor
内部使用全局 symbol 注册表来查找 symbol 的键。所以它不适用于非全局 symbol。如果 symbol 不是全局的,它将无法找到它并返回 undefined
# 4.7.4 系统 symbol
JavaScript 内部有很多“系统” symbol,我们可以使用它们来微调对象的各个方面 众所周知的 symbol (opens new window) 表的规范中:
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.toPrimitive
# 4.8 对象 — 原始值转换
当对象相加 obj1 + obj2
,相减 obj1 - obj2
,或者使用 alert(obj)
打印时会发生什么?
在此类运算的情况下,对象会被自动转换为原始值,然后对这些原始值进行运算,并得到运算结果(也是一个原始值)
# 4.8.1 转换规则
- 没有转换为布尔值。所有的对象在布尔上下文(context)中均为
true
,就这么简单。只有字符串和数字转换。 - 数字转换发生在对象相减或应用数学函数时。例如,
Date
对象(将在 info:date 一章中介绍)可以相减,date1 - date2
的结果是两个日期之间的差值。 - 至于字符串转换 —— 通常发生在我们像
alert(obj)
这样输出一个对象和类似的上下文中。
# 4.8.2 hint
类型转换在各种情况下有三种变体。它们被称为 "hint"
"string"
: 对象到字符串的转换,当我们对期望一个字符串的对象执行操作时,如 "alert":
```js
// 输出
alert(obj);
// 将对象作为属性键
anotherObj[obj] = 123;
```
"number"
: 对象到数字的转换,例如当我们进行数学运算时:
```js
// 显式转换
let num = Number(obj);
// 数学运算(除了二元加法)
let n = +obj; // 一元加法
let delta = date1 - date2;
// 小于/大于的比较
let greater = user1 > user2;
```
大多数内建的数学函数也包括这种转换。
"default"
: 在少数情况下发生,当运算符“不确定”期望值的类型时。
例如,二元加法 `+` 可用于字符串(连接),也可以用于数字(相加)。因此,当二元加法得到对象类型的参数时,它将依据 `"default"` hint 来对其进行转换。
此外,如果对象被用于与字符串、数字或 symbol 进行 `==` 比较,这时到底应该进行哪种转换也不是很明确,因此使用 `"default"` hint。
```js
// 二元加法使用默认 hint
let total = obj1 + obj2;
// obj == number 使用默认 hint
if (user == 1) { ... };
```
像 `<` 和 `>` 这样的小于/大于比较运算符,也可以同时用于字符串和数字。不过,它们使用 "number" hint,而不是 "default"。这是历史原因。
为了进行转换,JavaScript 尝试查找并调用三个对象方法:
- 调用
obj[Symbol.toPrimitive](hint)
—— 带有 symbol 键Symbol.toPrimitive
(系统 symbol)的方法,如果这个方法存在的话, - 否则,如果 hint 是
"string"
—— 尝试调用obj.toString()
或obj.valueOf()
,无论哪个存在。 - 否则,如果 hint 是
"number"
或"default"
—— 尝试调用obj.valueOf()
或obj.toString()
,无论哪个存在。
# 4.8.3 Symbol.toPrimitive
如果 Symbol.toPrimitive
方法存在,则它会被用于所有 hint,无需更多其他方法。
例如,这里 user
对象实现了它:
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
2
3
4
5
6
7
8
9
10
11
12
# 4.8.4 toString/valueOf
如果没有 Symbol.toPrimitive
,那么 JavaScript 将尝试寻找 toString
和 valueOf
方法:
- 对于
"string"
hint:调用toString
方法,如果它不存在,则调用valueOf
方法(因此,对于字符串转换,优先调用toString
)。 - 对于其他 hint:调用
valueOf
方法,如果它不存在,则调用toString
方法(因此,对于数学运算,优先调用valueOf
方法)。
默认情况下,普通对象具有 toString
和 valueOf
方法:
toString
方法返回一个字符串"[object Object]"
。valueOf
方法返回对象自身。
# 转换可以返回任何原始类型
关于所有原始转换方法,有一个重要的点需要知道,就是它们不一定会返回 "hint" 的原始值。
没有限制 toString()
是否返回字符串,或 Symbol.toPrimitive
方法是否为 "number"
hint 返回数字。
唯一强制性的事情是:这些方法必须返回一个原始值,而不是对象