Symbol
Symbol 是一种互不等价的标志,字面量对象除了可以使用字符串、数字作为属性键以外,还可以使用 Symbol 作为属性键,因此便可以利用 Symbol 值的互不等价特性来实现属性操作的互不干扰。
目录:
基本语法
生成唯一的 Symbol 值
执行 Symbol([description]) 函数可以生成一个与其他 Symbol 值互不等价的 Symbol 值,其中 Symbol() 函数可以接受一个除 Symbol 值以外的值作为该 Symbol 值的描述,以便通过开发者的辨认判断其为可选的。
const symbol = Symbol(); // Symbol()
const symbolForSomething = Symbol('something'); // Symbol(something)
const symbolWithNumber = Symbol(3.14); // Symbol(3.14)
const symbolWithObject = Symbol({'foo': 'bar'}); // Symbol([object Object])
// Don't use a symbol to be another symbol's description
const anotherSymbol = Symbol(symbol); // Uncaught TypeError: Cannot convert a Symbol value to a string
需要注意的是,我们设定的描述仅仅是起到了描述的作用,而不会对 Symbol 值本身起到任何的改变作用,因此,即使是两个具有相同描述值的 Symbol 值也并不具有等价性。
const symbol1 = Symbol('foo');
const symbol2 = Symbol('foo');
symbol1 == symbol2; // false
还要非常注意的一点是, Symbol 函数并不是一个构造函数,所以开发者不能使用 new 语句来生成一个 Symbol “对象”,否则将会抛出一个 TypeError 错误。
new Symbol(); // Uncaught TypeError: Symbol is not a constructor
由此可知,Symbol 是一种值类型而非引用类型,这就意味着如果将 Symbol 值作为函数形参进行传递,将会进行复制值传递而非引用传递,这跟其他值类型(字符串、数字等)的行为是一致的。
const symbol = Symbol('hello');
function fn1(_symbol){
return _symbol == symbol;
}
console.log(fn1(symbol)); // true
function fn2(_symbol){
_symbol = null;
console.log(_symbol);
}
fn2(symbol); // null
console.log(symbol); // Symbol(hello)
当然,如果真的希望得到一个 Symbol “对象”,可以使用 Object() 函数实现。
const symbol = Symbol('foo');
typeof symbol; // symbol
const symbolObj = Object(symbol);
typeof symbolObj; // object
注册全局可重用 Symbol
开发者可以通过一个 key 向当前运行时注册一个需要在其他程序中使用的 Symbol:Symbol.for([key]) : Symbol
cosnt symbol = Symbol.for('foo');
Symbol.for() 与 Symbol() 区别是,Symbol.for() 会根据传入的 key 在全局作用域中注册一个 Symbol 值,如果某一个 key 从未被注册到全局作用域中,便会创建一个 Symbol 值并根据 key 注册到全局环境中,如果该 key 已被注册,就会返回一个与第一次使用创建的 Symbol 的值等价的 Symbol 值。
const symbol = Symbol.for('foo');
const obj = {};
obj[symbol] = 'bar';
// ...
const anotherSymbol = Symbol.for('foo');
console.log(symbol === anotherSymbol); // true
console.log(obj[anotherSymbol]); // bar
获取全局 Symbol 的 key
用法:Symbol.keyFor(<global symbol>) : String
。
const symbol = Symbol.for('foo');
console.log(Symbol.keyFor(symbol)); // foo
常用 Symbol 值
定义名 | 描述 | 含义 |
---|---|---|
@@iterator | “Symbol.iterator” | 用于为对象定义一个方法并返回一个属于所对应对象的迭代器。该迭代器会被 for-of 循环语句所使用 |
@@hasInstance | “Symbol.hasInstance” | 用于为类定义一个方法。该方法会因为 instanceof 语句的使用而被调用,来检查一个对象是否是某一个类的实例 |
@@match | “Symbol.match” | 用于为正则表达式定义一个可被 String.prototype.match() 方法使用的方法,检查对应的字符串与当前正则表达式是否匹配 |
@@replace | “Symbol.replace” | 用于为正则表达式或对象定义一个方法。该方法会因为 String.prototype.replace() 方法的使用而被调用,用于处理当前字符串使用该正则表达式或对象作为替换标志时的内部处理逻辑 |
@@match | “Symbol.match” | 用于为正则表达式定义一个可被 String.prototype.match() 方法使用的方法,检查对应的字符串与当前正则表达式是否匹配 |
@@search | “Symbol.search” | 用于为正则表达式或对象定义一个方法。该方法会因为 String.prototype.search() 方法的使用而被调用,用于处理当前字符串使用该正则表达式或对象作为位置检索标志时的内部处理逻辑 |
@@split | “Symbol.split” | 用于为正则表达式或对象定义一个方法。该方法会因为 String.prototype.split() 方法的使用而被调用,用于处理当前字符串使用该正则表达式或对象作为分割标志时的内部处理逻辑 |
@@unscopables | “Symbol.unscopables” | 用于为对象定义一个属性,该属性用于描述该对象中哪些属性时可以被 with 语句所使用的 |
@@isConcatSpreadable | “Symbol.isConcatSpreadable” | 用于为对象定义一个属性,该属性用于决定该对象作为 Array.prototype.concat() 方法的参数时,是否会被展开 |
@@species | “Symbol.species” | 用于为类定义一个静态属性,该属性用于决定该类的默认构建函数 |
@@toPrimitive | “Symbol.toPrimitive” | 用于为对象定义一个方法,该方法会在该对象需要转换为值类型的时候被调用可以根据程序的行为决定该对象需要被转换成的值 |
@@toStringTag | “Symbol.toStringTag” | 用于为类定义一个属性,该属性可以决定这个类的实例在调用 toString() 方法时,其中的标签内容 |
iterator
在 ES6 中定义了可迭代对象(Iterable Object)和新的 for-of 循环语句,其中可迭代对象并不是一种类型,而是带有@@iterator 属性和可以被 for-of 循环语句所遍历的对象的统称。
ES6 中默认的可迭代对象有: 数组(Array)、字符串(String)、类型数组(TypedArray)、映射对象(Map)、集合对象(Set)和生成器实例(Generator),而在浏览器的运行环境中,DOM 元素集合(如 NodeList) 也是可迭代对象。
// Array
for(const el of [1, 2, 3]) console.log(el);
// String
for(const word of 'hello world') console.log(word);
// TypedArray
for(const value of new Uint8Array([0x00, 0xff])) console.log(value);
// Map
for(const entry of new Map([['a', 1], ['b', 2]])) console.log(entry);
// Set
for(const el of new Set([1, 2, 3, 3, 3])) console.log(el);
// Generator
function* fn(){ yield 1; }
for(const value of fn()) console.log(value);
可迭代对象都会有一个使用 Symbol.iterator 作为方法名的属性,该方法会返回一个迭代器(Iterator)。把迭代器看作一种协议,即迭代器协议(Iterator Protocol),该协议定义了一个方法 next(),含义是进入下一次迭代的迭代状态,第一次执行返回第一次的迭代状态,该迭代状态对象需要带有两个属性。
属性 | 类型 | 含义 |
---|---|---|
done | Boolean | 该迭代器是否已经迭代结束 |
value | Any | 当前迭代状态的值 |
利用 Symbol.iterator 在对象中实现一个迭代器需要将 Symbol.iterator 作为属性键,定义一个方法。
实现单向链表:
class Item{
constructor(value, prev = null, next = null){
this.value = value;
this.next = next;
}
get hasNext(){ return !!this.next}
}
class LinkedList{
constructor(iterable){ /* ... */}
push(value){ /* ... */}
pop(){ /* ... */}
// ...
get length(){ /* ... */}
get head(){ /* ... */}
get tail(){ /* ... */}
}
为其定义相关方法,使其支持 for-of 循环语句的迭代,首先为链表类添加一个以 Symbol.iterator 为属性键的方法:
class LinkedList{
[Symbol.iterator](){
// ...
}
}
假设我们需要让链表对象在 for-of 循环语句中是顺序遍历的,那么就可以将每一个元素都作为迭代器的一个状态:
[Symbol.iterator](){
let currItem = {next: this.head}
return {
next(){
currItem = currItem.next;
currItem.next = currItem.next || {hasNext: false}
return {
value: currItem.value,
done: !currItem.hasNext
}
}
}
}
此处需要注意的是,迭代器需要以一个空状态作为结束标志,也就是说即使迭代器遍历到了需要输出的最后一个元素,其所对应的状态依然是未结束状态(done: false),所以最后需要再输出一个空状态来表示结束。
hasInstance
Symbol.hasInstance 为开发者提供了可以用于扩展 instanceof 语句内部逻辑的权限,开发者可以将其作为属性键,用于为一个类定义静态方法,该方法的第一个形参便是被检测的对象,而该方法的返回值便是决定了当次 instanceof 语句的返回结果。
class Foo{
static [Symbol.hasInstance](obj){
console.log(obj); // {}
return true;
}
}
console.log({} instanceof Foo); // true
match
开发者可以通过 Symbol.match 来自行实现 match() 方法的运行逻辑。
const re = /foo/;
re[Symbol.match] = function(str){
const regexp = this;
console.log(str); // bar
// ...
return true;
}
'bar'.match(re); // true
unscopables
with 语句的作用是将一个对象变成一段代码的上下文,在这段代码中变量或者表达式的寻找都会先从这个对象的属性开始,但有些方法是不能被 with 语句所展开的,也就是说不能被 with 语句所包含的代码检索到。
const arr = [1, 2, 3, 4, 5];
with(arr){
console.log(slice(0, 3)); // [1, 2, 3]
console.log(entries()); // ReferenceError: entries is no defined
}
能不能被检索到都是通过 Symbol.unscopables 作为属性键所定义的,我们可以尝试着把 Array 类型的 entries 方法设置为 false(为 true 表示该属性禁止在 with 语句中被使用):
const arr = [1, 2, 3, 4, 5];
arr[Symbol.unscopables].entries = false;
with(arr){
console.log(entries()); // ArrayIterator {}
}
这也可以用在开发者自行创建的对象中:
const data = {
foo: 1,
bar: 2,
something:3
}
const something = 'boom';
data[Symbol.unscopables] = {
foo: true,
bar: false,
something: true
}
with(data){
console.log(bar); // 2
console.log(something); // boom
console.log(foo); // ReferenceError: foo is no defined
}
toPrimitive
Symbol.toPrimitive 提供给开发者自定义处理引用类型的对象转化为值类型的控制。
开发者可以使用 Symbol.toPrimitive 作为属性键为对象定义一个方法,这个方法接受一个参数,这个参数用于判断当前隐式转换的目标类型。
参数值 | 含义 |
---|---|
number | 该次隐式转换的目标类型为数字 |
string | 该次隐式转换的目标类型为字符串 |
default | 引擎无法判断该次目标类型 |
需要注意的式,这里的 default 并不是因为目标类型无法被转换,而是因为语法上容易造成混乱,比如: 10 + obj + “” ,处于严谨,在 ES6 中被定义为 default ,由开发者自行决定:
const obj = {};
console.log(+obj); // NaN
console.log(`${obj}`); // [object Object]
console.log(obj + ""); // [object Object]
const Ten = {
[Symbol.toPrimitive](hint){
switch(hint){
case 'number': return 10;
case 'string': return 'Ten';
case 'default': return true;
}
}
}
console.log(+Ten); // 10
console.log(`${Ten}`); // Ten
console.log(Ten + ""); // true
toStringTag
Symbol.toStringTag 的作用式可以决定这个类的实例在调用 toString() 方法时的标签内容:
class Bar{};
class Foo{
get [Symbol.toStringTag](){ return 'bar'; }
}
const obj = new Foo();
console.log(obj.toString()); // [object Bar]