skip to content
WNLee's Blog

说说前端装饰器

/ 47 min read

装饰器 decorator 是 ECMA 中的一个提案,目前处于 stage-2 的阶段。虽然处于试验阶段,...

概述

装饰器 decorator 是 ECMA 中的一个提案,目前处于 stage-2 的阶段。虽然处于试验阶段,但是经过Typescript、babel 的转化可以在日常的项目中使用。前端装饰器现阶段配合 class 进行使用,可以装饰类、类的属性、类的方法。使用的经过工具来试验发现还可以支持对象、函数等的装饰。装饰器的使用类似高阶函数,给函数传入一个类(和属性、方法),之后在原本的类似做修改或者返回一个新类。

现阶段使用装饰器是通过 Typescript 或者 Babel 转化,即使稳定之后因为兼容性问题使用转化后的代码来使用 class、decorator 的情况会持续比较长时间。因此,搞懂装饰器的实现变得有意义。这篇文章会涉及到装饰器实现的内容包含原型&原型链、继承、JavaScript 类型等,同时会涉及到 Reflect、Proxy 等一些应用。

原型与原型链

原型

Javascript 是弱类型语言,同时是解释性语言,与强类型编译性语言不同,具备自己的特点。JavaScript 一个特性是万物皆对象(null 和 undefined 除外),对象由构造函数创建而来,原型(prototype)是构造函数的一个属性,存在公共属性和方法,可以被实例对象共享。同时,原型是 JavaScript 实现继承的基础。

原型特指构建函数上的 prototype 属性,它是一个对象同时有 constructor 属性指向构造函数。实例化的对象没有 prototype 属性,有私有属性 proto 指向构造函数的原型,在 Chrome 等一些浏览器上有 [[Prototype]] 具备一样的特性。

1. 原型的一些属性

// 定义构造函数原型
function A() {}
A.prototype = { 
  constructor: A,
  xxx: 'xxx1',
  yyy: 'yyy1',  
};

// 实例化对象的 __proto__ 属性指向构建函数的 prototype
var a = new A();
a.__proto__ === A.prototype; // true

// 修改 prototype,可直接赋值修改,需要是对象并指定 contructor 属性
function B() {}
B.prototype = { 
  constructor: B,
};
B.prototype.m1 = function() { console.log('m1'); };

// 访问 [[Prototype]] 属性
Object.getPrototypeOf(a);
Object.getPrototypeOf(a) === A.prototype // true

// 修改 [[Prototype]] 属性
Object.setPrototypeOf(a, B.prototype);
Object.getPrototypeOf(a) === B.prototype; // true

2. 一些常用对象的构造函数及原型

// 置顶:null undefined 没有原型

// 数字类型的构造函数及原型
// 构造函数是 Number,原型 Number.prototype
var a = 1;
a.__proto__ === Number.prototype; // true

// 字符串类型的构造函数及原型
// 构造函数是 String,原型 String.prototype
var b = '1';
b.__proto__ === String.prototype; // true

// 布尔值的构造函数及原型
// 构造函数是 Boolean,原型 Boolean.prototype
var c = true;
c.__proto__ === Boolean.prototype; // true


// 函数的构造函数及原型
// 构造函数是 Function,原型 Function.prototype
function d() {}
d.__proto__ === Function.prototype; // true

// 数组的构造函数及原型
// 构造函数是 Array,原型 Array.prototype
var e = [];
e.__proto__ === Array.prototype; // true

// 对象的构造函数及原型
// 构造函数是 Object,原型 Object.prototype
var f = {};
f.__proto__ === Object.prototype; // true

// 正则表达式的构造函数及原型
// 构造函数是 RegExp,原型 RegExp.prototype
var g = /^\d/ig
g.__proto__ === RegExp.prototype; // true

// Symbol 的构造函数及原型
// 构造函数是 Symbol,原型是 Symbol.prototype
var h = Symbol('1');
h.__proto__ === Symbol.prototype; // true

3. 修改构造函数原型

 // 修改构造函原型,实例化对象本身不具备该属性,则该属性会发生变化
 
 function A() {}
 A.prototype.a = 1;
 
 var a = new A();
 var b = new A();
 
 console.log(a.a); // 1
 console.log(b.a); // 1
 
 b.a = 3;
 console.log(a.a); // 1
 console.log(b.a); // 3
 
 A.prototype.a = 2;
 
 console.log(a.a); // 2
 console.log(b.a); // 3

原型链

JavaScript 的原型链,是指当访问在实例化对象上不存在的属性时会在 proto 指向的原型(构造函数的原型)上查找,层层向上直到一个对象的原型对象为 null。这样由构造函数原型组成的链表结构称为原型链。几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。例如,初始化一个构造函数 function A() {},A.prototype 的构造函数是 Object。

function A() {}

A.prototype.__proto__ === Object.prototype; // true
A.prototype.__proto__.__proto__ === null; // true

new

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

function newMethod() {
  var o = {};
  var fn = arguments[0];
  var params = Array.prototype.slice.call(arguments, 1);
  
  var inst = fn.apply(o, params);
  
  if (inst === undefined || inst === null || o === inst) {
    o.__proto__ = fn.prototype
    inst = o;
  }
  
  return inst;
}

function A(name) { this.n = name; }
A.prototype.m1 = function() {};
console.log(newMethod(A, 'new')); // A{ n: 'new' }

function B(name) { this.n = name; return null; }
B.prototype.m1 = function() {};
console.log(newMethod(B, 'new')); // B{ n: 'new' }

function C(name) { this.n = name; return this; }
C.prototype.m1 = function() {};
console.log(newMethod(C, 'new')); // C{ n: 'new' }

function D(name) { this.n = name; return { a: name }; }
D.prototype.m1 = function() {};
console.log(newMethod(D, 'new')); // { a: 'new' }

继承

JavaScript 没有严格的继承概念,在 JavaScript 里说到的继承指的是原型链继承。在 ES6 增加了 class 类型,但它仍然是原型链继承的语法糖。什么是原型链继承,原型链继承是利用实例化对象访问属性采用构造函数逐层查找的特性,在原型链中加入原型对象即可以实现原型链继承。原型链继承的方式有几种:1)将 prototype 指向一个构造函数实例;2)使用 Object.create 创建原型对象;3)使用 Object.setPrototypeOf 修改构造函数的原型属性;4)修改构造函数原型的 proto 属性。使用业界流行库作为的继承实现作为示例:

1. Typescript 实现继承

Typescript 的继承实现相对而言比较简洁,没有使用比较新的特性,兼容性较好。原型链继承需要解决三个属性继承:静态属性、实例属性、原型属性。静态属性通过改变构造函数的 proto 指向或者 for…in 复制;实例属性使用构造函数调用被继承函数的方式;原型属性使用指定 prototype 到实例化对象的方式;

  • 实现 extendStatics 方法用于实现静态属性继承
    • 存在 Object.setPrototypeOf 的情况下,将 sub 的构造函数原型指向 super 构造函数
    • 否则,在存在 proto 的属性的情况下,将 sub 的 proto 指向 super 构造函数
    • 否则,使用 for in 将 super 的 ownProperty 复制到 sub 上
  • 创建一个构造函数,仅设置 constructor 属性指向 sub,并设置 prototype 指向 super,最后将 sub 的 prototype 指向构造函数的实例
  • 在 sub 的构造函数使用 super.call(this) 实现实例属性的继承
  • 最后,假设有 setter getter 存在,则使用 Object.defineProperty 给原型设置属性的 setter、getter。
var __extends = (this && this.__extends) || (function () {
  var extendStatics = function (d, b) {
      extendStatics = 
          Object.setPrototypeOf 
          || ({ __proto__: [] } instanceof Array 
             && function (d, b) { d.__proto__ = b; })
          || function (d, b) { 
                for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; 
             };
      return extendStatics(d, b);
  }
  return function (d, b) {
      extendStatics(d, b);
      function __() { this.constructor = d; }
      d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
  };
})();

var A = /** @class */ (function () {
    function A(name) {
        this.an = name;
    }
    A.prototype.m1 = function () {
        console.log('m1');
    };
    A.as = 1;
    return A;
}());
var B = /** @class */ (function (_super) {
    __extends(B, _super);
    function B(name) {
        var _this = _super.call(this, name) || this;
        _this.bn = name;
        return _this;
    }
    B.prototype.m2 = function () {
        console.log('m2');
    };
    B.bs = 1;
    return B;
}(A));

2. Babel 实现继承

Babel 的类继承实现上与 Typescript 的思路是一致的,区别是 Babel 使用的 Object.create 来生成 subClass 的 prototype 对象,同时 Babel 兼容了 Symbol,Reflect 一些使用场景,兼容性更高

  • _typeof 用于判断入参的类型,同时兼容了 Symbol pollyfill 判断
  • _inherits 用于设置 subClass 的 原型对象,指向用 Object.create 创建的 superClass 的原型的对象,同时设置 subClass 的 proto 设置为 superClass(覆盖静态属性)
  • _setPrototypeOf、_getPrototypeOf 是 Object.setProtoypeOf、Object.getPrototypeOf 的兼容实现
  • _defineProperty、_defineProperties 是 Object.defineProperty 的兼容实现和复数实现
  • _createSuper 生成 _super 方法,用于在运行时处理 SuperClass 的实例属性继承,babel 会判断可以使用 Reflect.construct 则用它来创建创建实例,负责则用 xxx.apply 实现,最后是对返回值做处理;
  • _createClass 将原型属性、静态属性分别挂到构造函数的原型、构造函数本身;
function _typeof(obj) {
  "@babel/helpers - typeof";
  return _typeof = 
      "function" == typeof Symbol && "symbol" == typeof Symbol.iterator 
          ? function (obj) {
                return typeof obj;
          } 
          : function (obj) {
                return 
                    obj 
                    && "function" == typeof Symbol 
                    && obj.constructor === Symbol 
                    && obj !== Symbol.prototype 
                        ? "symbol" 
                        : typeof obj;
          }, _typeof(obj);
}

function _inherits(subClass, superClass) {
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function");
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      writable: true,
      configurable: true
    }
  });
  Object.defineProperty(subClass, "prototype", {
    writable: false
  });
  if (superClass) _setPrototypeOf(subClass, superClass);
}

function _setPrototypeOf(o, p) {
  _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
    o.__proto__ = p;
    return o;
  };
  return _setPrototypeOf(o, p);
}

function _createSuper(Derived) {
  var hasNativeReflectConstruct = _isNativeReflectConstruct();
  return function _createSuperInternal() {
    var Super = _getPrototypeOf(Derived),
      result;
    if (hasNativeReflectConstruct) {
      var NewTarget = _getPrototypeOf(this).constructor;
      result = Reflect.construct(Super, arguments, NewTarget);
    } else {
      result = Super.apply(this, arguments);
    }
    return _possibleConstructorReturn(this, result);
  };
}

function _possibleConstructorReturn(self, call) {
  if (call && (_typeof(call) === "object" || typeof call === "function")) {
    return call;
  } else if (call !== void 0) {
    throw new TypeError("Derived constructors may only return object or undefined");
  }
  return _assertThisInitialized(self);
}

function _assertThisInitialized(self) {
  if (self === void 0) {
    throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
  }
  return self;
}

function _isNativeReflectConstruct() {
  if (typeof Reflect === "undefined" || !Reflect.construct) return false;
  if (Reflect.construct.sham) return false;
  if (typeof Proxy === "function") return true;
  try {
    Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
    return true;
  } catch (e) {
    return false;
  }
}

function _getPrototypeOf(o) {
  _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
    return o.__proto__ || Object.getPrototypeOf(o);
  };
  return _getPrototypeOf(o);
}

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 _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }
  return obj;
}

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

    this.an = name;
  }

  _createClass(A, [{
    key: "m1",
    value: function m1() {
      console.log('m1');
    }
  }]);

  return A;
}();

_defineProperty(A, "as", 1);

var B = /*#__PURE__*/ function (_A) {
  _inherits(B, _A);

  var _super = _createSuper(B);

  function B(name) {
    var _this;

    _classCallCheck(this, B);

    _this = _super.call(this, name);
    _this.bn = name;
    return _this;
  }

  _createClass(B, [{
    key: "m2",
    value: function m2() {
      console.log('m2');
    }
  }]);

  return B;
}(A);

_defineProperty(B, "bs", 1);

3. 实现 Extend 方法

实现一个 extend 方法用于实现原型链继承,主要分为四个部分:

  • 使用 Object.setPrototypeOf 来实现静态属性继承
  • 使用 Reflect.construct 与 apply 实现实例属性继承
  • 使用指定原型的 prototype 实现原型属性继承
  • 最后是返回值处理,假设返回值为 null 和 undefined 不做特殊处理,假设是新的 object 和 function 则分为新的 object 或者 function 对象
function asset(condition, message) {
  if (condition) throw Error(message);
}

function assignProperty(to, from) {
  for (let k in from) {
    if (from.hasOwnProperty(k)
      && k !== 'constructor'
    ) {
      to[k] = from[k];
    }
  }
}

function setPrototypeOf(subClass, superClass) {
  if (Object.setPrototypeOf) {
    Object.setPrototypeOf(subClass, superClass);
  } else if ({ __proto__: [] } instanceof Array) {
    subClass.__proto__ = superClass;
  } else {
    assignProperty(subClass, superClass);
  }
}

function getPrototypeOf(o) {
  if (Object.getPrototypeOf) {
    return Object.getPrototypeOf(o);
  } else if ({ __proto__: [] } instanceof Array) {
    return o.__proto__;
  } else {
    return o;
  }
}

function createSuper(superClass) {
  return function() {
    let result;
    if (Reflect.construct) {
      const newTarget = getPrototypeOf(this).constructor;
      result = Reflect.construct(superClass, arguments, newTarget);
    } else {
      result = superClass.apply(this, arguments);
    }
    return possibleConstructorReturn(this, result);
  };
}

function possibleConstructorReturn(_this, call) {
  if (typeof call === 'function' || typeof call === 'object') {
    return call;
  } else {
    return _this;
  }
}

function innerExtend(subClass, superClass) {
  subClass.prototype = Object.create(superClass.prototype ? superClass.prototype : null, { 
    constructor: {
      value: subClass,
      writable: true,
      configurable: true
    }
  });
}

function extend(subClass, superClass) {
  asset(typeof subClass !== 'function', 'SubClass need Function');

  const _super = createSuper(superClass);
  
  function _() {
    let _this;
    
    _this = _super.apply(this, arguments) || this;
    
    _this = possibleConstructorReturn(_this, subClass.apply(_this, arguments)) || _this;
    
    return _this;
  };
  
  /**
   * 原型继承
   */
  innerExtend(_, superClass);
  
  /**
   * 静态属性赋值
   */
  assignProperty(_, subClass);
  if (superClass) setPrototypeOf(_, superClass);
  
  /**
   * 补全 subClass 原型属性
   */
  assignProperty(_.prototype, subClass.prototype);
  
  return _;
}

function A(name) { this.n = name; this.Aa = 1; return { a: 1 } }
A.Ab = 2;
A.prototype.Ac = 3; 
function B(name) { this.n = name; this.Ba = 1 }
B.Bb = 2;
B.prototype.Bc = 3;

A = extend(A, B);

const a = new A('A');
console.log(a);
console.log(a instanceof A); // true
console.log(a instanceof B); // true

遍历原型链

  • 遍历原型链,判断是否实例对象是否具有某个属性
function getPrototypeOf(o) {
  if (o === null || o === undefined) {
    throw Error('Cannot convert undefined or null to object');
  }

  return Object.getPrototypeOf
    ? Object.getPrototypeOf(o)
    : (o).__proto__;
}

function checkPropertyInObject(target, property) {
  if (target === null || target === undefined || property === null || property === undefined) {
    return false;
  }
  
  if (Object.prototype.hasOwnProperty.call(target, property)) {
    return true;
  }
  
  const proto = getPrototypeOf(target);
  
  return checkPropertyInObject(proto, property);
}

console.log(checkPropertyInObject('string', 'length')); // true
console.log(checkPropertyInObject(1, 'valueOf')); // true
console.log(checkPropertyInObject(true, 'valueOf')); // true

class A { test1() {} }
class B extends A {}
console.log(checkPropertyInObject(new B(), 'test1')); // true
  • 实现 instanceof 功能

Instanceof 用于判断指定构造函数的原型(prototype)(右边)是否存在于某个对象的原型链上

function hasPrototype(o) {
  try {
    return 'prototype' in o;
  } catch {
    return false;
  }
}

function getPrototypeOf(o) {
  if (o === null || o === undefined) {
    throw Error('Cannot convert undefined or null to object');
  }

  return Object.getPrototypeOf
    ? Object.getPrototypeOf(o)
    : (o).__proto__;
}

function getInstanceOf(to, from) {
  if (to === null || to === undefined || from === null || from === undefined || !hasPrototype(from)) {
    return false;
  }
  
  const proto = getPrototypeOf(to);
  
  if (proto === from['prototype']) {
    return true;
  }
  
  return getInstanceOf(proto, from);
}

console.log(getInstanceOf(1, Number)); // true
console.log(getInstanceOf('', String)); // true
console.log(getInstanceOf(1, String)); // false
console.log(getInstanceOf({}, Object)); // true

class A { test1() {} }
class B extends A {}
console.log(getInstanceOf(new B(), A)); // true
console.log(getInstanceOf(new B(), Object)); // true
console.log(getInstanceOf(new B(), Function)); // false

JavaScript 类型构成

Javascript 的类型由原始类型(primitive value)和引用类型(reference type),引用类型也是真实意义的对象类型。

  1. 原始类型

null、undefined、number、string、boolean、symbol

  1. 对象类型

Object、Array、Function、Date、Regexp、Error。。。

  1. ES6类型

Symbol

类型转换(type convert)

基本所有语言的基础语法里都有类型转换,JavaScript 也不例外,JavaScript 类型的类型转换比较特殊。特别是对象类型的转换上。除了 null、undefined 之外,原始类型互转是调用 ToXxx 方法,比如转到 number 类型调用的是 ToNumber。对象类型转换为原始类型是调用 ToPrimitiveValue。原始类型转换为引用类型可以视为调用构造函数。

  1. convert to number
Number(null); // 0
Number(undefined); // 0
Number(true); // 1
Number(false); // 0
Number(''); // 0
Number('1'); // 1
Number('x'); // NaN
Number({}); // NaN
Number([]); // 0
Number([1]); // 1
Number([1, 2]); // NaN
Number(function() {}); // NaN

Number(new Date); // timestamp
  1. Convert to string
String(null); // 'null'
String(undefined); // 'undefined'
String(true); // 'true'
String(false); // 'false'
String(0); // '0'

// object to string
String({}); // [object Object]
String([1, 1]); // '1,1'
String(function () {}); // function () {}
String(new Date); // 'Thu Apr 21 2022 02:03:50 GMT+0800 (中国标准时间)'
String(/^/ig); // '/^/ig'
  1. Convert to boolean
Boolean(null); // false
Boolean(undefined); // false
Boolean(0); // false
Boolean(2); // true
Boolean({}); // true
Boolean([]); // true
Boolean(function () {}); // true
Boolean(new Date); // true
Boolean(/^/ig); // true
  1. Convert to object
Object(null); // {}
Object(undefined); // {}

Object(0); // = new Number(0)
Object(''); // = new String('')
Object(true); // = new Boolean(true)
Object(/^/ig); // = new RegExp(/^/)
  1. 实现类型转换
// ToBoolean
function ToBoolean(argument: any): boolean {
  return !!argument;
}

// ToNumber
function ToNumber(argument: any): number {
  return + argument;
}

// ToString
function ToString(argument: any): string {
  return '' + argument;
}

// ToPrimitive
// 

原始类型调用构造函数与 new 的区别

这里的讨论是把 null、undefined、symbol 排除开,讨论余下的 boolean、number、string 类型。调用原始类型的构造函数可以理解为调用了 ToXxx 方法,对入参进行类型转换。使用 new 构造函数创建原始类型,步骤按照 new 的步骤来。

  1. 新构造对象的[[Prototype]]内部属性设置为原生的Number prototype object —— 初始值为Number.prototype(The Number prototype object is itself a Number object (its [[Class]] is “Number”) whose value is +0.);
  2. 新构造对象的[[Class]]内部属性设置为Number;
  3. 新构造对象的[[PrimitiveValue]]内部属性被设置为:如果参数value未提供(即new Number()),则为+0;否则设置为ToNumber(value)的计算结果;
  4. 新构造对象的[[Extensible]]内部属性设置为true。

ES6 带来的一些变化

1. 类型判断

以往使用 Object.prototype.toString 判断类型,因为 es6 的出现会变得不准确,所需要要做些调整

// es6 之前的类型判断
const class2type = {};
['Undefined', 'Null', 'Boolean', 'Number', 'String', 'Function', 'Array'].forEach(function(item) {
  class2type[item.toLowerCase()] = '[object ' + item + ']';
});

function Type(tag) {
  return function(o) {
    return Object.prototype.toString.call(o) === class2type[tag];
  };
}

const isUndefined = Type('undefined');
const isNull = Type('null');
const isBoolean = Type('boolean');
const isNumber = Type('number');
const isString = Type('string');
const isFunction = Type('function');
const isArray = Type('array');

const isObject = function(o) {
  return typeof o === 'object' ? o === null ? false : true : false;
};

// es6 之后的判断
enum Tag {
  Undefined,
  Null,
  Boolean,
  Number,
  String,
  Symbol,
  Object
}

function Type(x: any): Tag {
  if (x === null) return Tag.Null;
  switch(typeof x) {
    case 'undefined':  return Tag.Undefined;
    case 'boolean': return Tag.Boolean;
    case 'number': return Tag.Number;
    case 'string': return Tag.String;
    case "symbol": return Tag.Symbol;
    case 'object': return x === null ? Tag.Null : Tag.Object;
    default: return Tag.Object;
  }
}

function isNull(x: any): x is null {
  return Type(x) === Tag.Null;
}

function isUndefined(x: any): x is undefined {
  return Type(x) === Tag.Undefined;
}

function isNumber(x: any): x is Number {
  return Type(x) === Tag.Number;
}

function isString(x: any): x is String {
  return Type(x) === Tag.String;
}

function isSymbol(x: any): x is Symbol {
  return Type(x) === Tag.String;
}

function isObject(x: any): x is Object {
  return Type(x) === Tag.Object;
}

function isFunction(x: any): x is Function {
  return typeof x === 'function';
}

function isArray(x: any): x is any[] {
  return Array.isArray
    ? Array.isArray(x)
    : x instanceof Object
      ? x instanceof Array
      : Object.prototype.toString.call(x) === "[object Array]";
}

2. 设置对象的 ToPrimitive 返回

// 修改 PrimitiveValue
const o = {};
o[Symbol.toPrimitive] = function(hint: any) {
  if (hint === 'string') {
    return '1';
  }
  return 1;
}
console.log(+ o); // 1
console.log('' + o); // '1'

// 修改 [[class]]
const o = {};
o[Symbol.toStringTag] = 'xxx';
console.log(Object.prototype.toString.call(o)); // '[object xxx]'
console.log(typeof o); // 'object'

装饰器

概述

JavaScript 装饰器(decorator)是 ECMA 中的一个提案,到目前没有正式的加入到规范里,但是已经可以通过各类手段实现这个效果。装饰器功能很好理解,如同名称一样,用来装饰某些东西,在JavaScript 里可以用于装饰类、属性、入参,因为 JavaScript 没有严格的类,类的实现基于构造函数,所以可以将装饰器看做装饰构造函数及实例属性、静态属性、原型属性,直接装饰构造函数会被 TS、Babel 等工具拦截。

简单实践

enum ETools {
  car = 40,
  bike = 15,
  walk = 5,
}

interface IDriveTools {
  new (): IDriveTools;
  prototype: IDriveToolsProto;
}

interface IDriveToolsProto {
  getSpeed(): void | number;
  distance(dis: number): void | string;
}

class DriveTools implements IDriveToolsProto {
  canDrive: boolean;
  type: ETools;

  constructor(target, type: ETools) {
    this.canDrive = type != ETools.car || (type === ETools.car && target?.age > 18);
    this.type = type;
  }
  
  getSpeed() {
    if (!this.canDrive) {
      throw Error('不能驾驶汽车');
    }
    return this.type;
  }
  
  distance(dis: number) {
    if (!this.canDrive) {
      throw Error('不能驾驶汽车');
    }
    const time = dis / this.type;
    return time.toFixed(2); 
  }
}

function Drivable(): PropertyDecorator  {
  return function(target: any, propertyKey: PropertyKey) {
    Object.defineProperty(target, propertyKey, {
      get: function() {
        return function(this: any, type: ETools) {
          return new DriveTools(this, type);
        }
      }
    });
  }
}

function ReadOnly(target: any, propertyKey: PropertyKey) {
  const descriptor = Object.getOwnPropertyDescriptor(target, propertyKey) || {};
  descriptor.writable = false;
  Object.defineProperty(target, propertyKey, descriptor);
}

function HasIdCard(cardType: string) {
  return function(targetClass) {
    targetClass.prototype.IdCard = cardType;
    return targetClass;
  }
}

@HasIdCard('primary')
class Student {
  @ReadOnly
  static type = 'student';

  sex: 'male' | 'femal';
  age: number;

  @Drivable()
  drive: (type: ETools) => InstanceType<typeof DriveTools>;

  constructor(opts) {
    this.sex = opts.sex;
    this.age = opts.age;
  }
}

const XiaoMing = new Student({ sex: 'male', age: 12 });
console.log(XiaoMing.drive(ETools.bike).getSpeed()); // 15 
console.log(XiaoMing.drive(ETools.walk).distance(15)); // '3.00'
try {
  XiaoMing.drive(ETools.car).getSpeed();
} catch (err) {
  console.log(err); // Error '不能驾驶汽车'
}

// @ts-ignore
console.log(XiaoMing.IdCard); // primary

Student.type = 'teacher';
console.log(Student.type);

装饰器顺序

  1. 同组(装饰同个对象)声明顺序是从上到下
  2. 同组(装饰同个对象)执行顺序是从下到上
  3. 同个类下的装饰器执行顺序: 实例方法/属性(声明顺序) -> 访问器 -> 静态方法/属性(声明顺序) -> 构造函数 -> 类
  4. 装饰方法的装饰器和参数装饰器顺序:
  • 声明顺序方法装饰器再到参数装饰器
  • 执行顺序参数装饰器再到方法装饰器,最后一个参数的装饰器会最先被执行
function f(key: string): any {
  console.log("evaluate: ", key);
  return function () {
    console.log("call: ", key);
  };
}

@f("Class Decorator")
class C {
  @f("Static Property")
  static prop?: number;

  @f("Static Method")
  static method(@f("Static Method Parameter") foo) {}

  constructor(@f("Constructor Parameter") foo) {}

  @f("Instance Method")
  method(@f("Instance Method Parameter") foo) {}

  @f("Instance Property")
  prop?: number;
  
  // 特殊版本可用
  @f("Accessor")
  get accessor() {
    return 'accessor';
  }
}

// 结果
evaluate: Instance Method
evaluate: Instance Method Parameter
call: Instance Method Parameter
call: Instance Method
evaluate: Instance Property
call: Instance Property
evaluate: Accessor
call: Accessor
evaluate: Static Property
call: Static Property
evaluate: Static Method
evaluate: Static Method Parameter
call: Static Method Parameter
call: Static Method
evaluate: Class Decorator
evaluate: Constructor Parameter
call: Constructor Parameter
call: Class Decorator


Typescript 内置的 reflect-metadata 数据
1. design:type:属性的类型
Reflect.getMetadata('design:type', target, propertyKey?);


2. design:paramtypes:方法的参数的类型
Reflect.getMetadata('design:paramtypes', target, propertyKey?);


3. design:returntype:方法的返回值的类型
Reflect.getMetadata('design:returntype', target, propertyKey?);

使用装饰器碰到的问题

  1. ClassDecorator 返回了新 Class,或者新增了属性导致 TS 类型报错,解法给 Class 增加类型定义
type Consturctor = { new (...args: any[]): any };
function AddTest<T extends Consturctor>(targetClass: T): T {
  return class extends targetClass {
    test: string = 'test123'
  };
}

// 通个一个额外的类增加类型定义
class Base {
  test: string;
}

@AddTest
class ClassA extends Base {
  prop: string;
}

const a = new ClassA()
console.log(a.test);
  1. 报错信息 [typescript TS1241: Unable to resolve signature of method decorator when called as an expression];针对报错设置好 配置项 target、lib.
  {
    "compilerOptions": {
      "experimentalDecorators": true,
      "emitDecoratorMetadata": true,
      
      // 可以解决大部分情况
      "lib": ["ES2015"],
      "target": "ES5"
    }
  }

Typescript 的装饰器实现

Typescript 装饰器实现比较简洁,逻辑清晰,增加 __decorate、__metadata、__param 函数用于处理装饰器场景(注意:emitDecoratorMetadata为false时使用 __metadata)。主要逻辑集中于 __decorate,而 __metadata 是对 Reflect.metadata 的一个兼容实现,__param 是处理参数装饰器的工厂函数可以记录参数的序号。__decorate 的逻辑如下:

  1. __decorate 的入参有四个,decorators 装饰器列表,target 装饰类,key 装饰属性,desc 属性的描述对象;
  2. 通过入参个数判断是对类进行装饰还是对属性进行装饰,对属性进行装需要初始化 desc 属性装饰器;
  3. 假设当前环境支持 Reflect.decorate,使用其完成装饰器工作;
  4. 否则降序遍历 decorators 列表,调用每个装饰器方法完成装饰器工作;
  5. 最后对于有透传描述对象进行 Object.defineProperty 处理;

除了增加处理函数外, Typescript 通过编译将代码进行转化,对每个有装饰器的类、属性都增加 __decorate 处理,参数装饰器会被当成对应属性的一个装饰器被调用。

// 内置代码
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length,
        r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc,
        d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else
        for (var i = decorators.length - 1; i >= 0; i--)
            if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) {
        decorator(target, key, paramIndex);
    }
};

// 编译结构
function f(key) {
    console.log("evaluate: ", key);
    return function () {
        console.log("call: ", key);
    };
}
var C = /** @class */ (function () {
    function C(foo) {}
    C.method = function (foo) {};
    C.prototype.method = function (foo) {};
    Object.defineProperty(C.prototype, "accessor", {
        // 特殊版本可用
        get: function () {
            return 'accessor';
        },
        enumerable: false,
        configurable: true
    });
    __decorate([
        f("Instance Method"),
        __param(0, f("Instance Method Parameter")),
        __metadata("design:type", Function),
        __metadata("design:paramtypes", [Object]),
        __metadata("design:returntype", void 0)
    ], C.prototype, "method", null);
    __decorate([
        f("Instance Property"),
        __metadata("design:type", Number)
    ], C.prototype, "prop", void 0);
    __decorate([
        f("Accessor"),
        __metadata("design:type", Object),
        __metadata("design:paramtypes", [])
    ], C.prototype, "accessor", null);
    __decorate([
        f("Static Property"),
        __metadata("design:type", Number)
    ], C, "prop", void 0);
    __decorate([
        f("Static Method"),
        __param(0, f("Static Method Parameter")),
        __metadata("design:type", Function),
        __metadata("design:paramtypes", [Object]),
        __metadata("design:returntype", void 0)
    ], C, "method", null);
    C = __decorate([
        f("Class Decorator"),
        __param(0, f("Constructor Parameter")),
        __metadata("design:paramtypes", [Object])
    ], C);
    return C;
}());

Babel 的装饰器实现

babel 对装饰器的实现上思路与 TS 类似,实现逻辑上有些许不同,babel 的代码会难懂一些。不同点:

  1. 装饰器的声明属性和执行顺序不同; 整个class从上往下开始声明装饰器 -> 声明参数装饰器并执行 -> 从上到下开始执行属性装饰器 -> 构造函数装饰器
  2. Babel 支持使用 initializer,同时在实例属性(非方法)处理上更合理;
  3. Babel 没有使用 Reflect.metadata;

Babel 的实现主要依赖两个函数 _applyDecoratedDescriptor、_initializerDefineProperty,_initializerDefineProperty 是用于在实例化过程中设置有初始值的实例属性的描述对象;主要逻辑集中在 _applyDecoratedDescriptor,实现如下:

  1. 共有 5 个参数,多了一个 context 是用于 initializer 入参,其余参数与 TS 相同;
  2. 初始化一个对象用于拷贝 descriptor 的值,假设有 value、或者 initializer,则调整为可以修改;
  3. 使用 reduce 倒叙遍历 decorators 生成一个新的 descriptor;
  4. 如果有 context 和 initializer 不为 undefined,则设置 descriptor 否则返回 descriptor,有初始值的实例属性因为没有上下文入参,所以 descriptor 会保留到实例化过程中去设置;
"use strict";

var _dec, _dec2, _dec3, _dec4, _dec5, _dec6, _class, _class2, _init, _descriptor, _class3;

function _initializerDefineProperty(target, property, descriptor, context) {
  if (!descriptor) return;
  Object.defineProperty(target, property, {
    enumerable: descriptor.enumerable,
    configurable: descriptor.configurable,
    writable: descriptor.writable,
    value: descriptor.initializer ? descriptor.initializer.call(context) : void 0
  });
}

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 _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }
  return obj;
}

function _initializerWarningHelper(descriptor, context) {
  throw new Error('Decorating class property failed. Please ensure that ' + 'proposal-class-properties is enabled and runs after the decorators transform.');
}

function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) {
  var desc = {};
  Object.keys(descriptor).forEach(function (key) {
    desc[key] = descriptor[key];
  });
  desc.enumerable = !!desc.enumerable;
  desc.configurable = !!desc.configurable;
  if ('value' in desc || desc.initializer) {
    desc.writable = true;
  }
  desc = decorators.slice().reverse().reduce(function (desc, decorator) {
    return decorator(target, property, desc) || desc;
  }, desc);
  if (context && desc.initializer !== void 0) {
    desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
    desc.initializer = undefined;
  }
  if (desc.initializer === void 0) {
    Object.defineProperty(target, property, desc);
    desc = null;
  }
  return desc;
}

function f(key) {
  console.log("evaluate: ", key);
  return function () {
    console.log("call: ", key);
  };
}

var C = (
  _dec = f("Class Decorator"), 
  _dec2 = f("Static Property"), 
  _dec3 = f("Static Method"), 
  _dec4 = f("Instance Method"), 
  _dec5 = f("Instance Property"), 
  _dec6 = f("Accessor"),
  _dec(
    _class = (
      _class2 = (
        _class3 = /*#__PURE__*/ function () {
          function C(foo) {
            _classCallCheck(this, C);

            _initializerDefineProperty(this, "prop", _descriptor, this);
          }

          C = f("Constructor Parameter")(C, undefined, 0) || C;

          _createClass(C, [{
            key: "method",
            value: function method(foo) {}
          }, {
            key: "accessor",
            get: // 特殊版本可用
              function get() {
                return 'accessor';
              }
          }], [{
            key: "method",
            value: function method(foo) {}
          }]);

          f("Static Method Parameter")(C.prototype, "method", 0);
          f("Instance Method Parameter")(C.prototype, "method", 0);
          return C;
        }(), 

        _defineProperty(_class3, "prop", void 0), 
        
        _class3
      ), 

      (_applyDecoratedDescriptor(
        _class2, 
        "prop", 
        [_dec2], 
        (
          _init = Object.getOwnPropertyDescriptor(_class2, "prop"), 
          _init = 
            _init 
              ? _init.value 
              : undefined, 
            {
              enumerable: true,
              configurable: true,
              writable: true,
              initializer: function initializer() {
                return _init;
              }
            }
        ), 
        _class2
      ), 

      _applyDecoratedDescriptor(
        _class2, 
        "method", 
        [_dec3], 
        Object.getOwnPropertyDescriptor(_class2, "method"), 
        _class2
      ), 

      _applyDecoratedDescriptor(
        _class2.prototype, 
        "method", 
        [_dec4], 
        Object.getOwnPropertyDescriptor(_class2.prototype, "method"), 
        _class2.prototype
      ),

      _descriptor = _applyDecoratedDescriptor(
        _class2.prototype, 
        "prop", 
        [_dec5], 
        {
          configurable: true,
          enumerable: true,
          writable: true,
          initializer: null
        }
      ), 

      _applyDecoratedDescriptor(
        _class2.prototype, 
        "accessor", 
        [_dec6], 
        Object.getOwnPropertyDescriptor(_class2.prototype, "accessor"),
        _class2.prototype)
      ),

      _class2
    )
  ) || _class
);

// 执行结果
evaluate:  Class Decorator
evaluate:  Static Property
evaluate:  Static Method
evaluate:  Instance Method
evaluate:  Instance Property
evaluate:  Accessor
evaluate:  Constructor Parameter
call:  Constructor Parameter
evaluate:  Static Method Parameter
call:  Static Method Parameter
evaluate:  Instance Method Parameter
call:  Instance Method Parameter
call:  Static Property
call:  Static Method
call:  Instance Method
call:  Instance Property
call:  Accessor
call:  Class Decorator

Proxy

Proxy 是 ES6 中的一个语法特性,使 Javascript 支持代理模式,能够对对象的基础操作进行拦截。是 VUE 3.0 实现双向绑定的基础。更深入了解可以查看 阮一峰 老师的文档。写的很详细,同时有各种实践 和注意事项。这里不再重复记录,后序有相关的实践或者心得再补充。

Reflect

Reflect 和 Proxy 是同期出现的,也是 ES6 的一个语法特性,我觉得他的出现一部分原因是配合 Proxy,一部分原因是为了规范对象的基础操作,一部分是元数据编程;他可以分为两个部分:1. 对象操作;2. 元数据编程(提案过程中);

  1. 对象基础操作

建议查看 阮一峰 老师的文档,有详细的解读;

  1. 元数据编程

这部分其实才是我主要说明的,因为这部分扩展被广泛的运用到装饰器中,还有切面编程、依赖注入、反射式编程。文档阅读起来没有清晰,建议可以直接查看源码实现。

方法:

  • Reflect.defineMetadata
  • Reflect.hasMetadata
  • Reflect.hasOwnMetadata
  • Reflect.getMetadata
  • Reflect.getOwnMetadata
  • Reflect.getMetadataKeys
  • Reflect.getOwnMetadataKey
  • Reflect.deleteMetadata
  • Reflect.decorate 文档里没有但是源码有实现的基础函数
  • @Reflect.metadata 内置的一个 metadata 装饰器

源码实现很清晰,通过学习可以学到很多东西,实现原理:

  • 使用 WeakMap<Object, Map<PropertyType, Map<>>> 作为数据结构,存储基于 对象&属性 的元数据映射
  • 通过对对象的原型链遍历进行 metadata 数据管理,依赖 Map 结构对功能进行实现

一些应用

  1. HOC

装饰器可以装饰类,以类为入参,对类进行修改或者返回一个新的类。在 React 仅支持 class component 进行状态管理的时候,HOC 是一个常被用来实现逻辑复用,同时,HOC 和装饰器的结合使用模式被推广。

  • 状态代理
import React, { Component } from 'react';

const withStorge = (storgeKey = '') => (WrappedComponent) => {
  return class extends Component {
    componentWillMount() {
      const data = localStorage.getItem(storgeKey);
      this.setState({ data });
    }
    render() {
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  };
}

@withStorge('key1')
class Component1 extends Component {
  render() {
    if (!this.props.data) {
      return null;
    }
    return <h1>{ this.props.data.title }</h1>
  }
}
  • 反向继承
import React, { Component } from 'react';

const orignCall = (fn) => { typeof fn === 'function' && fn() };

const withStorge = (storgeKey) => (WrappedComponent) => {
  return class extends WrappedComponent {
    componentWillMount() {
      const data = localStorage.getItem(storgeKey);
      this.setState({ data });

      orignCall(super.componentWillMount);
    }
    
    render() {
      return super.render();
    }  
  };
}


@withStorge('key1')
class Component1 extends Component {
  render() {
    if (!this.props.data) {
      return null;
    }
    return <h1>{ this.props.data.title }</h1>
  }
}
  1. 洋葱模型

洋葱模式是一种基于图形的概念模型,用于描述层次结构的各个级别之间的关系,类似于洋葱被切开的平面每层果肉的层级关系。在前端领域提到洋葱模型就会想到 koa 的中间件机制。可以通过学习 koa-compose 的实现来强化理解。

在讨论装饰器的过程中提到洋葱模型是因为装饰器的运行过程中会按一定层级顺序声明装饰器,再按一定层级顺序执行,这个跟洋葱模型正好契合。因此,结合一起讨论下洋葱模型在装饰器的运用。

  • Koa-compose 简易实现
// 主要逻辑
function compose(middleware) {
  return function(context, next) {
    let index = -1;
    return dispatch(0);

    function dispatch(i) {
      if (i <= index) {
        return Promise.reject(new Error(`next() called multiple times`));
      }

      index = i;

      let fn = middleware[i];
      if (i === middleware.length) fn = next;
      if (!fn) return Promise.resolve();
      try {
        Promise.resolve(fn(context, function next() {
          return dispatch(i + 1);
        }));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

function delay(time) { return new Promise(next => setTimeout(next, time)) }

class App {
  middleware: Function[] = []
  use(fn) { this.middleware.push(fn) }
  run(callback) {
    const cp = compose(this.middleware);
    const context  = { date: Date.now() };
    cp(context, callback);
  }
};

const app = new App;

app.use(async function(ctx, next) {
  console.log(`start mv1 ${Date.now() - ctx.date}`);
  await next();
  console.log(`end mv1 ${Date.now() - ctx.date}`);
});

app.use(async function(ctx, next) {
  console.log(`start mv2 ${Date.now() - ctx.date}`);
  await next();
  console.log(`end mv2 ${Date.now() - ctx.date}`);
})

app.use(async function(ctx, next) {
  console.log(`start mv3 ${Date.now() - ctx.date}`);
  await next();
  console.log(`end mv3 ${Date.now() - ctx.date}`);
});

app.run(async function(ctx) {
  console.log(`run ... ${Date.now() - ctx.date}`);
  await delay(200);
});
  • 装饰器运用洋葱模型
function compose(middleware) {
  return function(context, next) {
    let index = -1;
    return dispatch(0);

    function dispatch(i) {
      if (i <= index) {
        return Promise.reject(new Error(`next() called multiple times`));
      }

      index = i;

      let fn = middleware[i];
      if (i === middleware.length) fn = next;
      if (!fn) return Promise.resolve();
      try {
        Promise.resolve(fn(context, function next() {
          return dispatch(i + 1);
        }));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

function withFactory(middleware: Function[] = []) {
  function use(fn) {
    middleware.push(fn);
    return target => target;
  }
  
  function run(callback) {
    const cp = compose(middleware);
    return function (target) {
      const context  = { date: Date.now() };
      cp(context, callback);
      
      return target;
    };
  }
  
  return { use, run };
}

function delay(time) { return new Promise(next => setTimeout(next, time)) }

const { use, run } = withFactory();

@run(async function(ctx) {
  console.log(`run ... ${Date.now() - ctx.date}`);
  await delay(200);
})
@use(async function (ctx, next) {
  console.log(`start mv3 ${Date.now() - ctx.date}`);
  await next();
  console.log(`end mv3 ${Date.now() - ctx.date}`);
})
@use(async function (ctx, next) {
  console.log(`start mv2 ${Date.now() - ctx.date}`);
  await next();
  console.log(`end mv2 ${Date.now() - ctx.date}`);
})
@use(async function (ctx, next) {
  console.log(`start mv1 ${Date.now() - ctx.date}`);
  await next();
  console.log(`end mv1 ${Date.now() - ctx.date}`);
})
class ClassA {};
  1. 插件模式

插件模式因为其可插拔、灵活的特性被广泛的运用。可以举出许多的插件模式例子:koa 的中间件、webpack plugin、umijs 插件、请求库 umi-request 等等。插件模式特性是可以通过配置化的逻辑增加减少插件从而达到运行逻辑的增加减少,但是不会影响代码的运行,因为有严格的实现规范。达到的目的相同但是他们的实现会不同。现在了解到的实现方式有两个,也是比较流行的方式:1. 类似 koa 的中间件模式;2. 类似 webpack plugin 使用的 tapable 模式;

  • 类似 koa 的中间件模式

Koa 的中间件模式符合洋葱模型原则,采用的 compose 封装方式。类似一个管道,管道里有水(上下文)在流通,插件则是套在管道上的架构工具,可增加和拆卸。加工工具对水(上下文)进行加工,从而达到特定的目标;

  • 使用 tapable 实现 ref

Tapable 是业界里流行的插件模式实现方案,通过声明各种类型钩子(hook),再在钩子 tap 进任务,最终按钩子的实现类型完成任务。钩子有同步、异步、并行、串行、保险等方法。虽然钩子类型众多,但是暴露给用户的方式是发布订阅模式,做到了逻辑收口。Tapable 不仅是业界的优秀实现,同时代码逻辑有很多可以借鉴的地方;ref

  1. 控制反转 IoC(Inversion Of Control)

控制反转是面向对象编程中一致设计原则,可以用来减少代码设计之间的耦合度。在解决代码逻辑复用的同时,解决资源复用。同时,因为依赖配置收口到容器里,所以有变更时只需要修改配置文件。控制反转指的是将对象的创建和配置工作转移到容器。控制反转是设计原则,依赖注入(DI)和服务对位器(SL)是对控制反转(IoC)的实现。

  • DI 模式(Dependency Injection)

依赖注入模式的大致逻辑是类依赖对象的抽象,对象实例化和配置交由IoC容器控制,在类初始化的过程中将依赖对象的实现实例化后注入到类中。大致的代码逻辑实现如下:

import 'reflect-metadata';

type ConstructorOf<T = any> = new (...args: any[]) => T;

const INJECTOR_TOKEN = 'INJECTOR_TOKEN';
const INSTANCE_KEY = 'INSTANCE_KEY';

function Injectable(): ClassDecorator {
  return (target) => {
    const deps = Reflect.getMetadata('deps', target);
    return target;
  }
}

function Autowired(token?: string | symbol): PropertyDecorator  {
  return (target: object, key: string | symbol) => {
    let dependency = token;
    if (dependency === undefined) {
      dependency = Reflect.getMetadata('design:type', target, key);
    }

    const descriptor: PropertyDescriptor = {
      configurable: true,
      enumerable: true,
      
      get(this: any) {
        if (!this[INSTANCE_KEY]) {
          const injector = this[INJECTOR_TOKEN];

          if (!injector) {
            throw 'injector need instance';
          }

          this[INSTANCE_KEY] = injector.get(dependency);
        }
        return this[INSTANCE_KEY];
      },
    };
    return descriptor;
  };
}

type Token = string | symbol;

type TypeProvider = ConstructorOf<any>;

type ClassProvider = {
  token: string | symbol;
  useClass: ConstructorOf<any>;
};

type ValueProvider = {
  token: string | symbol;
  useValue: any;
}

function isTypeProvider(provider: any): provider is TypeProvider {
  return typeof provider === 'function';
} 

function isClassProvider(provider: any): provider is ClassProvider {
  return provider.useClass !== undefined;
} 

function isValueProvider(provider: any): provider is ValueProvider {
  return provider.useValue !== undefined;
} 


type Provider = TypeProvider | ClassProvider | ValueProvider;

let idIndex = 0;
function createId(name: string) {
  return `${name}` + idIndex++;
}

class Injector {
  providers: Provider[]
  instances: Record<Token, any>

  constructor() {
    this.providers = [];
    this.instances = {};

    this.addProvider({
      token: INJECTOR_TOKEN,
      useValue: this,
    });
  }

  addProvider(provider: Provider) {
    if (this.has(provider)) {
      throw `Provider added`;
    }

    this.providers.push(provider);
  }

  protected has(provider: Provider): boolean {
    const filteredProviders = this.providers.filter(item => {
      if (typeof item === 'object' && typeof provider === 'object') {
        return item.token === provider.token;
      }
      return item === provider;
    });

    return !!filteredProviders.length;
  }

  get<T extends ConstructorOf<any>>(token: T, args?: ConstructorParameters<T>): InstanceType<T>
  get<T extends Token>(token: T)
  get<T extends Token>(token: T, args?: ConstructorParameters<any>) {
    if (isTypeProvider(token)) {
      for (let key in this.instances) {
        if (this.instances[key] instanceof token) {
          return this.instances[key];
        }
      }

      const Func = this.providers.find(_ => _ === token) as TypeProvider;
      if (!Func) {
        throw 'Provider need add';
      }

      if (!Array.isArray(args)) {
        args = [];
      }

      const ret = new Func(...args);
      ret[INJECTOR_TOKEN] = this;
      this.instances[createId('instance')] = ret;
      return ret;
    } else {
      if (this.instances[token]) {
        return this.instances[token];
      }

      const creator = this.providers.find(_ => {
        if (!isTypeProvider(_)) {
          return _.token === token;
        }  
        return false;
      });

      if (!creator) {
        throw 'Provider need add';
      }

      if (isClassProvider(creator)) {
        const ret = new creator.useClass();
        ret[INJECTOR_TOKEN] = this;
        this.instances[createId('instance')] = ret;
        return ret;
      }

      if (isValueProvider(creator)) {
        return creator.useValue;
      }
    }
  }
}

interface IDrive {
  name: string;
  speed: number;
  
  distance: (d: number) => string;
}

class Drive implements IDrive {
  name: string;
  speed: number;

  distance(d: number) {
    return (d / this.speed).toFixed(2);
  }
}

@Injectable()
class Car extends Drive {
  name: string = 'Car';
  speed: number = 40;
}

interface IStudentParam {
  name: string;
  age: number;
}

@Injectable()
class Student {
  name: string;
  age: number;

  @Autowired('ICar')
  drive: IDrive;
  
  constructor(options: IStudentParam) {
    this.name = options.name;
    this.age = options.age;
  }

  introduce() {
    return `Name is ${this.name}, Age is ${this.age}.`;
  }

  distance(d: number) {
    return this.drive.distance(d);
  }
}

const injector = new Injector();
injector.addProvider(Student);
injector.addProvider({
  token: 'ICar',
  useClass: Car,
});

const LiXiao = injector.get(Student, [{ name: 'LiXiao', age: 18 }]);
console.log(LiXiao.introduce()); // Name is LiXiao, Age is 18.
console.log(LiXiao.distance(100)); // 2.50
  • SL 模式(Service Locator Pattern)

服务定位器模式与依赖注入的区别是增加了一个服务定位容器,在实例和依赖之间进行服务管理。

const instances: {[key: string] : any} = {};

// services/ServiceLocator.ts
export default {
  set<T>(serviceId: string, instance: T) {
    // ... error handling ...
    instances[serviceId] = instance;
  },

  get<T>(serviceId: string): T {
    const instance = instances[serviceId];
    // ... error handling ...
    return instance;
  }
}

// services/LogService.ts
export interface LogService {
    stream(onUpdate: (statements: LogStatement[]) => void): void;
}

class MockLogService implements LogService {
  stream(onUpdate: (statements: LogStatement[]) => void): void {
    window.setInterval(() => {
      onUpdate(createMockLogStatements());
    }, 200);
  }
}

import services from '@/services/ServiceLocator';
import { MockLogService } from '@/services/LogService';

services.set('log_service', new MockLogService());


import services from '@/services/ServiceLocator';
import { LogService } from '@/services/LogService';

const logs: LogService = services.get('log_service');
logs.stream((statements: LogStatement[]) => ....);
  • DIP(Dependency Inversion Principle)

依赖倒置通俗的解释是高层核心模块定义接口,底层持久模块实现接口。从而实现了底层对高层定义的依赖,高层从对底层依赖中解耦出来。和依赖注入的区别是依赖注入强调的是“注入”,由原本的组件寻找依赖,编程依赖提供给组件。将依赖同步到容器,再由容器统一注入。

依赖倒置原则经常结合依赖注入一起使用,同时在使用 Typescript 编程的场景里运用规范。JavaScript 的运用可以以固定方法及方法入参来实现,避开使用定义抽象类的方式。

  1. AOP 面向切面编程

参考资料

  1. 都2020年了,你还不会JavaScript 装饰器?
  2. JS由Number与new Number的区别引发的思考
  3. Constructor Properties of the Global Object
  4. JavaScript 中的相等性判断
  5. core-decorators
  6. TypeScript 装饰器完整指南
  7. IoC JavaScript 实现示例
  1. reflect-metadata的研究
  2. 洋葱模型
  3. 浅析koa的洋葱模型实现
  4. JavaScript 的 IoC、IoC Containers、DI、DIP和Reflect
  5. 我们为什么要用 IoC 和 AOP
  6. 用TypeScript装饰器实现一个简单的依赖注入
  7. 深刻理解JavaScript系列(22):S.O.L.I.D五大原则之依赖倒置原则DIP
  8. Simple Service Locator with TypeScript