ES6之符号与符号属性
引
在 JS 已有的基本类型(字符串、数值、布尔类型、 null 与 undefined )之外, ES6 引入了一种新的基本类型:符号(Symbol)。符号起初被设计用于创建对象私有成员,而这也是 JS 开发者期待已久的特性。在符号诞生之前,将字符串作为属性名称导致属性可以被轻易访问,无论命名规则如何。而“私有名称”意味着开发者可以创建非字符串类型的属性名称,由此可以防止使用常规手段来探查这些名称。
“私有名称”提案最终发展成为 ES6中的符号,而本章将会教你如何有效使用它。虽然它只保
留了实现细节(即:引入了非字符串类型的属性名)而丢弃了私有性意图,但它仍然显著有别于对象的其余属性。
创建一个符号
符号没有字面量形式,这在 JS 的基本类型中是独一无二的,有别于布尔类型的 true 或数
值类型的 42 等等。你可以使用全局 Symbol 函数来创建一个符号值,正如下面这个例子:
let firstName = Symbol();
let person = {};
person[firstName] = "Nicholas";
console.log(person[firstName]); // "Nicholas"
此代码创建了一个符号类型的 firstName 变量,并将它作为 person 对象的一个属性,而每
次访问该属性都要使用这个符号值。为符号变量适当命名是个好主意,这样你就可以很容易
地说明它的含义。
由于符号值是基本类型的值,因此调用 new Symbol() 将会抛出错误。你可以通过 new
Object(yourSymbol) 来创建一个符号实例,但尚不清楚这能有什么作用。
Symbol 函数还可以接受一个额外的参数用于描述符号值,该描述并不能用来访问对应属性,
但它能用于调试,例如:
let firstName = Symbol("first name");
let person = {};
person[firstName] = "Nicholas";
console.log("first name" in person); // false
console.log(person[firstName]); // "Nicholas"
console.log(firstName); // "Symbol(first name)"
Symbol是独一无二的,"first name"只是对Symbol的描述
符号的描述信息被存储在内部属性 [[Description]] 中,当符号的 toString() 方法被显式
或隐式调用时,该属性都会被读取。在本例中, console.log() 隐式调用了 firstName 变量
的 toString() 方法,于是描述信息就被输出到日志。此外没有任何办法可以从代码中直接
访问 [[Description]] 属性。我建议始终应给符号提供描述信息,以便更好地阅读代码或进
行调试。
由于符号是基本类型的值,因此你可以使用 typeof 运算符来判断一个变量是否为符
号。 ES6 扩充了 typeof 的功能以便让它在作用于符号值的时候能够返回 "symbol" ,例如
let symbol = Symbol("test symbol");
console.log(typeof symbol); // "symbol"
使用符号值
你可以在任意能使用“需计算属性名”的场合使用,还能在对象的“需计算字面量属性名”中使用符号,此外还可以在合使用符号。Object.defineProperty() 或 Object.defineProperties() 调用中使用它
共享符号值
你或许想在不同的代码段中使用相同的符号值,例如:假设在应用中需要在两个不同的对象
类型中使用同一个符号属性,用来表示一个唯一标识符。跨越文件或代码来追踪符号值是很
困难并且易错的,为此, ES6 提供了“全局符号注册表”供你在任意时间点进行访问。
若你想创建共享符号值,应使用 Symbol.for() 方法而不是 Symbol() 方法。 Symbol.for()
方法仅接受单个字符串类型的参数,作为目标符号值的标识符,同时此参数也会成为该符号
的描述信息。例如:
let uid = Symbol.for("uid");
let object = {};
object[uid] = "12345";
console.log(object[uid]); // "12345"
console.log(uid); // "Symbol(uid)"
Symbol.for() 方法首先会搜索全局符号注册表,看是否存在一个键值为 "uid" 的符号值。
若是,该方法会返回这个已存在的符号值;否则,会创建一个新的符号值,并使用该键值将
其记录到全局符号注册表中,然后返回这个新的符号值。这就意味着此后使用同一个键值去
调用 Symbol.for() 方法都将会返回同一个符号值,就像下面这个例子:
let uid = Symbol.for("uid");
let object = {
[uid]: "12345"
};
console.log(object[uid]); // "12345"
console.log(uid); // "Symbol(uid)"
let uid2 = Symbol.for("uid");
console.log(uid === uid2); // true
console.log(object[uid2]); // "12345"
console.log(uid2); // "Symbol(uid)"
本例中, uid 与 uid2 包含同一个符号值,因此它们可以互换使用。第一次调用
Symbol.for() 创建了这个符号值,而第二次调用则从全局符号注册表中将其检索了出来。
共享符号值还有另一个独特用法,你可以使用 Symbol.keyFor() 方法在全局符号注册表中根
据符号值检索出对应的键值,例如:
let uid = Symbol.for("uid");
console.log(Symbol.keyFor(uid)); // "uid"
let uid2 = Symbol.for("uid");
console.log(Symbol.keyFor(uid2)); // "uid"
let uid3 = Symbol("uid");
console.log(Symbol.keyFor(uid3)); // undefined
符号值的转换
类型转换是 JS 语言重要的一部分,能够非常灵活地将一种数据类型转换为另一种。然而符号
类型在进行转换时非常不灵活,因为其他类型缺乏与符号值的合理等价,尤其是符号值无法
被转换为字符串值或数值。因此将符号作为属性所达成的效果,是其他类型所无法替代的。
let uid = Symbol.for("uid"),
desc = String(uid);
console.log(desc);
String() 方法调用了 uid.toString() 来获取符号的字符串描述信息。但若你想直接将符号
转换为字符串,则会引发错误:
let uid = Symbol.for("uid"),
desc = uid + ""; // 引发错
检索符号属性
Object.keys() 与 Object.getOwnPropertyNames() 方法可以检索对象的所有属性名称,前者
返回所有的可枚举属性名称,而后者则返回所有属性名称而无视其是否可枚举。然而两者都
不能返回符号类型的属性,以保持它们在 ES5 中的功能不发生变化。而 ES6 新增了
Object.getOwnPropertySymbols() 方法,以便让你可以检索对象的符号类型属性。
Object.getOwnPropertySymbols() 方法会返回一个数组,包含了对象自有属性名中的符号值,
例如:
let uid = Symbol.for("uid");
let object = {
[uid]: "12345"
};
let symbols = Object.getOwnPropertySymbols(object);
console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(uid)"
console.log(object[symbols[0]]); // "12345"
使用知名符号暴露内部方法
ES6 定义了“知名符号”来代表 JS 中一些公共行为,而这些行为此前被认为只能是内部操作。
每一个知名符号都对应全局 Symbol 对象的一个属性,例如 Symbol.create 。
这些知名符号是:
- Symbol.hasInstance :供 instanceof 运算符使用的一个方法,用于判断对象继承关
系。
Symbol.hasInstance 方法只接受单个参数,即需要检测的值。如果该值是本函数的一个实
- Symbol.isConcatSpreadable:一个布尔类型值,在集合对象作为参数传递给Array.prototype.concat() 方法时,指示是否要将该集合的元素扁平化。
- Symbol.iterator :返回迭代器的一个方法。
- Symbol.match :供 String.prototype.match() 函数使用的一个方法,用于比较字符串。
- Symbol.replace :供 String.prototype.replace()函数使用的一个方法,用于替换子字符串。
- Symbol.search :供 String.prototype.search() 函数使用的一个方法,用于定位子字符串。
- Symbol.species :用于产生派生对象的构造器。
- Symbol.split :供 String.prototype.split() 函数使用的一个方法,用于分割字符串。
- Symbol.toPrimitive :返回对象所对应的基本类型值的一个方法。
- Symbol.toStringTag :供String.prototype.toString()函数使用的一个方法,用于创建对象的描述信息。
- Symbol.unscopables :一个对象,该对象的属性指示了哪些属性名不允许被包含在
with 语句中。
示例:
Symbol.hasInstance 属性
每个函数都具有一个 Symbol.hasInstance 方法,用于判断指定对象是否为本函数的一个实
例。这个方法定义在 Function.prototype 上,因此所有函数都继承了面对 instanceof 运算
符时的默认行为。 Symbol.hasInstance 属性自身是不可写入、不可配置、不可枚举的,从而
保证它不会被错误地重写。
Symbol.hasInstance 方法只接受单个参数,即需要检测的值。如果该值是本函数的一个实
例,则方法会返回 true 。为了理解该方法是如何工作的,可研究下述代码:
obj instanceof Array;
这句代码等价于:
Array[Symbol.hasInstance](obj);
Symbol.isConcatSpreadable
JS 在数组上设计了 concat() 方法用于将两个数组连接到一起,此处示范了如何使用该方
法:
let colors1 = [ "red", "green" ],
colors2 = colors1.concat([ "blue", "black" ]);
console.log(colors2.length); // 4
console.log(colors2); // ["red","green","blue","black"]
Symbol.isConcatSpreadable 属性是一个布尔类型的属性,它表示目标对象拥有长度属性与数
值类型的键、并且数值类型键所对应的属性值在参与 concat() 调用时需要被分离为个体。
该符号与其他的知名符号不同,默认情况下并不会作为任意常规对象的属性。它只出现在特
定类型的对象上,用来标示该对象在作为 concat() 参数时应如何工作,从而有效改变该对
象的默认行为。你可以用它来定义任意类型的对象,让该对象在参与 concat() 调用时能够
表现得像数组一样,例如:
let collection = {
0: "Hello",
1: "world",
length: 2,
[Symbol.isConcatSpreadable]: true
};
let messages = [ "Hi" ].concat(collection);
console.log(messages.length); // 3
console.log(messages); // ["hi","Hello","world"]
Symbol.match 、 Symbol.replace 、 Symbol.search 与Symbol.split
在 JS 中,字符串与正则表达式有着密切的联系,尤其是字符串具有几个可以接受正则表达式
作为参数的方法:
- match(regex) :判断指定字符串是否与一个正则表达式相匹配;
- replace(regex, replacement) :对正则表达式的匹配结果进行替换;
- search(regex) :在字符串内对正则表达式的匹配结果进行定位;
- split(regex) :使用正则表达式将字符串分割为数组。
这些与正则表达式交互的方法,在 ES6 之前其实现细节是对开发者隐藏的,使得开发者无法
将自定义对象模拟成正则表达式(并将它们传递给字符串的这些方法)。而 ES6 定义了 4 个
符号以及对应的方法,将原生行为外包到内置的 RegExp 对象上。
这 4 个符号表示可以将正则表达式作为字符串对应方法的第一个参数传入, Symbol.match
对应 match() 方法, Symbol.replace 对应 replace() , Symbol.search 对应 search()
, Symbol.split 则对应 split() 。这些符号属性被定义在 RegExp.prototype 上作为默认
实现,以供对应的字符串方法使用。
- Symbol.match :此函数接受一个字符串参数,并返回一个包含匹配结果的数组;若匹配
失败,则返回 null 。 - Symbol.replace :此函数接受一个字符串参数与一个替换用的字符串,并返回替换后的
结果字符串。 - Symbol.search :此函数接受一个字符串参数,并返回匹配结果的数值索引;若匹配失
败,则返回 -1。 - Symbol.split :此函数接受一个字符串参数,并返回一个用匹配值分割而成的字符串数
组。
在对象上定义这些属性,允许你创建能够进行模式匹配的对象,而无需使用正则表达式,并
且允许在任何需要正则表达式的方法中使用该对象。这里有一个例子,展示了这些符号的用
法:
// 有效等价于 /^.{10}$/
let hasLengthOf10 = {
[Symbol.match]: function(value) {
return value.length === 10 ? [value.substring(0, 10)] : null;
},
[Symbol.replace]: function(value, replacement) {
return value.length === 10 ?
replacement + value.substring(10) : value;
},
[Symbol.search]: function(value) {
return value.length === 10 ? 0 : -1;
},
[Symbol.split]: function(value) {
return value.length === 10 ? ["", ""] : [value];
}
};
let message1 = "Hello world", // 11 characters
message2 = "Hello John"; // 10 characters
let match1 = message1.match(hasLengthOf10),
match2 = message2.match(hasLengthOf10);
console.log(match1); // null
console.log(match2); // ["Hello John"]
let replace1 = message1.replace(hasLengthOf10, "Howdy!"),
replace2 = message2.replace(hasLengthOf10, "Howdy!");
console.log(replace1); // "Hello world"
console.log(replace2); // "Howdy!"
let search1 = message1.search(hasLengthOf10),
search2 = message2.search(hasLengthOf10);
console.log(search1); // -1
console.log(search2); // 0
let split1 = message1.split(hasLengthOf10),
split2 = message2.split(hasLengthOf10);
console.log(split1); // ["Hello world"]
console.log(split2); // ["", ""]
Symbol.toPrimitive
对于大部分常规对象,“数值模式”依次会有下述行为:
- 调用 valueOf() 方法,如果方法返回值是一个基本类型值,那么返回它;
- 否则,调用 toString() 方法,如果方法返回值是一个基本类型值,那么返回它;
- 否则,抛出一个错误。
类似的,对于大部分常规对象,“字符串模式”依次会有下述行为: - 调用 toString() 方法,如果方法返回值是一个基本类型值,那么返回它;
- 否则,调用 valueOf() 方法,如果方法返回值是一个基本类型值,那么返回它;
- 否则,抛出一个错误。
在多数情况下,常规对象的默认模式都等价于数值模式(只有 Date 类型例外,它默认使用
字符串模式)。通过定义 Symbol.toPrimitive 方法,你可以重写这些默认的转换行为。
使用 Symbol.toPrimitive 属性并将一个函数赋值给它,便可以重写默认的转换行为,例如:
function Temperature(degrees) {
this.degrees = degrees;
}
Temperature.prototype[Symbol.toPrimitive] = function(hint) {
switch (hint) {
case "string":
return this.degrees + "\u00b0"; // 温度符号
case "number":
return this.degrees;
case "default":
return this.degrees + " degrees";
}
};
let freezing = new Temperature(32);
console.log(freezing + "!"); // "32 degrees!"
console.log(freezing / 2); // 16
console.log(String(freezing));
Symbol.toStringTag
ES6 通过 Symbol.toStringTag 重定义了相关行为,该符号代表了所有对象的一个属性,定义
了 Object.prototype.toString.call() 被调用时应当返回什么值。对于数组来说,在
Symbol.toStringTag 属性中存储了 "Array" 值,于是该函数的返回值也就是 "Array" 。同样,你可以在自设对象上定义 Symbol.toStringTag 的值:
function Person(name) {
this.name = name;
}
Person.prototype[Symbol.toStringTag] = "Person";
let me = new Person("Nicholas");
console.log(me.toString()); // "[object Person]"
console.log(Object.prototype.toString.call(me)); // "[object Person]"
Symbol.unscopables
Symbol.unscopables 符号在 Array.prototype 上使用,以指定哪些属性不允许在 with 语句
内被绑定。 Symbol.unscopables 属性是一个对象,当提供该属性时,它的键就是用于忽略
with 语句绑定的标识符,键值为 true 代表屏蔽绑定。以下是数组的 Symbol.unscopables
属性的默认值:
// 默认内置在 ES6 中
Array.prototype[Symbol.unscopables] = Object.assign(Object.create(null), {
copyWithin: true,
entries: true,
fill: true,
find: true,
findIndex: true,
keys: true,
values: true
});
总结
符号是 JS 新引入的基本类型值,它用于创建不可枚举的属性,并且这些属性在不引用符号的
情况下是无法访问的。
虽然符号类型的属性不是真正的私有属性,但它们难以被无意修改,因此在需要提供保护以
防止开发者改动的场合中,它们非常合适。
你可以为符号提供描述信息以便更容易地辨识它们的值。全局符号注册表允许你使用相同的
描述信息,以便在不同的代码段中共享符号值,这样相同的符号值就可以在不同位置用于相
同目的。
Object.keys() 或 Object.getOwnPropertyNames() 不会返回符号值,因此 ES6 新增了一个
Object.getOwnPropertySymbols() 方法,允许检索符号类型的对象属性。而你依然可以使用
Object.defineProperty() 与 Object.defineProperties() 方法对符号类型的属性进行修改。
“知名符号”使用了全局符号常量(例如 Symbol.hasInstance ),为常规对象定义了一些功
能,而这些功能原先仅限内部使用。这些符号按规范使用 Symbol. 的前缀,允许开发者通过
多种方式去修改常规对象的行为。
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。