深入理解 ES6 #6-Symbol 和 Symbol 属性
🏷️ 《深入理解 ES6》
在 ES5 及早期版本中,JS 语言包含 5 中原始类型:
- 字符串型
- 数字型
- 布尔型
null
undefined
ES6 引入了第六种原始类型:
Symbol
创建 Symbol
let firstName = Symbol();
let person = {};
person[firstName] = "JiaJia";
console.log(person[firstName]); // "JiaJia"
Symbol 的辨识方法
使用 typeof
来检测辨识是否为 Symbol
。
let symbol = Symbol("test symbol");
console.log(typeof symbol); // "symbol"
Symbol 的使用方法
所有使用可计算属性名的地方,都可以使用 Symbol
。
let firstName = Symbol("first name");
let person = {
[firstName]: "JiaJia"
};
// 将属性设置为只读
Object.defineProperty(person, firstName, { writable: false });
let lastName = Symbol("last name");
Object.defineProperties(person, {
[lastName]: {
value: "Liu",
writable: false
}
});
console.log(person[firstName]); // "JiaJia"
console.log(person[lastName]); // "Liu"
Symbol 共享体系
ES6 提供了一个可以随时访问的全局 Symbol
注册表。
使用 Symbol.for()
方法创建可共享的 Symbol
,它只接受一个参数,也就是即将创建的 Symbol
的字符串标识符,这个参数同样也被用作 Symbol
的描述。
let uid = Symbol.for("uid");
let object = {};
object[uid] = "12345";
console.log(object[uid]); // "12345"
console.log(uid); // "Symbol(uid)"
Symbol.for()
方法首先在全局 Symbol
注册表中搜索键为“uid”的 Symbol
是否存在,如果存在,直接返回已有的 Symbol
;否则,创建一个新的 Symbol
,并使用这个键在 Symbol
全局注册表中注册,随即返回新创建的 Symbol
。
随后如果再传入同样的键调用 Symbol.for()
方法会返回相同的 Symbol
。
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)"
可以使用 Symbol.keyFor()
方法在 Symbol
全局注册表中检索与 Symbol
有关的键。
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
Symbol 与类型强制转换
其它类型没有与 Symbol
逻辑等价的值。
可以使用 Symbol
的 toString()
方法返回 Symbol
描述里的内容,但是直接与字符串拼接或者参与数值计算,则会抛出错误。
let uid = Symbol.for("uid"),
desc = String(uid);
console.log(desc); // "Symbol(uid)"
desc = uid + ""; // 报错
// Cannot convert a Symbol value to a string
let sum = uid / 1; // 报错
// Cannot convert a Symbol value to a number
Symbol 属性检索
Object.keys()
方法和 Object.getOwnPropertyNames()
方法可以检索对象中所有的属性名。
Object.keys()
方法返回所有可枚举的属性名;Object.getOwnPropertyNames()
方法不考虑属性的可枚举性一律返回。
为了保持 ES5 函数的原有功能,这两个方法都不支持 Symbol
属性。
ES6 中添加了一个 Object.getOwnPropertySymbols()
方法来检索 Symbol
属性。
该方法返回一个包含所有 Symbol
自有属性的数组。
let uid = Symbol("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"
通过 well-known Symbol 暴露内部操作
通过在原型链上定义与 Symbol
相关的属性来暴露更多的语言内部逻辑。
Symbol.hasInstance 方法
每个函数都要一个 Symbol.hasInstance
方法,用于确定对象是否为函数的实例。
该方法在 Function.prototype
中定义,所以所有函数都继承了 instanceof
属性的默认行为。
为了确保 Symbol.hasInstance
不会被意外重写,该方法被定义为不可写、不可配置并且不可枚举。
Symbol.hasInstance
方法只接受一个参数,即要检查的值。如果传入的值是函数的实例,则返回 true
。
obj instanceof Array;
上述代码等价于
Array[Symbol.hasInstance](obj);
本质上,ES6 只是将 instanceof
操作符重新定义为此方法的简写语法。
现在引入方法调用后,就可以随意改变 instanceof
的运行方式了。
function MyObject() {
}
let obj = new MyObject();
console.log(obj instanceof MyObject); // true
Object.defineProperty(MyObject, Symbol.hasInstance, {
value: function(v) {
return false;
}
});
console.log(obj instanceof MyObject); // false
只有通过
Object.defineProperty()
才能改写一个不可写属性。
Symbol.isConcatSpreadable 属性
JS 数组的 concat()
方法被设计用于拼接两个数组,但也可以接受非数组参数。
let colors1 = [ "red", "green" ],
colors2 = colors1.concat([ "blue", "black" ], "brown");
console.log(colors2.length); // 5
console.log(colors2); // ["red", "green", "blue", "black", "brown"]
JS 规范声明,凡是传入数组参数,就会自动将它们分解为独立元素。 ES6 之前无法调整这个特性。
Symbol.isConcatSpreadable
属性是一个布尔值,如果该属性值为 true
,则表示对象有 length
属性和数字键,故它的数值型属性值应该被独立添加到 concat()
调用的结果中。
这个 Symbol.isConcatSpreadable
属性默认情况下不会出现在标准对象中,它只是一个可选属性,用于增强作用于特定对象类型的 concat()
方法的功能,有效简化其默认特性。
下面方法自定义了一个在 concat()
调用中与数组类型的新类型:
let collection = {
0: "Hello",
1: "World",
length: 2,
[Symbol.isConcatSpreadable]: true
};
let message = [ "Hi" ].concat(collection);
console.log(message.length); // 3
console.log(message); // ["Hi", "Hello", "World"]
也可以将
Symbol.isConcatSpreadable
设置false
,来防止元素在调用concat()
方法时被分解。
let collection = {
0: "Hello",
1: "World",
length: 2,
[Symbol.isConcatSpreadable]: false
};
let message = [ "Hi" ].concat(collection);
console.log(message.length); // 2
console.log(message); // ["Hi", {0: "Hello", 1: "World", length: 2, Symbol(Symbol.isConcatSpreadable): false}]
Symbol.match、Symbol.replace、Symbol.search 和 Symbol.split 属性
字符串类型的几个方法可以接受正则表达式作为参数:
match(regex)
确定给定字符串是否匹配正则表达式 regexreplace(regex, replacement)
将字符串中匹配正则表达式 regex 的部分替换为 replacementsearch(regex)
在字符串中定位匹配正则表达式 regex 的位置索引split(regex)
按照匹配正则表达式 regex 的元素将字符串分切,并将结果存入数组中
在 ES6 中,可以使用对应的 4 个 Symbol
,自定义对象来替换正则表达式来进行匹配。
Symbol.match
接受一个字符串类型的参数,如果匹配成功则返回匹配元素的数组,否则返回null
Symbol.replace
接受一个字符串类型的参数和一个替换用的字符串,最终依然返回一个字符串Symbol.search
接受一个字符串参数,如果匹配到内容,则返回数字类型的索引位置,否则返回 -1Symbol.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;
},
[Symbol.search]: function(value) {
return value.length === 10 ? 0 : -1;
},
[Symbol.split]: function(value) {
return value.length === 10 ? ["", ""] : [value];
}
};
let message1 = "Hello world", // 11 个字符
message2 = "Hello Dlph"; // 10 个字符
let match1 = message1.match(hasLengthOf10),
match2 = message2.match(hasLengthOf10);
console.log(match1); // null
console.log(match2); // ["Hello Dlph"]
let replace1 = message1.replace(hasLengthOf10),
replace2 = message2.replace(hasLengthOf10);
console.log(replace1); // "Hello world"
console.log(replace2); // "Hello Dlph"
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 方法
在 JS 引擎中,当执行特定操作时,经常会尝试将对象转换到相应的原始值。
在 ES6 中,可以通过 Symbol.toPrimitive
方法更改这个原始值。
Symbol.toPrimitive
方法被定义在每一个标准类型的原型上,并且规定了当对象被转换为原始值时应当执行的操作。
该方法接受一个参数 类型提示(hint),该值只有三种选择:"number"、"string"和"default"。根据参数返回值分别为 数字、字符和无类型偏好的值。
数字模式:
- 调用
valueOf()
方法,如果结果为原始值,则返回; - 否则,调用
toString()
方法,如果结果为原始值,则返回; - 如果再无可选值,则抛出错误。
字符串模式:
- 调用
toString()
方法,如果结果为原始值,则返回; - 否则,调用
valueOf()
方法,如果结果为原始值,则返回; - 如果再无可选值,则抛出错误。
默认模式:
- 在大多数情况下,标准对象会将默认模式按数字模式处理(除了
Date
对象,在这种情况下,会将默认模式按照字符串模式处理)。
如果自定义了 Symbol.toPrimitive
方法,则可以覆盖这些默认的强制转换类型。
NOTE
默认模式只用于 ==
运算、+
运算及给 Date
构造函数传递一个参数时。
在大多数的操作中,使用的都是字符串模式或数字模式。
function Temperature(degrees) {
this.degrees = degrees;
}
Temperature.prototype[Symbol.toPrimitive] = function(hint) {
switch (hint) {
case "string":
return this.degrees + "\u00b0"; // degrees symbol
case "number":
return this.degrees;
case "default":
return this.degrees + " degrees";
}
};
var freezing = new Temperature(32);
console.log(freezing + "!"); // "32 degrees!"
console.log(freezing / 2); // 16
console.log(String(freezing)); // "32°"
+
操作符触发的是默认模式;/
操作符触发的是数字模式;String()
函数触发字符串模式。
NOTE
针对三种模式返回不同的值是可行的,但更常见的做法是,将默认模式设置设置成与字符串模式或数字模式相同的处理逻辑。
Symbol.toStringTag 属性
Symbol.toStringTag
所代表的属性在每一个对象中都存在,其定义了调用对象的 Object.prototype.toString.call()
方法时返回的值。
对于数组,调用该函数返回的值通常是 “Array”,它正是存储在对象的 Symbol.toStringTag
属性中的。
同样的,也可以为自己的对象定义 Symbol.toStringTag
的值。
function Person(name) {
this.name = name;
}
var me = new Person("JiaJia");
console.log(me.toString()); // "[object Object]"
console.log(Object.prototype.toString.call(me)); // "[object Object]"
// 为对象定义自己的 Symbol.toStringTag 值
Person.prototype[Symbol.toStringTag] = "Person";
// toString() 方法默认返回 Symbol.toStringTag 的值
console.log(me.toString()); // "[object Person]"
console.log(Object.prototype.toString.call(me)); // "[object Person]"
// 自定义 toString 方法
Person.prototype.toString = function() {
return this.name;
}
console.log(me.toString()); // "JiaJia"
// 自定义 toString() 方法后,不会影响 Object.prototype.toString.call() 方法的值
console.log(Object.prototype.toString.call(me)); // "[object Person]"
toString()
方法默认返回Symbol.toStringTag
的值。- 自定义
toString()
方法后,不会影响Object.prototype.toString.call()
方法的值
Symbol.unscopables 属性
with
语句的初衷是可以免于编写重复的代码。但加入 with
语句后,代码变的难以理解,执行性能很差且容易导致程序出错。最终,标准固定,在严格模式下不可以使用 with
语句。
尽管未来不会使用 with
语句,但是 ES6 仍在非严格模式下提供了向后兼容性。
var values = [1, 2, 3],
colors = ["red", "green", "blue"],
color = "black";
with(colors) {
// 相当于调用了 colors.push 方法
push(color);
push(...values);
}
console.log(colors); // ["red", "green", "blue", "black", 1, 2, 3]
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
includes: true
keys: true
});