前言

在阅读 《ECMAScript 6 入门》的时候,零散的看到有私有变量的实现,所以在此总结一篇。

1. 约定

实现

  1. class Example {
  2. constructor() {
  3. this._private = 'private';
  4. }
  5. getName() {
  6. return this._private
  7. }
  8. }
  9.  
  10. var ex = new Example();
  11.  
  12. console.log(ex.getName()); // private
  13. console.log(ex._private); // private

优点

  • 写法简单
  • 调试方便
  • 兼容性好

缺点

  • 外部可以访问和修改
  • 语言没有配合的机制,如 for in 语句会将所有属性枚举出来
  • 命名冲突

2. 闭包

实现一

  1. /**
  2. * 实现一
  3. */
  4. class Example {
  5. constructor() {
  6. var _private = '';
  7. _private = 'private';
  8. this.getName = function() {return _private}
  9. }
  10. }
  11.  
  12. var ex = new Example();
  13.  
  14. console.log(ex.getName()); // private
  15. console.log(ex._private); // undefined

优点

  • 无命名冲突
  • 外部无法访问和修改

缺点

  • constructor 的逻辑变得复杂。构造函数应该只做对象初始化的事情,现在为了实现私有变量,必须包含部分方法的实现,代码组织上略不清晰。
  • 方法存在于实例,而非原型上,子类也无法使用 super 调用
  • 构建增加一点点开销

实现二

  1. /**
  2. * 实现二
  3. */
  4. const Example = (function() {
  5. var _private = '';
  6.  
  7. class Example {
  8. constructor() {
  9. _private = 'private';
  10. }
  11. getName() {
  12. return _private;
  13. }
  14. }
  15.  
  16. return Example;
  17.  
  18. })();
  19.  
  20. var ex = new Example();
  21.  
  22. console.log(ex.getName()); // private
  23. console.log(ex._private); // undefined

优点

  • 无命名冲突
  • 外部无法访问和修改

缺点

  • 写法有一点复杂
  • 构建增加一点点开销

3. Symbol

实现

  1. const Example = (function() {
  2. var _private = Symbol('private');
  3.  
  4. class Example {
  5. constructor() {
  6. this[_private] = 'private';
  7. }
  8. getName() {
  9. return this[_private];
  10. }
  11. }
  12.  
  13. return Example;
  14. })();
  15.  
  16. var ex = new Example();
  17.  
  18. console.log(ex.getName()); // private
  19. console.log(ex.name); // undefined

优点

  • 无命名冲突
  • 外部无法访问和修改
  • 无性能损失

缺点

  • 写法稍微复杂
  • 兼容性也还好

4. WeakMap

实现

  1. /**
  2. * 实现一
  3. */
  4. const _private = new WeakMap();
  5.  
  6. class Example {
  7. constructor() {
  8. _private.set(this, 'private');
  9. }
  10. getName() {
  11. return _private.get(this);
  12. }
  13. }
  14.  
  15. var ex = new Example();
  16.  
  17. console.log(ex.getName()); // private
  18. console.log(ex.name); // undefined

如果这样写,你可能觉得封装性不够,你也可以这样写:

  1. /**
  2. * 实现二
  3. */
  4. const Example = (function() {
  5. var _private = new WeakMap(); // 私有成员存储容器
  6.  
  7. class Example {
  8. constructor() {
  9. _private.set(this, 'private');
  10. }
  11. getName() {
  12. return _private.get(this);
  13. }
  14. }
  15.  
  16. return Example;
  17. })();
  18.  
  19. var ex = new Example();
  20.  
  21. console.log(ex.getName()); // private
  22. console.log(ex.name); // undefined

优点

  • 无命名冲突
  • 外部无法访问和修改

缺点

  • 写法比较麻烦
  • 兼容性有点问题
  • 有一定性能代价

5. 最新提案

  1. class Point {
  2. #x;
  3. #y;
  4.  
  5. constructor(x, y) {
  6. this.#x = x;
  7. this.#y = y;
  8. }
  9.  
  10. equals(point) {
  11. return this.#x === point.#x && this.#y === point.#y;
  12. }
  13. }

那么为什么不直接使用 private 字段呢?比如说这样:

  1. class Foo {
  2. private value;
  3.  
  4. equals(foo) {
  5. return this.value === foo.value;
  6. }
  7. }

简单点来说,就是嫌麻烦,当然也有性能上的考虑……

举个例子,如果我们不使用 #,而是使用 private 关键字:

  1. class Foo {
  2. private value = '1';
  3.  
  4. equals(foo) {
  5. return this.value === foo.value;
  6. }
  7. }
  8.  
  9. var foo1 = new Foo();
  10. var foo2 = new Foo();
  11.  
  12. console.log(foo1.equals(foo2));

在这里我们新建了两个实例,然后将 foo2 作为参数传入了 foo1 的实例方法中。

那么我们可以获取 foo2.value 的值吗?如果我们直接 foo2.value 肯定是获取不到值的,毕竟是私有变量,可是 equals 是 Foo 的一个类方法,那么可以获取到的吗?

答案是可以的。

其实这点在其他语言,比如说 Java 和 C++ 中也是一样的,类的成员函数中可以访问同类型实例的私有变量,这是因为私有是为了实现“对外”的信息隐藏,在类自己内部,没有必要禁止私有变量的访问,你也可以理解为私有变量的限制是以类为单位,而不是以对象为单位,此外这样做也可以为使用者带来便利。

既然获取值是可以的,那么打印的结果应该为 true,但是如果我们传入的值不是 Foo 的实例,而是一个其他对象呢?

  1. var foo1 = new Foo();
  2.  
  3. console.log(foo1.equals({
  4. value: 2
  5. }));

当然这里代码也是可以正常运行的,但是对于编译器来说,就有一点麻烦了,因为编译器不知道 value 到底是 foo 的正常属性还是私有属性,所以编译器需要做判断,先判断 foo 是不是 Foo 的实例,然后再接着获取值。

这也意味着每次属性访问都需要做这样一个判断,而引擎已经围绕属性访问做了高度优化,懒得改,而且还降低速度。

不过除了这个工作之外,还会有一些其他的内容需要考虑,比如说:

  • 你必须将私有的 key 编码进每个词法环境
  • for in 可以遍历这些属性吗?
  • 私有属性和正常属性同名的时候,谁会屏蔽谁?
  • 怎么防止私有属性的名称不被探测出来。
    关于使用 # 而不使用 private 更多的讨论可以参考这个 Issue

当然这些问题都可以被解决啦,就是麻烦了点。

而如果你选择 #,实现的方式将跟 JavaScript 对象属性完全没有关系,将会使用 private slots 的方式以及使用一个新的 slot 查找语法,总之就是会比 private 的实现方式简单很多。

参考

ES6 系列

ES6 系列目录地址:https://github.com/mqyqingfeng/Blog

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。