模拟实现私有属性

340 阅读4分钟

前言

私有属性已经出来很长时间了,目前在Stage 3阶段(2020/6/30)日,目前 chrome 浏览器已经支持了,在讲解之前先看看怎么使用

class IncreasingCounter {
  #count = 0;
  get value() {
    console.log("Getting the current value!");
    return this.#count;
  }
  increment() {
    this.#count++;
  }
}
const counter = new IncreasingCounter();
counter.#count; // 报错
counter.#count = 42; // 报错

上面代码#count是私有属性,只能在类内部使用,在外部使用就会报错,下面就讲讲怎么来模拟实现它。

闭包

最先想到的就是使用闭包的形式,如果我们在内部调用外部的变量那么根据作用域的查找规则,我们可以在类内部引用它,而在外部永远也不能访问。

const IncreasingCounter = (function () {
  let count = 42;
  return class {
    get value() {
      console.log("Getting the current value!");
      return count;
    }
    increment() {
      count++;
    }
  };
})();
const counter = new IncreasingCounter();
counter.#count; // 报错
counter.#count = 42; // 报错

优点:

  • 实现了私有属性,外部无法访问
  • 无命名冲突

缺点:

  • 有额外的构建开销

Symbol

es6 新增了Symbol类型,它表示独一无二的值,我们可以利用这个特性模拟私有属性。

const IncreasingCounter = (function () {
  const _count = Symbol("count");
  return class {
    get value() {
      console.log("Getting the current value!");
      return this[_count];
    }
    constructor() {
      this[_count] = 42;
    }
    increment() {
      this[_count]++;
    }
  };
})();
const counter = new IncreasingCounter();
counter.#count; // 报错
counter.#count = 42; // 报错

优点:

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

缺点:

  • 有兼容性问题

WeakMap

const IncreasingCounter = (function () {
  const _private = new WeakMap();
  return class {
    get value() {
      console.log("Getting the current value!");
      const value = _private.get("count");
      return value;
    }
    constructor() {
      _private.set("count", 42);
    }
    increment() {
      const value = _private.get("count");
      _private.set("count", ++value);
    }
  };
})();
const counter = new IncreasingCounter();
counter.#count; // 报错
counter.#count = 42; // 报错

优点:

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

缺点:

  • 有兼容性问题
  • 有性能上的损失

babel 做法

先直接看 babel 转换后的代码是怎么样的

"use strict";

function _instanceof(left, right) {
  if (
    right != null &&
    typeof Symbol !== "undefined" &&
    right[Symbol.hasInstance]
  ) {
    return !!right[Symbol.hasInstance](left);
  } else {
    return left instanceof right;
  }
}

function _classCallCheck(instance, Constructor) {
  if (!_instanceof(instance, Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(target, descriptor.key, descriptor);
  }
}

function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  return Constructor;
}

function _classPrivateFieldSet(receiver, privateMap, value) {
  var descriptor = privateMap.get(receiver);
  if (!descriptor) {
    throw new TypeError("attempted to set private field on non-instance");
  }
  if (descriptor.set) {
    descriptor.set.call(receiver, value);
  } else {
    if (!descriptor.writable) {
      throw new TypeError("attempted to set read only private field");
    }
    descriptor.value = value;
  }
  return value;
}

function _classPrivateFieldGet(receiver, privateMap) {
  var descriptor = privateMap.get(receiver);
  if (!descriptor) {
    throw new TypeError("attempted to get private field on non-instance");
  }
  if (descriptor.get) {
    return descriptor.get.call(receiver);
  }
  return descriptor.value;
}

var _count = new WeakMap();

var IncreasingCounter = /*#__PURE__*/ (function () {
  function IncreasingCounter() {
    _classCallCheck(this, IncreasingCounter);

    _count.set(this, {
      writable: true,
      value: 0,
    });
  }

  _createClass(IncreasingCounter, [
    {
      key: "increment",
      value: function increment() {
        var _this$count;

        _classPrivateFieldSet(
          this,
          _count,
          (_this$count = +_classPrivateFieldGet(this, _count)) + 1
        ),
          _this$count;
      },
    },
    {
      key: "value",
      get: function get() {
        console.log("Getting the current value!");
        return _classPrivateFieldGet(this, _count);
      },
    },
  ]);

  return IncreasingCounter;
})();

var counter = new IncreasingCounter();
counter.#count; // 报错
counter.#count = 42; // 报错

上面的代码,我之前在从 babel 看 class(上)有讲解过,我们只看私有变量相关的

"use strict";

function _classPrivateFieldSet(receiver, privateMap, value) {
  var descriptor = privateMap.get(receiver);
  if (!descriptor) {
    throw new TypeError("attempted to set private field on non-instance");
  }
  if (descriptor.set) {
    descriptor.set.call(receiver, value);
  } else {
    if (!descriptor.writable) {
      throw new TypeError("attempted to set read only private field");
    }
    descriptor.value = value;
  }
  return value;
}

function _classPrivateFieldGet(receiver, privateMap) {
  var descriptor = privateMap.get(receiver);
  if (!descriptor) {
    throw new TypeError("attempted to get private field on non-instance");
  }
  if (descriptor.get) {
    return descriptor.get.call(receiver);
  }
  return descriptor.value;
}

var _count = new WeakMap();

var IncreasingCounter = /*#__PURE__*/ (function () {
  function IncreasingCounter() {}

  _createClass(IncreasingCounter, [
    {
      key: "increment",
      value: function increment() {
        var _this$count;

        _classPrivateFieldSet(
          this,
          _count,
          (_this$count = +_classPrivateFieldGet(this, _count)) + 1
        ),
          _this$count;
      },
    },
    {
      key: "value",
      get: function get() {
        console.log("Getting the current value!");
        return _classPrivateFieldGet(this, _count);
      },
    },
  ]);

  return IncreasingCounter;
})();

var counter = new IncreasingCounter();
counter.#count; // 报错
counter.#count = 42; // 报错

去除了不必要的代码,babel的做法跟我们上述WeakMap的做法差不多,不过它多了几步

  1. 每个私有属性都用一个WeakMap值存储
  2. 赋值和读取值的时候进行检查,getset属性存不存在,如果存在就直接调用函数,否则直接赋值或者读取

上面babel的代码多了很多的边界检查,下面模拟写一版伪代码。

const IncreasingCounter = (function () {
  const _count = new WeakMap();
  return class {
    // 初始化赋值
    constructor() {
      _count.set(this, {
        writable: true,
        value: 0,
      });
    }

    get value() {
      const descriptor = _count.get(this);
      if (!descriptor) {
        throw new TypeError("attempted to get private field on non-instance");
      }
      console.log("Getting the current value!");
      return this.descriptor;
    }
    increment() {
      if (!descriptor.writable) {
        throw new TypeError("attempted to set read only private field");
      }

      if (!descriptor.writable) {
        throw new TypeError("attempted to set read only private field");
      }
      descriptor.value = descriptor.value++;
    }
  };
})();
var counter = new IncreasingCounter();
counter.#count; // 报错
counter.#count = 42; // 报错

到这一步基本上就把私有变量说的差不多了,剩下的说几个比较容易混淆的点

其他

静态私有方法怎么实现的

静态私有属性实现与私有属性实现没有太多区别,先看一段babel的代码

var Test = /*#__PURE__*/ (function () {
  function Test() {
    _classCallCheck(this, Test);
  }

  _createClass(Test, null, [
    {
      key: "obtain",
      value: function obtain() {
        _classStaticPrivateMethodGet(Test, Test, _computeRandomNumber).call(
          Test
        );
      },
    },
  ]);

  return Test;
})();

var _computeRandomNumber = function _computeRandomNumber() {
  return _classStaticPrivateFieldSpecGet(Test, Test, _totallyRandomNumber);
};

var _totallyRandomNumber = {
  writable: true,
  value: 4,
};
Test.obtain();

_totallyRandomNumber是定义静态属性变量信息,_classStaticPrivateMethodGet是静态方法相关的调用函数,它的源码也很简单

function _classStaticPrivateMethodGet(receiver, classConstructor, method) {
  if (receiver !== classConstructor) {
    throw new TypeError("Private static access of wrong provenance");
  }
  return method;
}

检查构造函数是否相同,相同返回method

_classStaticPrivateFieldSpecGet函数做的就是返回_totallyRandomNumber变量的信息,源码如下

function _classStaticPrivateFieldSpecGet(
  receiver,
  classConstructor,
  descriptor
) {
  if (receiver !== classConstructor) {
    throw new TypeError("Private static access of wrong provenance");
  }
  if (descriptor.get) {
    return descriptor.get.call(receiver);
  }
  return descriptor.value;
}

结论: 对比私有属性没有太多变化,只是检查从WeakMap变成了对比构造函数

继承能使用么?

嗯。。。答案是不能,因为我试过

class Root {
  #foo = 456;
}

class Child extends Root {
  getName() {
    return this.#foo;
  }
}
const value = new Child();
console.log(value.getName());

可以在 chrome 尝试下,如果真的想用可以使用typescript,而且从babel转义的过程来看,子类实际上并没有与父类建立联系出现这种结果也在意料之中了。

最后

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