JavaScript 原始类型类型三(Symbol)

322 阅读13分钟

Symbol(符号)是ECMAScript6 新增的数据类型。符号是原始值,且符号实例是唯一,不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。 尽管听起来跟私有属性有点类似,但符号并不是为了提供私有属性的行为才增加的。相反符号就是用来创建唯一记号,进而用做非字符串形式的对象属性。

符号的基本用用法

符号需要使用Symbol()函数初始化。因为符号本身是原始类型,所以typeof 操作符对符号返回symbol。

let sym = Symbol();
console.log(typeof sym);//symbol

调用Symbol()函数时,也可以传入一个字符串参数作为对符号的描述,将来可以通过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识完全无关。

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()实例并将其作为对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性。

最重要的是,Symbol()函数不能与new 关键字一起作为构造函数使用。

使用全局符号注册表

如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号表中创建并重用符号。 为此,需要使用Symbol.for()方法。

let foolGobalSymbol = Symbol.for('foo');
let otherFooGlobalSymbol = Symbol.for('foo');

console.log(foolGobalSymbol ==  otherFooGlobalSymbol); // true

即使采用相同的符号描述,在全局注册表中定义的符号跟使用Symbol()定义的符号也并不相同。

let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');

console.log(localSymbol ==  globalSymbol); // false

全局注册表中的符号必须使用字符串键来创建,因此作为参数传递给Symbol.for()的任何值都会被转换为字符串。此外,注册表中使用的键同时也会被用作符号描述。

let emptyGlobalSymbol = Symbol.for();
console.log(emptyGlobalSymbol);// Symbol(undefined)

还可以使用Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回undefined。

let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');
console.log(Symbol.keyFor(localSymbol));// undefined
console.log(Symbol.keyFor(globalSymbol));// foo

使用符号作为属性

凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和Object.defineProperty()/Object.defineProerties()定义的属性。对象字面量只能在计算属性语法中使用符号作为属性。

let s1 = Symbol('foo');
let s2 = Symbol('bar');
let s3 = Symbol('baz');
let 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'});
conslo.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()返回对象实例的符号属性数组。这两个方法的返回值互斥。Object.getOwnPropertyDescriptors会返回包含常规和符号属性的描述符的对象。Reflect.ownKeys()会返回两种类型的键。

因为符号属性是对内存中符号的一个引用,所以直接创建并用做属性的符号不会丢失。但是,如果没有显示的保存对这些属性的引用。那么必须便历对象的所有符号属性才能找到相应的属性键。

常用内置符号

ECMAScript 6也引入了一批常用内置符号,用于暴露语言内部行为,开发者可以直接访问,重写或模拟这些行为。这些内置符号都以Symbol 工厂符号属性的形式存在。
这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如我们知道for-of循环会在相关对象上使用Symbol.iterator属性,那么就可以通过在自定义对象上重新定义Symbol.iterator的值,来改变在迭代该对象时的行为。
这些内置符号没有什么特别之处,它们就是全局函数Symbol的普通字符串属性,指向一个符号的实例。所有内置符号属性都是不可写,不可枚举,不可配置的。

Symbol.asyncIterator

根据ECMAScript规法,这个符号作为一个属性表示“一个方法,该方法返回对象默认的AsyncIterator。由for-await-of 语句使用”。换句话说,这个符号表示实现异步迭代器API的函数。
for-await-of 循环会利用这个函数执行异步迭代操作。循环时,它们会调用以Symbol.asyncIterator为键的函数,并期望这个函数会返回一个实现迭代器API的对象。很多时候,返回的对象是实现该API的AsyncGenerator。

class Foo {
    async *[Symbol.asyncIterator](){}
}

let f = new Foo();
console.log(f[Symbol.asyncIterator]);// AsyncGenerator{<supspended>}

技术上,这个由Symbol.asyncIterator 函数生成的对象应该通过其next()方法陆续返回Promise 实例。可以通过显示的调用next()方法返回,也可以隐式的通过异步生成器函数返回。

    class Emitter {
       constructor(max){
           this.max = max;
           this.asyncIdx = 0;
       }
       async *[Symbol.asyncIterator]() {
           while(this.asyncIndex < this.max){
              yield new Promise ((resolve) => resolve(this.asyncIdx++));
           }
       }
    }
    
    async function asyncCount() {
        let emitter = new Emitter(5);
        for await(const x of emitter) {
           console.log(x);
        }
    }
    asyncCount();
    //0
    //1
    //2
    //3
    //4

Symbol.hasInstance

根据ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法决定一个构造器函数是否认可一个对象是它的实例。由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 函数来确定关系。以Symbol.hasInstance 为键的函数会执行同样的操作,只是操作数对调了一下。

    function Foo (){}
    let f = new Foo();
    console.log(Foo[Symbol.hasInstance](f));// true
    
    class Bar{}
    let b = new Bar();
    console.log(Bar[Symbol.hasInstance](b));//true

这个属性定义在Function 的原型上,因此默认在所有函数和类上都可以调用。由于instanceof 操作符会在原型链上寻找这个属性定义,就跟在原型链上寻找其他属性一样,因此可以在继承的类上通过静态方法重新定义这个函数。

    class Bar {}
    class Baz extends Bar {
        static [Symbol.hasInstance](){
            return false;
        }
    }
    
    let b = new Baz();
    console.log(Bar[Symbol.hasInstance](b));//true
    console.log(b instanceof Bar);//true
    console.log(Baz[Symbol.hasInstance](b))//false
    console.log(b instanceof Baz);// false
    

Symbol.isConcatSpreadable

根据ECMAScript 规范,这个符号作为一个属性表示“一个布尔值如果是true,则意味着对象应该用Array.prototype.concat()打平其数组元素”。ES6中的Array.prototype.concat()方法会根据收到的对象类型选择如何将一个数组对象拼接成数组实例。覆盖Symbol.isConcatSpreadable的值可以修改这个行为。
数组对象默认情况下会被打平到已有的数组,false或假值会导致整个对象被追加到数组末尾。
类数组对象默认情况下会被追加到数组末尾,true 或真值会导致这个类数组对象被打平到数组实例。
其他不是类数组对象的对象在Symbol.isConcatSpreadable被设置为true 的情况下被忽略。

    let initial = ['foo'];
    let array == ['bar'];
    console.log(array[Symbol.isConcatSpreadable]); // undefined
    console.log(inital.concat(array));// ['foo','bar']
    array[Symbol.isConcatSpreadable] = false;
    console.log(initial.concat(array)); // ['foo',Array(1)]
    
    let arrayLikeObject = {length :1, 0: 'baz'};
    console.log(arrayLikeObject[Symbol.isConcatSpreadable]);//undeifned
    console.log(initial.concat(arrayLikeObject)); // ['foo',{0: 'baz', length: 1, Symbol(Symbol.isConcatSpreadable): true}]
    arrayLikeObject[Symbol.isConcatSpreadable] = true;
    console.log(initial.concat(arrayLikeObject));// ['foo','baz']
    
    
    let otherObject = new Set().add('qux');
    console.log(otherObject[Symbol.isConcatSpreadable]);// undefined
    otherObject[Symbol.isConcatSpreadable] = true;
    console.log(initial.concat(otherObject));// ['foo']

Symbol.iterator

根据ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器,由for-of语句使用”。换句话说,这个符号表示实现迭代器API的函数。
for-of循环这样的语言结构会利用这个函数执行迭代操作。循环时,它们会调用以Symbol.iterator为键的函数,并默认这个函数会返回一个实现迭代器API的对象。很多时候,返回的对象是实现该API的Generator:

    class Foo{
        *[Symbol.iterator] (){}
    }
    let f = new Foo();
    console.log(f[Symbol.iterator]());
    //Generator{<suspended>}

技术上,这个由Symbol.iterator 函数生成的 对象应该通过其next()方法陆续返回值。可以通过显示的调用next()方法返回,也可以隐式的通过生成器函数返回。

    class Emitter {
        contructor(max){
            this.max = max;
            this.idx = 0;
            
        }
        
        *[Symbol.iterator] () {
            while(this.idx < this.max){
                yield this.idx++;
            }
        }
    }
    
    
    function count() {
        let emitter = new Emitter(5);
        for(const x of emitter) {
            console.log(x);
        }
    }
    count();
    //0
    //1
    //2
    //3
    //4
    

Symbol.match

根据ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法用正则表达式去匹配字符串。由String.prototype.match()方法使用”。String.prototype.match()方法会使用以Symbol.match为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String方法的有效参数:

   console.log(RegExp.prototype[Symbol.match]);
   //[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
    console.log('barbaz'.match(new StringMatcher('foo')));//false

Symbol.replace

根据ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式,该方法替换一个字符串中匹配的字串。由String.prototype.replace()方法调用”。String.prototype.replace()方法会使用以Symbol.replace为键的函数来对正则表达式求值。正则表达式的原型上默认由这个函数的定义,因此所有正则表达式实例默认是这个String方法的有效参数:

 console.log(RegExp.prototype[Symbol.replace]);
   //[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 {
        constroctor(str){
            this.str = str;
        }
        [Symbol.replace](target,replacement) {
            return target.split(this.str).join(replacement);
        }
    }
    console.log('barfoobaz'.replace(new StringReplacer('foo','qux'))); //"barquxbaz"

Symbol.serach

根据ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由String.prototype.serach()方法使用”。String.prototype.serach()方法会使用以Symbol.serach为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String方法的有效参数:

 console.log(RegExp.prototype[Symbol.serach]);
   //[Symbol.search]() {[native code]}
   
   console.log('foobar'.replace(/bar/));
   //3

给这个方法传入非正则表达式会导致该值被转换为RegExp对象。如果想修改这种行为,让方法直接使用参数,可以重新定义Symbol.serach函数以取代默认对正则表达式求值的行为,从而让serach()方法使用非正则表达式实例。Symbol.serch 函数接收一个参数,就是调用match()方法的字符串实例。返回的值没有限制。

    class FooSercher {
        static [Symbol.serach] (target) {
            return target.indexOf('foo');
        }
    }
    
    console.log('foobar'.serach(FooSercher));// 0
    console.log('barfoo'.serach(FooSercher));// 3
    console.log('barbaz'.serach(FooSercher));// -1
    class StringSercher {
        constructor(str){
            this.str = str;
        }
    
        [Symbol.serach] (target) {
            return target.indexOf('foo');
        }
    }
    console.log('foobar'.serach(new StringSercher('foo')));// 0
    console.log('barfoo'.serach(new StringSercher('foo')));// 3
    console.log('barbaz'.serach(new StringSercher('qux')));// -1
    

Symbol.species

根据ECMAScript 规范,这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象的构造函数”。这个属性在内置类型中最常用,用于对内置类型实例方法的返回值 暴露实例化派生对象的方法。用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  // 为啥是true?暂时理解为创建baz实例时,没有访问get 方法,所以此时原型没有改变,下面进行concat操作时,会访问get 方法,进行原型的替换。
    baz = baz.concat('baz');
    console.log(baz instanceof Array);//true
    console.log(baz instanceof Baz);//false  

Symbol.split

根据ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由 String.prototype.split()方法使用”。String.prototype.split()方法会使用以Symbol.split为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式默认是这个String方法的有效参数:

   console.log(RegExp.prototype[Symbol.split]);
   //[Symbol.split]() {[native code]}
   
   console.log('foobarbaz'.split(/bar/));// ['foo','baz']

给这个方法传入非正则表达式会导致该值被转换为RegExp对象。如果想修改这种行为,让方法直接使用参数,可以重新定义Symbol.split 函数以取代默认对正则表达式求值的行为,从而让split()方法使用非正则表达式实例。Symbol.split 函数接收一个参数,就是调用split()方法的字符串实例。返回的值没有限制:

    class FooSplitter {
        static [Symbol.split](target){
            return target.split('foo');
        }
    }
    console.log('barfoobaz'.split(FooSplitter));
    // ["bar","baz"]
    
  class StringSplitter {
      constructor(str){
          this.str = str;
      }
      [Symbol.split](target) {
          return target.split(this.str);
      }
  }
  
  console.log('barfoobaz'.split(new StringSplitter('foo')));
  
  

Symbol.toPrimitive

根据ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始值。由ToPrimitive 抽象操作使用”。很多内置操作都会尝试强制将对象转换为原始值,包括字符串,数值和未制定的原始类型。对于一个自定义对象实例,通过这个实例的Symbol.toPrimitive属性上定义一个函数可以改变默认行为。
根据提供给这个函数的参数(string,number或defalut),可以控制返回的原始值。

    class Foo {}
    let foo = new Foo();
    console.log(3 + foo); // "3[onject Object]"
    console.log(3 - foo); // NaN
    console.log(String(foo)); // [object Object]
    
    
    
    class Bar {
        this[Symbol.toPrimitive] = function(hint){
            switch (hint){
                case 'number':
                    return 3;
                case 'string':
                    return "string bar";
                case 'default':
                    return 'default bar';   
            }
        } 
    }
    
    let bar = new Bar();
    
    
    console.log(3 + foo); // "3default bar"
    console.log(3 - foo); // 0
    console.log(String(foo)); // "string bar"
**Symbol.toPrimitive**,这个symbol与引用类型和值类型做运算有关系,我们知道运算的时候,引用类型要有一个强制转换的过程,通过这个接口可以控制它。
hint值是字符串,有三个:default,number和string。一般来讲+链接运算符传入的是default,乘法等算数运算符传入的是number,String(str)传入的是string.

Symbol.toStringTag

根据ECMAScript 规范,这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法Object.prototype.toString()使用”。 通过toString()方法获取对象标识时,会检索由Symbol.toStringTag指定的实例标识符,默认为“Object”。内置类型已经指向了这个值, 但自定义类型实例还需要明确定义:

    let s = new Set();
    
    console.log(s);// Set(0) {}
    console.log(s.toString()); // [Object Set]
    console.log(s[Symbol.toStringTag])// Set
    
    class Foo {}
    let foo = new Foo();
    console.log(foo);// Foo {}
    console.log(foo.toString()); // [object Object]
    console.log(foo[Symbol.toStringTag])// undefined
    
    class Bar {
        constructor() {
            this[Symbol.toStringTag] = 'Bar';
        }
    }

    let bar = new Bar();
    console.log(foo);// Bar {}
    console.log(foo.toString()); // [object Bar]
    console.log(foo[Symbol.toStringTag])// Bar