ES6的Symbol

本文主要介绍了ECMAScript 6引入的新原始类型Symbol。包括其创建方式、共享体系,强调不能将其强制转换为字符串和数字类型。还介绍了检索对象中Symbol属性的方法,以及通过well - known Symbol暴露内部操作,如Symbol.hasInstance、Symbol.isConcatSpreadable等方法和属性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

一、Symbol简介

    ECMAScript 6引入了一种新的原始类型:Symbol,它的功能类似于一种标识唯一性的ID。

    所有原始值,除了Symbol以外都有各自的字面形式,例如布尔类型的true或数字类型的42。

二、创建Symbol

    可以通过全局的Symbol函数创建一个Symbol

        let firstName = Symbol("first name");
        let person = {};

        person[firstName] = "Nicholas";
        console.log(person[firstName]);    // "Nicholas"

    由于Symbol是原始类型,因此调用new Symbol()会导致程序抛出错误。

    Symbol函数接受一个可选参数,以添加一段文本描述即将创建的Symbol,建议每次创建Symbol时都添加这样一段描述,以便于阅读代码和调试Symbol程序:

        let firstName = Symbol("first name");
        let person = {};

        person[firstName] = "Nicholas";

        console.log("first name" in person);    // false
        console.log(person[firstName]);         // "Nicholas"
        console.log(firstName);                 // "Symbol(first name)"
        console.log(firstName.toString());      // "Symbol(first name)"

      Symbol的描述被存储在内部的[[Description]]属性中,只有当调用SymboltoString()方法时才可以读取这个属性。  

    注意:Symbol是原始值,且ECMAScript6同时扩展了typeof操作符,支持返回"Symbol",所以可以用typeof来检测变量是否为Symbol类型:

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

   

三、Symbol共享体系

    有时我们可能希望在不同的对象中共享同一个Symbol,一般而言,在很大的代码库中或跨文件追踪Symbol非常困难而且容易出错,出于这些原因,ECMAScript6提供了一个可以随时访问的全局Symbol注册表

    如果想创建一个可共享的Symbol,要使用Symbol.for()方法。

    它只接受一个参数,也就是即将创建的Symbol的字符串标识符,这个参数同样也被用作Symbol的描述:

        let uid = Symbol.for('uid');
        let object = {
            [uid]: "12345"
        };

        console.log(object[uid]);       // "12345"
        console.log(uid);               // "Symbol(uid)"

        let uid2 = Symbol.for('uid');

        console.log(uid == uid2);       // true
        console.log(object[uid2]);      // "12345"
        console.log(uid2);              // "Symbol(uid)"

    Symbol.for()方法首先在全局Symbol注册表中搜索键为"uid"的Symbol是否存在,如果存在,直接返回已有的Symbol;否则创建一个新的Symbol,并使用这个键在Symbol全局注册表中注册,随即返回新创建的Symbol

    可以使用Symbol.keyFor()方法在Symbol全局注册表中检索与Symbol有关的键:

        let uid = Symbol.for('uid');
        console.log(Symbol.keyFor(uid));    // 'uid'

        let uid2 = Symbol.for('uid');
        console.log(Symbol.keyFor(uid2));   // 'uid'

        let uid3 = Symbol('uid');
        console.log(Symbol.keyFor(uid3));   // undefined

    由于在Symbol全局注册表中不存在uid3这个Symbol,也就是不存在与之有关的键,所以最终返回undefined

    Symbol全局注册表是一个类似全局作用域的共享环境,也就是说不能假设目前环境中存在哪些键。

    当使用第三方组件时,尽量使用Symbol键的命名空间以减少命名冲突

 

四、Symbol与类型强制转换

    其他类型没有与Symbol逻辑等价的值,因而Symbol使用起来不是很灵活,尤其是不能将Symbol强制转换为字符串和数字类型,否则如果不小心将其作为对象属性,最终会导致不一样的执行结果。

        let uid = Symbol.for('uid');
            desc = uid + "";           // 报错!

        let uid2 = Symbol.for('uid');
            sum = uid2 / 1;            // 报错!

    Symbol与一个字符串拼接,会导致程序抛出错误,也不能将Symbol强制转换为数字类型。

 

五、Symbol属性检索

    ES6添加了一个Object.getOwnPropertySymbols()方法来检索对象中的Symbol属性,它的返回值是一个包含所有Symbol自有属性的数组:

        let uid = Symbol.for('uid');
        let object = {
            [uid]: '12345',
            abc: '1234213412'
        };

        let symbols = Object.getOwnPropertySymbols(object);

        console.log(symbols.length);        // 1
        console.log(symbols[0]);            // "Symbol(uid)"
        console.log(object[symbols[0]]);    // "12345"

 

六、通过well-known Symbol暴露内部操作

    ECMAScript6主要通过在原型链上定义与Symbol相关的属性来暴露更多的语言内部逻辑。

    ECMAScript开放了以前JavaScript中常见的内部操作,并通过预定义一些well-known Symbol来表示。每一个这类Symbol都是Symbol对象的一个属性。 

6.1、Symbol.hasInstance方法

    每一个函数中都有一个Symbol.hasInstance方法,用于确定对象是否为函数的实例。

    该方法在Function.prototype中定义,所以所有函数都继承了instanceof属性的默认行为。

    为了确保Symbol.hasInstance不会被意外重写,该方法被定义为不可写、不可配置并且不可枚举。

    Symbol.hasInstance方法只能接受一个参数,即要检查的值。如果传入的值是函数的实例,则返回true:

obj instanceof Array;

    以上这行代码等价于下面这行:

Array[Symbol.hasInstance](obj);

    现在使用Symbol.hasInstance方法就可以随意改变instanceof的运行方式了。

    假设想定义一个无实例的函数,就可以将Symbol.hasInstance的返回值硬编码为false:

        function MyObject(v){
            
        }

        Object.defineProperty(MyObject, Symbol.hasInstance, {
            value: function(v){
                return false;
            }
        });

        let obj = new MyObject();

        console.log(obj instanceof MyObject);

    也可以基于任意条件,通过值检查来确定被检测的是否为实例。

    例如可以将1~100的数字定义为一个特殊数字类型的实例:

        function SpecialNumber(){
            // 空函数
        }

        Object.defineProperty(SpecialNumber, Symbol.hasInstance, {
            value: function(v){
                return (v instanceof Number) && (v >= 1 && v <= 100);
            }
        });

        var two = new Number(2),
            zero = new Number(0);
        
        console.log(two instanceof SpecialNumber);  // true
        console.log(zero instanceof SpecialNumber); // false

6.2、Symbol.isConcatSpreadable属性

    JavaScript数组的concat()方法被设计用于拼接两个数组:

        let colors1 = ["red", "green"],
            colors2 = colors1.concat(["blue", "black"]),
            colors3 = colors1.concat(["blue", "black"], "brown");

        console.log(colors2.length);        // 4
        console.log(colors2);               // ["red", "green", "blue", "black"]
        console.log(colors3.length);        // 5
        console.log(colors3);               // ["red", "green", "blue", "black", "brown"]

    JavaScript规范声明,凡是传入了数组参数,就会自动将它们分解为独立元素。在ECMAScript6标准以前,我们根本无法调整这个特性。

    Symbol.isConcatSpreadable属性是一个布尔值,如果该属性值为true,则表示对象有length属性和数字键,故它的数值型属性值应该被独立添加到concat()调用的结果中。

    它与其他well-known Symbol不同的是,这个Symbol属性默认情况下不会出现在标准对象中,它只是一个可选属性,用于增强作用于特定对象类型的concat()方法的功能,有效简化其默认特性。

    可以通过以下方法,定义一个在concat()调用中与数组行为相近的新类型:

        let collection = {
            0: 'Hello',
            1: 'world',
            length: 2,
            [Symbol.isConcatSpreadable]: true
        };

        let messages = ["Hi"].concat(collection);     // 将collection中的属性分解为独立元素

        console.log(messages.length);
        console.log(messages);      // ["Hi", "Hello", "world"]

    以上示例中,定义了一个类数组对象collection:它有一个length属性,还有两个数字键,Symbol.isConcatSpreadable属性值为true表明属性值应当作为独立元素添加到数组中。

 

6.4、Symbol.toPrimitive方法

    在JavaScript引擎中,当执行特定操作时,经常会尝试将对象转换到相应的原始值,例如,比较一个字符串和对象,如果使用双等号(==)运算符,对象会在比较操作执行前被转换为一个原始值。

    到底使用哪一个原始值以前是由内部操作决定的,但在ES6的标准中,通过Symbol.toPrimitive方法可以更改那个暴露出来的值。

    Symbol.toPrimitive方法被定义在每一个标准类型的原型上,并且规定了当对象被转换为原始值时应当执行的操作。每当执行原始值转换时,总会调用Symbol.toPriitive方法并传入一个值作为参数,这个值在规范中被称作类型提示(hint)。类型提示参数的值只有三种选择:

  • "number"
  • "string"
  • "default"

    传递这些参数时,Symbol.toPrimitive返回的分别是:数字、字符串或无类型偏好的值。

    对于大多数标准对象,数字模式有以下特性,根据优先级的顺序排列如下:

    1、调用valueOf()方法,如果结果为原始值,则返回

    2、否则,调用toString()方法,如果结果为原始值,则返回

    3、如果再无可选值,则抛出错误

    同样,对于大多数标准对象,字符串模式有以下优先级排序:

    1、调用toString()方法,如果结果为原始值,则返回

    2、否则,调用valueOf()方法,如果结果为原始值,则返回

    3、如果再无可选值,则抛出错误

        function Temperature(degrees){
            this.degrees = degrees;
        }

        Temperature.prototype[Symbol.toPrimitive] = function(hint){
            switch(hint) {
                case "string":      // 字符串模式
                    return this.degrees + "\u00b0";
                case "number":      // 数字模式
                    return this.degrees;     
                case "default":     // 默认模式
                    return this.degrees + " degrees";
            }
        };

        var freezing = new Temperature(32);

        console.log(freezing + "!");        // "32 degrees!"
        console.log(freezing / 2);          // 16
        console.log(String(freezing));      // "32°"

    每一条console.log()语句将触发不同的hint参数值:

  • +运算符触发默认模式
  • /运算符触发数字模式
  • String()函数触发字符串模式

 

6.5、Symbol.toStringTag属性

    ES6之前面对存在多个全局执行环境,数组类型识别这样的问题时,通常采用这样的解决方案:

        function isArray(value){
            return Object.prototype.toString.call(value) === "[object Array]";
        }

        console.log(isArray([]));       // true

    ES6通过Symbol.toStringTag这个Symbol改变了调用Object.prototype.toString()时返回的身份标识。

    这个Symbol所代表的属性在每一个对象中都存在,其定义了调用对象的Object.prototype.toString.call()方法时返回的值。

        function Person(name){
            this.name = name;
        }

        Person.prototype[Symbol.toStringTag] = "Person";

        var me = new Person('Nicholas');

        console.log(me.toString());                     // "[object Person]"
        console.log(Object.prototype.toString.call(me));    // "[object Person]"

    Person.prototype继承了Object.prototype.toString()方法,所以调用me.toString()方法时也使用了Symbol.toStringTag的返回值。

    然而,仍然可以定义自己的toString()方法,这不会影响Object.prototype.toString.call()方法的使用,但却可以提供一个不同的值:

        function Person(name){
            this.name = name;
        }

        Person.prototype[Symbol.toStringTag] = "Person";

        Person.prototype.toString = function(){
            return this.name;
        }

        var me = new Person('Nicholas');

        console.log(me.toString());                     // "Nicholas"
        console.log(Object.prototype.toString.call(me));    // "[object Person]"

    由于Person不再继承自Object.prototype.toString()方法,因而调用me.toString()方法时返回的是一个不同的值。

    注意:除非另有说明,所有对象都会从Object.prototype继承Symbol.toStringTag这个属性,且默认的属性值为"Object"。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值