yliu

时来天地皆同力,运去英雄不自由

Babel to Class之私有属性(3)


前言

这是《Babel to Class》系列的第三篇

这个系列主要讲 Class 的普通写法、继承、私有属性和原生构造函数继承 babel 是如何实现这一过程的。

注:文章顺序存在关联,请按照顺序阅读。

前置知识点

WeakMap

WeakMap 是 ES6 新加入的数据结构,它与 map 类似,但是键为弱引用。

弱引用就是指不计入垃圾回收次数,当引用被垃圾回收 WeakMap 对应键值自动消失。

更多内容请参考文档

WeakSet

WeakMap 类似,不同在于 WeakSet 只有键名,没有键值。

更多内容请参考文档

Class 私有属性

之前 Class 要实现私有属性只能通过,Symbol 或者外部变量来实现,不过都各有缺点,现在新的提案 proposal-private-methods 添加了私有属性和私有方法,使用方式很简单,直接#<name>即可。

class Test {
  #age = 12;
  #getage() {}
}

私有属性只能在构造函数内部调用,如果在非内部调用则抛出异常(在 static 上调用也是可以的)。

示例文件

class Test {
  #age = 17;
  static #name = 'zhangsan';

  #getAge() {
    console.log(this.#age);
  }
  static #getName() {
    console.log(this.#name);
  }

  all() {
    this.#getAge();
    Test.#getName();
  }
}
const test = new Test();
test.all();

以下内容根据此文件进行讲解。

babel

'use strict';

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof 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);
  Object.defineProperty(Constructor, 'prototype', { writable: false });
  return Constructor;
}

function _classPrivateMethodInitSpec(obj, privateSet) {
  _checkPrivateRedeclaration(obj, privateSet);
  privateSet.add(obj);
}

function _classPrivateFieldInitSpec(obj, privateMap, value) {
  _checkPrivateRedeclaration(obj, privateMap);
  privateMap.set(obj, value);
}

function _checkPrivateRedeclaration(obj, privateCollection) {
  if (privateCollection.has(obj)) {
    throw new TypeError(
      'Cannot initialize the same private elements twice on an object'
    );
  }
}

function _classStaticPrivateMethodGet(receiver, classConstructor, method) {
  _classCheckPrivateStaticAccess(receiver, classConstructor);
  return method;
}

function _classPrivateMethodGet(receiver, privateSet, fn) {
  if (!privateSet.has(receiver)) {
    throw new TypeError('attempted to get private field on non-instance');
  }
  return fn;
}

function _classStaticPrivateFieldSpecGet(
  receiver,
  classConstructor,
  descriptor
) {
  _classCheckPrivateStaticAccess(receiver, classConstructor);
  _classCheckPrivateStaticFieldDescriptor(descriptor, 'get');
  return _classApplyDescriptorGet(receiver, descriptor);
}

function _classCheckPrivateStaticFieldDescriptor(descriptor, action) {
  if (descriptor === undefined) {
    throw new TypeError(
      'attempted to ' + action + ' private static field before its declaration'
    );
  }
}

function _classCheckPrivateStaticAccess(receiver, classConstructor) {
  if (receiver !== classConstructor) {
    throw new TypeError('Private static access of wrong provenance');
  }
}

function _classPrivateFieldGet(receiver, privateMap) {
  var descriptor = _classExtractFieldDescriptor(receiver, privateMap, 'get');
  return _classApplyDescriptorGet(receiver, descriptor);
}

function _classExtractFieldDescriptor(receiver, privateMap, action) {
  if (!privateMap.has(receiver)) {
    throw new TypeError(
      'attempted to ' + action + ' private field on non-instance'
    );
  }
  return privateMap.get(receiver);
}

function _classApplyDescriptorGet(receiver, descriptor) {
  if (descriptor.get) {
    return descriptor.get.call(receiver);
  }
  return descriptor.value;
}

var _age = /*#__PURE__*/ new WeakMap();

var _getAge = /*#__PURE__*/ new WeakSet();

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

    _classPrivateMethodInitSpec(this, _getAge);

    _classPrivateFieldInitSpec(this, _age, {
      writable: true,
      value: 17,
    });
  }

  _createClass(Test, [
    {
      key: 'all',
      value: function all() {
        _classPrivateMethodGet(this, _getAge, _getAge2).call(this);

        _classStaticPrivateMethodGet(Test, Test, _getName).call(Test);
      },
    },
  ]);

  return Test;
})();

function _getAge2() {
  console.log(_classPrivateFieldGet(this, _age));
}

function _getName() {
  console.log(_classStaticPrivateFieldSpecGet(this, Test, _name));
}

var _name = {
  writable: true,
  value: 'zhangsan',
};
var test = new Test();
test.all();

以下方法出现在之前讲解过的篇幅中就不再赘述

  • _classCallCheck
  • _defineProperties
  • _createClass

_checkPrivateRedeclaration

function _checkPrivateRedeclaration(obj, privateCollection) {
  if (privateCollection.has(obj)) {
    throw new TypeError(
      'Cannot initialize the same private elements twice on an object'
    );
  }
}

验证 WeakSet 上是否存在构造函数,如果存在直接抛出错误。

_classPrivateMethodInitSpec

function _classPrivateMethodInitSpec(obj, privateSet) {
  _checkPrivateRedeclaration(obj, privateSet);
  privateSet.add(obj);
}

WeakSet 上添加构造函数。

_classPrivateFieldInitSpec

function _classPrivateFieldInitSpec(obj, privateMap, value) {
  _checkPrivateRedeclaration(obj, privateMap);
  privateMap.set(obj, value);
}

WeakMap 上添加私有属性(属性描述符形式)。

_classPrivateMethodGet

function _classPrivateMethodGet(receiver, privateSet, fn) {
  if (!privateSet.has(receiver)) {
    throw new TypeError('attempted to get private field on non-instance');
  }
  return fn;
}

验证是否在构造函数内部调用,如果外部则抛出错误 试图在非实例上获取私有字段

_classCheckPrivateStaticAccess

function _classCheckPrivateStaticAccess(receiver, classConstructor) {
  if (receiver !== classConstructor) {
    throw new TypeError('Private static access of wrong provenance');
  }
}

判断静态属性的来源是否合法,例如

class A {
  static #name() {}
}

class Test {
  static #name() {
    A.#name();
  }

  all() {
    Test.#name();
  }
}

const test = new Test();
test.all();

我们在 Test 获取 A.#name 这种情况下就要抛出异常。

_classStaticPrivateMethodGet

function _classStaticPrivateMethodGet(receiver, classConstructor, method) {
  _classCheckPrivateStaticAccess(receiver, classConstructor);
  return method;
}

验证通过返回 method

_classExtractFieldDescriptor

function _classExtractFieldDescriptor(receiver, privateMap, action) {
  if (!privateMap.has(receiver)) {
    throw new TypeError(
      'attempted to ' + action + ' private field on non-instance'
    );
  }
  return privateMap.get(receiver);
}

验证 WeakMap 是否有构造函数,如果没有则抛出非法获取实例错误,否则返回对应的 key。

_classApplyDescriptorGet

function _classApplyDescriptorGet(receiver, descriptor) {
  if (descriptor.get) {
    return descriptor.get.call(receiver);
  }
  return descriptor.value;
}

返回私有属性,根据属性描述符,如果是 get、set 这种形式储存,直接调用 get 函数 返回,否则返回 value

_classCheckPrivateStaticFieldDescriptor

function _classCheckPrivateStaticFieldDescriptor(descriptor, action) {
  if (descriptor === undefined) {
    throw new TypeError(
      'attempted to ' + action + ' private static field before its declaration'
    );
  }
}

判断属性描述符是否存在,不存在抛出异常。

_classStaticPrivateFieldSpecGet

function _classStaticPrivateFieldSpecGet(
  receiver,
  classConstructor,
  descriptor
) {
  _classCheckPrivateStaticAccess(receiver, classConstructor);
  _classCheckPrivateStaticFieldDescriptor(descriptor, 'get');
  return _classApplyDescriptorGet(receiver, descriptor);
}

返回静态私有方法

  • 调用 _classCheckPrivateStaticAccess 确保构造函数和调用函数一致;
  • 调用 _classCheckPrivateStaticFieldDescriptor 确保静态私有属性一致;
  • 调用 _classApplyDescriptorGet 返回私有属性;

_classPrivateFieldGet

function _classPrivateFieldGet(receiver, privateMap) {
  var descriptor = _classExtractFieldDescriptor(receiver, privateMap, 'get');
  return _classApplyDescriptorGet(receiver, descriptor);
}

返回静态私有属性。

执行过程

上面已经放过 babel 过的代码,这里就不重复了,重点看下执行的过程,从var _age = /*#__PURE__*/ new WeakMap();这段代码开始。

  • 创建 _age_getAgeWeak 对象;
  • 调用 _createClass 给 Test 完成 prototype 赋值;
  • new Test,进入 Test 构造函数内部;
  • 调用 _classCallCheck 确保为 new 调用;
  • 调用 _classPrivateMethodInitSpec,让 _getAge 添加当前 this
  • 调用 _classPrivateFieldInitSpec,给 _age 添加属性,key 为 this,value 为属性描述符;
  • 调用 all 方法;
  • 执行 _classPrivateMethodGet(this, _getAge, _getAge2).call(this);,它的作用验证通过后调用 _getAge2
  • _getAge2 内部执行 _classPrivateFieldGet,返回 _age 设置的属性描述符;
  • 返回 all 内部执行 _classStaticPrivateMethodGet(Test, Test, _getName).call(Test);这一行,它的作用是验证后执行 _getName
  • _getName 内部执行 _classStaticPrivateFieldSpecGet 它返回属性描述符;
  • 输出结果,结束;

实现私有属性方法

从上面过程中可以看到,实现私有方法的主要步骤是通过 WeakMap 来完成,如果不考虑验证过程,可以很简单实现。

const privateName = new WeakMap();

class Test {
  constructor() {
    privateName.set(this, 'zhangsan');
  }
  getName() {
    return privateName.get(this);
  }
}

最后

如果有什么不正确或者书写错误欢迎指出。