es-next-2

Proxy

概述

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
});

上面代码对一个空对象架设了一层拦截,重定义了属性的读取(get)和设置(set)行为。这里暂时先不解释具体的语法,只看运行结果。对设置了拦截行为的对象 obj,去读写它的属性,就会得到下面的结果。

obj.count = 1
//  setting count!
++obj.count
//  getting count!
//  setting count!
//  2

上面代码说明,Proxy 实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。

构造函数

基本使用

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。

var proxy = new Proxy(target, handler);

Proxy 对象的所有用法,都是上面这种形式,不同的只是 handler 参数的写法。

参数

示例

拦截操作一览

同一个拦截器函数,可以设置拦截多个操作。

var handler = {
  get: function(target, name) {
    if (name === 'prototype') {
      return Object.prototype;
    }
    return 'Hello, ' + name;
  },

  apply: function(target, thisBinding, args) {
    return args[0];
  },

  construct: function(target, args) {
    return {value: args[1]};
  }
};

var fproxy = new Proxy(function(x, y) {
  return x + y;
}, handler);

fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true

对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。

下面是 Proxy 支持的拦截操作一览,一共 13 种。

用途

一个技巧是将 Proxy 对象,设置到 object.proxy属性,从而可以在 object 对象上调用。

var object = { proxy: new Proxy(target, handler) };

Proxy 实例也可以作为其他对象的原型对象

var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});

let obj = Object.create(proxy);
obj.time // 35

上面代码中,proxy 对象是 obj 对象的原型,obj 对象本身并没有 time 属性,所以根据原型链,会在 proxy 对象上读取该属性,导致被拦截。

Proxy 实例的方法

get()

get 方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选

前两个必填?并不是,不填也是 ok 的,只是一般都会用到而已

拦截读取操作

get 方法的用法,上文已经有一个例子,下面是另一个拦截读取操作的例子。

var person = {
  name: "张三"
};

var proxy = new Proxy(person, {
  get: function(target, property) {
    if (property in target) {
      return target[property];
    } else {
      throw new ReferenceError("Property \"" + property + "\" does not exist.");
    }
  }
});

proxy.name // "张三"
proxy.age // 抛出一个错误

上面代码表示,如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回 undefined

通过 Handler 给 Proxy 对象添加属性

访问不存在的属性, 也会被拦截

可以通过这个实现给所有的 proxy 对象都添加统一的属性: 如果访问的是 ReactiveFlags 属性, 可以直接返回对应的值.

这种做法和继承相比有啥优势和劣势呢?

  return function get(target, key, receiver) {
    const isExistInReactiveMap = () =>
      key === ReactiveFlags.RAW && receiver === reactiveMap.get(target);

    const isExistInReadonlyMap = () =>
      key === ReactiveFlags.RAW && receiver === readonlyMap.get(target);

    const isExistInShallowReadonlyMap = () =>
      key === ReactiveFlags.RAW && receiver === shallowReadonlyMap.get(target);

    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly;
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly;
    } else if (
      isExistInReactiveMap() ||
      isExistInReadonlyMap() ||
      isExistInShallowReadonlyMap()
    ) {
      return target;
    }
}

Get 方法可以继承

let proto = new Proxy({}, {
  get(target, propertyKey, receiver) {
    console.log('GET ' + propertyKey);
    return target[propertyKey];
  }
});

let obj = Object.create(proto);
obj.foo // "GET foo"

上面代码中,拦截操作定义在 Prototype 对象上面,所以如果读取 obj 对象继承的属性时,拦截会生效。

Receiver

下面是一个 get 方法的第三个参数的例子,它总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例。

const proxy = new Proxy({}, {
  get: function(target, property, receiver) {
    return receiver;
  }
});
proxy.getReceiver === proxy // true

上面代码中,proxy 对象的 getReceiver 属性是由 proxy 对象提供的,所以 receiver指向 proxy 对象

const proxy = new Proxy({}, {
  get: function(target, property, receiver) {
    return receiver;
  }
});

const d = Object.create(proxy);
d.a === d // true

上面代码中,d 对象本身没有 a 属性,所以读取 d.a 的时候,会去 d 的原型 proxy 对象找。这时,receiver指向 d,代表原始的读操作所在的那个对象。

Cinfigurable

如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错

const target = Object.defineProperties({}, {
  foo: {
    value: 123,
    writable: false,
    configurable: false
  },
});

const handler = {
  get(target, propKey) {
    return 'abc';
  }
};

const proxy = new Proxy(target, handler);

proxy.foo
// TypeError: Invariant check failed

实现数组读取负数的索引

下面的例子使用 get 拦截,实现数组读取负数的索引

function createArray(...elements) {
  let handler = {
    get(target, propKey, receiver) {
      let index = Number(propKey);
      if (index < 0) {
        propKey = String(target.length + index);
      }
      return Reflect.get(target, propKey, receiver);
    }
  };

  let target = [];
  target.push(...elements);
  return new Proxy(target, handler);
}

let arr = createArray('a', 'b', 'c');
arr[-1] // c

上面代码中,数组的位置参数是 -1,就会输出数组的倒数第一个成员。

利用 Proxy,可以将读取属性的操作(get),转变为执行某个函数,从而实现属性的链式操作

var pipe = (function () {
  return function (value) {
    var funcStack = [];
    var oproxy = new Proxy({} , {
      get : function (pipeObject, fnName) {
        if (fnName === 'get') {
          return funcStack.reduce(function (val, fn) {
            return fn(val);
          },value);
        }
        funcStack.push(window[fnName]);
        return oproxy;
      }
    });

    return oproxy;
  }
}());

var double = n => n * 2;
var pow    = n => n * n;
var reverseInt = n => n.toString().split("").reverse().join("") | 0;

pipe(3).double.pow.reverseInt.get; // 63

实现一个生成各种 DOM 节点的通用函数 Dom

下面的例子则是利用 get 拦截,实现一个生成各种 DOM 节点的通用函数dom

const dom = new Proxy({}, {
  get(target, property) {
    return function(attrs = {}, ...children) {
      const el = document.createElement(property);
      for (let prop of Object.keys(attrs)) {
        el.setAttribute(prop, attrs[prop]);
      }
      for (let child of children) {
        if (typeof child === 'string') {
          child = document.createTextNode(child);
        }
        el.appendChild(child);
      }
      return el;
    }
  }
});

const el = dom.div({},
  'Hello, my name is ',
  dom.a({href: '//example.com'}, 'Mark'),
  '. I like:',
  dom.ul({},
    dom.li({}, 'The web'),
    dom.li({}, 'Food'),
    dom.li({}, '…actually that\'s it')
  )
);

document.body.appendChild(el);

set()

set 方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象属性名属性值和 Proxy 实例本身,其中最后一个参数可选。

假定 Person 对象有一个 age 属性,该属性应该是一个不大于 200 的整数,那么可以使用 Proxy 保证 age 的属性值符合要求。

let validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }

    // 对于满足条件的 age 属性以及其他属性,直接保存
    obj[prop] = value;
  }
};

let person = new Proxy({}, validator);

person.age = 100;

person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错

上面代码中,由于设置了存值函数 set,任何不符合要求的 age 属性赋值,都会抛出一个错误,这是数据验证的一种实现方法。利用 set 方法,还可以数据绑定,即每当对象发生变化时,会自动更新 DOM。

有时,我们会在对象上面设置内部属性,属性名的第一个字符使用下划线开头,表示这些属性不应该被外部使用。结合 getset 方法,就可以做到防止这些内部属性被外部读写。

const handler = {
  get (target, key) {
    invariant(key, 'get');
    return target[key];
  },
  set (target, key, value) {
    invariant(key, 'set');
    target[key] = value;
    return true;
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}
const target = {};
const proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property

下面是 set 方法第四个参数的例子。

const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = receiver;
  }
};
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
proxy.foo === proxy // true

上面代码中,set 方法的第四个参数 receiver,指的是原始的操作行为所在的那个对象,一般情况下是 proxy 实例本身,请看下面的例子。

const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = receiver;
  }
};
const proxy = new Proxy({}, handler);
const myObj = {};
Object.setPrototypeOf(myObj, proxy);

myObj.foo = 'bar';
myObj.foo === myObj // true

上面代码中,设置 myObj.foo 属性的值时,myObj 并没有 foo 属性,因此引擎会到 myObj 的原型链去找 foo 属性。myObj 的原型对象 proxy 是一个 Proxy 实例,设置它的 foo 属性会触发 set 方法。这时,第四个参数 receiver 就指向原始赋值行为所在的对象 myObj

注意,如果目标对象自身的某个属性,不可写且不可配置,那么 set 方法将不起作用。

const obj = {};
Object.defineProperty(obj, 'foo', {
  value: 'bar',
  writable: false,
});

const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = 'baz';
  }
};

const proxy = new Proxy(obj, handler);
proxy.foo = 'baz';
proxy.foo // "bar"

上面代码中,obj.foo 属性不可写,Proxy 对这个属性的 set 代理将不会生效。

注意,严格模式下,set 代理如果没有返回 true,就会报错。

'use strict';
const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = receiver;
    // 无论有没有下面这一行,都会报错
    return false;
  }
};
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
// TypeError: 'set' on proxy: trap returned falsish for property 'foo'

上面代码中,严格模式下,set 代理返回 false 或者 undefined,都会报错。

apply()

apply 方法拦截函数的调用、callapply 操作

apply 方法可以接受三个参数,分别是目标对象目标对象的上下文对象(this目标对象的参数数组

var handler = {
  apply (target, ctx, args) {
    return Reflect.apply(...arguments);
  }
};

下面是一个例子。

var target = function () { return 'I am the target'; };
var handler = {
  apply: function () {
    return 'I am the proxy';
  }
};

var p = new Proxy(target, handler);

p()
// "I am the proxy"
// 上面代码中,变量p是 Proxy 的实例,当它作为函数调用时(p()),就会被apply方法拦截,返回一个字符串。

下面是另外一个例子。

var twice = {
  apply (target, ctx, args) {
    return Reflect.apply(...arguments) * 2;
  }
};
function sum (left, right) {
  return left + right;
};
var proxy = new Proxy(sum, twice);
proxy(1, 2) // 6
proxy.call(null, 5, 6) // 22
proxy.apply(null, [7, 8]) // 30
// 上面代码中,每当执行proxy函数(直接调用或call和apply调用),就会被apply方法拦截。

另外,直接调用 Reflect.apply 方法,也会被拦截。

Reflect.apply(proxy, null, [9, 10]) // 38

自定义的方法

const target = {
  m: function () {
    console.log(this === proxy);
  }
};
const handler = {};

const proxy = new Proxy(target, handler);

target.m() // false
proxy.m()  // true

通过 proxy 访问 m 属性,也会被代理

Proxy.revocable()

Proxy.revocable 方法返回一个可取消的 Proxy 实例。

let target = {};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked

Proxy.revocable 方法返回一个对象,该对象的 proxy 属性是 Proxy 实例,revoke 属性是一个函数,可以取消 Proxy 实例。上面代码中,当执行 revoke 函数之后,再访问 Proxy 实例,就会抛出一个错误。

Proxy.revocable 的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。

This 问题

虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。

主要原因就是在 Proxy 代理的情况下,目标对象内部的 this 关键字会指向 Proxy 代理。

const target = {
  m: function () {
    console.log(this === proxy);
  }
};
const handler = {};

const proxy = new Proxy(target, handler);

target.m() // false
proxy.m()  // true

上面代码中,一旦 proxy 代理 target.m,后者内部的 this 就是指向 proxy,而不是 target

下面是一个例子,由于 this 指向的变化,导致 Proxy 无法代理目标对象。

const _name = new WeakMap();

class Person {
  constructor(name) {
    _name.set(this, name);
  }
  get name() {
    return _name.get(this);
  }
}

const jane = new Person('Jane');
jane.name // 'Jane'

const proxy = new Proxy(jane, {});
proxy.name // undefined

上面代码中,目标对象 janename 属性,实际保存在外部 WeakMap 对象 _name 上面,通过 this 键区分。由于通过 proxy.name 访问时,this 指向 proxy,导致无法取到值,所以返回 undefined

此外,有些原生对象的内部属性,只有通过正确的 this 才能拿到,所以 Proxy 也无法代理这些原生对象的属性。

const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);

proxy.getDate();
// TypeError: this is not a Date object.

上面代码中,getDate 方法只能在 Date 对象实例上面拿到,如果 this 不是 Date 对象实例就会报错。这时,this 绑定原始对象,就可以解决这个问题。

const target = new Date('2015-01-01');
const handler = {
  get(target, prop) {
    if (prop === 'getDate') {
      return target.getDate.bind(target);
    }
    return Reflect.get(target, prop);
  }
};
const proxy = new Proxy(target, handler);

proxy.getDate() // 1

实例

https://zhuanlan.zhihu.com/p/40772736

实例:Web 服务的客户端

Proxy 对象可以拦截目标对象的任意属性,这使得它很合适用来写 Web 服务的客户端。

const service = createWebService('http://example.com/data');

service.employees().then(json => {
  const employees = JSON.parse(json);
  // ···
});

上面代码新建了一个 Web 服务的接口,这个接口返回各种数据。Proxy 可以拦截这个对象的任意属性,所以不用为每一种数据写一个适配方法,只要写一个 Proxy 拦截就可以了。

function createWebService(baseUrl) {
  return new Proxy({}, {
    get(target, propKey, receiver) {
      return () => httpGet(baseUrl+'/' + propKey);
    }
  });
}

同理,Proxy 也可以用来实现数据库的 ORM 层。

Reflect

概述

Reflect 对象与 Proxy 对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect 对象的设计目的有这样几个。

  1. Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty),反射Reflect 对象上。现阶段,某些方法同时在 ObjectReflect 对象上部署,未来的新方法将只部署在 Reflect 对象上。也就是说,从 Reflect 对象上可以拿到语言内部的方法。

  2. 修改某些 Object 方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc) 在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc) 则会返回 false

    // 老写法
    try {
      Object.defineProperty(target, property, attributes);
      // success
    } catch (e) {
      // failure
    }
    
    // 新写法
    if (Reflect.defineProperty(target, property, attributes)) {
      // success
    } else {
      // failure
    }
    
  3. Object 操作都变成函数行为。某些 Object 操作是命令式,比如 name in objdelete obj[name],而 Reflect.has(obj, name)Reflect.deleteProperty(obj, name) 让它们变成了函数行为

    // 老写法
    'assign' in Object // true
    
    // 新写法
    Reflect.has(Object, 'assign') // true
    
  4. Reflect 对象的方法Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为,你总可以在 Reflect获取默认行为

    Proxy(target, {
      set: function(target, name, value, receiver) {
        var success = Reflect.set(target,name, value, receiver);
        if (success) {
          console.log('property ' + name + ' on ' + target + ' set to ' + value);
        }
        return success;
      }
    });
    

静态方法

Reflect 对象一共有 13 个静态方法

Reflect.get(target, propertyKey, receiver)

Reflect.set(target, propertyKey, Value, receiver)

Reflect.has(obj, propertyKey)

Reflect.deleteProperty(obj, propertyKey)

Reflect.construct(target, args)

Reflect.getPrototypeOf(obj)

Reflect.setPrototypeOf(obj, newProto)

Reflect.apply(func, thisArg, args)

Reflect.defineProperty(target, propertyKey, attributes)

Reflect.getOwnPropertyDescriptor(target, propertyKey)

Reflect.isExtensible (target)

Reflect.preventExtensions(target)

Reflect.ownKeys (target)

实例:使用 Proxy 实现观察者模式

Iterator

Iterator(遍历器)的概念

默认 Iterator 接口

概述

原生具备 Iterator 接口的数据结构

部署对象的 Iterator 接口

部署类数组对象的 Iterator 接口

调用 Iterator 接口的场合

解构赋值

扩展运算符

yield*

其他场合

字符串的 Iterator 接口

遍历器对象的 return(),throw()

for...of

ES6 借鉴 C++、Java、C# 和 Python 语言,引入了 for...of 循环,作为遍历所有数据结构的统一的方法

一个数据结构只要部署了 Symbol.iterator 属性,就被视为具有 iterator 接口,就可以用 for...of 循环遍历它的成员。也就是说,for...of 循环内部调用的是数据结构的 Symbol.iterator 方法。

for...of 循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如 arguments 对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。

注意:不同的数据结构 for...of 循环可以获取到的值是不同的

数组

数组原生具备 iterator 接口(即默认部署了 Symbol.iterator 属性),for...of 循环本质上就是调用这个接口产生的遍历器,可以用下面的代码证明。

const arr = ['red', 'green', 'blue'];

for(let v of arr) {
  console.log(v); // red green blue
}

const obj = {};
obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);

for(let v of obj) {
  console.log(v); // red green blue
}

上面代码中,空对象 obj 部署了数组 arrSymbol.iterator 属性,结果 objfor...of 循环,产生了与 arr 完全一样的结果。

JavaScript 原有的 for...in 循环,只能获得对象的键名,不能直接获取键值。ES6 提供 for...of 循环,允许遍历获得键值。

var arr = ['a', 'b', 'c', 'd'];

for (let a in arr) {
  console.log(a); // 0 1 2 3
}

for (let a of arr) {
  console.log(a); // a b c d
}

上面代码表明,for...in 循环读取键名,for...of 循环读取键值。如果要通过 for...of 循环,获取数组的索引,可以借助数组实例的 entries 方法和 keys 方法

注意

for...of 循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟 for...in 循环也不一样。

let arr = [3, 5, 7];
arr.foo = 'hello';

for (let i in arr) {
  console.log(i); // "0", "1", "2", "foo"
}

for (let i of arr) {
  console.log(i); //  "3", "5", "7"
}

上面代码中,for...of 循环不会返回数组 arrfoo 属性。

Set 和 Map 结构

遍历顺序

类似数组的对象 @@@

计算生成的数据结构

对象

遍历顺序 @@@

注意

与其他遍历语法的比较 @@@

数组遍历方法

for...in

优点

Iterator 接口与 Generator 函数

Decorator

参考:

基本介绍:https://aotu.io/notes/2016/10/24/decorator/index.html

https://juejin.im/post/59f1c484f265da431c6f8940

在 ES6 中增加了对类对象的相关定义和操作(比如 classextends ),这就使得我们在多个不同类之间共享或者扩展一些方法或者行为的时候,变得并不是那么优雅。这个时候,我们就需要一种更优雅的方法来帮助我们完成这些事情。

Python 中的装饰器

装饰器是在 python 2.4 里增加的功能,它的主要作用是给一个已有的方法或类扩展一些新的行为,而不是去直接修改它本身。

def decorator(f):
    print "my decorator"
    return f

@decorator
def myfunc():
    print "my function"

myfunc()

# my decorator
# my function

这里的 @decorator 就是我们说的装饰器。在上面的代码中,我们利用装饰器给我们的目标方法执行前打印出了一行文本,并且并没有对原方法做任何的修改。代码基本等同于

def decorator(f):
    def wrapper():
        print "my decorator"
        return f()
    return wrapper

def myfunc():
    print "my function"

myfunc = decorator(myfuc)

体感上是类似于高阶函数,不对接受的函数做修改,而是执行一些操作之后再执行原本的函数

当我们对某个函数应用了装饰以后,其实就改变了被装饰方法的入口引用,使其重新指向了装饰器返回的方法的入口点,从而来实现我们对原函数的扩展、修改等操作。

Javascript 中的装饰器(第一版)

是的,在 ES6 之前,装饰器对于 JS 来说确实显得不太重要,你只是需要加几层 wrapper 包裹就好了(虽然也会显得不那么优雅)。但是在 ES6 提出之后,当我们需要在多个不同的类之间共享或者扩展一些方法或行为的时候,代码会变得错综复杂,极其不优雅,这也就是装饰器被提出的一个很重要的原因。

话说从装饰器被提出已经有一年多的时间了,同时期的很多其他新的特性已经随着 ES6 的推进而被大家广泛使用,而这货现在却还停留在 stage 2 的阶段,也很少被人提及和应用。那么,装饰器到底是在 Javascript 中是怎样表现的呢?我们下面来一起看一下吧!

据说最近已经被打回重修了,现阶段不推荐使用 decorator

Javascript 中的装饰器

先来看一下装饰器在代码中是长成什么样子吧

@decorator
class Cat {}

class Dog {
    @decorator
    run() {}
}

嗯,代码中的 @decorator 就是 JS 中的装饰器,看起来基本和 python 中的样子一样,以 @ 作为标识符,可以作用于类,也可以作用于类的属性。那么接下来,我们就来看看它具体的表现及运行原理吧。

ES6 中的类

首先我们先来看一下关于 ES6 中的类吧

class Cat {
    say() {
        console.log("meow ~");
    }
}

上面这段代码是 ES6 中定义一个类的写法,其实只是一个语法糖,而实际上当我们给一个类添加一个属性的时候,会调用到 Object.defineProperty 这个方法,它会接受三个参数:targetnamedescriptor ,所以上面的代码实际上在执行时是这样的:

function Cat() {}
Object.defineProperty(Cat.prototype, "say", {
    value: function() { console.log("meow ~"); },
    enumerable: false,
    configurable: true,
    writable: true
});

好了,有了上面这段代码以后,我们再来看看装饰器在 JS 中到底是怎么样工作的吧!

类装饰器

当一个装饰器作用于类的时候,大概是这个样子的:

function isAnimal(target) {
    target.isAnimal = true;
   return target;
}

@isAnimal
class Cat {
    ...
}

console.log(Cat.isAnimal);    // true

是不是很像之前我们在 python 中看到的装饰器?

所以这段代码实际上基本等同于:

Cat = isAnimal(function Cat() { ... });

相当于是给 Cat 添加了一个静态属性

类方法装饰器

比如有的时候,我们希望把我们的部分属性置成只读,以避免别人对其进行修改,如果使用装饰器的话,我们可以这样来做:

function readonly(target, name, descriptor) {
    discriptor.writable = false;
    return discriptor;
}

class Cat {
    @readonly
    say() {
        console.log("meow ~");
    }
}

var kitty = new Cat();

kitty.say = function() {
    console.log("woof !");
}

kitty.say()    // meow ~

我们通过上面的代码把 say 方法设置成了只读,所以在我们后面再次对它赋值的时候就不会生效,调用的还是之前的方法。

在上面的代码中我们可以看到,我们在定义装饰器的时候,参数是有三个targetnamedescriptor

没错,就是我们上文提到过的关于类的定义那一块儿的 Object.defineProperty 的参数,所以其实装饰器在作用于属性的时候,实际上是通过 Object.defineProperty 来进行扩展和封装的。

所以在上面的这段代码中,装饰器实际的作用形式是这样的:

let descriptor = {
    value: function() {
        console.log("meow ~");
    },
    enumerable: false,
    configurable: true,
    writable: true
};

descriptor = readonly(Cat.prototype, "say", descriptor) || descriptor;

Object.defineProperty(Cat.prototype, "say", descriptor);

嗯嗯,是不是这样看就清楚很多了呢?这里也是 JS 里装饰器作用于类和作用于类的属性的不同的地方。

我们可以看出,当装饰器作用于类本身的时候,我们操作的对象也是这个类本身,而当装饰器作用于类的某个具体的属性的时候,我们操作的对象既不是类本身,也不是类的属性,而是它的描述符(descriptor),而描述符里记录着我们对这个属性的全部信息,所以,我们可以对它自由的进行扩展和封装,最后达到的目的呢,就和之前说过的装饰器的作用是一样的。

当然,如果你喜欢的话,也可以直接在 target 上进行扩展和封装,比如

function fast(target, name, descriptor) {
    target.speed = 20;

    let run = descriptor.value;
    descriptor.value = function() {
        run();
        console.log(`speed ${this.speed}`);
    }

    return descriptor;
}

class Rabbit {
    @fast
    run() {
        console.log("running~");
    }
}

var bunny = new Rabbit();

bunny.run();
// running~
// speed 20

console.log(bunny.speed);   // 20

关键点:作用于类的原型,透过 defineproperty

类属性装饰器

类似于方法装饰器,但它的入参少了属性描述符。原因则是目前没有方法在定义原型对象成员同时去描述一个实例的属性(创建描述符)。

function addProps(): PropertyDecorator {
  return (target, propertyKey) => {
    console.log(target);
    console.log(propertyKey);
  };
}

class A {
  @addProps()
  originProps: any;
}

访问操作符装饰器

与属性方法类似,就不详述了。

class Person {
  @nonenumerable
  get kidCount() { return this.children.length; }
}

function nonenumerable(target, name, descriptor) {
  descriptor.enumerable = false;
  return descriptor;
}

参数装饰器

参数装饰器的入参首要两位与属性装饰器相同,第三个参数则是参数在当前函数参数中的索引

function paramDeco(params?: any): ParameterDecorator {
  return (target, propertyKey, index) => {
    console.log(target);
    console.log(propertyKey);
    console.log(index);
    target.constructor.prototype.fromParamDeco = '呀呼!';
  };
}

class B {
  someMethod(@paramDeco() param1: any, @paramDeco() param2: any) {
    console.log(`${param1}  ${param2}`);
  }
}

new B().someMethod('啊哈', '林不渡!');
// @ts-ignore
console.log(B.prototype.fromParamDeco);

TS 中的装饰器

装饰器与注解

装饰器与注解实际上也有一定区别,由于并没有学过 Java,这里就不与 Java 中的注解进行比较了。而只是说我所认为的二者差异:

但实际上,TS 中的装饰器通常是同时包含了这两种效能的,它可能消费元数据的同时也提供了元数据供别的装饰器消费。

装饰器顺序

装饰器求值顺序来自于 TypeScript 官方文档一节中的装饰器说明。

类中不同声明上的装饰器将按以下规定的顺序应用:

  1. 参数装饰器,然后依次是方法装饰器 *,访问符装饰器,或 * 属性装饰器应用到每个实例成员。
  2. 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
  3. 参数装饰器应用到构造函数。
  4. 类装饰器应用到类。

注意这个顺序,后面我们能够实现元数据读写,也正是因为这个顺序。

当存在多个装饰器来装饰同一个声明时,则会有以下的顺序:

首先,由上至下依次对装饰器表达式求值,得到返回的真实函数(如果有的话)

而后,求值的结果会由下至上依次调用

(有点类似洋葱模型)

function foo() {
    console.log("foo in");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("foo out");
    }
}

function bar() {
    console.log("bar in");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("bar out");
    }
}

class A {
    @foo()
    @bar()
    method() {}
}

// foo in
// bar in
// bar out
// foo out

Reflect MetaData

基本元数据读写

Reflect Metadata 是属于 ES7 的一个提案,其主要作用是在声明时去读写元数据。TS 早在 1.5+ 版本就已经支持反射元数据的使用,目前想要使用,我们还需要安装 reflect-metadata 与在 tsconfig.json 中启用 emitDecoratorMetadata 选项。

你可以将元数据理解为用于描述数据的数据,如某个对象的键、键值、类型等等就可称之为该对象的元数据。我们先不用太在意元数据定义的位置,先做一个简单的阐述:

为类或类属性添加了元数据后,构造函数的原型(或是构造函数,根据静态成员还是实例成员决定)会具有 Metadata 属性,该属性内部包含一个 Map 结构,键为属性键,值为元数据键值对。

reflect-metadata 提供了对 Reflect 对象的扩展,在引入后,我们可以直接从 Reflect 对象上获取扩展方法。

import 'reflect-metadata';

@Reflect.metadata('className', 'D')
class D {
  @Reflect.metadata('methodName', 'hello')
  public hello(): string {
    return 'hello world';
  }
}

const d = new D();
console.log(Reflect.getMetadata('className', D));
console.log(Reflect.getMetadata('methodName', d));

可以看到,我们给类 D 与 D 内部的方法 hello 都注入了元数据,并通过 getMetadata(metadataKey, target) 这个方式取出了存放的元数据。

我们注意到,想要取出注入到类 D 中的元数据,target 需要为类 D。而想要取出注入到类的方法上的元数据,target 需要为实例 d。原因其实我们实际上在上面的装饰器执行顺序提到了,这是由于注入在方法、属性、参数上的元数据实际上是被添加在了实例对应的位置上,因此需要实例化才能取出。

TS 内置元数据

Reflect 允许程序去检视自身,基于这个效果,我们可以在装饰器运行时去检查其类型相关信息,如目标类型、目标参数的类型以及方法返回值的类型,这需要借助 TS 内置的元数据 metadataKey 来实现,以一个检查入参的例子为例:

import 'reflect-metadata';

class Point {
  x: number;
  y: number;
}

class Line {
  private _p0: Point;
  private _p1: Point;

  @validate
  set p0(value: Point) {
    this._p0 = value;
  }
  get p0() {
    return this._p0;
  }

  @validate
  set p1(value: Point) {
    this._p1 = value;
  }
  get p1() {
    return this._p1;
  }
}

function validate<T>(
  target: any,
  propertyKey: string,
  descriptor: TypedPropertyDescriptor<T>
) {
  let set = descriptor.set!;
  descriptor.set = function (value: T) {
    let type = Reflect.getMetadata('design:type', target, propertyKey);
    if (!(value instanceof type)) {
      throw new TypeError('Invalid type.');
    }
    set(value);
  };
}

这个例子来自于 TypeScript 官方文档,但实际上不能正常执行。因为在经过装饰器处理后,set 方法的 this 将会丢失。但我猜想官方的用意只是展示 design:type 的用法。

在这个例子中,我们基于 Reflect.getMetadata('design:type', target, propertyKey); 获取到了装饰器对应声明的属性类型,并确保在 setter 被调用时检查值类型。

这里的 design:type 即是 TS 的内置元数据,你可以理解为 TS 在编译前还手动执行了@Reflect.metadata("design:type", Point)。TS 还内置了**design:paramtypes(获取目标参数类型)与 design:returntype(获取方法返回值类型)**这两种元数据字段来提供帮助。但有一点需要注意,即使对于基本类型,这些元数据也返回对应的包装类型,如 number -> [Function: Number]

实例:解析路由装饰器

@controller('/user')
export class UserController {
  @get('/all')
  async getAllUser(): Promise<void> {
    // ...
  }

  @post('/update')
  async updateUser(): Promise<void> {
    // ...
  }
}

首先思考 controllerget/post 装饰器,我们需要使用这几个装饰器注入哪些信息:

@controller

首先是对于整个类,我们需要将 path: "/user" 这个数据注入:

// 工具常量枚举
export enum METADATA_MAP {
  METHOD = 'method',
  PATH = 'path',
  GET = 'get',
  POST = 'post',
  MIDDLEWARE = 'middleware',
}

const { METHOD, PATH, GET, POST } = METADATA_MAP;

export const controller = (path: string): ClassDecorator => {
  return (target) => {
    Reflect.defineMetadata(PATH, path, target);
  };
};

@get

而后是方法装饰器,我们选择一个高阶函数(柯里化)去吐出各个方法的装饰器,而不是为每种方法定义一个。

// 方法装饰器 保存方法与路径
export const methodDecoCreator = (method: string) => {
  return (path: string): MethodDecorator => {
    return (_target, _key, descriptor) => {
      Reflect.defineMetadata(METHOD, method, descriptor.value!);
      Reflect.defineMetadata(PATH, path, descriptor.value!);
    };
  };
};

// 首先确定方法,而后在使用时才去确定路径
const get = methodDecoCreator(GET);
const post = methodDecoCreator(POST);

生成路由表

接下来我们要做的事情就很简单了:

const routeGenerator = (ins: Object) => {
  const prototype = Object.getPrototypeOf(ins);

  const rootPath = Reflect.getMetadata(PATH, prototype['constructor']);

  const methods = Object.getOwnPropertyNames(prototype).filter(
    (item) => item !== 'constructor'
  );

  const routeGroup = methods.map((methodName) => {
    const methodBody = prototype[methodName];

    const path = Reflect.getMetadata(PATH, methodBody);
    const method = Reflect.getMetadata(METHOD, methodBody);
    return {
      path: `${rootPath}${path}`,
      method,
      methodName,
      methodBody,
    };
  });
  console.log(routeGroup);
  return routeGroup;
};

DI 工具库

实际上,一个依赖注入工具库必定会提供的就是 从容器中获取实例注入对象到容器中的两个方法,如上面的 provideinject,TypeDI 的 ServiceInject

装饰器(第三版)

前两版是类似于 python decorator 的语义,第三版是静态语义,类似于弱化的宏

区别

Latest 语法

可选链操作符

概述

可选链操作符 ( ?. ) 允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 操作符的功能类似于 . 链式操作符,不同之处在于,在引用为空 (nullish ) (null 或者 undefined) 的情况下不会引起错误,该表达式短路返回值是 undefined。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined

语法

obj?.prop
obj?.[expr]
arr?.[index]
func?.(args)

实例

let nestedProp = obj.first?.second;

通过使用 ?. 操作符取代 . 操作符,JavaScript 会在尝试访问 obj.first.second 之前,先隐式地检查并确定 obj.first 既不是 null 也不是 undefined。如果 obj.firstnull 或者 undefined,表达式将会短路计算直接返回 undefined

这等价于以下表达式,但实际上没有创建临时变量:

let temp = obj.first;
let nestedProp = ((temp === null || temp === undefined) ? undefined : temp.second);

needBatcherByDataChanges

targetNode = dataChanges[i]?.option?.target?.node;

var targetNode = (_c = (_b = (_a = dataChanges[i]) === null || _a === void 0 ? void 0 : _a.option) === null || _b === void 0 ? void 0 : _b.target) === null || _c === void 0 ? void 0 : _c.node;

可选链与函数调用

当尝试调用一个可能不存在的方法时也可以使用可选链。这将是很有帮助的,比如,当使用一个 API 的方法可能不可用时,要么因为实现的版本问题要么因为当前用户的设备不支持该功能。

函数调用时如果被调用的方法不存在,使用可选链可以使表达式自动返回 undefined 而不是抛出一个异常。

let result = someInterface.customMethod?.();

注意: 如果存在一个属性名且不是函数, 使用 ?. 仍然会产生一个 TypeError 异常 (x.y is not a function)

注意: 如果 someInterface 自身是 null 或者 undefined ,异常 TypeError 仍会被抛出 someInterface is null 如果你希望允许 someInterface 也为 null 或者 undefined ,那么你需要像这样写 someInterface?.customMethod?.()

处理可选的回调函数或者事件处理器

如果使用 解构赋值 来解构的一个对象的回调函数或 fetch 方法,你可能得到不能当做函数直接调用的不存在的值,除非你已经校验了他们的存在性。使用 ?. 的你可以忽略这些额外的校验:

//  ES2019的写法
function doSomething(onContent, onError) {
  try {
    // ... do something with the data
  }
  catch (err) {
    if (onError) { // 校验onError是否真的存在
      onError(err.message);
    }
  }
}
// 使用可选链进行函数调用
function doSomething(onContent, onError) {
  try {
   // ... do something with the data
  }
  catch (err) {
    onError?.(err.message); // 如果onError是undefined也不会有异常
  }
}

可选链不能用于赋值

let object = {};
object?.property = 1; // Uncaught SyntaxError: Invalid left-hand side in assignment

空值合并运算符

空值合并操作符??)是一个逻辑操作符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。

逻辑或操作符(|| 不同,逻辑或操作符会在左侧操作数为 假值 时返回右侧操作数。也就是说,如果使用 || 来为某些变量设置默认值,可能会遇到意料之外的行为。比如为假值(例如,''0)时。见下面的例子。

https://cloud.tencent.com/developer/article/1600583