如何设计ES6中class实现私有属性呢?

AI 概述
为什么会出现 class何为私有属性?私有属性提案如何设计实现私有属性呢?约定命名闭包进阶版闭包闭包的做法产生的问题?SymbolWeakMapProxy 代理设置拦截新式做法附:TypeScript 中的处理方式 为什么会出现 class 其实,学过 java 的小伙伴一定对class熟悉不过了,那为什么 JS 里面还要引入 class 呢?...
目录
文章目录隐藏
  1. 为什么会出现 class
  2. 私有属性提案
  3. 如何设计实现私有属性呢?
  4. 闭包
  5. Symbol
  6. 新式做法

为什么会出现 class

其实,学过 java 的小伙伴一定对class熟悉不过了,那为什么 JS 里面还要引入 class 呢?

在 es6 之前,虽然 JS 和 Java 同样都是 OOP(面向对象)语言,但是在 JS 中,只有对象而没有类的概念。

es6 中class的出现拉近了 JS 和传统 OOP 语言的距离。但是,它仅仅是一个语法糖罢了,不能实现传统 OOP 语言一样的功能。在其中,比较大的一个痛点就是私有属性问题。

何为私有属性?

私有属性是面向对象编程(OOP)中非常常见的一个特性,一般满足以下的特点:

  • 能被class内部的不同方法访问,但不能在类外部被访问;
  • 子类不能继承父类的私有属性。

在 Java 中,可以使用 private 实现私有变量,但是可惜的是, JS 中并没有该功能。

私有属性提案

2015 年 6 月,ES6 发布成为标准,为了纪念这个历史性时刻,这个标准又被称为 ES2015,至此,JavaScript 中的 class 从备胎中转正。但是没有解决私有属性这个问题,产生了一个提案——在属性名之前加上 # ,用于表示私有属性。

class Foo {
  #a;  // 定义私有属性
  constructor(a, b) {
    this.#a = a;
    this.b = b
  }
}

上述代码私有属性的声明,需要先经过 Babel 等编译器编译后才能正常使用。

至于为什么不用 private 关键字呢?参考大佬说的就是有一大原因是向 Python 靠拢,毕竟从 es6 以来, JS 一直向着 Python 发展。

如何设计实现私有属性呢?

上文我们介绍了 class 出现原因,以及它没有解决私有属性这个问题,那么我们作为 JSer 们,如何自己设计一下呢?带着好奇心来探讨一下吧:

约定命名

目前使用最广的方式:约定命名,既然还没有解决,我们不是可以自己定义一下嘛,对于特殊命名的就把它当做私有属性使用不就可以了吗?大家都遵循这个规范,不就解决这个问题了吗?

/* 约定命名 */
class ClassA {
  constructor(x) {
    this._x = x;
  }
  getX() {
    return this._x;
  }
}

let classa = new ClassA(1);
/* 此时可以访问我们自定义私有属性命名的 _x */
console.log(classa._x); // 1
console.log(classa.getX()); // 1

显然,上述方法简单方便,大家按照规范来就可以了,也比较好阅读他人代码。

闭包

闭包的一个好处就是可以保护内部属性,也是我开头想要实现的一种方式,做法就是将属性定义在 constructor 作用域内,如下代码:

/* 闭包 */
class ClassB {
  constructor(x) {
    let _x = x;
    this.getX = function(){
      return _x;
    }
  }
}
let classb = new ClassB(1);
/* 此时不可以访问我们自定义私有属性命名的 _x */
console.log(classb._x); // undefined
console.log(classb.getX()); // 1

显然,如果私有属性越来越多,那么看起来就很臃肿,对后续维护造成了一定的麻烦,对于他人阅读也是不太友好。同时呢,引用私有变量的方法又不能定义在原型链上。

进阶版闭包

可以通过 IIFE(立即执行函数表达式)建立一个闭包,在其中建立一个变量以及 class,通过 class 引用变量实现私有变量。

/* 进阶版闭包 */
const classC = (function () {
  let _x;

  class ClassC {
    constructor(x) {
      _x = x;
    }
    getX() {
      return _x;
    }
  }
  return ClassC;
})();

let classc = new classC(3);
/* 此时不可以访问我们自定义私有属性命名的 _x */
console.log(classc._x); // undefined
console.log(classc.getX()); // 3

这种方式就有点 模块化 的思想了

闭包的做法产生的问题?

上述,我们用了闭包和进阶版闭包来解决私有属性这个问题,但是这是有问题的,我们以进阶版闭包为例:

/* 进阶版闭包带来的问题 */
const classC = (function () {
  let _x;

  class ClassC {
    constructor(x) {
      _x = x;
    }
    getX() {
      return _x;
    }
  }
  return ClassC;
})();

let classc1 = new classC(3);
/* 此时不可以访问我们自定义私有属性命名的 _x */
console.log(classc1._x); // undefined
console.log(classc1.getX()); // 3

/* 问题引出:此时新创建一个实例 */
let classc2 = new classC(4);
/* 出现了问题:实例之间会共享变量 */
console.log(classc1.getX()); // 4

从上述代码可以发现,用闭包创建私有变量是不行的,实例之间会共享变量,就好像几个人都实例化了,但是操作地还是同一个属性,这显然是不可取的。

Symbol

利用 Symbol 变量可以作为对象 key 的特点,我们可以模拟实现更真实的私有属性。

/* Symbol */
const classD = (function () {
  const _x = Symbol('x');
  class ClassD {
    constructor(x) {
      this[_x] = x;
    }
    getX() {
      return this[_x];
    }
  }
  return ClassD;
})();

let classd = new classD(4);
/* 此时不可以访问我们自定义私有属性命名的 _x */
console.log(classd._x); // undefined
console.log(classd.getX()); // 4
classd[_x] = 1;
console.log(classd[_x]); // ReferenceError: _x is not defined

关于上述代码,

Sysmol 要配合 import/export 模板语法。比如 A.js 里面你定义了 class A 和 Symbol(就用你的写法),对外只暴露 class A。然后在别的 js 文件引入 class A 实例化,拿不到 Symbol 的值,而且无法通过’.’去访问变量名(Symbol 唯一,不暴露外界拿不到)。这样才是私有。

通过模板化的角度,我们对外暴露 ClassDSymbol 唯一,不会暴露,外界拿不到,但是这个也不是毫无破绽,看如下代码:

console.log(classd[Object.getOwnPropertySymbols(classd)[0]]); // 4

原来,ES6 的 Object.getOwnPropertySymbols 可以获取 symbol 属性,今天又学到了新东西。

为了解决上述问题,我们又要引出一个新的东西:WeakMap

WeakMap

/* WeakMap  */
const classE = (function () {
  const _x = new WeakMap();
  class ClassE {
    constructor(x) {
      _x.set(this, x);
    }
    getX() {
      return _x.get(this);;
    }
  }
  return ClassE;
})();

let classe = new classE(5);
/* 此时不可以访问我们自定义私有属性命名的 _x */
console.log(classe._x); // undefined
console.log(classe.getX()); // 5

这种方式就很好解决了私有属性的问题,至于 WeakMap 和 Map 相关知识,我打算在下一篇文章继续探讨,这个知识目前也不算是特别了解,大概了解不能遍历、弱引用这些,可以关注后续的文章。

这里有个问题,如果是要支持多个私有变量的话,这儿用 Map 有没有啥问题呢?

于是我就尝试了一下多个私有变量,先看如下代码:

/* WeakMap  */
const classE = (function () {
  const _x = new WeakMap();
  class ClassE {
    constructor(x, y) {
      _x.set(this, x);
      _x.set(this, y);
    }
    getX() {
      return _x.get(this);;
    }
  }
  return ClassE;
})();

let classe = new classE(5, 6);
/* 此时不可以访问我们自定义私有属性命名的 _x */
console.log(classe.getX()); // 6

诶,发现问题了没有,我们最后输出的只有 _y 这个私有属性,原来出现了覆盖问题,那么该如何解决这个问题呢?

既然私有属性要和实例进行关联,那么是不是可以创建一个包含所有私有属性对应的对象来维护呢?这样所有私有属性就都存储在其中了,也就解决多个私有变量问题啦,同时,这种技术也有好处,就是在遍历属性时或者在执行JSON.stringify时不会展示出实例的私有属性。

但它依赖于一个放在类外面的可以访问和操作的WeakMap变量。

const map = new WeakMap();
// 创建一个在每个实例中存储私有变量的对象
const internal = (obj) => {
  if (!map.has(obj)) {
    map.set(obj, {});
  }
  return map.get(obj);
}

class ClassE {
  constructor(name, age) {
    internal(this).name = name;
    internal(this).age = age;
  }
  get userInfo() {
    return '姓名:' + internal(this).name + ',年龄:' + internal(this).age;
  }
}

const classe1 = new ClassE('mybj', 18);
const classe2 = new ClassE('mybbj123.com', 19);

console.log(classe1.userInfo); // 姓名:mybj,年龄:18
console.log(classe2.userInfo); // 姓名:mybbj123.com,年龄:19
/* 无法访问私有属性 */
console.log(classe1.name); // undefined
console.log(classe2.age); // undefined

Proxy 代理设置拦截

Proxy 是 JavaScript 中一项美妙的新功能,它将允许你有效地将对象包装在名为 Proxy 的对象中,并拦截与该对象的所有交互。我们将使用 Proxy 并遵照上面的命名约定来创建私有变量,但可以让这些私有变量在类外部访问受限。

Proxy 可以拦截许多不同类型的交互,但我们要关注的是getsetProxy允许我们分别拦截对一个属性的读取和写入操作。创建Proxy时,你将提供两个参数,第一个是打算包裹的实例,第二个是您定义的希望拦截不同方法的“处理器”对象。

我们的处理器将会看起来像是这样:

const handler = {
  get: function(target, key) {
    if (key[0] === '_') {
      throw new Error('Attempt to access private property');
    }
    return target[key];
  },
  set: function(target, key, value) {
    if (key[0] === '_') {
      throw new Error('Attempt to access private property');
    }
    target[key] = value;
  }
};

在每种情况下,我们都会检查被访问的属性的名称是否以下划线开头,如果是的话我们就抛出一个错误从而阻止对它的访问。

通过以上方法保留使用instanceof的能力(闭包那一块就出现了这个问题),但是此时又有一个新的问题:

当我们尝试执行JSON.stringify时会出现问题,因为它试图对私有属性进行格式化。为了解决这个问题,我们需要重写toJSON函数来仅返回“公共的”属性。我们可以通过更新我们的get处理器来处理toJSON的特定情况:

注:这将覆盖任何自定义的 toJSON 函数。

 get: function(target, key) {
  if (key[0] === '_') {
    throw new Error('Attempt to access private property');
  } else if (key === 'toJSON') {
    const obj = {};
    for (const key in target) {
      if (key[0] !== '_') {           // 只复制公共属性
        obj[key] = target[key];
      }
    }
    return () => obj;
  }
  return target[key];
}

那么我们就可以整合一下代码了:

class Student {
  constructor(name, age) {
    this._name = name;
    this._age = age;
  }
  get userInfo() {
    return '姓名:' + this._name + ',年龄:' + this._age;
  }
}

const handler = {
  get: function (target, key) {
    if (key[0] === '_') { // 访问私有属性,返回一个 error
      throw new Error('Attempt to access private property');
    } else if (key === 'toJSON') {
      const obj = {};
      for (const key in target) { // 只返回公共属性
        if (key[0] !== '_') {
          obj[key] = target[key];
        }
      }
      return () => obj;
    }
    return target[key]; // 访问公共属性,默认返回
  },
  set: function (target, key, value) {
    if (key[0] === '_') {
      throw new Error('Attempt to access private property');
    }
    target[key] = value;
  }
}

const stu = new Proxy(new Student('mybj', 21), handler);

console.log(stu.userInfo);           // 姓名:mybj,年龄:21
console.log(stu instanceof Student); // true
console.log(JSON.stringify(stu));  // "{}"
for (const key in stu) {           
  console.log(key);  // _name  _age
}

我们现在已经封闭了我们的私有属性,而预计的功能仍然存在,唯一的警告是我们的私有属性仍然可被遍历。for(const key in stu) 会列出 _name 和 _age 。

为了解决上述私有属性遍历问题,我又想到了可以操作对象属性对应的属性描述符,然后配置 enumerable ,正好 Proxy 可以处理这个问题,它可以拦截对 getOwnPropertyDescriptor 的调用并操作我们的私有属性的输出,代码如下:

getOwnPropertyDescriptor(target, key) {
  const desc = Object.getOwnPropertyDescriptor(target, key);
  if (key[0] === '_') {
    desc.enumerable = false;
  }
  return desc;
}

详细内容可参考:

Object.getOwnPropertyDescriptor 参考文档

终于,我们迎来了最终完整版本,整合代码如下:

class Student {
  constructor(name, age) {
    this._name = name;
    this._age = age;
  }
  get userInfo() {
    return '姓名:' + this._name + ',年龄:' + this._age;
  }
}

const handler = {
  get: function (target, key) {
    if (key[0] === '_') { // 访问私有属性,返回一个 error
      throw new Error('Attempt to access private property');
    } else if (key === 'toJSON') {
      const obj = {};
      for (const key in target) { // 只返回公共属性
        if (key[0] !== '_') {
          obj[key] = target[key];
        }
      }
      return () => obj;
    }
    return target[key]; // 访问公共属性,默认返回
  },
  set: function (target, key, value) {
    if (key[0] === '_') {
      throw new Error('Attempt to access private property');
    }
    target[key] = value;
  },
  // 解决私有属性能遍历问题,通过访问属性对应的属性描述符,然后设置 enumerable 为 false
  getOwnPropertyDescriptor(target, key) {
    const desc = Object.getOwnPropertyDescriptor(target, key);
    if (key[0] === '_') {
      desc.enumerable = false;
    }
    return desc;
  }
}

const stu = new Proxy(new Student('mybj', 21), handler);

console.log(stu.userInfo);           // 姓名:mybj,年龄:21
console.log(stu instanceof Student); // true
console.log(JSON.stringify(stu));  // "{}"
for (const key in stu) {           // No output 不能遍历私有属性
  console.log(key);
}
stu._name = 'Lionkk';                  // Error: Attempt to access private property

新式做法

就发展趋势来看,TS 已经成为前端必备的技能之一,TypeScript 的 private 很好解决了私有属性这个问题,后续学习了 ts 之后再补充吧。

附:TypeScript 中的处理方式

TypeScript 是 JavaScript 的一个超集,它会编译为原生 JavaScript 用在生产环境。允许指定私有的、公共的或受保护的属性是 TypeScript 的特性之一。

class Student {
  private name;
  private age;

  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  get userInfo() {
    return '姓名:' + this.name + ',年龄:' + this.age;
  }
}

const stu = new Student('mybj', 21);
console.log(stu.userInfo);           // 姓名:mybj,年龄:21

使用 TypeScript 需要注意的重要一点是,它只有在 编译 时才获知这些类型,而私有、公共修饰符在编译时才有效果。如果你尝试访问 stu.name,你会发现,居然是可以的。只不过 TypeScript 会在编译时给你报出一个错误,但不会停止它的编译。

// 编译时错误:属性 ‘name’ 是私有的,只能在 ‘Student ’ 类中访问。
console.log(stu.name); // 'mybj'

TypeScript 不会自作聪明,不会做任何的事情来尝试阻止代码在运行时访问私有属性。我只把它列在这里,也是让大家意识到它并不能直接解决问题。

另外,TypeScript 的 class 私有变量最终编译也是通过 WeakMap 来实现的。

以上关于如何设计ES6中class实现私有属性呢?的文章就介绍到这了,更多相关内容请搜索码云笔记以前的文章或继续浏览下面的相关文章,希望大家以后多多支持码云笔记。

「点点赞赏,手留余香」

0

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 admin@mybj123.com 进行投诉反馈,一经查实,立即处理!
重要:如软件存在付费、会员、充值等,均属软件开发者或所属公司行为,与本站无关,网友需自行判断
码云笔记 » 如何设计ES6中class实现私有属性呢?

发表回复