最新公告  | 
  • CTRL + D 加入收藏不迷路哦

  • 欢迎您光临码云笔记网,一个关注WEB前端开发的个人技术博客!

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

为什么会出现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 来实现的。

1. 本站所有免费资源来源于用户上传和网络,因此不包含技术服务请大家谅解!如有侵权请邮件联系客服!
2. 本站不保证所提供下载的免费资源的准确性、安全性和完整性,免费资源仅供下载学习之用!如有链接无法下载、失效,请联系客服处理!
3. 您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容资源!如用于商业或者非法用途,与本站无关,一切后果请用户自负!
4. 如果您也有好的资源或技术教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
5. 加入前端开发QQ群:565733884,我们大家一起来交流技术!
码云笔记 » 如何设计ES6中class实现私有属性呢?

发表评论

准备开启WordPress网站建设推广?

联系我们 定制开发