ES6之Symbol

ES6引入了第6种原始类型:Symbol。起初,人们用它来创建对象的私有成员;在Symbol出现之前,人们一直通过属性名来访问所有属性,无论属性名由什么元素构成,全部通过一个字符串类型的名称来访问。

私有名称原本是为了让开发者创建非字符串属性名称而设计的,但是一般的技术无法检测这些属性的私有名称

创建Symbol

可以通过全局的Symbol函数创建一个Symbol:

1
2
3
4
let firstName = Symbol();
let person = {};
person[firstName] = "Nicholas";
console.log(person[firstName]);

由于Symbol是原始值,因此调用new Symbol()会导致程序抛出错误。

Symbol接受一个可选参数,其可以添加一段文本描述即将创建的Symbol,这段描述不可以用作属性访问,只是为了便于阅读和调试Symbol程序;symbol的描述被存储在内部的[[Description]]属性中,只有当调用Symbol的toString()方法时才可以读取这个属性;

检测一个Symbol:

1
2
let name = Symbol('hi');
console.log(typeof name); // symbol

Symbol的使用方法

所有使用可计算属性名的地方,都可以使用Symbol。如Object.defineProperty等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let firstName = Symbol("first name");
// use a computed object literal property
let person = {
[firstName]: "Nicholas"
};
// make the property read only
Object.defineProperty(person, firstName, { writable: false });
let lastName = Symbol("last name");
Object.defineProperties(person, {
[lastName]: {
value: "Zakas",
writable: false
}
});

Symbol共享体系

在不同的代码中共享同一个Symbol,ES6提供了一个可以随时访问的全局Symbol注册表;如果要创建一个共享的Symbol,要使用Symbol.for方法,它只接受一个参数,也就是即将创建的Symbol的字符串标示,这个参数同样也被用作Symbol的描述;

1
2
3
4
5
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。

1
2
3
4
let uid = Symbol.for("uid");
let uid2 = Symbol.for("uid");
console.log(uid === uid2); // true
console.log(uid2); // 'Symbol(uid)'

还有一个与Symbol共享有关的特性,Symbol.keyFor方法在Symbol全局注册表中检索与Symbol有关的键;

1
2
3
4
5
6
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全局注册表中不存在uid3这个Symbol(没有通过Symbol.for注册),返回undefined;

Symbol与类型强制转换

不能将Symbol强制转换为字符串和数字类型;Symbol在需要逻辑判断表达式中为真;

1
2
3
4
var uid = Symbol.for("uid"),
desc = uid + ""; // Cannot convert a Symbol value to a string
var sum = uid / 1; // Cannot convert a Symbol value to a number
!!uid; // true

Symbol属性检索

Object.keys和Object.getOwnpropertyNames不能检索出Symbol属性,ES6中Object.getOwnpropertySymbols来支持这个功能,返回对象中的所有Symbol属性的数组;

1
2
3
4
5
6
7
8
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'

通过well-known Symbol暴露内部操作

ES6通过在原型链上定义与Symbol相关的属性来暴露更多的语言内部逻辑;开放了以前JS中常见的内部操作,并通过预定义一些well-known Symbol来表示。每一个这类Symbol都是symbol对象的一个属性,例如Symbol.match

Symbol.hasInstance

一个在执行instanceof时调用的内部方法,用于检测对象的继承信息;

Function.prototype中有一个Symbol.hasInstance方法,每一个函数都继承了这个方法,用于确定对象是否是函数的实例。该方法被定义为不可写,不可配置并且不可枚举。

1
Object.getOwnPropertyDescriptor(Function.prototype, Symbol.hasInstance); // {writable: false, enumerable: false, configurable: false, value: ƒ}

看一段代码:

1
2
3
Array.hasOwnProperty(Symbol.hasInstance); // false
Function.prototype.hasOwnProperty(Symbol.hasInstance); // true
Array instanceof Function; // true

从上面可以看出Array是Function的实例;Function.prototype具有自有属性Symbol.hasInstance,Array没有Symbol.hasInstance;

obj instanceof Array实际等价于Array[Symbol.hasInstance](obj),由于Array没有Symbol.hasInstance,所以会从原型链中寻找,最终在Function.prototype中找到,最终实际执行的就是Function.prototype[Symbol.hasInstance](obj)

明白了instanceof的实际调用过程,就可以按我们的意愿来定制一些内容,例如定义一个无实例的函数:

1
2
3
4
5
6
7
8
9
10
function MyObject() {
// empty
}
Object.defineProperty(MyObject, Symbol.hasInstance, {
value: function(v) {
return false;
}
});
let obj = new MyObject();
console.log(obj instanceof MyObject); // false

Symbol.isConcatSpreadable

concat元素

1
2
3
4
let colors1 = [ "red", "green" ],
colors2 = colors1.concat([ "blue", "black" ]);
console.log(colors2.length); // 4
console.log(colors2); ["red","green","blue","black"]

concat数组

1
2
3
4
let colors1 = [ "red", "green" ],
colors2 = colors1.concat([ "blue", "black" ], "brown");
console.log(colors2.length); // 5
console.log(colors2); // ["red","green","blue","black","brown"]

JS规范声明,concat凡是传入了数组参数,就会自动将它们分解成独立元素,在ES6之前,我们根本无法调整这个特性;

Symbol.isConcatSpreadable属性是一个布尔值,如果该属性为true,则表示对象由length属性和数字键,故它的数值型属性应该被独立添加到cancat()调用结果中;与其他well-known Symbol属性不同的是,这个Symbol属性默认情况下不会出现标准对象,它只是一个可选对象,用于增强作用于特定对象类型的concat()方法的功能,有效简化其默认特性;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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"]
let collection1 = {
0: "Hello",
1: "world",
length: 2,
[Symbol.isConcatSpreadable]: true
};
let messages = [ "Hi" ].concat(collection1);
console.log(messages.length); // 2
console.log(messages); // ["Hi",{...}]

Symbol.match Symbol.replace Symbol.search Symbol.split

这四个Symbol用在String对应的四个方法String.match/replace/search/split中且当参数为正则表达式时。

可以定义一个对象的这四个Symbol属性方法,这对象就可以代替正则表达式来应用在上面的对应场景中;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// effectively equivalent to /^.{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),
replace2 = message2.replace(hasLengthOf10);
console.log(replace1); // "Hello world"
console.log(replace2); // "Hello John"
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引擎中,经常会尝试将对象转换到相应的原始值,例如比较一个对象与字符串;对象被转换为原始值,会有三种选择:数字,字符串或无类型偏好的值。对于大多数标准对象,当对象被转换为数字时:

  1. 调用valueOf方法,如果结果为原始值,则返回
  2. 否则,调用toString方法,如果结果值为原始值,则返回
  3. 如果再无可选值,则抛出异常

当对象被转换为字符串时:

  1. 调用toSring方法,如果结果为原始值,则返回
  2. 否则,调用valueOf方法,如果结果值为原始值,则返回
  3. 如果再无可选值,则抛出异常

大多数情况下,标准对象会将默认模式按数字模式处理(除了Date对象);默认模式只用于==运算,+运算及给Date构造函数传递一个参数时。

在ES6标准中,通过Symbol.toPrimitive方法可以更改那个暴露出来的原始值;Symbol.toPrimitive被定义在每一个标准类型的原型上,并且规定了当对象被转换为原始值时应该执行的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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°"

Symbol.toStringTag

当检测一个数据的类型时,可能会用instanceof,这样在使用iframe标签情况下会有一定问题,不同的iframe代表不同的领域,不同领域的构造函数是不同的,当数据传递到不同的领域时,使用instanceof会得不到预期的结果;

这时我们可能会用Object.prototype.toString.call来处理;

如区分原生JSON对象和自建对象

1
2
3
4
function supportsNativeJSON() {
return typeof JSON !== "undefined" &&
Object.prototype.toString.call(JSON) === "[object JSON]";
}

在ES6中重新定义了原生对象过去的状态,通过Symbol.toStringTag改变了调用Object.prototype.toString方法时返回的身份标示;这个Symbol所代表的属性在每一个对象中都存在,其定义了调用对象的Object.prototype.toString.call方法时返回的值。

1
2
3
4
5
6
7
function Person(name) {
this.name = name;
}
Person.prototype[Symbol.toStringTag] = "Person";
var me = new Person("Nicholas");
console.log(me.toString()); // [object Person]
console.log(Object.prototype.toString.call(me)); // [object Person]

Symbol.unscopables

The Symbol.unscopables well-known symbol is used to specify an object value of whose own and inherited property names are excluded from the with environment bindings of the associated object.

1
2
3
4
5
6
7
8
var keys = [];
with (Array.prototype) {
keys.push('something');
}
Object.keys(Array.prototype[Symbol.unscopables]);
// ["copyWithin", "entries", "fill", "find", "findIndex", "includes", "keys", "values"]

也可以改变对象的Symbol.unscopables属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = {
foo: 1,
bar: 2
};
obj[Symbol.unscopables] = {
foo: false,
bar: true
};
with (obj) {
console.log(foo); // 1
console.log(bar); // ReferenceError: bar is not defined
}