ES6学习笔记5--符号

  |  

前言

符号与符号属性


6



创建符号值

在JS已有的基本类型之外,ES6引入了一种新的基本类型:符号(Symbol)。

符号没有字面量形式,可以使用全局Symbol函数来创建一个符号值

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

Symbol函数还可以接受一个额外的参数用于描述符号值,该描述并不能用来访问对应属性,但能用于调试

1
2
let firstName = Symbol("first Name");
console.log(firstName); // "Symbol(first name)"

符号的描述信息被存储在内部属性[[Description]]中,当符号的toString()方法被显示或隐式调用时,改属性都会被读取。此外没有任何办法可以从代码中直接访问[[Description]]属性

建议

应始终给符号提供描述信息,以便更好的阅读代码或进行调试

识别符号值

由于符号是基本类型的值,所以可以使用typeof运算符来判断一个变量是否为符号。ES6扩展了typeof的功能以便让它作用于符号值的时候能返回”symbol”

除此之外还有其它方法可以判断一个变量是否为符号,但是typeof运算依然是最准确、最优先的判断方法

使用符号值

可以在任意能使用“需计算属性值”的场合使用符号,此外可以在Object.defineProperty()Object.defineProperties()调用中使用它

1
2
3
4
5
6
7
8
9
10
11
12
13
let firstName = Symbol("first name");
let person = {
[firstName]: "Nicholas"
};

Object.defineProperty(person, firstName, { writable: false });
let lastName = Symbol("last name");
Object.defineProperties(person, {
[lastName]: {
value: 'Zakas',
wirtable: false
}
});

共享符号值

当我们需要在不同的代码段,或者跨文件或者代码来使用符号值来作为唯一标识符时,我们可以使用共享符号值。

ES6提供“全局符号注册表”用来在任意时间任意地点来进行访问。

若想创建共享符号值,使用Symbol.for()方法,Symbol.for()方法接受单个字符串类型的参数,作为目标符号值的标识符,同时此参数也会成为该符号的描述信息

1
2
3
4
let uid = Symbol.for('uid');
let object = {};
object[uid] = "12345";
console.log(uid); //"Symbol(uid)"

Symbol.for()方法首先会搜索全局符号注册表,查询是否存在一个键为”uid”的符号值。有则返回现有;无,则创建。后续如果用同一个键,则会返回同一个符号值

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

共享符号值还有要给独特用法,可以使用Symbol.keyiFor()方法在全局符号注册表中根据符号值检索出对应的键值

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

注意:
全局符号注册表类似于全局作用域,是一个共享环境,所以在使用时,不应当假设某些值已经存在其中,且在使用第三方组件,或创建第三方组件时,应当避免污染全局符号注册表

符号值的转换

符号类型在进行转换时非常不灵活,因为缺乏符号值与其它类型的合理等价,尤其是符号值无法被转换为字符串或数值。因此将符号作为属性所达成的效果,是其它类型所无法代替的

之前的例子使用console.log()来输出符号值,之所以能这么做使用因为输出调用了符号的toString()方法。此外可以使用String()方法来获得相同的结果

1
2
3
let uid = Symbol.for("uid");
let desc = String(uid);
console.log(desc); //"Symbol(uid)"

String()方法同样会调用符号的toString()方法。

但是当你想直接将符号转换为字符串,比如使用字符串拼接时,会引发错误

类似的也不能将符号转换成数值,对符号使用任何的数学运算符都会导致错误

检索符号属性

在ES5中可以使用Object.keys()方法来检索对象所有可枚举的属性名,可以使用Object.getOwnPropertyNames()方法来检索对象的所有属性名(包括可枚举和非可枚举)

但是这些方法都无法检索到符号类型属性,ES6并没有在这两个方法上做出扩充,以维护这两个方法的原有功能。ES6创建了一个新的的方法Object.getOwnPropertySymbols()方法,以便检索对象的符号类型属性

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"

Object.getOwnPropertySymbols()方法会返回一个数组,包含了对象自有属性中的符号值

使用知名符号暴露内部方法

所有对象初始情况下都不包含任何自有符号类型属性,但对象可以从它们的原型上继承符号类型属性。

ES6预定义了一些次类型属性,它们被成为”知名符号”。

ES6定义了”知名符号”来代表JS符号来代表JS中一些公共行为,而这些行为此外被认为只能是内部操作。

知名符号:

  • Symbol.hasInstance:供instanceof运算符使用的一个方法,用于判断对象继承关系
  • 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.toString:供String.prototype.toString()函数使用的一个方法,用于创建对象的描述信息
  • Symbol.unscopables:一个对象,该对象的属性指示了哪些属性名不允许被包含在with语句中

注意:
重写知名符号定义的方法,会把一个普通对象改编成奇异对象,因为它改变了一些默认的内部行为。这并不会对代码造成实际的影响,它只是改变了规范所描述的对象的特征

Symbol.hasInstance属性

这个属性定义在Function.prototype上,因此所有函数都继承了面对instanceof运算符时的默认行为。

Symbol.hasInstance方法接受单个参数,及要检测的值。如果该值是本函数的一个实例,就会返回true

1
2
3
obj instanceof Array;
//等价于
Array[Symbol.hasInstance](obj);

ES6从本质上将instanceof运算符重定义为上述方法调用的简写语法,这样使用Instanceof比那会触发一次方法调用

Symbol.hasInstance属性本身是不可吸入、不可配置、不可枚举的,从而保证它不会被错误的重写。但是实际上我们还是能改变该运算符的操作

1
2
3
4
5
6
7
8
9
10
function MyObject() {

}
Object.defineProperty(MyObject, Symbol.hasInstance, {
value: function(v) {
return false;
}
});
let obj = new MyObject();
console.log(obj instanceof MyObject);

注意:

instanceof的操作数必须是一个对象,以便触发Symbol.hasInstance调用,若操作数并非对象,instanceof只会简单的返回false

最好仅在必要时对自己的函数重写Symbol.hasInstance方法

Symbol.isConcatSpreadable

JS在数组上设计了concat()方法用于将两个数组连接到一起

1
2
let colors1 = ["red", "green"],
colors2 = colors1.concat(["blue", "black"]);

concat()方法也可以接受非数组的参数,此时这些参数只是简单的添加到数组末尾

1
2
let colors1 = ["red", "green"],
colors2 = colors1.concat(["blue", "black"], "brown");

JS规范要求数组类型的参数需要被自动分离出各个子项,在ES6之前,没有任何手段可以改变这种行为。

ES6添加Symbol.isConcatSpreadable属性,他表示目标对象拥有长度属性与数值类型的键、并且数值类型键所对应的属性值在参与concat()调用时需要被分离为个体。

该符号与其它知名符号不同,默认情况下它并不成为任意常规对象的属性。它只出现在特定类型的对象上,用来标示改对象在作为concat()参数时应如何工作,从而有效改变该对象的默认行为

1
2
3
4
5
6
7
8
let collection = {
0: "hello",
1: "world",
length: 2,
[Symbol.isConcatSpreadable]: true
};
let mess = ["hi"].concat(collection);
console.log(mess);

除此之外,还可以将数组的子类的Symbol.isConcatSpreadable属性值设为false,用于在concat()调用时避免项目被分离

Symbol中关于字符串的操作

在JS中,字符串与正则息息相关,字符串有几个可以接受正则表达式作为参数的方法

  • match(regex):判断指定字符串是否与一个正则表达式相匹配
  • replace(regex, replacement):对正则表达式的匹配结果进行替换
  • search(regex):在字符串内对正则表达式的匹配结果进行定位
  • split(regex):使用正则表达式将字符串分割为数组

这些与正则表达式交互的方法,在ES6之前是完全封装的,其内部实现并没有对外公开,使得开发者无法将自定义对象模拟成一个正则表达式,从而传入这些方法,完成对字符串的处理。

ES6定义了4个符号及对应方法,将原生行为外包到内置的RegExp对象上

在对象上定义这些属性,允许你创建能够进行模式匹配的对象,而无需使用正则表达式,并且允许在任何需要正则表达式的方法中使用改对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let  hasLengthOf10 = {
[Symbol.match]: function(value) {
return value.length === 10 ? [value.substring(0, 10)] : null;
},
[Symbol.replace]: function(value) {
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",
message2 = "Hello Jhon";
let match1 = message1.match(hasLengthOf10);
let match2 = message2.match(hasLengthOf10);
console.log(match1);
console.log(match2);

这里hasLengthOf10对象并不是正则表达式,但是它依然可以作为参数传入这些函数,并起到了正则表达式不能实现的过滤,这种自定义模式匹配将实现更多的可能。

Symbol.toPrimitive

JS在使用特定运算符是会进行隐式转换,以便将对象转换为基本类型值。

而在ES6之前,具体的转换过程并没有被公布

ES6通过Symbol.toPrimitive方法来暴露转换过程,以便让对应的方法可以被修改

Symbol.toPrimitive方法被定义在所有常规类型的原型上,规定了在对象被转换为基本类型值的时候会发生什么。在需要转换时,Symbol.toPrimitive将会被调用,并按照规范传入一个提示性的字符串参数,这个字符串参数由JS引擎自动传入。该参数有3中可能:当参数为”number”的时候,返回一个数值。当参数为”string”时,返回一个字符串。当参数为”default”是,对返回类型没有要求

数值模式的判断过程:

  • 调用valueOf()方法,如果方法返回值是一个基本类型值,返回这个值
  • 否则,调用toString()方法,如果方法返回一个基本类型值,返回这个值
  • 否则,抛出错误

字符串模式的判断过程:

  • 调用toString()方法,如果方法返回一个基本类型,返回这个值
  • 否则,调用valueOf()方法,如果方法返回一个基本类型值,返回这个值
  • 否则,抛出错误

在多数情况下,常规对象的默认模式都等价于数值模式(Date类型例外,它默认使用字符串模式)

注意:
默认模式值在使用===运算符、+运算符、或则传递单一参数给Date构造器的时候使用

使用Symbol.toPrimitive属性,并传递一个参数,即可重写默认的转换行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 / 2;
case "dafault":
return this.degrees + " degrees";
}
}
let freezing = new Temperature(32);
console.log(freezing + "!");
console.log(freezing / 2);
console.log(String(freezing));

Symbol.toStringTag

ES6通过Symbol.toStringTag重定义了相关行为,该符号代表所有对象的一个属性,定义了Object.prototype.toString.call()被调用时应当返回什么值。对于数组来说,在Symbol.toStringTag属性中存储了”Array”值,于是该函数的返回值就是”Array”。

同样的,我们可以在自设对象上定义Symbol.toStringTag的值

1
2
3
4
5
6
7
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]"

这个例子在Person的原型上定义了Symbol.toStringTag属性,用于提供它的默认的字符串表现形式。由于Person的原型继承Object.prototype.toString()方法,Symbol.toStringTag的返回值在调用me.toString()的时候也会被使用,因为这是使用的是Object.prototype.toString的方法。不过我们仍然可以在该对象上定义toString()方法,让他有不同的返回值,而不影响Object.prototype.toString.call()方法

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

这个实例中,由于我们在Person的原型上定义了toString方法,所以Person类不再继承Object.prototype.toString方法,所以调用me.toString()时会显示不同的结果。

注意:

除非进行特殊指定,否则所有对象都会从Object.prototype继承Symbol.toStringTag属性,其默认的属性值是字符串”Object”.

改变原生对象的字符串标签也是可能的,只需要在对象的原型上对Symbol.toStringTag。虽然这样的行为并不被推荐,但是语言本身并没有禁止该行为。

1
2
3
Array.prototype[Symbol.toStringTag] = "Magic";
let values = [];
console.log(Object.prototype.toString.call(values));
文章目录
  1. 1. 创建符号值
  2. 2. 使用符号值
    1. 2.1. 共享符号值
  3. 3. 符号值的转换
  4. 4. 检索符号属性
  5. 5. 使用知名符号暴露内部方法
    1. 5.1. Symbol.hasInstance属性
    2. 5.2. Symbol.isConcatSpreadable
    3. 5.3. Symbol中关于字符串的操作
    4. 5.4. Symbol.toPrimitive
    5. 5.5. Symbol.toStringTag
|