JavaScript - Symbol类型

Symbol(符号)是ECMAScript 6新增的数据类型。

符号是原始值,且符号实例唯一、不可变的。

用途:确保对象属性使用唯一标识符,不会发生属性冲突的危险。用来创建唯一记号,进而用作非字符串形式的对象属性。

1. 基本用法

符号需要使用Symbol()函数初始化。

let sym = Symbol();

// 因为符号本身是原始类型,所以typeof操作符对符号返回symbol
console.log(typeof sym); // symbol

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

// Symbol的值是唯一的,不会出现相同值的常量
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
console.log(genericSymbol == otherGenericSymbol); // false

// 可以传入一个字符串参数作为对符号的描述
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(fooSymbol == otherFooSymbol); // false

符号没有字面量语法。 按照规范,只要创建Symbol()实例并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性。

let genericSymbol = Symbol();
console.log(genericSymbol); // Symbol()

let fooSymbol = Symbol('foo');
console.log(fooSymbol); // Symbol(foo)

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
console.log(mySymbol);

如果想使用符号包装对象,可以借用Object()函数:

let mySymbol = Symbol();
let myWarppedSymbol = Object(mySymbol);
console.log(typeof myWarppedSymbol); // "object"

2. 使用全局符号注册表

如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为,在全局符号注册表中创建并重用符号。

Symbol.for()方法:

let fooGlobalSymbol = Symbol.for('foo');
console.log(typeof fooGlobalSymbol); // symbol

Symbol.for()对每个字符串键都执行幂等操作。

  • 第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。
  • 后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。
// 创建新符号
let fooGlobalSymbol = Symbol.for('foo');
// 重用已有符号
let otherFooGlobalSymbol = Symbol.for('foo');

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

采用相同符号,在全局注册表中定义的符号跟使用Symbol()定义的符号也不等同:

// 使用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 s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); // foo

// 创建普通符号
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined

如果传给Symbol.keyFor()的不是符号,则该方法抛出TypeError。

Symbol.keyFor(123); // TypeError: 123 is not a symbol

3. 使用符号作为属性

凡是可以使用字符串或数值作为属性的地方,都可以使用符号。

包括对象字面量属性和 Object.defineProperty(obj, prop, descriptor) / Object.defineProperties() 定义的属性。

对象字面量只能在计算属性语法中使用符号作为属性。

let s1 = Symbol('foo'),
    s2 = Symbol('bar'),
    s3 = Symbol('baz'),
    s4 = Symbol('qux');

let o = {
    // [属性],会对属性进行读取,并且转换成字符串。[s1]是读取了Symbol的字符串键'foo'
    [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): baz val,
                //  Symbol(foo): foo val, Symbol(bar): qux val}

let s1 = Symbol('foo'),
    s2 = Symbol('bar');

let o = {
    [s1]: 'foo val',
    [s2]: 'bar val',
    baz: 'baz val',
    qux: 'qux val'
};

// Object.getOwnPropertySymbols()返回对象实例的符号属性数组
console.log(Object.getOwnPropertySymbols(o)); // [ Symbol(foo), Symbol(bar) ]

// Object.getOwnPropertyNames()返回对象实例的常规属性数组
console.log(Object.getOwnPropertyNames(o)); // [ 'baz', 'qux' ]

// Object.getOwnPropertyDescriptors()会返回同时包含常规和符号属性描述符的对象
console.log(Object.getOwnPropertyDescriptors(o));
// {
//     baz: {
//       value: 'baz val',
//       writable: true,
//       enumerable: true,
//       configurable: true
//     },
//     qux: {
//       value: 'qux val',
//       writable: true,
//       enumerable: true,
//       configurable: true
//     },
//     [Symbol(foo)]: {
//       value: 'foo val',
//       writable: true,
//       enumerable: true,
//       configurable: true
//     },
//     [Symbol(bar)]: {
//       value: 'bar val',
//       writable: true,
//       enumerable: true,
//       configurable: true
//     }
//   }

// Reflect.ownKeys()会返回两种类型的键
console.log(Reflect.ownKeys(o)); // [ 'baz', 'qux', Symbol(foo), Symbol(bar) ]

注意:Object.getOwnPropertyNames()Object.getOwnProperty-Symbols()两个方法的返回值彼此互斥。


因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。

但是,如果没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键。

let o = {
    [Symbol('foo')]: 'foo val',
    [Symbol('bar')]: 'bar val'
};
console.log(o); // { [Symbol(foo)]: 'foo val', [Symbol(bar)]: 'bar val' }

let barSymbol = Object.getOwnPropertySymbols(o).find((Symbol) => 
				Symbol.toString().match(/bar/));
console.log(barSymbol); // Symbol(bar)

4. Symbol.asyncIterator

这个符号表示实现异步迭代器API的函数。

  • for-await-of循环会利用这个函数执行异步迭代操作。
  • 循环时,它们会调用以Symbol.asyncIterator为键的函数,并期望这个函数会返回一个实现迭代器API的对象。很多时候,返回的对象是实现该API的AsyncGenerator。
class Foo {
    async * [Symbol.asyncIterator]() {}
}

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

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

class Emitter {
    constructor(max) {
        this.max = max;
        this.asyncIdx = 0;
    }
    async * [Symbol.asyncIterator]() {
        while (this.asyncIdx < thia.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

5. Symbol.hasInstance

该方法决定一个构造器对象是否认可一个对象是它的实例。

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

6. Symbol.isConcatSpreadable

这个符号作为属一个属性,表示一个布尔值,如果是true,则意味着对象应该用Array.prototype.concat()展开其数组元素。

ES6中的Array.prototype.concat()方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。

let initial = ['foo'];
let array = ['bar'];

console.log(array[Symbol.isConcatSpreadable]); // undefined
console.log(initial[Symbol.isConcatSpreadable]); // undefined

// 默认情况下,展开其元素连接到结果中
console.log(initial.concat(array)); // [ 'foo', 'bar' ]

// 设置 Symbol.isConcatSpreadable 为 false,整个对象被追加到数组末尾
array[Symbol.isConcatSpreadable] = false;
console.log(initial.concat(array)); // [ 'foo', [ 'bar', [Symbol(Symbol.isConcatSpreadable)]: false ] ]

// 类数组对象默认情况下会被追加到数组末尾
let arrayLikeObject = { length: 1, 0: 'baz' };
console.log(arrayLikeObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(arrayLikeObject)); // [ 'foo', { '0': 'baz', length: 1 } ]

// 设置 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
console.log(initial.concat(otherObject)); // [ 'foo', Set(1) { 'qux' } ]

otherObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(otherObject)); // [ 'foo' ]

7. Symbol.iterator

这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。由for-of语句使用”,这个符号表示实现迭代器API的函数。

class Foo { * [Symbol.iterator]() {}
}
let f = new Foo();
// 返回的对象是实现该API的Generator
console.log(f[Symbol.iterator]()); // Object [Generator] {}

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

class Emitter {
    constructor(max) {
        this.nax = 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

8. Symbol.match

这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式去匹配字符串。由String.prototype.match()方法使用”。

console.log(RegExp.prototype[Symbol.match]); // [Function: [Symbol.match]]
console.log('foobar'.match(/bar/)); // [ 'bar', index: 3, input: 'foobar', groups: undefined ]
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('qux'))); // false

9. Symbol.replace

这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串。由String.prototype.replace()方法使用”。

console.log(RegExp.prototype[Symbol.replace]); // [Function: [Symbol.replace]]
console.log('foobarbaz'.replace(/bar/, 'qux')); // fooquxbaz

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.str).join(replacement);
        }
}
console.log('barfoobaz'.replace(new StringReplacer('foo'), 'qux')); // barquxbaz

10. Symbol.search

这个符号作为一个属性表示“一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由String.prototype.search()方法使用”。

console.log(RegExp.prototype[Symbol.search]); // [Function: [Symbol.search]]
console.log('foobar'.search(/bar/)); // 3

如果想让方法直接使用参数,可以重新定义Symbol.search函数以取代默认对正则表达式求值的行为,从而让search()方法使用非正则表达式实例。

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
console.log('barfoo'.search(new StringSearcher('foo'))); // 3
console.log('barbaz'.search(new StringSearcher('qux'))); // -1

11. Symbol.species

这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象的构造函数”。这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。

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

12. Symbol.split

这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由String.prototype.split()方法使用”。

console.log(RegExp.prototype[Symbol.split]); // [Function: [Symbol.split]]
console.log('foobarbaz'.split(/bar/)); // [ 'foo', 'baz' ]
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'))); // [ 'bar', 'baz' ]

13. Symbol.toPrimitive

这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始值。由ToPrimitive抽象操作使用”。

很多内置操作都会尝试强制将对象转换为原始值,包括字符串、数值和未指定的原始类型。

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 'stringbar';
                case 'default':
                default:
                    return 'defaultbar';
            }
        }
    }
}
let bar = new Bar();
console.log(3 + bar); // 3defaultbar
console.log(3 - bar); // 0
console.log(String(bar)); // stringbar

14. Symbol.toStringTag

这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法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(bar); // Bar { [Symbol(Symbol.toStringTag)]: 'Bar' }
console.log(bar.toString); // [Function: toString]
console.log(bar[Symbol.toStringTag]); // Bar

15. Symbol.unscopables(不推荐使用)

这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性,都会从关联对象的with环境绑定中排除”。

设置这个符号并让其映射对应属性的键值为true,就可以阻止该属性出现在with环境绑定中

let o = { foo: 'bar' };
with(o) {
    console.log(foo); // bar
}
o[Symbol.unscopables] = {
    foo: true
};
with(o) {
    console.log(foo); // 报错
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值