Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

实现私有变量的几种方法 #3

Open
ZhaZhengRefn opened this issue Jul 28, 2018 · 0 comments
Open

实现私有变量的几种方法 #3

ZhaZhengRefn opened this issue Jul 28, 2018 · 0 comments

Comments

@ZhaZhengRefn
Copy link
Owner

ZhaZhengRefn commented Jul 28, 2018

实现私有变量的几种方法

背景

这篇文章是多日前的笔记,放上issue方便整理查看。。

命名约定

以下划线为属性名称的前缀,并没有什么卵用

Symbol

私有变量名使用Symbol类型,假如外部无法访问Symbol值,那么也可以无法读取私有变量。但是可以使用getOwnProperySymbolsReflect.keys()遍历

const xSymbol = Symbol('x')
const ySymbol = Symbol('y')

class Foobar {
    constructor(x, y) {
        this[xSymbol] = x
        this[ySymbol] = y
    }

    toString() {
        console.log(`arguments: ${this[xSymbol]}-${this[ySymbol]}`)
    }

    get args() {
        return `${this[xSymbol]}--${this[ySymbol]}`
    }
}

const foobar = new Foobar(1, 2)

console.log(Object.getOwnPropertyNames(foobar))//[]

Object.getOwnPropertySymbols(foobar).forEach(k => {//1 2
    console.log(foobar[k])
})
Reflect.ownKeys(foobar).forEach(k => {//1 2
    console.log(foobar[k])
})

WeakMap

将私有变量储存于WeakMap中。虽然仍然可以通过get接口读取存储在WeakMap中的实例的私有变量,但可以避免直接遍历实例读取出其中的私有属性。
用普通对象也可以实现,而使用WeakMap的原因在于: 可以防止内存泄露,假如实例其他引用被删除后,WeakMap对应键名会自动消失。(详细可以看本目录的WeakMap笔记)

const map = new WeakMap()

const internal = instance => {
    if(!map.has(instance)){
        map.set(instance, {})
    }
    return map.get(instance)
}

class Foobar {
    constructor(x, y) {
        internal(this).x = x
        internal(this).y = y
    }

    toString() {
        console.log(`arguments: ${internal(this).x}-${internal(this).y}`)
    }

    get args() {
        console.log(internal(this))
        return `${internal(this).x}--${internal(this).y}`
    }
}

const foobar = new Foobar(1, 2)

console.log(Object.getOwnPropertyNames(foobar))//[]
foobar.toString()//arguments: 1-2

闭包

通过闭包将数据封装在调用时创建的函数作用域,从而使外部无法访问

未封装前的类

未封装前的类,外部可以访问并修改传入的变量,非常不安全

In

class Foo {
    constructor(x, y){
        this.x = x
        this.y = y
    }
    
    toString(){
        console.log(`arguments: ${this.x}-${this.y}`)
    }

    get args(){
        return `${this.x}--${this.y}`
    }
}

Out

const foo = new Foo(1, 2)

foo.toString()//arguments: 1-2
foo.x = 4
foo.toString()//arguments: 4-2

初次尝试封装

实现封装后,变量仅能从内部,即类中访问。

In

function Foo(...args){

    const this$ = {}//私有变量集

    class Foo {
        constructor(x, y){
            this$.x = x
            this$.y = y
        }

        toString(){
            console.log(`arguments: ${this$.x}-${this$.y}`)
        }

        get args(){
            return `${this$.x}--${this$.y}`
        }        
    }

    return new Foo(...args)
}

Out

const foo = new Foo(1, 2)

foo.toString()//arguments: 1-2
foo.x = 4
foo.toString()//arguments: 1-2

修复instanceof操作符

使用第一次封装用的技术后,由于构造函数(外层Foo)返回的是一个新的对象(内层Foo的实例),因此instanceof操作符的结果可能会有误。
为了让结果更容易理解,我修改了内层类名。

问题

function Foo(...args) {

    const this$ = {} //私有变量集

    class Foobar {
        constructor(x, y) {
            this$.x = x
            this$.y = y
        }

        toString() {
            console.log(`arguments: ${this$.x}-${this$.y}`)
        }

        get args() {
            return `${this$.x}--${this$.y}`
        }
    }

    return new Foobar(...args)
}

const foo = new Foo(1,2)

console.log(foo instanceof Foo)//false

原因

instanceof操作符返回的检测的是:实例的原型链上是否存在构造函数的原型,即

function C(){}
const c = new C()
c instanceof C//true,因为Object.getPrototypeOf(c) === C.prototype

因此解决问题的方案很简单,将实例的原型设置为构造函数即可(注意,instanceof 只检测是否存在于原型链上)

In

function Foo(...args) {

    const this$ = {} //私有变量集

    class Foobar {
        constructor(x, y) {
            this$.x = x
            this$.y = y
        }

        toString() {
            console.log(`arguments: ${this$.x}-${this$.y}`)
        }

        get args() {
            return `${this$.x}--${this$.y}`
        }
    }

    return Object.setPrototypeOf(new Foobar(...args), this)
}

Out

const foo = new Foo(1,2)

console.log(foo instanceof Foo)//true

从丢失getter的现象来审视当前的原型链

背景

使用上一步的方法封装私有变量,但是会发现其中的getter丢失了。

const foo = new Foo(1,2)

console.log(foo.args)//undefined

首先来审视上一步封装方法背后的原型链是怎么样的:
内部Foobar实例 -> 外部Foo -> Object
原因在于,类中的getter-setter是挂载在原型上的,即Foobar.prototype
因此重写Foobar实例的原型后,会丢失getter。

解决问题的答案也就很明显了,将Foobar原型就近挂载到原型链上。

In

function Foo(...args) {

    const this$ = {} //私有变量集

    class Foobar {
        constructor(x, y) {
            this$.x = x
            this$.y = y
        }

        toString() {
            console.log(`arguments: ${this$.x}-${this$.y}`)
        }

        get args() {
            return `${this$.x}--${this$.y}`
        }
    }

    const instance = new Foobar(...args)
    Object.setPrototypeOf(Object.getPrototypeOf(instance), this)
    return instance
}

此时的原型链为:Foobar(instance) -> Foobar.prototype -> Foo.prototype -> Object

Out

const foo = new Foo(1, 2)
console.log(foo.args)//arguments: 1-2

Proxy

拦截get-set

最基础的用法是通过读取key,命中命名约定的私有变量则拦截

class Foo {
    constructor(x, y){
        this._x = x
        this._y = y
    }

    log(){
        console.log(`arguments: ${this._x}--${this._y}`)
    }

    get name() {
        return `${this._x}--${this._y}`
    }
}
const isPrivate = key => {
    if(key[0] === '_'){
        throw new Error('Attempt to access private property.')
    }
}
const handler = {
    get(target, key){
        isPrivate(key)
        return Reflect.get(target, key)
    },

    set(target, key, value){
        isPrivate(key)
        return Reflect.set(target, key, value)        
    }
}

const foo = new Foo(1, 2)
foo._x//Error...

这个版本的proxy已经能完成最基础的工作,禁止访问私有变量。接下来继续处理可能会出现的两个问题。

重写toJSON方法

由于调用JSON.stringify时仍然会调用私有变量导致报错,因此这里需要做个处理。

//...
const handler = {
    get(target, key){
        isPrivate(key)
        /* Here */
        if(key === 'toJSON'){
            const obj = {}
            for(const k in target){
                if(k[0] !== '_'){
                    obj[k] = target[k]
                }
            }
            return () => obj
        }
        return Reflect.get(target, key)
    },

    set(target, key, value){
        isPrivate(key)
        return Reflect.set(target, key, value)        
    }
}

避免遍历时读取私有变量

由于在使用for-in遍历目标时会读取私有变量导致报错,因此这里也需要做个处理--在遍历私有变量时,将私有变量的描述符设置为不可枚举

const handler = {
    get(target, key){
        isPrivate(key)
        /* Here */
        if(key === 'toJSON'){
            const obj = {}
            for(const k in target){
                if(k[0] !== '_'){
                    obj[k] = target[k]
                }
            }
            return () => obj
        }
        return Reflect.get(target, key)
    },

    set(target, key, value){
        isPrivate(key)
        return Reflect.set(target, key, value)        
    },

    getOwnPropertyDescriptor(target, key){
        const desc = Reflect.getOwnPropertyDescriptor(target, key)
        if (key[0] === '_') {
            desc.enumerable = false
        }
        return desc
    }    
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant