ES6 Symbol ! Why ? How ? When ?


前几天看小胡子哥的网站,发现这么一篇文章里面有下面这样一些代码,可以看到这段代码里面用了很多ES6的新特性,比如import,class,extends,static,for..of循环,还有下面要讲的Symbol。先前知道symbol但是一直没有去用过它,其实也并不知道到底有什么实质的作用,只知道是简单的创建唯一值。觉得是时候好好理解理解了。主要学习自阮一峰老师的书,es6入门。感谢阮老师。

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
import Baz from 'bazGroup';

class Foo extends Baz {
static classMethod() {
return 'hello';
}
constructor(...args) {
this.args = args;
}
* [Symbol.iterator]() {
for (let arg of this.args) {
yield arg;
}
}
}

class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}

Bar.classMethod();

for (let x of new Foo('hello', 'world')) {
console.log(x);
}

下面从几个方面介绍:Symbol是什么?Symbol的出现原因?Symbol怎么用 ?Symbol有什么作用?


Symbol的出现

Symbol是什么

Symbol是ES6新引入的一种数据类型。它是JavaScript语言的第七种数据类型,前面6种是我们熟知的:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

抓住,第一它是一种新的数据类型,它不是字符串,也不是对象。第二是一种特殊的、不可变的数据类型,可以作为对象属性的标识符使用。

要注意一旦创建后就不可更改,不能对它们设置属性,在严格模式下尝试这样做,你将得到一个 报错得到TypeError。它们可以作为属性名,这时它们和字符串的属性名没有什么区别。
另一方面,每个 Symbol 都是独一无二的,不与其它 Symbol 重复(即便是使用相同的 Symbol 描述创建)。

我有看到一些书里说这里的Symbol和Lisp和Ruby里面的Symbol类似,但是不完全一样。他们解释说。在 Lisp 中,所有标识符都是 Symbol;在 JavaScript 中,标识符和大多数属性仍然是字符串,Symbol 只是提供了一个额外的选择。因为我没有用过Lisp和Ruby当然也就没有发言权了。23333


Symbol出现的原因

ES5的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是ES6引入Symbol的原因。 – from 阮一峰老师

然后在stackoverflow-why-bring-symbols-to-javascript上面也看到了一个解释,我觉得也很好,因为也告诉了我们,Symbol其实并不能完全的去保护成员变量的privacy,还告诉我们现在主要的用途就是unique。

Enabling private properties, like kangax describes in his answer, was indeed the original motivation for introducing symbols into JavaScript.

Unfortunately, however, they ended up being severely downgraded, and not private after all, because you can find them via reflection. Specifically, via the Object.getOwnPropertySymbols method and through proxies.

They are now known as unique symbols, and their only use is to avoid name clashes between properties. For example, EcmaScript itself can now introduce extension hooks via certain methods you can put on objects (e.g. to define their iteration protocol) without running the risk of clashing with user names.

Whether that is strong enough a motivation to add symbols to the language is debatable.


Symbol怎么用?

Symbol值通过Symbol函数生成,注意千万不要加new关键字,否则会报错。

1
2
//description 可选
Symbol([description])

注意这里的description是可选的,字符串。注意【符号的描述(description)是用于调试的而不是访问符号本身】。

1
2
3
var sym1 = Symbol();
var sym2 = Symbol("foo");
var sym3 = Symbol("foo");

由symbol生成的都是唯一的,所以当你去判断sym2 == sym3时,得到的是false,实际上,上面的三个值都是不相同的。那这个值到底是什么呢?当你试图去打印:

1
2
3
4
5
console.log(sym1 === sym3); //false
console.log(sym2 === sym3); //false
console.log(sym === sym3); //false
console.log(sym2); // Symbol(foo)
console.log(sym1); //Symbol()

虽然如果你单独打印sym2和sym3的时候,都会得到一个值:Symbol('foo'),看起来是相同的,但两个值已经由程序生成了两个完全不同的值。你可以去使用这些你生成的变量。而且不用担心会有重名问题。这就是这个Symbol最好的地方。

sym1和sym2/sym3除了值不一样,都是独一无二的,还有就是sym2/sym3多了一个描述,当你打印或者调试的时候就可以很清楚的知道是哪个symbol了。当然最好这个description不要一样了,否则你调试的时候回去怀疑到底是哪一个Symbol

下面typeof运算符的结果,表明变量sym2Symbol数据类型,而不是字符串之类的其他类型,是一种新的数据类型。

1
2
3
4
console.log(typeof Symbol())
// <- 'symbol'
console.log(typeof Symbol('foo'))
// <- 'symbol'

注意Symbol不能够与替他类型做运算,也就是不能强制转换,但是可以转换为string或者bool类型。但是不能转换为数字类型。转换为字符串当然都是得到的相同的结果。所以我们最好用String/toString()来转换。由于转换后的值相同,其实没有什么太大的用,是不是?

1
2
3
4
5
6
7
8
9
10
var sym = Symbol('test');

String(sym) // 'Symbol(test)'
sym.toString() // 'Symbol(test)'

var sym = Symbol();
Boolean(sym) // true

Number(sym) // TypeError: can't convert symbol to number
sym - 5 //TypeError: can't convert symbol to number

Symbol的用途

给对象添加唯一属性和方法

由于每一个Symbol值都是不相等的,这意味着Symbol值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。

也就是现在对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的Symbol类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//直接在obj上添加key
const MY_KEY = Symbol();
let obj = {};

obj[MY_KEY] = 123;
console.log(obj[MY_KEY]); // 123

//在对象内部定义属性
const MY_KEY = Symbol();
let obj = {
[MY_KEY]: 123
};

//在对象内部定义方法
const FOO = Symbol();
let obj = {
[FOO]() {
return 'bar';
}
};
console.log(obj[FOO]());

这个过程发生了下面的事情:

  1. 调用 Symbol() 方法将创建一个新的 Symbol 类型的值,并且该值不与其它任何值相等。

  2. 我们利用Symbol 类型的值可以作为对象的属性名,正是由于它不与任何其它值相等,对应的属性也不会发生冲突。

  3. 和数组一样,我们只能通过[]来获得值,不能够通过. 点号来获取值

总结下:我们可以用下面几种方法来生成一个对象的唯一属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var mySymbol = Symbol();

// 第一种写法
var a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
var a = {
[mySymbol]: 'Hello!'
};

// 第三种写法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
a[mySymbol] // "Hello!"

上面是定义属性,下面看看定义方法,其实是一模一样的。
要注意由于不能使用点运算符,所以在对象里定义属性的时候,也只能用方括号。所以你可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let s = Symbol();

//普通方法
var obj = {
[s] : function(args) { //... }
}
//或者这样写(对象增强)
var obj = {
[s](args){ //... }
}
//调用方法
obj[s](1,2,3);

//错误写法
var obj = {
s(args) { //... }
s : function(){ //... }
}

在最后一个错误写法中:如果s不放在方括号中,该属性的键名就是字符串s,而不是s所代表的那个Symbol值。obj.s(args);这就是把s当作一个普通的键名了。


Using symbols to represent concepts

这是第二种我们可以用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
var COLOR_RED    = 'RED';
var COLOR_ORANGE = 'ORANGE';
var COLOR_YELLOW = 'YELLOW';
var COLOR_GREEN = 'GREEN';
var COLOR_BLUE = 'BLUE';
var COLOR_VIOLET = 'VIOLET';

function getComplement(color) {
switch (color) {
case COLOR_RED:
return COLOR_GREEN;
case COLOR_ORANGE:
return COLOR_BLUE;
case COLOR_YELLOW:
return COLOR_VIOLET;
case COLOR_GREEN:
return COLOR_RED;
case COLOR_BLUE:
return COLOR_ORANGE;
case COLOR_VIOLET:
return COLOR_YELLOW;
default:
throw new Exception('Unknown color: '+color);
}
}

这样我们避免了hard-coding,也就是硬编码。防止我们每次都去改动函数中的’red’,’blue’,这之类的。但是这还是有问题,比如一个人:

1
var MOOD_BLUE = 'BLUE';

他又定义了上面一个颜色。那么这样以来switch的时候就会有重复的值了。就不太好。如果我们用Symbol可以很好的解决这个问题。

1
2
3
4
5
6
const COLOR_RED    = Symbol();
const COLOR_ORANGE = Symbol();
const COLOR_YELLOW = Symbol();
const COLOR_GREEN = Symbol();
const COLOR_BLUE = Symbol();
const COLOR_VIOLET = Symbol();

这样以来,当我们使用Symbol而不是字符串的时候,我们再也不用去改动函数了。


Symbols as keys of internal properties

1
2
3
4
5
6
7
8
9
10
11
12
// One WeakMap per private property
const PASSWORD = new WeakMap();
class Login {
constructor(name, password) {
this.name = name;

PASSWORD.set(this, password);
}
hasPassword(pw) {
return PASSWORD.get(this) === pw;
}
}

符号不提供真正的隐私,因为它很容易找到对象的符号值的属性键。但是,保证一个属性密钥永远不会与任何其他属性密钥冲突往往是不够的。如果你真的想阻止外部访问私有数据,你需要使用WeakMaps或关闭。

1
2
3
4
5
6
7
8
9
10
const PASSWORD = Symbol();
class Login {
constructor(name, password) {
this.name = name;
this[PASSWORD] = password;
}
hasPassword(pw) {
return this[PASSWORD] === pw;
}
}

Symbol do not guarantee true privacy but can be used to separate public and internal properties of objects.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var Pet = (function() {
function Pet(type) {
this.type = type;
}
Pet.prototype.getType = function() {
return this.type;
}
return Pet;
}());

var a = new Pet('dog');
console.log(a.getType());//Output: dog
a.type = null;
//Modified outside
console.log(a.getType());//Output: undefined

上面的这个例子中type可以被外部所访问。一般我们的方法是通过闭包来解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var Pet = (function() {
function Pet(type) {
this.getType = function(){
return type;
};
}
return Pet;
}());

var b = new Pet('dog');
console.log(b.getType());//dog
b.type = null;
//Stays private
console.log(b.getType());//dog

现在如果我们通过symbol的话,就可以减少不必要的闭包。我们已经提到过很多次,symbol虽然不能够保证正真的隐私,但是可以将外部环境和内部环境分离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Pet = (function() {
var typeSymbol = Symbol('type');
function Pet(type) {
this[typeSymbol] = type;
}
Pet.prototype.getType = function(){
return this[typeSymbol];
}
return Pet;
}());

var a = new Pet('dog');
console.log(a.getType());//Output: dog
a.type = null;
//stays private
console.log(a.getType());//Output: dog

Symbols are invisible to all “reflection” methods before ES6. This can be useful in some scenarios, but they’re not private by any stretch of imagination, as we’ve just demonstrated with the Object.getOwnPropertySymbols API.

上面学习自:Samar Panda来自stackoverflow回答。感谢。


几个重点的API

使用Symbol() 函数 不会在你的整个代码库中创建一个可用的全局符号。 要创建跨文件可用的symbols,甚至跨域(每个都有它自己的全局作用域) , 使用这个方法Symbol.for() 和 Symbol.keyFor() 从全局symbol的注册处设置和取得symbols。

Symbol.for(‘desc’)

Symbol.for为Symbol值登记的名字,是全局环境的,也就是说是全局共享的Symbol。

1
2
Symbol.for('foo') === Symbol.for('foo')         //true
Symbol('foo') === Symbol('foo') //false

我们知道当我们每次调用Symbol的时候都会生成一个新的值,那如果我们想重用一个Symbol怎么办呢?Symbol.for方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。

Symbol.keyFor(‘desc’)

Symbol.keyFor方法返回一个已登记的Symbol类型值的key。注意这里的desc必须是已经登记过了的,也就是一定要是用Symbol.for来声明的。

1
2
3
4
5
var sym1 = Symbol.for("foo");
console.log(Symbol.keyFor("foo")); //"foo"

var sym2 = Symbol("bar");
console.log(Symbol.keyFor("bar")); //undefined

上述代码中:变量sym2属于未登记的Symbol值,所以返回undefined。

Symbol.iterator

对象的Symbol.iterator属性,指向该对象的默认遍历器方法。对象进行for…of循环时,会调用Symbol.iterator方法,返回该对象的默认遍历器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++]
};
} else {
return { done: true };
}
}
};
}
};
for (let x of obj) {
console.log(x);
}

An object is iterable if it has a method whose key is the symbol (stored in) Symbol.iterator.


获取Symbol的方法

总结下获取Symbol的几种方法

  1. Symbol() 每次调用时都返回一个唯一的 Symbol。
  2. Symbol.for(string) 从 Symbol 注册表中返回相应的 Symbol,与上个方法不同的是,Symbol 注册表中的 Symbol 是共享的。也就是说,如果你调用 Symbol.for(“cat”) 三次,都将返回相同的 Symbol。当不同页面或同一页面不同模块需要共享 Symbol 时,注册表就非常有用。
  3. Symbol.iterator 返回语言预定义的一些 Symbol,每个都有其特殊的用途。

这里需要知道一个很大的区别:

Symbol.for()与Symbol()这两种写法,都会生成新的Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的Symbol类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for(“cat”)30次,每次都会返回同一个Symbol值,但是调用Symbol(“cat”)30次,会返回30个不同的Symbol值。 –FROM RUAN YIFeng


属性的遍历

Symbol作为属性名,该属性不会出现在for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有Symbol属性名。

Object.getOwnPropertySymbols方法返回一个数组,成员是当前对象的所有用作属性名的Symbol值。看看下面几个例子。

1
2
3
4
5
6
7
8
var foo = {
[Symbol()]: 'foo',
[Symbol('foo')]: 'bar',
[Symbol.for('bar')]: 'baz',
what: 'ever'
}
console.log([...foo])
// <- []

Object.keys()也是不行的。

1
2
console.log(Object.keys(foo))
// <- ['what']

JSON.stringify()是不行的。

1
2
console.log(JSON.stringify(foo))
// <- {"what":"ever"}

for in 可以吗?不可以

1
2
3
4
for (let key in foo) {
console.log(key)
// <- 'what'
}

getOwnPropertyNames当然也不可以。

1
2
console.log(Object.getOwnPropertyNames(foo))
// <- ['what']

难道没有方法获得吗?如果认真看了前面我们说的,symbols是不会真正保护隐私,因为会有一个getOwnPropertySymbols的方法可以获得它们。

1
2
console.log(Object.getOwnPropertySymbols(foo))
// <- [Symbol(), Symbol('foo'), Symbol.for('bar')]

1
2
3
4
5
6
for (let symbol of Object.getOwnPropertySymbols(foo)) {
console.log(foo[symbol])
// <- 'foo'
// <- 'bar'
// <- 'baz'
}

总结

其实还有好多个symbol的API,到用到的时候再去查吧。基本上常用的就是上面那些。主要学到了symbol是什么,有什么用,可以用到什么地方。其实这新特性还是很好的。参考学习自下面几篇文章,感谢,也分享给大家。

  1. http://es6.ruanyifeng.com/#docs/symbol
  2. https://ponyfoo.com/articles/es6-symbols-in-depth
  3. why-bring-symbols-to-javascript