6 implementations of private properties, how many have you used?

A class is a template for creating an object, consisting of a series of properties and methods that represent data and operations on the same concept.

Some properties and methods are external, but some are only for internal use, that is, private, so how to implement private properties and methods?

I don't know how everyone will implement it. I sorted it out. I have used about 6 methods. Let's take a look at them separately:

_prop

The easiest way to distinguish between private and shared is to add an underscore _ to distinguish it from the name.

for example:

class Dong {
    constructor() {
        this._name = 'dong';
        this._age = 20;
        this.friend = 'guang';
    }

    hello() {
        return 'I\'m ' + this._name + ', '  + this._age + ' years old';
    }
}

const dong = new Dong();

console.log(dong.hello());
复制代码

Here Dong has private attributes _name, _age, and a common attribute friend.

But this method is just a naming convention, telling developers that this property and method are private and should not be called, but it is not mandatory after all, and it cannot be prevented if others use it.

However, this method is still used quite a lot and has a long history.

So how can true privateness be achieved based on this specification? This is where Proxy is used:

Proxy

Proxy can define the logic of get, set, and Object.keys of the target object. You can make a judgment at this layer. If it starts with an underscore _, it is not allowed to access, otherwise it can be accessed.

For example, this class:

class Dong {
    constructor() {
        this._name = 'dong';
        this._age = 20;
        this.friend = 'guang';
    }

    hello() {
        return 'I\'m ' + this._name + ', '  + this._age + ' years old';
    }
}

const dong = new Dong();
复制代码

We do not directly call the property method of its object, but first use a layer of Proxy to constrain the behavior of get, set, and getKeys:

const dong = new Dong();
 
const handler = {
    get(target, prop) {
        if (prop.startsWith('_')) {
            return;
        }
        return target[prop];
   },
   set(target, prop, value) {
    if (prop.startsWith('_')) {
        return;
     }
     target[prop] = value;
   },
   ownKeys(target, prop) {
      return Object.keys(target).filter(key => !key.startsWith('_'))
   },
 }
 
const proxy = new Proxy(dong, handler)
复制代码

We define handlers for get, set, and ownKeys for dong through new Proxy:

  • get: Returns empty if it starts with an underscore _, otherwise returns the property value target[prop] of the target object
  • set: Return directly if it starts with an underscore _, otherwise set the property value of the target object
  • ownKeys: When accessing keys, filter out the properties starting with an underscore in the target object and return

This implements the privatization of properties starting with an underscore:

Let's test:

const proxy = new Proxy(dong, handler)

for (const key of Object.keys(proxy)) {
    console.log(key, proxy[key])
}
复制代码

Indeed, only the methods with common properties are printed here, and the two properties starting with the underscore are not printed.

We implemented truly private properties based on the naming convention of _prop!

Try calling the following method:

Why is it undefined?

Because the this of the proxy.hello method also points to the proxy, it will also be restricted, so do the following processing:

If a method is used, bind it with this as the target object.

这样 hello 方法就可以访问到那些 _ 开头的私有属性了:

我们通过 Proxy 给下划线的命名规范实现了真正的私有属性,但是要定义一层 Proxy 比较麻烦,有没有不定义 Prxoy 的方式呢?

确实有,比如 Symbol:

Symbol

Symbol 是 es2015 添加的一个 api,用于创建唯一的值。基于这个唯一的特性,我们就可以实现私有属性。

比如这样:

const nameSymbol = Symbol('name');
const ageSymbol = Symbol('age');

class Dong {
    constructor() {
        this[nameSymbol] = 'dong';
        this[ageSymbol] = 20;
    }

    hello() {
        return 'I\'m ' + this[nameSymbol] + ', '  + this[ageSymbol] + ' years old';
    }
}

const dong = new Dong();

复制代码

我们不再用 name 和 age 作为私有属性名了,而是用 Symbol 生成唯一的值来作为名字。

这样外面因为拿不到属性名,就没法取到对应的属性值:

这种方式比 Proxy 的方式更简单一些,也是用的很多的一种实现私有属性的方式。

如果想暴露出去,可以定义个 get 方法:

但是这种私有属性是真的没法访问么?

不是的,有一个 api 叫做 Object.getOwnPropertySymbols,可以取到对象的所有 Symbols 属性,然后就可以拿到属性值了:

所以说这种方式只是 Object.keys 取不到对应的属性而已,不如 Proxy 那种方式完善。

那不用 Proxy 的方式,还比有没有 Symbol 更完善的呢?

那可以试试这种:

WeakMap

外面可以访问到属性和方法是因为我们把它挂到了 this 上,那不挂到 this 上外面不就访问不到了么?

比如用一个 Map 保存私有属性:

const privateFields = new Map();

class Dong {
    constructor() {
        privateFields.set('name', 'dong');
        privateFields.set('age', 20);
    }

    hello() {
        return 'I\'m ' + privateFields.get('name') + ', '  + privateFields.get('name') + ' years old';
    }
}
复制代码

我们测试下:

这样貌似可以,但不知道大家有没有发现其中的问题:

  • 所有对象都用同一个 Map,之间相互影响
  • 对象销毁了这个 Map 依然存在

怎么解决这个问题呢?

不知道大家用没用过 WeakMap,它的特性是只能用对象作为 key,对象销毁,这个键值对就销毁。

完美解决了上面两个问题:

  • 因为是用对象作为 key 的,那不同的对象是放在不同的键值对上的,相互没影响
  • 对象销毁的时候,对应的键值对就销毁,不需要手动管理

貌似是很完美,我们实现下:

const dongName = new WeakMap();
const dongAge = new WeakMap();

const classPrivateFieldSet = function(receiver, state, value) {
    state.set(receiver, value);
}

const classPrivateFieldGet = function(receiver, state) {
    return state.get(receiver);
}


class Dong {
    constructor() {
        dongName.set(this, void 0);
        dongAge.set(this, void 0);

        classPrivateFieldSet(this, dongName, 'dong');
        classPrivateFieldSet(this, dongAge, 20);
    }

    hello() {
        return 'I\'m ' + classPrivateFieldGet(this, dongName) + ', '  + classPrivateFieldGet(this, dongAge) + ' years old';
    }
}
复制代码

每个属性定义了一个 WeakMap 来维护,key 为当前对象,值为属性值,get 和 set 使用 classPrivateFieldSet 和 classPrivateFieldGet 这两个方法,最终是通过从 WeakMap 中存取的。

在构造器里初始化下当前对象对应的属性值,也就是 dongName.set(this, void 0),这里的 void 0 的返回值是 undefined,一个意思。

测试下:

哇,通过 WeakMap 也能实现私有属性!

不过这里的 classPrivateFieldGet 没必要定义吧,直接 xxMap.get 不就行么?

确实,包一层的目的是为了可以加一些额外的逻辑,这里也可以直接从 weakMap 取。

但这样写起来也很麻烦呀,有没有更简单的方式呢?

能不能设计一种语法糖,它自动编译成这种方式呢?

想的没错,确实有这种语法糖:

#prop

现在有一个私有属性的 es 草案,可以通过 # 的方式来标识私有属性和方法。

比如这样:

class Dong {
    constructor() {
        this.#name = 'dong';
        this.#age = 20;
        this.friend = 'guang';
    }
    hello() {
        return 'I\'m ' + this.#name + this.#age + 'years old';
    }
}
复制代码

这里的 name 和 age 都是私有的,而 friend 是共有的。

这种新语法 JS 引擎没那么快支持,但是可以通过 babel 或者 ts 编译器来编译成低版本语法的方式来提前用。

比如 babel 有 @babel/proposal-private-property-in-object 的插件,它可以实现这种语法的编译:

babel 就是把 #prop 编译成上面那种 WeakMap 的方式来实现的。

这个插件在 @babel/preset-env 的预设里,会自动引入:

除了 babel,ts 里也可以直接用这种语法:

也是会编译成 WeakMap 的方式来实现。

其实 ts 实现的新语法还是不少的,比如 ? 和 ?? 分别是可选链和默认值的语法,下面这两种写法等价:

const res = data?.name ?? 'dong';
const res2 = data && data.name  || 'dong';
复制代码

这种新语法都是直接可用的,babel 的话需要引入下 proposal 插件。

对了,我记得 ts 里 class 也是有 private 的修饰符的,那个不也是私有属性么?

其实它是私有属性但也不完全是,我们来看一下:

ts private

ts 可以通过 private 来修饰属性、方法的可见性:

  • private 表示属性私有,只有 class 内部可访问
  • protected 表示保护,只有 class 和子 class 可访问
  • public 表示共有,外部也可访问

类型检查和提示的时候是有区别的,比如 private 属性在 class 外部不可访问:

而 class 内部是可以访问的:

但是这种约束只是用于类型检查的,只存在编译期间,运行时并没有这种约束。

我们可以看下编译后的代码:

可以看到没有做任何处理。

而如果用 #prop 的方式,除了编译时是 private 的,运行时也是:

所以,要实现真正的 private 的话,还是用 #prop 的方式,如果只是编译时约束那声明下 private 就行。

总结

class 用于定义围绕某个概念的一系列属性和方法,这些属性和方法有的是内部用的,有的是对外的。只有内部用的属性、方法需要实现私有化。

实现私有属性方法,我树立了 6 种方式:

  • 通过下划线 _prop 从命名上区分
  • 通过 Proxy 来定义 get、set、ownKeys 的逻辑
  • 通过 Symbol 来定义唯一的属性名,不能通过 keys 拿到
  • 通过 WeakMap 来保存所有对象的私有属性和方法
  • 通过 #prop 的 es 新语法实现私有,babel 和 tsc 会把它们编译成 WeakMap 的方式
  • 通过 ts 的 private 在编译时约束

这六种方式,有三种只是伪私有,比如 _prop(依然可以访问)、ts 的 private(运行时可访问)、Symbol(可以通过 Object.getOwnSymbols 拿到 symbol 来访问)。

另外三种是真正的私有,包括 Proxy、WeakMap、#prop(目前是编译为 WeakMap 的方式)。

这 6 种实现私有属性的方式,你用过几种?

Guess you like

Origin juejin.im/post/7080131411503972366