js的私有属性,Symbol实现和es10的class # 和闭包模式和TS private

1,427 阅读6分钟

Symbol实现

为什么说可以通过Symbol来实现私有属性?因为只要你获取不到作为键的Symbol引用,那么你确实无法获取属性值!

但是es6同时又给出了Object.getOwnPropertySymbols接口,这让你可以在运行时轻松获取所有的Symbol键引用。因此略微遗憾,Symbol的方式,并不能完全做到私有化。

但是想到java私有属性依然可以通过反射在运行时来获取。所以我发觉Symbol的方式就似乎可以接受了。安全是一个锁链问题,一环扣一环,通常你所期望的安全,其实是在你所负责的这一环中做出取舍的结果。

class 中使用 # 

首先看下js中的变量命名定义是这样的:第一个字符必须是字母/下划线/符号 $,剩下的字符则可以加上数字

即是说,为了实现class中的私有属性,付出的代价是使用#开头命名变量,这打破了js变量定义规则,那么为什么要付出这么大的代价?

答案是为了换取绝对的私有,class中声明的私有属性,你无法在运行时获取,只有在class定义范围内才能访问这个私有属性。

这是等价交换啊,你付出的越多,得到的也就越多

此外,还有一个#的好处,就是在class内部,class的实例也能互相访问私有属性,比如:

class Dog {
    #name;
    constructor(name){
        this.#name = name;
    } 
    hello(dog){
        console.log(this.#name + '说:你好' + dog.#name);
    }
}

new Dog('huahua').hello(new Dog('xiaoming')) // huahua说:你好xiaoming

关于这个提案的讨论可以参考

GitHub - tc39/proposal-class-fields: Orthogonally-informed combination of public and private fields proposals

注意其中是这样形容的:No backdoor to access private 即你没有任何可能在外部访问私有属性。以及analogous to mechanisms like closures and WeakMap 形容了#语法是类似闭包的效果,下面会说到

但是很多很多人讨厌这个#语法,比如这里说的

勉为其难再次喷一下 private fields - 知乎 (zhihu.com)

还有广大coders的反对声音

A summary of feedback regarding the # sigil prefix · Issue #100 · tc39/proposal-class-fields · GitHub

但其实我不太理解啦,为什么喷这个语法,这是我在知乎上问别人的,等待一个回答:

我作为一个入门前端一年的小白,没有理解到答主所说的代价在哪里?我翻了好多观点,包括tc39提案的issue讨论,基本上都是说ugly。有人说不需要hard private,同时又说#很丑,但这难道不是正好吗,因为这意味着#并不会大量充斥在代码中。还是说,这是基于破窗理论的担心?如果剔除审美,剔除来对来自其他语言的风格喜好,这个#语法的问题在哪?其实答主也说了,丑只是问题最小的那个,那问题到底在哪?如果只是“不需要”,那么“增加新语法”对于不需要的人来说,影响在哪里?是不提倡用hard private吗,但这不就成了信仰的问题了,这不好吧,因为这无关切实的利弊啊

回答来了,在下面的图片中,可以先看图片,再看我这段话:比较接近哲学层面,不确定性没有被消除,而是被转移了。其实这是9月份的回答,其实当时我仍然没有太get到这种转移,现在10月份了,我跑去把当时这个回答截图搬过来,此时我觉得突然又有新的想法:这两天在看个objective-c的东西,是cordova的项目,我想导入个private下的h文件,我发现导不了,不知道是不是我的问题,结果只能退而求其次,我去导上层的public对象就行,但即便我得到这个引用,仍然只能调用一下其拥有的方法,在不改源码的情况下我不知道该如何重写/注册某个生命周期的回调(我想给cordova的webView扩展个shouldStartLoadWithRequest 方法),据说objective-c是动态语言,但我还没看懂,总之这件事情让我觉得private的强制性让我很难受。而恰恰这种事情在js中很轻松的,只要我获得了一个对象的引用,那么我不必去改源码,也能在我自己的代码中随意修改这个对象的行为,对于一些我不想要的行为,我可以进行定制屏蔽(通常不会这么做的,实在是因为版本问题和没啥意义的行为,我需要改掉,比如我的项目是不依赖url来跳转路由的,奇葩,但是我做okta登录会发生重定向并且从url中需要获取参数然后删掉参数,这个过程url会变化,所以我想屏蔽掉这个url检测)总之,强制的private在前端来说,似乎确实,没什么必要。前端的代码是跑在千千万万个客户端中的,说实话,这是基因,基因决定了前端代码天然存在“不安全”因素,因此private的意义确实是被大大降低了,前端关注仍然是UI。所以private的意义何在呢?加之前端生态最大的困境就是大量的开源库维护不力,因此弱private的特性反而解放了前端代码的适配能力。而当我们重新赋予js这种强private能力,谁又能保证大家不会滥用这种能力呢?

闭包

首先闭包场景是指一个函数引用了另一个函数作用域变量。用闭包实现私有变量,也能够做到焊死车门!这其实就是最最最早期js的做法。比如:

const Dog = (() => {const name = 'hua hua'; return function() {alert(name)};})()

除了在new Dog()狗狗出生的时候,狗狗的名字一闪而过,如果你没有拿小本本记下来,那你再也不能知道这只狗狗的名字了。虽然hua hua会一只存在于内存中,伴随着狗狗的整个生命周期,但是无论如何你拿不到。虽然每只狗狗都叫hua hua,但你可以加个Math.random升级一下。

当然,所谓的weakMap的做法,其实也是基于闭包,就是能更好的在对象的维度上支持私有属性,比如伪代码: _weakMap.set(this, privateProxy -> 初始化一个空对象) 注意到这里的键是this 可以想象这样的做法,正好就契合将来每一个实例都拥有自己的私有空间的诉求。但本质上仍然是闭包,即对外不导出_weakMap对象。参考 arrays - Multiple private properties in JavaScript with WeakMap - Stack Overflow

Typescript private 修饰符

这个其实只是在编辑器中提醒你不要这么干而已,是没有去限制运行时获取所谓的私有属性的。

最后贴个彩蛋,在ts中写class的#语法会怎样呢?

class test {  
    #name = 'haha'
    #age = 17
}

最终编译成这个样子

var test = /** @class */ (function () {  
    function test() {      
        _test_name.set(this, 'haha');      
        _test_age.set(this, 17);  
    }  
    return test;
}());
_test_name = new WeakMap(), _test_age = new WeakMap();

总结

要绝对的私有化,要么使用class的#语法,要么使用闭包来封装。如果不想要那么笨重,使用轻量的Symbol我感觉就很好,因为其实就算用Object.getOwnPropertySymbols获取到所有Symbol键,但只要Symbol不带描述,那么还是无法准确知道这个键的含义的,特别是Symbol键较多的时候。也可以认为,这个接口暴露的信息其实也很有限的!