- 熟悉Symbol几种特性
- 1. Symbol 值通过 Symbol 函数生成,使用 typeof,结果为 "symbol"
- 2. Symbol 函数前不能使用 new 命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。
- 3. instanceof 的结果为 false
- 4. Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
- 5. 如果 Symbol 的参数是一个对象,就会调用该对象的 toString 方法,将其转为字符串,然后才生成一个 Symbol 值。
- 6. Symbol 函数的参数只是表示对当前 Symbol 值的描述,相同参数的 Symbol 函数的返回值是不相等的。
- 7. Symbol 值不能与其他类型的值进行运算,会报错。
- 8. Symbol 值可以显式转为字符串。
- 9. Symbol 值可以作为标识符,用于对象的属性名,可以保证不会出现同名的属性。
- 10. Symbol 作为属性名,该属性不会出现在 for...in、for...of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols 方法,可以获取指定对象的所有 Symbol 属性名。
- 11. 如果我们希望使用同一个 Symbol 值,可以使用 Symbol.for。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
- 12. Symbol.keyFor 方法返回一个已登记的 Symbol 类型值的 key。
- 分析
- 第二版
- 第三版
- 第四版
- 第五版
- 10. Symbol 作为属性名,该属性不会出现在 for...in、for...of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols 方法,可以获取指定对象的所有 Symbol 属性名。
- 11. 有时,我们希望重新使用同一个Symbol值,Symbol.for方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。
- 12. Symbol.keyFor 方法返回一个已登记的 Symbol 类型值的 key。
- 完整实现
模拟实现 Symbol 类型
熟悉Symbol几种特性
ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。
1. Symbol 值通过 Symbol 函数生成,使用 typeof,结果为 "symbol"
typeof Symbol(); // "symbol"
2. Symbol 函数前不能使用 new 命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。
new Symbol(); // TypeError: Symbol is not a constructor
3. instanceof 的结果为 false
var s = Symbol('foo');
s instanceof Symbol; // false
2
4. Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
s1 = Symbol('foo'); // Symbol(foo)
5. 如果 Symbol 的参数是一个对象,就会调用该对象的 toString 方法,将其转为字符串,然后才生成一个 Symbol 值。
const obj = {
toString() {
return 'abc';
}
};
Symbol(obj); // Symbol(abc)
2
3
4
5
6
6. Symbol 函数的参数只是表示对当前 Symbol 值的描述,相同参数的 Symbol 函数的返回值是不相等的。
// 没有参数的情况
var s1 = Symbol();
var s2 = Symbol();
s1 === s2; // false
// 有参数的情况
var s1 = Symbol('foo');
var s2 = Symbol('foo');
s1 === s2; // false
2
3
4
5
6
7
8
9
7. Symbol 值不能与其他类型的值进行运算,会报错。
var sym = Symbol('My symbol');
"your symbol is " + sym; // TypeError: can't convert symbol to string
2
3
8. Symbol 值可以显式转为字符串。
var sym = Symbol('My symbol');
String(sym); // 'Symbol(My symbol)'
sym.toString(); // 'Symbol(My symbol)'
2
3
4
9. Symbol 值可以作为标识符,用于对象的属性名,可以保证不会出现同名的属性。
var mySymbol = Symbol();
// 第一种写法
var a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
var a = {
[mySymbol]: 'Hello!'
};
// 第三种写法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol]; // "Hello!"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
10. Symbol 作为属性名,该属性不会出现在 for...in、for...of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols 方法,可以获取指定对象的所有 Symbol 属性名。
var obj = {};
var a = Symbol('a');
var b = Symbol('b');
obj[a] = 'Hello';
obj[b] = 'World';
var objectSymbols = Object.getOwnPropertySymbols(obj);
objectSymbols;
// [Symbol(a), Symbol(b)]
2
3
4
5
6
7
8
9
10
11
11. 如果我们希望使用同一个 Symbol 值,可以使用 Symbol.for。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
s1 === s2; // true
2
3
4
12. Symbol.keyFor 方法返回一个已登记的 Symbol 类型值的 key。
var s1 = Symbol.for("foo");
Symbol.keyFor(s1); // "foo"
var s2 = Symbol("foo");
Symbol.keyFor(s2); // undefined
2
3
4
5
分析
看完以上的特性,你觉得哪些特性是可以模拟实现的呢?
当调用 Symbol 的时候,会采用以下步骤:
- 如果使用 new ,就报错
- 如果 description 是 undefined,让 descString 为 undefined
- 否则 让 descString 为 ToString(description)
- 如果报错,就返回
- 返回一个新的唯一的 Symbol 值,它的内部属性 [[Description]] 值为 descString
考虑到还需要定义一个 [[Description]] 属性,如果直接返回一个基本类型的值,是无法做到这一点的,所以我们最终还是返回一个对象。
第一版
// 第一版
(function() {
var root = this;
var SymbolPolyfill = function Symbol(description) {
// 实现特性第 2 点:Symbol 函数前不能使用 new 命令
// 可以用ES6 if (new.target !== void 0) 来判断是否使用new创建实例
if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');
// 实现特性第 5 点:如果 Symbol 的参数是一个对象,就会调用该对象的 toString 方法,将其转为字符串,然后才生成一个 Symbol 值。
var descString = description === undefined ? undefined : String(description)
var symbol = Object.create(null)
Object.defineProperties(symbol, {
'__Description__': {
value: descString,
writable: false,
enumerable: false,
configurable: false
}
});
// 实现特性第 6 点,因为调用该方法,返回的是一个新对象,两个对象之间,只要引用不同,就不会相同
return symbol;
}
root.SymbolPolyfill = SymbolPolyfill;
})();
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
只是参照着规范,我们已经实现了特性的第 2、5、6 点。
第二版
我们来看看其他的特性该如何实现:
1. 使用 typeof,结果为 "symbol"。
利用 ES5,我们并不能修改 typeof 操作符的结果,所以这个无法实现。
3. instanceof 的结果为 false
因为不是通过 new 的方式实现的,所以 instanceof 的结果自然是 false。
4. Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述。主要是为了在控制台显示,或者转为字符串时,比较容易区分。
当我们打印一个原生 Symbol 值的时候:
console.log(Symbol('1')); // Symbol(1)
可是我们模拟实现的时候返回的却是一个对象,所以这个也是无法实现的,当然你修改 console.log 这个方法是另讲。
8. Symbol 值可以显式转为字符串。
var sym = Symbol('My symbol');
console.log(String(sym)); // 'Symbol(My symbol)'
console.log(sym.toString()); // 'Symbol(My symbol)'
2
3
4
当调用 String 方法的时候,如果该对象有 toString 方法,就会调用该 toString 方法,所以我们只要给返回的对象添加一个 toString 方法,即可实现这两个效果。
// 第二版
// 前面面代码相同 ……
var symbol = Object.create({
toString: function() {
return 'Symbol(' + this.__Description__ + ')';
},
});
// 后面代码相同 ……
2
3
4
5
6
7
8
9
10
11
第三版
9. Symbol 值可以作为标识符,用于对象的属性名,可以保证不会出现同名的属性。
看着好像没什么,这点其实和第 8 点是冲突的,这是因为当我们模拟的所谓 Symbol 值其实是一个有着 toString 方法的 对象,当对象作为对象的属性名的时候,就会进行隐式类型转换,还是会调用我们添加的 toString 方法,对于 Symbol('foo') 和 Symbol('foo')两个 Symbol 值,虽然描述一样,但是因为是两个对象,所以并不相等,但是当作为对象的属性名的时候,都会隐式转换为 Symbol(foo) 字符串,这个时候就会造成同名的属性。举个例子:
var a = SymbolPolyfill('foo');
var b = SymbolPolyfill('foo');
console.log(a === b); // false
var o = {};
o[a] = 'hello';
o[b] = 'hi';
console.log(o); // {Symbol(foo): 'hi'}
2
3
4
5
6
7
8
9
10
为了防止不会出现同名的属性,毕竟这是一个非常重要的特性,迫不得已,我们需要修改 toString 方法,让它返回一个唯一值,所以第 8 点就无法实现了,而且我们还需要再写一个用来生成 唯一值的方法,就命名为 generateName,我们将该唯一值添加到返回对象的 Name 属性中保存下来。
// 第三版
(function() {
var root = this;
var generateName = (function(){
var postfix = 0;
return function(descString){
postfix++;
return '@@' + descString + '_' + postfix
}
})()
var SymbolPolyfill = function Symbol(description) {
if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');
var descString = description === undefined ? undefined : String(description)
var symbol = Object.create({
toString: function() {
return this.__Name__;
}
})
Object.defineProperties(symbol, {
'__Description__': {
value: descString,
writable: false,
enumerable: false,
configurable: false
},
'__Name__': {
value: generateName(descString),
writable: false,
enumerable: false,
configurable: false
}
});
return symbol;
}
root.SymbolPolyfill = SymbolPolyfill;
})();
var a = SymbolPolyfill('foo');
var b = SymbolPolyfill('foo');
console.log(a === b); // false
var o = {};
o[a] = 'hello';
o[b] = 'hi';
console.log(o); // Object { "@@foo_1": "hello", "@@foo_2": "hi" }
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
第四版
我们再看看接下来的特性。
7.Symbol 值不能与其他类型的值进行运算,会报错
以 + 操作符为例,当进行隐式类型转换的时候,会先调用对象的 valueOf 方法,如果没有返回基本值,就会再调用 toString 方法,所以我们考虑在 valueOf 方法中进行报错,比如:
var symbol = Object.create({
valueOf: function() {
throw new Error('Cannot convert a Symbol value')
}
})
console.log('1' + symbol); // 报错
2
3
4
5
6
7
看着很简单的解决了这个问题,可是如果我们是显式调用 valueOf 方法呢?对于一个原生的 Symbol 值:
var s1 = Symbol('foo')
console.log(s1.valueOf()); // Symbol(foo)
2
是的,对于原生 Symbol,显式调用 valueOf 方法,会直接返回该 Symbol 值,而我们又无法判断是显式还是隐式的调用,所以这个我们就只能实现一半,要不然实现隐式调用报错,要不然实现显式调用返回该值,那……我们选择不报错的那个吧,即后者。
我们迫不得已的修改 valueOf 函数:
// 第四版
// 前面面代码相同 ……
var symbol = Object.create({
toString: function() {
return this.__Name__;
},
valueOf: function() {
return this;
}
});
// 后面代码相同 ……
2
3
4
5
6
7
8
9
10
11
12
第五版
10. Symbol 作为属性名,该属性不会出现在 for...in、for...of 循环中,也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols 方法,可以获取指定对象的所有 Symbol 属性名。
嗯,无法实现。
11. 有时,我们希望重新使用同一个Symbol值,Symbol.for方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。
这个实现类似于函数记忆,我们建立一个对象,用来储存已经创建的 Symbol 值即可。
12. Symbol.keyFor 方法返回一个已登记的 Symbol 类型值的 key。
遍历 forMap,查找该值对应的键值即可。
// 第五版
// 前面代码相同 ……
var SymbolPolyfill = function() { ... }
var forMap = {};
Object.defineProperties(SymbolPolyfill, {
'for': {
value: function(description) {
var descString = description === undefined ? undefined : String(description)
return forMap[descString] ? forMap[descString] : forMap[descString] = SymbolPolyfill(descString);
},
writable: true,
enumerable: false,
configurable: true
},
'keyFor': {
value: function(symbol) {
for (var key in forMap) {
if (forMap[key] === symbol) return key;
}
},
writable: true,
enumerable: false,
configurable: true
}
});
// 后面代码相同 ……
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
完整实现
综上所述:
无法实现的特性有:1、4、7、8、10
可以实现的特性有:2、3、5、6、9、11、12
最后的实现如下:
(function() {
var root = this;
var generateName = (function(){
var postfix = 0;
return function(descString){
postfix++;
return '@@' + descString + '_' + postfix
}
})()
var SymbolPolyfill = function Symbol(description) {
if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');
var descString = description === undefined ? undefined : String(description)
var symbol = Object.create({
toString: function() {
return this.__Name__;
},
valueOf: function() {
return this;
}
})
Object.defineProperties(symbol, {
'__Description__': {
value: descString,
writable: false,
enumerable: false,
configurable: false
},
'__Name__': {
value: generateName(descString),
writable: false,
enumerable: false,
configurable: false
}
});
return symbol;
}
var forMap = {};
Object.defineProperties(SymbolPolyfill, {
'for': {
value: function(description) {
var descString = description === undefined ? undefined : String(description)
return forMap[descString] ? forMap[descString] : forMap[descString] = SymbolPolyfill(descString);
},
writable: true,
enumerable: false,
configurable: true
},
'keyFor': {
value: function(symbol) {
for (var key in forMap) {
if (forMap[key] === symbol) return key;
}
},
writable: true,
enumerable: false,
configurable: true
}
});
root.SymbolPolyfill = SymbolPolyfill;
})()
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70