一文详解JS私有属性的6种实现方式

目录
  • _prop
  • Proxy
  • Symbol
  • WeakMap
  • #prop
  • ts private
  • 总结

class 是创建对象的模版,由一系列属性和方法构成,用于表示对同一概念的数据和操作。

有的属性和方法是对外的,但也有的是只想内部用的,也就是私有的,那怎么实现私有属性和方法呢?

不知道大家会怎么实现,我梳理了下,我大概用过 6 种方式,我们分别来看一下:

_prop

区分私有和公有最简单的方式就是加个下划线 _,从命名上来区分。

比如:

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());

这里的 Dong 就有私有属性 _name、_age,公有属性 friend。

但是这种方式只是一种命名规范,告诉开发者这个属性、方法是私有的,不要调用,但终究不是强制的,如果别人要用也阻止不了。

不过这种方式用的还是挺多的,历史比较悠久。

那怎么能基于这种规范实现真正的私有呢?这就要用到 Proxy 了:

Proxy

Proxy 可以定义目标对象的 get、set、Object.keys 的逻辑,可以在这一层做一下判断,如果是下划线 _ 开头就不让访问,否则就可以访问。

比如还是这个 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();

我们不直接调用它的对象的属性方法了,而是先用一层 Proxy 来约束下 get、set、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)

我们通过 new Proxy 来给 dong 定义了 get、set、ownKeys 的 handler:

  • get: 如果以下划线 _ 开头就返回空,否则返回目标对象的属性值 target[prop]
  • set: 如果以下划线 _ 开头就直接返回,否则设置目标对象的属性值
  • ownKeys: 访问 keys 时,过滤掉目标对象中下划线开头的属性再返回

这样就实现了下划线开头的属性的私有化:

我们测试下:

const proxy = new Proxy(dong, handler)

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

确实,这里只打印了公有属性的方法,而下划线开头的那两个属性没有打印。

我们基于 _prop 这种命名规范实现了真正的私有属性!

再调用下方法试试:

咋是 undefined 了?

因为 proxy.hello 方法的 this 也是指向 proxy 的,也会受限制,所以要再做下处理:

如果用的是方法,那就给它绑定 this 为目标对象。

这样 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 的方式)。

有的是从属性名上想办法,比如 _prop 和 Symbol,有的是从 this 上想办法,比如 Proxy(包一层) 和 WeakMap(不挂到 this),有的是从语言本身想办法,比如 ts 的 private 或者 es 新语法的 #prop。

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

以上就是一文详解JS私有属性的6种实现方式的详细内容,更多关于JS私有属性的资料请关注我们其它相关文章!

(0)

相关推荐

  • javascript使用闭包模拟对象的私有属性和方法

    最近因为做了一个项目,其中涉及到了js私有方法,这个概念在其语言里面是很常见的,很多语言都有private这个关键字,只要在一个类的前面加上private就表示申明了一个私有方法,但是javascript在面向对象的方面没有那么多的特征,他没有专门的private关键字,.要做到这一点就必须使用js自己的一些特性来变相的完成. 首先javascript里面有一个高级特性叫闭包,简单的说js的闭包可以理解成是一种现象或者特性,一般出现在两个函数嵌套的情况下,看例子: function a(){ v

  • 详解JavaScript 新语法之Class 的私有属性与私有方法

    译者按: 为什么偏要用 # 符号? 原文:JavaScript's new #private class fields •译者:Fundebug 本文采用意译,版权归原作者所有 proposal-class-fields与proposal-private-methods定义了 Class 的私有属性以及私有方法,这 2 个提案已经处于 Stage 3,这就意味着它们已经基本确定下来了,等待被加入到新的 ECMAScript 版本中.事实上,最新的 Chrome 已经支持了 Class 私有属性.

  • js面向对象之公有、私有、静态属性和方法详解

    现下,javascript大行其道,对于网站开发人员来说,javascript是必需掌据的一门语言,但随着jquery等框架的流行和使用,许多人对于原生javascript缺乏深入的理解,习惯了函数式的编辑风格,对于闭包.原型总是说不清道不明.对于js面向对象蹩脚的用着,而要了解js面向对象,就必需先了解js中什么是公有方法.特权方法.静态方法 方法/步骤 1.公有属性和公有方法 function User(name,age){ this.name = name;//公有属性 this.age

  • 如何在JavaScript中实现私有属性的写类方式(一)

    之前讨论过JavaScript中的写类方式.但没有讨论私有的实现.这篇看下. 我们知道JS中私有属性的实现本质就是 var + closure.如下 复制代码 代码如下: function Person(n, a){     // public     this.name = n;     // private     var age = a;     this.getName = function(){         return this.name;     }     this.getA

  • 如何在JavaScript中实现私有属性的写类方式(二)

    上一篇写了个工具函数$class,这篇再完善以下.实现以下功能 1,继承 2,子类继承父类时,不继承父类的私有属性 复制代码 代码如下: /**  * @param {String} className  * @param {String/Function} superCls  * @param {Function} classImp  */function $class(className, superCls, classImp){     if(superCls === '') superC

  • JavaScript中的私有/静态属性介绍

    •模拟块级作用域 大家都知道在JavaScript中没有块级作用域的概念,我们可以通过使用闭包来模拟实现块级作用域,看下面的示例: 复制代码 代码如下: (function () { for (var i = 0; i < 10; i++) { //Do Nothing } alert(i); //输出10 })(); 第6行可以访问到for循环块中的变量i,如果我们稍微修改以上代码,把for循环块放置在闭包中,情况就不一样了: 复制代码 代码如下: (function () { (functi

  • 一文详解JS私有属性的6种实现方式

    目录 _prop Proxy Symbol WeakMap #prop ts private 总结 class 是创建对象的模版,由一系列属性和方法构成,用于表示对同一概念的数据和操作. 有的属性和方法是对外的,但也有的是只想内部用的,也就是私有的,那怎么实现私有属性和方法呢? 不知道大家会怎么实现,我梳理了下,我大概用过 6 种方式,我们分别来看一下: _prop 区分私有和公有最简单的方式就是加个下划线 _,从命名上来区分. 比如: class Dong { constructor() {

  • 详解js产生对象的3种基本方式(工厂模式,构造函数模式,原型模式)

    1.工厂模式 工厂模式是软件工程领域一种广为人知的设计模式,而由于在ECMAScript中无法创建类,因此用函数封装以特定接口创建对象.其实现方法非常简单,也就是在函数内创建一个对象,给对象赋予属性及方法再将对象返回即可. function a(name){ var b = new object(); b.name = name; b.say = function(){ alert(this.name); } return b } 函数内部产生b对象并返回. 2.构造函数模式 function

  • 一文详解JS中的事件循环机制

    目录 前言 1.JavaScript是单线程的 2.同步和异步 3.事件循环 前言 我们知道JavaScript 是单线程的编程语言,只能同一时间内做一件事,按顺序来处理事件,但是在遇到异步事件的时候,js线程并没有阻塞,还会继续执行,这又是为什么呢?本文来总结一下js 的事件循环机制. 1.JavaScript是单线程的 JavaScript 是一种单线程的编程语言,只有一个调用栈,决定了它在同一时间只能做一件事.在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行.在

  • 一文详解JS 类型转换方法以及如何避免隐式转换

    目录 前言 类型转换的概念 5 种转换方法 1. 字符串转换为数字 2. 数字转换为字符串 3. 布尔值转换为字符串或数字 4. 对象转换为原始值 5. 空值和未定义值转换为数字或字符串 隐式转换 总结 前言 当我们使用JavaScript编写代码时,类型转换是一个非常重要的概念.JavaScript是一种弱类型语言,这意味着变量可以自动转换为另一种类型,而不需要我们明确地指定.在这篇文章中,我们将探讨JavaScript类型转换的概念.转换方法,并针对隐式转换进行分析. 类型转换的概念 Jav

  • 详解Java创建线程的五种常见方式

    目录 Java中如何创建线程呢? 1.显示继承Thread,重写run来指定现成的执行代码. 2.匿名内部类继承Thread,重写run来执行线程执行的代码. 3.显示实现Runnable接口,重写run方法. 4.匿名内部类实现Runnable接口,重写run方法 5.通过lambda表达式来描述线程执行的代码 [面试题]:Thread的run和start之间的区别? Thread类的具体用法 Thread类常见的一些属性 中断一个线程 1.方法一:让线程run完 2.方法二:调用interr

  • 详解Nginx 和 PHP 的两种部署方式的对比

    详解Nginx 和 PHP 的两种部署方式的对比 2种部署方式简介 第一种 前置1台nginx服务器做HTTP反向代理和负载均衡 后面N太服务器的Nginx做Web服务,并调用php-fpm提供的fast cgi服务 此种部署方式最为常见,web服务和PHP服务在同一台服务器上都有部署 第二种 前置1台nginx服务器做Web服务 后面服务器只部署php-fpm服务,供nginx服务器调用 前置1台nginx服务器,在调用后面多例php-fpm服务时,也可以做到负载均衡 如下图 : 对比 从系统

  • 详解C++中常用的四种类型转换方式

    目录 1.静态类型转换:static_cast(exp) 2.动态类型转换:dynamic_cast(exp) 3.常类型转换:const_case(exp) 4. 解释类型转换: reinterpret_cast(exp) 1.静态类型转换:static_cast(exp) 1.1静态类型转换主要用于两种转换环境 1.1.1 C++内置类型的转换:与C风格强转类似. 与c相同的地方: #include <iostream> using namespace std; int main() {

  • 详解js私有作用域中创建特权方法

    本文实例分享了js私有作用域中创建特权方法,供大家参考,具体内容如下 特权方法就是有权访问私有变量和私有函数的公有方法: function MyObject(){ var privateVariable = 10; function privateFunction(){ return false; } this.publicMethod = function(){ privateVariable ++; return privateFunction(); }; } var x = new MyO

  • 详解Java合并数组的两种实现方式

    最近在写代码时遇到了需要合并两个数组的需求,突然发现以前没用过,于是研究了一下合并数组的方式,总结如下. 1.System.arraycopy()方法 (1) 解析 通过阅读JDK源码,我可以知道方法原型如下: public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length); 其中: src是源数组 srcPos是源数组复制的起始位置 dest是目标数组 destP

  • 详解Spring中bean的几种注入方式

    首先,要学习Spring中的Bean的注入方式,就要先了解什么是依赖注入.依赖注入是指:让调用类对某一接口的实现类的实现类的依赖关系由第三方注入,以此来消除调用类对某一接口实现类的依赖. Spring容器中支持的依赖注入方式主要有属性注入.构造函数注入.工厂方法注入.接下来将为大家详细介绍这三种依赖注入的方式以及它们的具体配置方法. 1.属性注入 属性注入即通过setXXX( )方法注入bean的属性值或依赖对象.由于属性注入方式具有可选择性和灵活性高的特点,因此它也是实际开发中最常用的注入方式

随机推荐