阅读 130

【JS学习笔记】Symbol

这是我参与更文挑战的第6天,活动详情查看: 更文挑战

Symbol

符号是一个原始数据类型、和Number、String、Boolean不同,它没有字面量语法。

普通符号创建

const syb = Symbol();
const syb1 = Symbol("对符号的描述");
复制代码

没有字面量语法的创建方式,只能通过Symbol("描述")方法创建。

不能把Symbol当作构造函数调用,new Symbol()会报错。

可以传入一个String参数,用于描述这个符号实例。不是String类型会转换成String。

特点

  1. 没有字面量。

  2. typeof Symbol()返回"symbol"。

  3. 符号实例是唯一的、不可变的。

    const syb = Symbol();
    const syb1 = Symbol();
    syb !== syb1
    复制代码
  4. 符号可以作为对象的属性名存在,称为符号属性。

    • 开发者可以通过精心的设计,让这些属性无法通过常规方式被外界访问(私有属性)。利用闭包,将符号实例作为对象的属性名,无法访问符号实例,自然无法通过对象的属性名访问属性了。说明:[]为计算属性名,里面可以放表达式。

      var person = (function() {
          const name = Symbol("私有属性_名字");
          return {
              [name]: "Herb",
              getName() {
                  return this[name];
              }
          }
      }())
      复制代码
    • 符号属性是不能枚举的,因此在 for-in 循环、Object.keys 方法都无法读取到符号属性。

    • Object.getOwnPropertyNames尽管可以得到自身所有无法枚举的属性名,但是仍然无法读取到符号属性名。

    • Object.getOwnPropertyDescriptor获取某个对象的某个属性和符号的属性描述符对象(该属性必须直接属于该对象)。

    • ES6新增Object.getOwnPropertySymbols方法,可以读取符号属性名,返回一个数组。

  5. 符号无法被隐式转换,因此不能被用于数学运算、字符串拼接或其他隐式转换的场景,但符号可以显式的转换为字符串,通过 String 转型函数进行转换即可(实际上是调用toString()方法)。

    var syb = Symbol("符号");
    var sybStr = String(syb);
    String(syb) == syb.toString()
    复制代码
  6. 如果想使用符号包装对象,可以借用Object()函数

    var sybObj = Object(Symbol("符号对象"));
    typeof sybObj == "object"
    sybObj.constructor
    // ƒ Symbol() { [native code] }
    sybObj instanceof Symbol
    // true
    复制代码

共享符号

  1. for

    通过Symbol.for("符号描述")创建共享符号,可以在不同对象中用相同的符号作为属性名。

    创建后,会把符号存储在全局符号注册表中,以符号描述作为键,符号实例作为名。

    调用Symbol.for("查找")时,会先查看注册表中有没有这个符号描述("查找"),如果没有就创建一个Symbol("查找"),如果有的话,就根据键返回名。

    let foo = Symbol.for("foo");
    let foo1 = Symbol.for("foo");
    foo == foo1
    
    let bar = Symbol("bar");
    let bar1 = Symbol.for("bar");
    bar !== bar1
    
    let unDes = Symbol.for();
    // Symbol(undefined)
    复制代码

    内部实现大概类似如此:

    const symbolFor = (() => {
        const global = {};
        return des => {
            if(!global[des]) {
                global[des] = Symbol(des);
            }
            return global[des];
        }
    })();
    复制代码

    利用立即执行函数保存global变量,形成全局符号注册表。

    作为参数传给Symbol.for()的任何值(除了符号实例)都会被转换为字符串。如果是符号实例就会抛出TypeError,应该是经过了特殊处理。

  2. keyFor

    Symbol.keyFor()来查询全局注册表,接受一个符号实例,返回全局符号注册表中对应的键(符号描述),如果不是全局符号,就返回undefined。

    如果参数不是符号,则抛出TypeError。

知名符号

ES6引入了一批常用内置符号,用于暴露语言内部行为。

  1. Symbol.hasInstance

    这个符号作为一个属性表示一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。

    这个属性定义在Function原型上,所有函数和类上都可以调用。

    在ES6中,instanceof操作符会使用Symbol.hasInstance函数来确定关系。

    class Person{
        constructor(name, age, sex) {
            this.name = name;
            this.age = age;
            this.sex = sex;
        }
    }
    复制代码

    var p = new Person("Herb", 18, "男");
    var isPersonInstance = p instanceof Person;
    // true
    复制代码

    p在Person的原型链上,p的隐式原型__proto__就是Person的原型。p.__proto__ == Person.prototype结果为true。

    isPersonInstance = Person[Symbol.hasInstance](p);
    // true
    复制代码

    两者方式是等价的。


    Person instanceof Function
    // true
    Person[Symbol.hasInstance] === Function.prototype[Symbol.hasInstance] 
    // true
    复制代码

    Symbol.hasInstance方法定义在Function的原型上,因为Person构造函数是由Function创建的,所以Person继承了Function。

    在Person及其原型链上寻找Symbol.hasInstance方法,由于没有在Person上重写,最后找到了Function.prototype。

    let propertyObj = Object.getOwnPropertyDescriptor(Function.prototype, Symbol.hasInstance);
    // writable: false
    复制代码

    查看其属性描述符发现,Symbol.hasInstance不能被改写。

    所以不能直接用:

    Person[Symbol.hasInstance] = function() {
        console.log("重写Function.prototype的方法");
        return false;
    }
    复制代码

    会在原型链上查找有无Symbol.hasInstance属性,最后发现Function原型上有,修改后发现修改失败。

    所以可以直接在Person类中写一个静态方法。

    var obj = {};
    Object.prototype.name = "herb";
    Object.defineProperty(Object.prototype, "name", { writable: false });
    obj.name = "ice";
    obj.name   // "herb"
    obj        // {}
    复制代码

    属性是无法修改的。也不会出现在obj上。

    var obj = {}
    Object.prototype.name = "herb";
    obj.name = "ice";
    obj.name          // "ice"
    obj               // { name: "ice" }
    复制代码

    没有设置属性是不可写的时候,是能添加到对象上的。


    class Person{
        constructor(name, age, sex) {
            this.name = name;
            this.age = age;
            this.sex = sex;
        }
        // 在Person类的prototype上定义了Symbol.hasInstance方法,无意义
        [Symbol.hasInstance]() {
            console.log("Person改写Function原型上的方法");
            return false;
        }
        // 在类上重写了函数,是类的静态方法
        static [Symbol.hasInstance]() {
            console.log("Person改写Function原型上的方法");
            return false;
        }
    }
    复制代码

    p instanceof Person
    Person[Symbol.hasInstance](p)
    // Person改写Function原型上的方法
    // false
    复制代码

    使用类的静态方法书写形式,可以重写。

    Object.defineProperty(Person, Symbol.hasInstance, {
        value: function() {
            console.log("defineProperty重写")
            return false;
        }
    })
    // defineProperty重写
    // false
    复制代码

    也可以定义一个对象的属性,也就是类的属性(静态)。

    知名符号可以让我们更多的参与到js语言的内部实现上来。

  2. Symbol.toPrimitive

    这个符号作为属性表示一个方法,该方法将对象转换为相应的原始值。

    通过在一个实例的Symbol.toPrimitive属性上定义一个函数,这个函数可以改变默认行为。

    class Team{
        constructor(teamName, teamNum) {
            this.teamName = teamName;
            this.teamNum = teamNum;
            this[Symbol.toPrimitive] = function(type){
                switch(type) {    
                    case "number":
                        return this.teamNum;
                    case "default":
                    case "string":
                        return this.teamName;
                }
            }
        } 
    }
    复制代码

    const team = new Team("特种部队", 4);
    Number(team)     // 4
    +team            // 4
    team + 1         // "特种部队1"
    team + "nb"      // "特种部队nb"
    team * 5		 // 20
    String(team)	 // "特种部队"
    复制代码

    进行数学运算时,type为number。

    出现加号和字符串时,type为default。

    调用String()转型函数时,type为string。

    这里还要再研究一下。

    把这个属性放在实例中,就会重复创建多次。也可以放在原型链上。


    class Team{
        constructor(teamName, teamNum) {
            this.teamName = teamName;
            this.teamNum = teamNum;
        }
        [Symbol.toPrimitive](type) {
            switch(type) {    
                case "number":
                    return this.teamNum;
                case "default":
                case "string":
                    return this.teamName;
            }
        }
    }
    复制代码

    不过这样会对Team.prototype产生影响。

    String(Team.prototype)
    // "undefined"
    复制代码
  3. Symbol.toStringTag

    这个符号作为一个属性表示一个字符串,用于创建对象的默认字符串描述。

    由内置方法Object.prototype.toString()使用。

    通过toString()方法获取对象标识时,会检索由Symbol.toStringTag指定的实例标识符,默认为"Object"。

    class Bar {
        constructor() {
            this[Symbol.toStringTag] = "Bar";
        }
        // [Symbol.toStringTag] = "Bar"
    }
    var bar = new Bar();
    bar.toString();
    // "[object Bar]"
    复制代码

    写成实例属性或类的静态属性都可以。

文章分类
前端
文章标签