超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

ES6 - Symbol 类型

概述

Symbol 类型是一种特殊的、不可变的数据类型,主要用于解决名称冲突的问题,是 es6 引入的第七种原始数据类型,其他六种分别是:nullundefinedbooleanstringnumberobject

创建 Symbol

Symbol([description])
创建一个全新的 Symboldescription 是对 Symbol 实例的描述,先来看几个示例:

var s0 = Symbol();
var s1 = Symbol("desc");
var s2 = Symbol("desc");

console.log(s0) // Symbol()
console.log(s1) // Symbol(desc)
typeof s1; // "symbol"
s1 === s2 // false

从上述示例可以看出,即使传递给 Symbol 同样的 “描述” ,得到的 Symbol 也都是独立的。

Symbol.for

在某些场合下可能希望复用之前所创建的 Symbol,使用 Symbol.for 创建的 Symbol 可以做到这一点:

Symbol.for([key])

Symbol.for() 方法创建的的 symbol 会被放入一个全局 symbol 注册表中, Symbol.for(key) 方法并不是每次都会创建一个新的 symbol ,它会首先检查给定的 key 是否已经在注册表中了。假如是,则会直接返回上次存储的那个,否则,新建一个与该 key 关联的 symbol ,并放入全局 symbol 注册表中。

var s1 = Symbol.for("foo"); // 创建一个 symbol 并放入 symbol 注册表中, key 为 "foo"
var s2 = Symbol.for("foo"); // 从 symbol 注册表中读取 key 为"foo"的 symbol

s1 === s2 // true

Symbol.keyFor

Symbol.keyFor(symbol);

该方法用于从 symbol 注册表中获取与某个 symbol 关联的键:

// 创建一个 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 属性名遍历

使用 Symbol 作为对象的属性名是,无论使用 for...infor...of 循环或者 Object.keys()Object.getOwnPropertyNames() 方法均不会被遍历到,当然有一个Object.getOwnPropertySymbols 方法,可以获取指定对象的所有 Symbol 属性名:

var obj = {};
obj.a = 'a';
obj[Symbol('b')] = 'b';

for (var i in obj) {
console.log(i);
}
// a

for (var v in obj) {
console.log(v);
}
// a

console.log(Object.keys(obj)); // ["a"]
console.log(Object.getOwnPropertyNames(obj)); // ["a"]

console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(b)]

在上面代码中,Object.getOwnPropertySymbols 可以获取 Symbol 键名,但是无法获取到非 Symbol 键名,如果在需要遍历所有键名的场合可以尝试 Reflect.ownKeys :

var obj = {};
obj.a = 'a';
obj[Symbol('b')] = 'b';

console.log(Reflect.ownKeys(obj)); // ["a", Symbol(b)]

Symbol 的内置属性

除了可以定义自己使用的 Symbol 以外,JavaScript 还内建了一些在 ES5 之前没有暴露给开发者的符号,指向内部语言行为。这些符号可以使用以下属性访问:

Symbol.hasInstance

对象的 Symbol.hasInstance 属性指向一个函数,该函数用于判断某对象是否为某构造器的实例,即当调用 instanceof 运算符的时候实际上是调用对象的 Symbol.hasInstance 的属性对应的函数:

class MyArray {  
static (instance) {
return Array.isArray(instance);
}
}

console.log([] instanceof MyArray); // true
// 等价于:
console.log(MyArray[Symbol.hasInstance]([])); // true

Symbol.isConcatSpreadable

对象的 Symbol.isConcatSpreadable 属性指向一个布尔值,该函数用于配置某对象作为 Array.prototype.concat() 方法的参数时是否展开其数组元素。

首先对于数组而言,Symbol.isConcatSpreadable 属性默认为 true , 可以展开:

var a = [1, 2, 3];
var b = [4, 5];

console.log(a.concat(b)); // [1, 2, 3, 4, 5]

设置 数组 bSymbol.isConcatSpreadable 值为 false

var a = [1, 2, 3];
var b = [4, 5];

b[Symbol.isConcatSpreadable] = false;

console.log(a.concat(b)); // [1, 2, 3, [4, 5]]

类似数组的对象也可以展开,但它的 Symbol.isConcatSpreadable 属性默认为 false

var a = [1, 2, 3];
var b = {
length: 2,
0: 4,
1: 5
};

console.log(a.concat(b)); // [1, 2, 3, Object]

b[Symbol.isConcatSpreadable] = true;

console.log(a.concat(b)); // [1, 2, 3, 4, 5]

Symbol.iterator

对象的 Symbol.iterator 属性为对象定义了默认的迭代器。该迭代器可以被 for...of 循环结构使用。

var myIterable = {}
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};

[...myIterable] // [1, 2, 3]

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

这四个属性代表的行为类似,这里以 Symbol.match 属性为例说明:

对象的 Symbol.match 属性,指向一个函数。当执行 String.prototype.match() 时,如果该属性存在,会调用它。也就是说 str.match(obj) 等价于调用 obj[Symbol.match](str)

var a = {};
"Hello, world!".match(a); // null

var b = {
value: 'world',
[Symbol.match](string) {
return string.match(this.value);
}
}
"Hello, world!".match(b); // ["world", index: 7, input: "Hello, world!"]

同样的,执行 str.search(obj)str.replace(obj, replaceStr)str.split(obj, limit) 等价于执行 obj[Symbol.search](str)obj[Symbol.replace](str, replaceStr)obj[Symbol.split](str, limit)

var a = {
value: 'world',
[Symbol.search](string) {
return string.indexOf(this.value);
},
[Symbol.replace](string, replaceStr) {
return string.replace(this.value, replaceStr);
},
[Symbol.split](string, limit) {
return string.split(this.value, limit);
}
}

"Hello, world!".search(a); // 7
"Hello, world!".replace(a, 'Beijing'); // "Hello, Beijing!"
"Hello, world!".split(a); // ["Hello, ", "!"]

Symbol.species

对象的 Symbol.species 属性,指向一个构造函数。创建实例的时候,会使用这个方法的返回值作为构造函数,来创建新的对象

class MyArray1 extends Array {
}
var a1 = new MyArray1(1,2,3);
var mapped1 = a1.map(x => x * x);

console.log(mapped1 instanceof MyArray1); // true
console.log(mapped1 instanceof Array); // true

class MyArray2 extends Array {
// 覆盖 species 到父级的 Array 构造函数上
static get [Symbol.species]() { return Array; }
}
var a2 = new MyArray2(1,2,3);
var mapped2 = a2.map(x => x * x);

console.log(mapped2 instanceof MyArray2); // false
console.log(mapped2 instanceof Array); // true

在ES5 中 Array.prototype.map 等函数实现类似如下:

Array.prototype.map = function (callback) {
var returnValue = new Array(this.length);
this.forEach(function (item, index, array) {
returnValue[index] = callback(item, index, array);
});
return returnValue;
}

在ES6 中,Array.prototype.map 等函数实现奖创建对象升级为使用 Symbol.species 属性,其实现类似如下:

Array.prototype.map = function (callback) {
var Species = this.constructor[Symbol.species];
var returnValue = new Species(this.length);
this.forEach(function (item, index, array) {
returnValue[index] = callback(item, index, array);
});
return returnValue;
}

这种修改可以保证在自定义的数组的子类上执行这些函数,可以让返回的结果的类型能够回归到 Array 类型,否则,直接执行这些函数返回的类型是 MyArray 子类型。数组的 slicefilterspliceconcat 方法都是这样。

Symbol.toPrimitive

对象的 Symbol.toPrimitive 属性,指向一个函数。当此对象被转为原始类型的值时,会调用这个方法。

比如,当执行 +object 的时候,实际上会调用 object[Symbol.toPrimitive]('number') ,当执行 ''+object ,实际上会调用 object[Symbol.toPrimitive]('string') , 执行 if(object) 实际上会调用 object[Symbol.toPrimitive]('default') , 通过 Symbol.toPrimitive 可以自由改变对象被类型转换的返回结果:

// 没有 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) {
console.log(`hint is ${hint}`);
if (hint === 'string') {
return 1;
} else if (hint === 'number') {
return 2;
} else {
return 3;
}
}
};

console.log(+obj2);
// hint is number
// 2

console.log(`${obj2}`);
// hint is string
// 1

console.log(obj2 + "");
// hint is default
// 3

注意,hint 的取值只有 numberstringdefault 三种,当在进行类型转换的时候,如果 hint === 'number' 分支的返回结果不能转换为数字,则会返回 NAN

Symbol.toStringTag

对象的 Symbol.toStringTag 属性,指向一个函数,当对象被调用 Object.prototype.toString 方法的时候,这个函数的返回标签会出现在 toString 方法返回的字符串中:

var obj = {};
obj.toString(); // "[object Object]"

var obj = {
get [Symbol.toStringTag]() {
return 'Obj';
}
}
obj.toString(); // "[object Obj]"

应用场景

存储对象的基础属性

Symbol 唯一的特性可以保证作为对象的属性名,其属性值不会被误修改:

Symbol('foo') === Symbol('foo'); // false

var obj = {};
var foo1 = Symbol('foo');
var foo2 = Symbol('foo');
obj[foo1] = 1;
obj[foo2] = 2;

obj[foo1] === 1 // true
obj[foo2] === 2 // true

由于外界拿不到 foo1foo2 的引用,就可以保证了 它们对应的属性值不会被修改。

但是,这只能防止被误修改,但是实际上还是能被修改的:

var obj = {
[Symbol('foo')]: 1
}

console.log(obj); // Object {Symbol(foo): 1}

var symbolKeys = Object.getOwnPropertySymbols(obj);
obj[symbolKeys[0]] = 2;
console.log(obj); // Object {Symbol(foo): 2}

因此,这种存储方式并不是完全私有的,可以存放一些不想被误修改的数据又不介意被修改的数据。

替代标识用的静态变量

比如当记录日志的时候,通常会有多个日志的级别:debuginfowarn ,以前通常是这么写的:

var levels = {
DEBUG: 'debug',
INFO: 'info',
WARN: 'warn',
};
// 或
var levels = {
DEBUG: 1,
INFO: 2,
WARN: 4,
};

这样写也可以工作,但是这些值并不唯一,可能会和其他值冲突。而实际上这些值取什么并无意义,只需要确保不会和其他值冲突即可。因此,完全可以使用 Symbol 替代:

var levels = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn'),
};
console.log(levels.DEBUG, 'debug message'); // Symbol(debug) "debug message"
console.log(levels.INFO, 'info message'); // Symbol(info) "info message"

提供一个修改的钩子

比如之前提到的 Symbol.toStringTag 等就是一个 toString 方法的钩子,使用这个可以改变自定义的对象的显示名称,这在开发工具函数也很有用:

var inspect = Symbol('inspect');
var log = (obj) => {
if (obj[inspect]) {
console.log(obj[inspect]);
return ;
}
console.log(obj);
}

var obj1 = {};
log(obj1); // Object {}

var obj2 = {
[inspect]: "This is object2",
};
log(obj2); // This is object2

如上述例子,编写了一个增强的 log ,默认使用 console.log 直接打印对象,但是如果对象存在 inspect 这个属性值 则打印这个属性值的内容,这个属性值就是提供的一个改变函数行为的钩子。

参考