Symbol是ES6开始新增的基本数据类型(primitive data type).它最大的特点是每一个Symbol([description])函数返回symbol类型的值是唯一的.它可以作为对象的key以避免key冲突.

1
2
3
4
5
6
7
8
9
10
11
12
const symbol1 = Symbol(); // 可以不传任何参数
const symbol2 = Symbol(42);
const symbol3 = Symbol('foo');

console.log(typeof symbol1);
// expected output: "symbol"

console.log(symbol3.toString());
// expected output: "Symbol(foo)"

console.log(Symbol('foo') === Symbol('foo'));
// expected output: false

Symbol是不能使用new 操作符的! 如果使用new 操作符,JS引擎会提示:’TypeError: Symbol is not a constructor’.

symbol是基本数据类型,并不是对象所以不能用new操作符,并且不能给symbol值添加属性.

Symbol不能被继承,子类化.

如果description是个对象,则调用该对象的toString方法,将其转为字符串,然后才生成一个 Symbol 值。

1
2
3
4
5
6
7
const obj = {
toString() {
return 'abc';
}
};
const sym = Symbol(obj);
sym // Symbol(abc)

Symbol 属性和方法

Symbol.asyncIterator

Symbol.asyncIterator 符号指定了一个对象的默认异步迭代器。如果一个对象设置了这个属性,它就是异步可迭代对象,可用于for await...of循环。

一个异步可迭代对象必须要有Symbol.asyncIterator属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const myAsyncIterable = new Object();
myAsyncIterable[Symbol.asyncIterator] = async function*() {
yield "hello";
yield "async";
yield "iteration!";
};

(async () => {
for await (const x of myAsyncIterable) {
console.log(x);
// expected output:
// "hello"
// "async"
// "iteration!"
}
})();

Symbol.iterator

Symbol.iterator 为每一个对象定义了默认的迭代器。该迭代器可以被 for...of 循环使用

1
2
3
4
5
6
7
8
9
10
11
12
13
var myIterable = {}
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
for(let k of myIterable){
console.log(k)
}
// 1
// 2
// 3

一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。

数组原生具备iterator接口(即默认部署了Symbol.iterator属性),for...of循环本质上就是调用这个接口产生的遍历器,可以用下面的代码证明。

1
2
3
4
5
6
7
8
9
10
11
12
const arr = ['red', 'green', 'blue'];

for(let v of arr) {
console.log(v); // red green blue
}

const obj = {};
obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);

for(let v of obj) {
console.log(v); // red green blue
}

Symbol.match

Symbol.match用于标识对象是否具有正则表达式的行为。比如, String.prototype.startsWith()String.prototype.endsWith()String.prototype.includes() 这些方法会检查其第一个参数是否是正则表达式,是正则表达式就抛出一个TypeError。现在,如果 match symbol 设置为 false(或者一个 假值),就表示该对象不打算用作正则表达式对象。

1
2
3
4
5
"/bar/".startsWith(/bar/); 

// startWith 第一个参数应该是个字符串 不能死正则表达式
// Throws TypeError, 因为 /bar/ 是一个正则表达式
// 且 Symbol.match 没有修改。

但是,如果你将 Symbol.match 置为 false,使用 match 属性的表达式检查会认为该象不是正则表达式对象。startsWithendsWith 方法将不会抛出 TypeError

1
2
3
4
var re = /foo/;
re[Symbol.match] = false;
"/foo/".startsWith(re); // true
"/baz/".endsWith(re); // false

对象的Symbol.match属性,指向一个函数。当执行str.match(myObject)时,如果该属性存在,会调用它,返回该方法的返回值。

1
2
3
4
5
6
7
8
9
10
11
String.prototype.match(regexp)
// 等同于
regexp[Symbol.match](this)

class MyMatcher {
[Symbol.match](string) {
return 'hello world'.indexOf(string);
}
}

'e'.match(new MyMatcher()) // 1

Symbol.replace

Symbol.replace 这个属性指定了当一个字符串替换所匹配字符串时所调用的方法。String.prototype.replace() 方法会调用此方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
const x = {};
x[Symbol.replace] = (...s) => {
console.log(s);
return s.join();
}

'Hello'.replace(x, 'World') // ["Hello", "World"]
// "Hello,World"

//Symbol.replace方法会收到两个参数,
// 第一个参数是replace方法正在作用的对象,上面例子是Hello,
// 第二个参数是替换后的值,上面例子是World。
// 函数返回值就是 String.replace的结果

对象的Symbol.search属性,指向一个方法,当该对象被String.prototype.search方法调用时,会返回该方法的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
String.prototype.search(regexp)
// 等同于
regexp[Symbol.search](this)

class MySearch {
constructor(value) {
this.value = value;
}
[Symbol.search](string) {
return string.indexOf(this.value);
}
}
'foobar'.search(new MySearch('foo')) // 0

Symbol.split

对象的Symbol.split属性,指向一个方法,当该对象被String.prototype.split方法调用时,会返回该方法的返回值。

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
String.prototype.split(separator, limit)
// 等同于
separator[Symbol.split](this, limit)

class MySplitter {
constructor(value) {
this.value = value;
}
[Symbol.split](string) {
let index = string.indexOf(this.value);
if (index === -1) {
return string;
}
return [
string.substr(0, index),
string.substr(index + this.value.length)
];
}
}

'foobar'.split(new MySplitter('foo'))
// ['', 'bar']

'foobar'.split(new MySplitter('bar'))
// ['foo', '']

'foobar'.split(new MySplitter('baz'))
// 'foobar'


// 上面方法使用Symbol.split方法,重新定义了字符串对象的split方法的行为

Symbol.hasInstance

Symbol.hasInstance 用于判断某对象是否为某构造器的实例。因此你可以用它自定义 instanceof 操作符在某个类上的行为。

1
2
3
4
5
6
class MyArray {  
static [Symbol.hasInstance](instance) {
return Array.isArray(instance);
}
}
console.log([] instanceof MyArray); // true

上面代码中,MyClass是一个类,new MyClass()会返回一个实例。该实例的Symbol.hasInstance方法,会在进行instanceof运算时自动调用,判断左侧的运算子是否为Array的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Even {
static [Symbol.hasInstance](obj) {
return Number(obj) % 2 === 0;
}
}

// 等同于
const Even = {
[Symbol.hasInstance](obj) {
return Number(obj) % 2 === 0;
}
};

1 instanceof Even // false
2 instanceof Even // true
12345 instanceof Even // false

Symbol.isConcatSpreadable

对象的Symbol.isConcatSpreadable属性等于一个布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开。

1
2
3
4
5
6
7
let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr1[Symbol.isConcatSpreadable] // undefined

let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']

上面代码说明,数组的默认行为是可以展开,Symbol.isConcatSpreadable默认等于undefined。该属性等于true时,也有展开的效果。

上面代码说明,数组的默认行为是可以展开,Symbol.isConcatSpreadable默认等于undefined。该属性等于true时,也有展开的效果。

1
2
3
4
5
let obj = {length: 2, 0: 'c', 1: 'd'};
['a', 'b'].concat(obj, 'e') // ['a', 'b', obj, 'e']

obj[Symbol.isConcatSpreadable] = true;
['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']

Symbol.isConcatSpreadable属性也可以定义在类里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A1 extends Array {
constructor(args) {
super(args);
this[Symbol.isConcatSpreadable] = true;
}
}
class A2 extends Array {
constructor(args) {
super(args);
}
get [Symbol.isConcatSpreadable] () {
return false;
}
}
let a1 = new A1();
a1[0] = 3;
a1[1] = 4;
let a2 = new A2();
a2[0] = 5;
a2[1] = 6;
[1, 2].concat(a1).concat(a2)
// [1, 2, 3, 4, [5, 6]]

上面代码中,类A1是可展开的,类A2是不可展开的,所以使用concat时有不一样的结果。

注意,Symbol.isConcatSpreadable的位置差异,A1是定义在实例上,A2是定义在类本身,效果相同。

Symbol.unscopables

对象的Symbol.unscopables属性,指向一个对象。该对象指定了使用with关键字时,哪些属性会被with环境排除。

1
2
3
4
5
6
7
8
9
10
11
12
13
Array.prototype[Symbol.unscopables]
// {
// copyWithin: true,
// entries: true,
// fill: true,
// find: true,
// findIndex: true,
// includes: true,
// keys: true
// }

Object.keys(Array.prototype[Symbol.unscopables])
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'includes', 'keys']

上面代码说明,数组有 7 个属性,会被with命令排除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 没有 unscopables 时
class MyClass {
foo() { return 1; }
}

var foo = function () { return 2; };

with (MyClass.prototype) {
foo(); // 1
}

// 有 unscopables 时
class MyClass {
foo() { return 1; }
get [Symbol.unscopables]() {
return { foo: true };
}
}

var foo = function () { return 2; };

with (MyClass.prototype) {
foo(); // 2
}

上面代码通过指定Symbol.unscopables属性,使得with语法块不会在当前作用域寻找foo属性,即foo将指向外层作用域的变量。

Symbol.species

对象的Symbol.species属性,指向一个构造函数。

1
2
3
4
5
6
7
8
9
class MyArray extends Array {
}

const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);

b instanceof MyArray // true
c instanceof MyArray // true

上面代码中,子类MyArray继承了父类ArrayaMyArray的实例,bca的衍生对象。你可能会认为,bc都是调用数组方法生成的,所以应该是数组(Array的实例),但实际上它们也是MyArray的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Array1 extends Array {
static get [Symbol.species]() { return Array; }
}

const a = new Array1(1, 2, 3);
const mapped = a.map(x => x * x);

console.log(a instanceof Array1);
// expected output: true

console.log(mapped instanceof Array1);
// expected output: false

console.log(mapped instanceof Array);
// expected output: true

Symbol.toPrimitive

对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。

Symbol.toPrimitive被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式。

  • Number:该场合需要转成数值
  • String:该场合需要转成字符串
  • Default:该场合可以转成数值,也可以转成字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 一个没有提供 Symbol.toPrimitive 属性的对象,参与运算时的输出结果
var obj1 = {};
console.log(+obj1); // NaN
console.log(`${obj1}`); // "[object Object]"
console.log(obj1 + ""); // "[object Object]"

// 接下面声明一个对象,手动赋予了 Symbol.toPrimitive 属性,再来查看输出结果
var obj2 = {
[Symbol.toPrimitive](hint) {
if (hint == "number") {
return 10;
}
if (hint == "string") {
return "hello";
}
return true;
}
};
console.log(+obj2); // 10 -- hint 参数值是 "number"
console.log(`${obj2}`); // "hello" -- hint 参数值是 "string"
console.log(obj2 + ""); // "true" -- hint 参数值是 "default"

Symbol.toStringTag

Symbol.toStringTag 是一个内置 symbol,它通常作为对象的属性键使用,对应的属性值应该为字符串类型,这个字符串用来表示该对象的自定义类型标签,通常只有内置的 Object.prototype.toString() 方法会去读取这个标签并把它包含在自己的返回值里。

许多内置的 JavaScript 对象类型即便没有 toStringTag 属性,也能被 toString() 方法识别并返回特定的类型标签,比如:

1
2
3
4
5
6
7
Object.prototype.toString.call('foo');     // "[object String]"
Object.prototype.toString.call([1, 2]); // "[object Array]"
Object.prototype.toString.call(3); // "[object Number]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
// ... and more

另外一些对象类型则不然,toString() 方法能识别它们是因为引擎为它们设置好了 toStringTag 标签:

1
2
3
4
Object.prototype.toString.call(new Map());       // "[object Map]"
Object.prototype.toString.call(function* () {}); // "[object GeneratorFunction]"
Object.prototype.toString.call(Promise.resolve()); // "[object Promise]"
// ... and more

但你自己创建的类不会有这份特殊待遇,toString() 找不到 toStringTag 属性时只好返回默认的 Object 标签:

1
2
3
4
5
6
7
8
9
10
11
12
class ValidatorClass {}

Object.prototype.toString.call(new ValidatorClass()); // "[object Object]"

// 加上 toStringTag 属性,你的类也会有自定义的类型标签了:
class ValidatorClass {
get [Symbol.toStringTag]() {
return "Validator";
}
}

Object.prototype.toString.call(new ValidatorClass()); // "[object Validator]"

Symbol.for()

Symbol.for(key) 方法会根据给定的键 key,来从运行时的 symbol 注册表中找到对应的 symbol,如果找到了,则返回它,否则,新建一个与该键关联的 symbol,并放入全局 symbol 注册表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Symbol.for("foo"); // 创建一个 symbol 并放入 symbol 注册表中,键为 "foo"
Symbol.for("foo"); // 从 symbol 注册表中读取键为"foo"的 symbol


Symbol.for("bar") === Symbol.for("bar"); // true,证明了上面说的
Symbol("bar") === Symbol("bar"); // false,Symbol() 函数每次都会返回新的一个 symbol


var sym = Symbol.for("mario");
sym.toString();
// "Symbol(mario)",mario 既是该 symbol 在 symbol 注册表中的键名,又是该 symbol 自身的描述字符串

// 为了防止冲突,最好给你要放入 symbol 注册表中的 symbol 带上键前缀。
Symbol.for("mdn.foo");
Symbol.for("mdn.bar");

Symbol.keyFor()

Symbol.keyFor(sym) 方法用来获取 symbol 注册表中与某个 symbol 关联的键

sym必选参数,存储在 symbol 注册表中的某个 symbol

如果全局注册表中查找到该symbol,则返回该symbol的key值,形式为string。如果symbol未在注册表中,返回undefined

1
2
3
4
5
6
7
8
9
10
// 创建一个 symbol 并放入 Symbol 注册表,key 为 "foo"
var globalSym = Symbol.for("foo");
Symbol.keyFor(globalSym); // "foo"

// 创建一个 symbol,但不放入 symbol 注册表中
var localSym = Symbol();
Symbol.keyFor(localSym); // undefined,所以是找不到 key 的

// well-known symbol 们并不在 symbol 注册表中
Symbol.keyFor(Symbol.iterator) // undefined

Symbol.prototype.toString()

toString() 方法返回当前 symbol 对象的字符串表示。Symbol 对象拥有自己的 toString 方法,因而遮蔽了原型链上的

Symbol 对象拥有自己的 toString 方法,因而遮蔽了原型链上的

1
2
3
4
5
6
Symbol("foo") + "bar";      
// TypeError: Can't convert symbol to string
Symbol("foo").toString() + "bar"
// "Symbol(foo)bar",就相当于下面的:
Object(Symbol("foo")).toString() + "bar"
// "Symbol(foo)bar"

Symbol.prototype.valueOf()

valueOf() 方法返回当前 symbol 对象所包含的 symbol 原始值。

在 JavaScript 中,虽然大多数类型的对象在某些操作下都会自动的隐式调用自身的 valueOf() 方法或者 toString() 方法来将自己转换成一个原始值,但 symbol 对象不会这么干,symbol 对象无法隐式转换成对应的原始值

1
2
3
4
5
6
7
8
9
10
Object(Symbol("foo")) + "bar";
// TypeError: can't convert symbol object to primitive
// 无法隐式的调用 valueOf() 方法

Object(Symbol("foo")).valueOf() + "bar";
// TypeError: can't convert symbol to string
// 手动调用 valueOf() 方法,虽然转换成了原始值,但 symbol 原始值不能转换为字符串

Object(Symbol("foo")).toString() + "bar";
// "Symbol(foo)bar",需要手动调用 toString() 方法才行

Symbol.prototype.description

description 是一个只读属性,它会返回 Symbol 对象的可选描述的字符串。

1
2
3
4
5
6
7
8
9
10
11
console.log(Symbol('desc').description);
// expected output: "desc"

console.log(Symbol.iterator.description);
// expected output: "Symbol.iterator"

console.log(Symbol.for('foo').description);
// expected output: "foo"

console.log(Symbol('foo').description + 'bar');
// expected output: "foobar"

Symbol 实际应用

value值的唯一性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const LEVEL_INFO = Symbol('INFO')
const LEVEL_DEBUG = Symbol('DEBUG')
const LEVEL_WARN = Symbol('WARN')
const LEVEL_ERROR = Symbol('ERROR')

function log(msg, level) {
switch(level) {
case LEVEL_WARN:
console.warn(msg); break
case LEVEL_ERROR:
console.error(msg); break;
case LEVEL_DEBUG:
console.log(msg);
debugger; break;
case LEVEL_INFO:
console.log(msg);
}
}

私有的对象方法

Symbol作为对象的key时,是不会被迭代的.从而可以创建一个私有的对象方法

1
2
3
4
5
6
7
8
9
10
11
12
const print = Symbol('print')

const user = {
name: 'Stefan',
age: 37,
[print]: function() {
console.log(`${this.name} is ${this.age} years old`)
},
add () {console.log('add')}
}
console.log(Object.keys(user))
// ["name", "age", "add"]

Symbol in Typescript

Typescript是完全支持的 symbol

1
2
3
4
5
6
7
const sym = Symbol('foo')

function extendObject(obj: any, sym: symbol, value: any) {
obj[sym] = value
}

extendObject({}, sym, 42)// 给对象添加各种 值

给一个变量设置 symbol值,并通过 typeof 在声明变量的的symbol

1
2
3
4
5
6
const PROD: unique symbol = Symbol('Production mode')
const DEV: unique symbol = Symbol('Development mode')

function showWarning(msg: string, mode: typeof DEV | typeof PROD) {
// ...
}

参考

https://tc39.es/ecma262/#sec-symbol-objects

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol

http://es6.ruanyifeng.com/#docs/symbol