Symbol类型:
-
ES6新增数据类型。符号是原始值,且符号实例是唯一、不可变的。
-
符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
-
symbol并不是为了提供私有属性的行为而新增的。
-
符号就是用来·创建唯一记号,进而用作非字符串形式的对象属性。
-
symbol的基本使用:
- 符号使用Symbol()函数初始化。因为符号本身是原始类型,typeof返回’symbol’
let sym = Symbol(); console.log(typeof sym); // 'symbol'
- 调用Symbol()函数时,也可以传入一个字符串参数作为对符号的描述(description),方便日后通过这个字符串来调试代码。
- 这个字符串参数与符号定义或标识完全无关。
let genericSymbol = Symbol(); let otherGenericSymbol = Symbol(); let fooSymbol = Symbol('foo'); let otherFooSymbol = Symbol('foo'); console.log(genericSymbol == otherGenericSymbol); // false console.log(fooSymbol == otherFooSymbol) // false
-
【重要】Symbol()函数不能与new关键字一起作为构造函数使用。这样做是为了避免创建符号包装对象。
-
Boolean、String、Number都支持构造函数且可用于初始化包含原始值的包装对象:
let myBoolean = new Boolean();
console.log(typeof myBoolean); // 'object'
let myString = new String();
console.log(typeof myString); // "object"
let myNumber = new Number();
console.log(typeof myNumber); // "object"
let mySymbol = new Symbol(); // typeerror
- 但可以使用Object()函数来实现符号包装对象:
let my Symbol = Symbol();
let myws = Object(mySymbol);
typeof myws; // 'object'
-
使用全局符号注册表
-
如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符作为键,在全局符号注册表中创建并重用符号。
-
使用Symbol.for()方法
-
Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,若不存在则生成一个新符号实例并添加到注册表中。后续使用发现之,返回之。
-
即使采用相同的符号描述,在全局注册表中定义的符号跟使用Symbol()定义的符号也并不完全等同。
-
全局注册表中的符号必须使用字符串键来创建,因此作为参数传给Symbol.for()的任何值都会被转换成字符串。
-
此外,注册表中使用的键同时也会被用作符号描述。
let emptyGS = Symbol.for(); emptyGS // Symbol(undefined)
- Symbol.keyFor(value):查询全局注册表;value是符号,返回该全局符号对应的字符串键。
- 若查询的不是全局符号,返回undefined;
- 若value不是符号,则抛出TypeError;
let s = Symbol.for('foo'); // 全局符号 let s2 = Symbol('bar'); // 普通符号 Symbol.keyFor(s); // foo Symbol.keyFor(s2); // undefined
-
-
使用符号作为属性
- 凡是可以使用字符串或数值作为属性的地方,都可以使用符号。
- 包括对象字面量属性和Object.defineProperty()/Object.defineProperties()定义的属性。
- 对象字面量只能在计算属性语法中使用符号作为属性。
let s1 = Symbol('foo'), s2 = Symbol('bar'), s3 = Symbol('baz'), s4 = Symbol('qux'); let o = { [s1]: 'foo val' }; // 这样也可以:o[s1] = 'foo val'; console.log(o); // {Symbol(foo): foo val} Object.defineProperty(o, s2, {value: 'bar val'}); console.log(o); // {Symbol(foo): foo val, Symbol(bar): bar val} Object.defineProperties(o, { [s3]: {value: 'baz val'}, [s4]: {value: 'qux val'} }); console.log(o); // {Symbol(foo): foo val, Symbol(bar): bar val, // Symbol(baz): baz val, Symbol(qux): qux val}
- Object.getOwnPropertyNames():返回对象实例的常规属性数组
- Object.getOwnPropertySymbols():返回对象实例的符号属性数组。
- 这2个方法的返回值彼此互斥。
- Object.getOwnPropertyDescriptions():返回同时包含常规和符号属性描述的对象。
- Reflect.ownKeys():返回2种类型的键。
let s1 = Symbol('foo'), s2 = Symbol('bar'); let o = { [s1]: 'foo val', [s2]: 'bar val', baz: 'baz val', qux: 'qux val' }; console.log(Object.getOwnPropertySymbols(o)); // [Symbol(foo), Symbol(bar)] console.log(Object.getOwnPropertyNames(o)); // ["baz", "qux"] console.log(Object.getOwnPropertyDescriptors(o)); // {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}} console.log(Reflect.ownKeys(o)); // ["baz", "qux", Symbol(foo), Symbol(bar)]
- 因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。
- 若没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键。
let o = { [Symbol('foo')]: 'foo val', [Symbol('bar')]: 'bar val' } console.log(o); // {Symbol(foo): 'foo val', Symbol(bar): 'bar val'} let barSymbol = Object.getOwnPeopertySymbols(o).find((symbol) => symbol.toString().match(/bar/)) console.log(barSymbol) // Symbol(bar)
-
常用内置符号
-
ES6引入了一批常用内置符号well-known symbol,用于暴露语言内部行为,开发者可以直接调用、重写或,模拟这些行为。
-
这些内置符号都以Symbol工厂函数数字字符串属性的形式存在。
-
内置符号最重要的用途就是重新定义它们,从而改变原生结构的行为。
-
例如:
- for-of循环会在对象上使用Symbol.iterator属性。那么就可以在自定义对象上重新定义Symbol.iterator的值,来改变for-of在迭代该对象时的行为。
-
所有内置符号属性都是不可写、不可枚举、不可配置的。
-
-
Symbol.asyncIterator
- 这个符号作为一个属性表示“一个方法,该方法返回对象默认的AsyncIterator。由for-await-of语句使用”。
- 这个符号其实就是表示实现异步迭代器API的函数。
- for-await-of循环会利用这个函数执行异步迭代操作。循环时,它们调用以Symbol.asyncIterator为键的函数,并期望这个函数返回一个实现迭代器API的对象。很多时候,返回的对象是实现该API的AsyncGenerator:
class Foo { async *[Symbol.asyncIterator]() {} } let f = new Foo(); consoel.log(f[Symbol.asyncIterator]()); // AsyncGenerator {<suspended>}
- 技术上,这个由Symbol.asyncIterator函数生成的对象应该通过其next()方法陆续返回Promise实例。
- 可以通过显示地调用next()方法返回,也可以隐式地通过异步生成器函数返回:
class Emitter { constructor (max) { this.max = max; this.asyncIdx = 0; } async *[Symbol.asyncIterator]() { while (this.asyncIdx < this.max) { yield new Promise((resolve, reject) => resolve(this.asyncIdx)); } } } async function asyncCount () { let emitter = new Emitter(5); for await (const x of emitter) { console.log(x); } } // 0 // 1 // 2 // 3 // 4
- Symbol.asyncIterator是ES2018规范定义的,只有版本非常新的浏览器支持它
-
Symbol.hasInstance
- 这个符号作为一个属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由instanceof操作符使用”
- instanceof操作符可以用来确定一个对象实例的原型链上是否有原型。instanceof的典型使用场景:
function Foo() { let f = new Foo(); console.log(f instanceof Foo); // true } class Bar {} let b = new Bar() console.log(b instanceof Bar); // true
- 在ES6中,instanceof操作符会使用Symbol.hasInstance函数来确认关系。
function Foo () {} let f = new Foo() Foo[Symbol.hasInstance](f); // true class Bar {} let b = new Bar(); console.log(Bar[Symbol.hasInstance](b)); // true
class Bar {} class Baz extends Bar { static [Symbol.hasInstance]() { return false } } let b = new Baz(); Bar[Symbol.hasInstance](b); // true b instanceof Bar; // true Baz[Symbol.hasInstance](b); // false b instanceof Baz; // false
-
Symbol.isConcatSpreadable
- 这个符号作为一个属性表示“一个布尔值,如果是 true,则意味着对象应该用 Array.prototype.concat()打平其数组元素”
-
Symbol.iterator
- 这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。由for-of语句使用”
-
Symbol.match
- 根据 ECMAScript规范,这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式去匹配字符串。由 String.prototype.match()方法使用”。
- String.prototype.match()方法会使用以Symbol.match为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义。
RegExp.prototype[Symbol.match] // f [Symbol.match] () { [native code] } console.log('foobar'.match(/bar/)); // ["bar", index: 3, input: "foobar", groups: undefined]
- 给这个方法传入非正则表达式会导致该值被转换成RegExp对象。如果想改变这种行为,让方法直接使用参数,则可以重新定义 Symbol.match 函数以取代默认对正则表达式求值的行为,从而让match()方法使用非正则表达式实例。Symbol.match 函数接收一个参数,就是调用 match()方法的字符串实例。
- 返回值没有限制:
class FooMatcher { static [Symbol.match] (target) { return target.includes('foo'); } } console.log('foobar'.match(FooMatcher)); // true console.log('barbaz'.match(FooMatcher)); // false class StringMatcher { constructor (str) { this.str = str; } [Symbol.match] (target) { return target.includes(this.str); } } console.log('foobar'.match(new StringMatcher('foo'))); // true
-
Symbol.replace
- 根据ES规范,这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串”。由String.prototype.replace()方法使用。
- String.prototype.replace()方法回使用以Symbol.replace为键的函数来对正则表达式求值。
- 正则表达式的原型上默认有这个函数的定义:
console.log(RegExp.prototype[Symbol.replace]); // f [Symbol.replace]() [native code] console.log('foobarbaz'.replace(/bar/, 'qux')); // 'fooquxbaz
- 给这个方法传入非正则表达式值会导致该值被转换为RegExp对象。
- 要想改变这种行为,让方法直接使用参数,可以重新定义Symbol.replace函数以取代默认对正则表达式求值的行为,从而让replace()使用非正则表示式实例。
- Symbol.replace函数接收两个参数,即调用replace()方法的字符串实例和替换字符串。返回的值没有限制:
class FooReplacer { static [Symbol.replace] (target, replacement) { return target.split('foo').join(replacement); } } console.log('barfoobaz'.replace(FooReplacer, 'qux')); // "barquxbaz" class StringReplacer { constructor(str) { this.str = str; } [Symbol.replace] (target, replacement) { return target.split(this.target.split(this.str).join(replacement)); } } console.log('barfoobaz'.replace(new StringReplacer('foo'), 'qux')); // "barquxbaz"
-
Symbol.search
- 根据ES规范,这个符号作为一个属性表示“一个正则表达式方法,此方法返回字符串中匹配正则表达式的索引。由String.prototype.search()方法使用”
- String.prototype.search()方法会使用Symbol.search为键的函数来对正则表达式进行求值。
- 正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String方法的有效参数:
console.log(RegExp.prototype[Symbol.search]); // f [SYmbol.search] () { [native code]} console.log('foobar'.search(/bar/)); // 3
- 给这个方法传入非正则表达式会导致该值被转换成RegExp对象。如果想改变这种行为,让方法直接使用参数,可以重新定义Symbol.search函数以取代默认对正则表达式求值的行为,从而让search()方法使用非正则表达式实例。
- Symbol.search函数接收一个参数,就是调用match()方法的字符串实例。返回的值没有限制:
class FooSearcher { static [Symbol.search] (target) { return target.indexOf('foo'); } } console.log('foobar'.search(FooSearcher)); // 0 console.log('barfoo'.search(FooSearcher)); // 3 console.log('barbaz'.search(FooSearcher)); // -1; 表示没有找到 class StringSearcher { constructor (str) { this.str = str } [Symbol.search] (target) { return target.indexOf(this.str); } } console.log('foobar'.search(new StringSearcher('foo'))); // 0
-
Symbol.species
- 根据ES规定,这个符号作为一个属性表示“一个函数值,该函数值作为创建派生对象的构造函数。”
- 这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。
- 用Symbol.species定义静态的获取器(getter)方法,可以覆盖创建实例的原型定义:
class Bar extends Array {} class Baz extends Array { static get [Symbol.species]() { return Array; } } let bar = new Bar(); console.log(bar instanceof Array); // true console.log(bar instanceof Bar); // true bar = bar.concat('bar'); console.log(bar instanceof Array); // true console.log(bar instanceof Bar); // true let baz = new Baz(); console.log(baz instanceof Array); // true console.log(baz instanceof Baz); // true baz = baz.concat('baz'); console.log(baz instanceof Array); // true console.log(baz instanceof Baz); // false
-
Symbol.split
- ES规定,这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由String.prototype.split()方法使用”。
- String.prototype.split()方法使用Symbol.split为键的函数来对正则表达式求值。正则表达式的原型上默认由这个函数的定义:
console.log(RegExp.prototype[Symbol.split]); // f [Symbol.split] () { [native code] } console.log('foobarbaz'.split(/baz/)); // ['foo', 'baz']
- 给这个方法传入非正则表达式会导致该值被转换成RegExp对象。如果想改变这种行为,让方法直接使用参数,可以重新定义Symbol.split函数以取代默认对正则表达式求值的行为,从而让方法直接使用非正则表达式实例。
class FooSplitter { static [Symbol.split] (target) { return target.split('foo'); } } console.log('barfoobaz'.split(FooSpliter)); // ["bar", "baz"] class StringSplitter { constructor (str) { this.str = str } [Symbol.split] (target) { return target.split(this.str) } } console.log('barfoobaz'.split(new StringSplitter('foo'))); // ["bar", "baz"]
-
Symbol.toPrimitive
- ES规定,这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始值。由ToPrimitive抽象操作使用”。
- 很多内置操作都会尝试强制将对象转换为原始值,包括字符串、数值、未指定的原始类型。
- 对于一个自定义对象实例,通过在这个实例的Symbol.toPrimitive属性上定义一个函数可以改变默认行为。
- 根据提供给这个函数的参数(string、number或default),可以控制返回值的原始值:
class Foo {} let foo = new Foo(); console.log(3 + foo); // "3[object Object]" console.log(3 - foo); // NaN console.log(String(foo)); // "[object Object]" class Bar { constructor() { this[Symbol.toPrimitive] = function(hint) { switch(hint) { case 'number': return 3; case 'string': return 'string bar'; case 'default': default: return 'default bar'; } } } } let bar = new Bar(); console.log(3 + bar); // "3default bar" console.log(3 - bar); // 3-3 => "0" console.log(String(bar)); // "string bar" Number(bar); // 3
-
Symbol.toStringTag
- 根据ES规定,这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法Object.prototype.toString()方法使用”
- 通过toString()方法获取对象标识时,会检索由Symbol.toStringTag指定的实例标识符,默认为“Object”。内置类型已经指定了这个值,但自定义类实例还需要明确定义:
let s = new Set(); s // Set(0) {} s.toString() // "[object Set]" s[Symbol.toStringTag]; // "Set" class Foo {} let foo = new Foo(); foo // Foo {} foo.toString() // [object Pbject] foo[Symbol.toStringTag] // undefined class Bar { constructor() { this[Symbol.toStringTag] = 'Bar'; } } let bar = new Bar(); console.log(bar); // Bar {} bar.toString() // [object Bar] bar.[Symbol.toStringTag] // Bar
-
Symbol.unscopables
- 根据ES,这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性,都会从关联对象的with环境百年孤独中排除”
- 设置这个符号并让其映射对应属性的键值为true,就可以阻止该属性出现在with环境绑定中:
let o = { foo: 'bar' }; with (o) { console.log(foo); // bar } o[Symbol.unscopables] = { foo: true } width (o) { console.log(foo); // ReferenceError }
- 不推荐使用 with,因此也不推荐使用 Symbol.unscopables。