JTangming / blog

My repository on GitHub.
Other
53 stars 0 forks source link

JavaScript 中的私有变量 #14

Open JTangming opened 5 years ago

JTangming commented 5 years ago

参考原文:Private Variables in JavaScript 本文非直译,如有理解不对的地方,请指正。

近年来 Javascript 不断有新特性或语法加进来,不过还是始终保持一切皆对象的原则,基于运行时的概念,Javascript 并没有所谓的公共、私有属性的概念。

自 ES6 以后,虽然 Javascript 有了,但不像 C、Java 那样有专门的修饰符来控制变量的访问权限,Javascript 所有的属性都需要在函数中定义。以下文章内容将介绍如何实现私有变量。

约定命名规则的方式

最简单粗暴的方式就是在团队中约定命名规范,通常是以下划线作为属性名称的前缀(e.g. _count)。其本质上并没有阻止变量的访问权限,仅仅是开发之间默认的规则,即不能乱引用和修改我的变量。

WeakMap

WeakMap 虽然不会阻止对数据的访问,但是它能将私有变量和用户的可操作对象分开,示例代码如下:

const map = new WeakMap();
// Create an object to store private values in per instance
const internal = obj => {
  if (!map.has(obj)) {
    map.set(obj, {});
  }
  return map.get(obj);
}
class Shape {
  constructor(width, height) {
    internal(this).width = width;
    internal(this).height = height;
  }
  get area() {
    return internal(this).width * internal(this).height;
  }
}
const square = new Shape(10, 10);
console.log(square.area);      // 100
console.log(map.get(square));  // { height: 100, width: 100 }

以上代码中,将 WeakMap 的关键字设置为私有属性所属对象的实例,通过一个函数来管理返回值,如无则创建之,所有的属性将被存储在其中。在 Shape 实例中,遍历属性或者在执行 JSON.stringify 等都不会展示出实例的私有属性。

Symbol

Symbol 的实现方式与 WeakMap 类似,不过这种实现方式需要为每个私有属性创建一个 Symbol,但是在类外还是可以访问该 Symbol,即还是可以拿到这个私有属性。示例代码如下:

const widthSymbol = Symbol('width');
const heightSymbol = Symbol('height');
class Shape {
  constructor(width, height) {
    this[widthSymbol] = width;
    this[heightSymbol] = height;
  }
  get area() {
    return this[widthSymbol] * this[heightSymbol];
  }
}
const square = new Shape(10, 10);
console.log(square.area);         // 100
console.log(square.widthSymbol);  // undefined
console.log(square[widthSymbol]); // 10

闭包

前面介绍的几种方式仍然允许从类外访问私有属性,闭包是将私有变量数据封装在调用时创建的函数作用域内,从内部返回函数的结果,从而使这一作用域无法从外部访问。闭包想必不用再介绍了吧,这里可以联想一下常出现的一到面试题:JavaScript 实现一个私有变量,每次调用一个函数自动加 1。

Proxy

通俗的说 Proxy 在数据外层套了个壳,然后通过这层壳访问内部的数据,即在原对象的基础上进行了功能的衍生而又不影响原对象。理解如何使用 Proxy 实现私有变量,这里我们需要关注 set 和 get。

使用 Proxy 的方式是:let proxy = new Proxy(target, handler);,Proxy 构造函数中的两个参数具体是:

一个示例代码如下:

class Shape {
  constructor(width, height) {
    this._width = width;
    this._height = height;
  }
  get area() {
    return this._width * this._height;
  }
}

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;
  }
}

const square = new Proxy(new Shape(10, 10), handler);
console.log(square.area);             // 100
console.log(square instanceof Shape); // true
square._width = 200; // Error: Attempt to access private property

如上代码,我们实例化了一个 Proxy,在 target 参数中引入了 Shape 对象,该类里边实现了两个变量,通过 square 是没法直接访问里边的 _width 和 _height 的,这就通过代理实现了私有变量的效果。

TypeScript 中的 private

TypeScript 是 JavaScript 的超集,最终是编译为原生 JavaScript 用在生产环境。TS 支持指定私有、公共和受保护的属性,这里就不再像原文那样举例了。

需要提醒的是,使用 TypeScript 只有在编译时才能获知到这些类型,而私有、公共和受保护修饰符在编译时才起作用。所以,其实你可以使用 x.y 访问内部私有变量的,只不过 TypeScript 会在编译时给你报出一个错误,但不会停止它的编译。其实并不是真正意义上的私有变量,只是在开发编译的时候能够提醒到开发人员,间接达到实现私有变量的效果。

private fields 的方式,即 # 符号

其实在 TC39 中已有提案引入 private fields,目前还在 Stage 3 阶段,它使用 # 符号表示它是私有的,# 的使用方式与以上提的命名约定方式非常类似,但对变量的实际访问权限提供了限制。