zl程序教程

您现在的位置是:首页 >  后端

当前栏目

随笔-ES6 Symbol

ES6 symbol 随笔
2023-09-14 09:13:41 时间

Symbol简介

Symbol是ES6推出的JS第六大简单数据类型,其余五个分别是:

undefined null number string boolean

由于它是简单数据类型,所以不支持new

 

Symbol本身是一个函数,

 

通过Symbol()函数调用可以返回一个独一无二的值,Symbol()返回值仅用来表示唯一值,而不能用来和其他数据类型的值进行运算。

 

Symbol()常用来解决对象属性命令冲突问题

在JS开发中,我们经常需要给对象添加属性,但是当对象原本的属性很多时,或者对我们不可见时,我们就无法轻易给出要添加的属性的属性名了,因为可能会发送命名覆盖,比如重写call方法的例子

Function.prototype._call = function(thisArg, ...args){
    thisArg.fn = this // 草率定义了'fn'为thisArg的属性
    const result = thisArg.fn(...args)
    delete thisArg.fn
    return result
}

const obj = {
    0: 0,
    1: 1,
    2: 2,
    3: 3,
    length: 4,
    fn: 123
}

Array.prototype.forEach._call(obj, (item)=>{
    console.log(item)
})

 

 注意上面程序Function.prototype._call方法中定义 thisArg.fn = this 是一种非常草率的行为,因为我们不知道thisArg对象是否有一个叫fn属性名的属性,如果有的话,我们就会将thisArg原有的fn属性给覆盖了。

所以我们需要一个在thisArg对象中没有出现过的属性,在没有Symbo之前提出的方案是:

随机字符串+遍历对象属性(包括原型上)去重

这是一种繁琐,且费时的操作,这里就不演示了。

但是当ES6 Symbol提出后,解决了对象属性命名冲突的问题,因为Symbol()返回的每一个值都是独一无二,绝不会重复的。

我们利用Symbol对_call进行优化

Function.prototype._call = function(thisArg, ...args){
    const fn = Symbol()
    thisArg[fn] = this // 注意这里fn是一个变量,我们需要使用[]获取动态属性名,而不是使用.
    const result = thisArg[fn](...args)
    delete thisArg[fn]
    return result
}

const obj = {
    0: 0,
    1: 1,
    2: 2,
    3: 3,
    length: 4,
    fn: 123
}

Array.prototype.forEach._call(obj, (item)=>{
    console.log(item)
})

 可以看到obj的原本属性没有受到任何影响。

Symbol定义对象私有属性

由于Symbol()返回的值是独一无二的,所以如果我们不对外提供Symbol()返回值,则外部无法访问到Symbol()定义的属性

const obj = {
    [Symbol()]: '123'
}

console.log(obj)

 

 

给Symbol()打上标识

当我们用Symbol()返回值作为对象属性时,发现语义化不清晰,不知道Symbol()返回值到底是什么属性,是干什么的?Symbol函数支持传入一个标识字符串,来标识Symbol()属性的语义

const obj = {
    [Symbol('name')] : 'zhangsan',
    [Symbol('age')]: 28
}

打上标识后的Symbol()返回值语义更加清晰,同时保持着唯一性。

那么Symbol('name') 和 Symbol('name') 返回的值相同吗?

答案是:不相同

我们应该要理解 Symbol('name') 本质上还是Symbol(),只是为其返回值加了一个标识而已,所以即使标识相同,Symbol('name') 和 Symbol('name')返回值也不相同。 

 

Symbol实现既保证对象属性唯一,又保证外部可访问

即定义一个不会发生命名冲突,但是又对外开放的对象属性

Symbol函数提供了一个for静态方法,该静态方法也能返回一个唯一值,但是和Symbol()不同的是,for方法可以指定一个key字符串,且JS内部会缓存 "key => symbol唯一值" 键值对到全局的symbol注册表,作用是当下次Symbol.for再次传入相同key时,会从全局的symbol注册表找到key对应的symbol唯一值 作为返回值,即如下

通过这个特性可以实现 保证属性在对象内部唯一,又对外部开放

const obj = {
    [Symbol.for('name')]: 123
}

console.log(obj[Symbol.for('name')])

 

另外对于Symbol还有一个静态方法keyFor,支持传入一个symbol唯一值,返回其key值

因为 全局的symbol注册表 中key必须是唯一的,且key对于的symbol值也是唯一的,所以既可以根据key找到symbol值,也可以根据symbol值找到key

另外还有一点 全局的symbol注册表 的全局性说明:

支持跨文件和跨域,这个我还没有验证。

Symbol内置值

Symbol上还有一些静态属性(内置值),这些Symbol内置值代表了内部语言行为,

比如我们要对一个对象进行for...of迭代时,for...of底层会去找被迭代对象的[Symbol.iterator]属性,如果被迭代对象有该属性,则执行函数调用 [Symbol.iterator]() 看是否可以返回一个 迭代器对象,若是,则可以进行for...of迭代,如不是则无法进行for...of迭代。

我们可以发现Symbol.iterator就是一个Symbol内置值,iterator作为Symbol的静态属性,而且该Symbol内置值和for...of的底层运行逻辑存在关系

const obj = {
    0: 0,
    1: 1,
    2: 2,
    3: 3,
    length: 4,
    [Symbol.iterator](){
        let cursor = 0
        let length = this.length
        let that = this
        return {
            next(){
                return {
                    value: that[cursor++],
                    done: cursor > length
                }
            }
        }
    }
}

for(let i of obj) {
    console.log(i)
}

 

再比如当我们使用instanceof检测类型是否在对象的原型链上时,会先去找类型身上的[Symbol.hasInstance]属性,找到则执行函数调用 [Symbol.hasInstance](对象) ,该函数的返回值就是instanceof的结果 

// function Person() {}

// Person[Symbol.hasInstance] = function(instance) {
//     console.log('进来了')
//     while (true) {
//         if (Object.getPrototypeOf(instance) === Person.prototype) {
//             return true
//         } else {
//             if (Object.getPrototypeOf(instance)) {
//                 instance = Object.getPrototypeOf(instance)
//             } else {
//                 return false
//             }
//         }
//     }
// }

class Person {
    static[Symbol.hasInstance](instance) {
        console.log('进来了')
        while (true) {
            if (Object.getPrototypeOf(instance) === Person.prototype) {
                return true
            } else {
                if (Object.getPrototypeOf(instance)) {
                    instance = Object.getPrototypeOf(instance)
                } else {
                    return false
                }
            }
        }
    }
}

const p = new Person()

console.log(({}) instanceof Person)
console.log(p instanceof Person)

另外,好像当Symbol内置值是类的静态属性时,只有ES6 class支持,而es5构造函数不支持

其他内置值使用,请看MDNSymbol - JavaScript | MDN (mozilla.org)

遍历对象的Symbol属性

当我们给对象添加一个Symbol类型属性时

let obj = {
    a: 1,
    b: 2,
    [Symbol()]: 3,
    [Symbol('c')]: 4,
    [Symbol.for('d')]: 5
}

Object.getOwnPropertyDescriptors(obj)

可以发现Symbol类型属性都是可遍历,可删除,可修改的

但是当我们for...in遍历时,却发现只能遍历出非Symbol类型的属性,即使Symbol类型属性是可遍历的

 

另外 Object.getOwnPropertyNames也不支持返回对象Symbol类型属性

以及Object.keys也不支持返回对象Symbol类型属性

 

另外 JSON.stringif也不支持序列化对象的Symbol类型属性

 

 目前Object.getOwnPropertySymbols支持返回对象的Symbol属性

 以及反射支持支持返回对象的Symbol属性