阅读 54

JS中的私有属性和只读属性

什么是私有属性

私有属性是面向对象编程(OOP)中非常常见的一个特性,一般是指能被class内部的不同方法访问,但不能在类外部被访问,大多数语言都是通过public、private、protected 这些访问修饰符来实现访问控制的。

私有属性(方法)的意义很大程度在于将class的内部实现隐藏起来,而对外接口只通过public成员进行暴露,以减少外部对该class内部实现的依赖或修改。

简单地说,对于一个class来说,外部调用方其实只关注部分,不关注class内部的具体实现,也不会使用到一些内部的变量或者方法,这种时候,如果我把所有东西都暴露给外部的话,一方面增加了使用方的理解成本和接受成本,另一方面也增加了class本身的维护成本和被外部破坏的风险。

因此,只暴露和用户交互的接口,其他不交互的部分隐藏为私有的,既可以促进体系运行的可靠程度,(防止外部猪队友疯狂修改你的class),也可以减小使用者的信息负载(我只需要调用一个方法来获取我要的东西,其他的我不管)。

Js中的私有属性

众所周知,JavaScript 中没有 public、private、protected 这些访问修饰符(access modifiers),而且长期以来也没有私有属性这个概念,对象的属性/方法默认都是public的。

这意味着你写一个function或者class,外部其实可以任意访问,任意修改,所以你可以在js中看到很多看起来很hack的写法,从外部修改一个值,莫名的内部的运行逻辑就变化了。这本身是一件非常危险的事,同时对于一个开发而言,怎么能允许,所以通过对逻辑和数据进行一定封装和魔改,JS开发者们走上了曲线实现“私有属性”之路。

自欺欺人型

自欺欺人型,我说他是私有,那么他就是私有,不允许辩驳。或者说约定俗成,以一种不成文的规定,在变量前加上下划线"_"前缀,约定这是一个私有属性;但是实际上这一类属性与正常属性没有任何区别,你在外部仍然可以访问,仅仅是指你在访问是看到了这个前缀,哦,原来这是一个私有,我不该直接访问。

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

理论上,ts的private修饰符也是这一类,尽管ts实现了private,public等访问修饰符,但是实质只会在编译阶段进行检查,编译后的结果是不会实现访问控制的,也就是运行时是完全没用的。

闭包

闭包是指在 JavaScript 中,内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后.

基于这个特性,通过创建一个闭包,我们能模拟实现一个私有变量

var Person = function(name){     var _name = name;     this.getName = function(){         return _name;     };     this.setName = function(str){         _name = str;     }; }; var person = new Person('hugh'); person.name;        // undefined, name是Person函数内的变量,外部无法直接访问 person.getName();   // 'hugh' person.setName('test');  复制代码

或者 class形式的

class Person {   constructor(name) {     // 私有属性     let _name = name;      this.setName = function (name) {       _name = name;     };     this.getName = function () {       return _name;     };   }   greet() {     console.log(`hi, i'm ${this.getName()}`);   } } 复制代码

闭包是一个比较好的实现,借助js本身的特性实现了访问控制,但是同样毕竟是个魔改,仍然遗留了一点问题,你需要为每个变量定义getter和setter,否则你甚至无法在class内部获取到整个私有变量,但是当你定义了getter,外部也可以通过这个getter来获取私有变量。

所以闭包实现了你无法直接读取内部的私有属性,同样,在class内部你也无法直接使用这个私有属性。

symbol和weakMap

我们可以知道,实现私有属性,只要是外部无法知道这个属性名,只有内部知道的属性名,就可以做到外部无法访问的特性,基于ES6的新语法symbol和weakMap,我们可以去实现这个能力。

基于symbol

Symbol 是ES6 引入了一种新的原始数据类型,表示独一无二的值,并且可以所谓对象的属性名。一个完全独一无二,并且除了通过变量直接获取,无法被明确表示的属性名。完美。

var Person = (function(){     const _name = Symbol('name');     class  Person {         constructor(name){             this[_name] = name;         }         get name(){ return this[_name]         }     }     return Person }()) let person = new Person('hugh'); person.name  // hugh 复制代码

基于WeakMap

WeakMap也是和symbol一样,是一个把对象作为key的map,所以我们可以把实例本身作为key

var Person = (function(){     const _name = new WeakMap();     class  Person {         constructor(name){             _name.set(this, name)         }         get name(){ return _name.get(this)         }     } return Person }()) let person = new Person('hugh'); person.name  // hugh 复制代码

class提案

当然好消息是ES2019中已经增加了对 class 私有属性的原生支持,只需要在属性/方法名前面加上 '#' 就可以将其定义为私有,并且支持定义私有的 static 属性/方法,同时我们现在也可以通过 Babel 已使用(babel会把#编译成上面weakMap的形式来实现私有属性),并且 Node v12 中也增加了对私有属性的支持。

class Person {   // 私有属性   #name;    constructor(name) {     this.#name = name;   } get name(){ return this.#name;   } } 复制代码

至于为什么是#,而不是常用的private修饰符,可以看这篇文章 zhuanlan.zhihu.com/p/47166400

只读属性

只读属性与上面私有变量有点类似,逻辑上你只要给你的私有属性增加一个getter,而不增加setter那么他就是一个只读属性。

class Person {   constructor(name) {     // 私有属性     let _name = name;      this.name = function () {       return _name;     };   } } 复制代码

比较麻烦的是你必须使用getter方法来获取属性,当然我们可以通过class的get来简化这个

class Person {   // 私有属性   #name;    constructor(name) {     this.#name = name;   } get name(){ return this.#name;   } } 复制代码

然而对于简单类型这个就是比较完美的只读属性了,但是对于对象,数组等复杂类型,你仍然可以通过外部去增加属性。

class Person {   // 私有属性   #name;    constructor() {     this.#name = {};   } get name(){ return this.#name;   } } let person  = new Person(); person.name.title = 'hugh'; person.name // {title:'hugh'} 复制代码

为了让对象类型的属性不可变,我们可以将这个属性freeze

使用Object.freeze()冻结的对象中的现有属性值是不可变的,不可编辑,不可新增。用Object.seal()密封的对象可以改变其现有属性值,但是不可新增。

class Person {   // 私有属性   #name;    constructor() {     this.#name = {title:'hugh'}; Object.freeze(this.#name)   } get name(){ return this.#name;   } } 复制代码

当你freeze这个属性后,会造成一个问题就是,你在class内部也无法修改这个属性了,所以如果你是希望外部只读,但是会有方法可以修改这个值的话,那么就不可以使用freeze了。

Object.defineProperty与proxy

要设置一个对象的值可读,我们可以用更简单的办法,使用defineProperty,将其writable设为false

var obj = {}; Object.defineProperty( obj, "<属性名>", {   value: "<属性值>",   writable: false }); 复制代码

当然其限制也很大:

  1. 无法阻止整个对象的替换,也就是obj可以被直接赋值

  2. 需要对对象的每个属性进行设置,同时对于新增属性无法生效(除非你在新增的时候再调用一下这个)

  3. 嵌套对象也无法阻止对内部的编辑修改。

对此我们可以使用es6的proxy来进行优化,proxy能实现defineProperty的大多数功能,又没有以上的问题

var obj = {}; const objProxy = new Proxy(obj, {     get(target,propKey,receiver) {       return Reflect.get(target, propKey, receiver);     },     set() { // 拦截写入属性操作       console.error('obj is not writeable');       return true;     },   }); 复制代码

对此,我们就不需要关心obj内部属性的新增了(尽管,对于嵌套对象,仍然无法阻止)

基于以上方案,我们可以对一开始的只读属性进行优化

class Person {   // 私有属性   #name;    constructor() {     this.#name = {};   } get name(){ return new Proxy(this.#name, {     get(target,propKey,receiver) {       return Reflect.get(target, propKey, receiver);     },     set() { // 拦截写入属性操作       console.error('obj is not writeable');       return true;     },   });   }   addName(name){ this.#name[name] = name; } } let person  = new Person(); person.name.title = 'hugh'; // obj is not writeable person.addName('hugh') 复制代码

对于proxy的兼容我们可以引入Google的proxy-polyfill,但是需要注意,proxy-polyfill由于需要遍历对象的所有属性,为每个属性设置defineProperty,所以必须是已知对象的属性,对于对象内新增的属性无法监听,所以proxy-pollyfill seal了target和proxy,已防止你新增属性


作者:hughhe
链接:https://juejin.cn/post/6904849160004960264


文章分类
后端
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐