说说 ES6 中的 new 操作

在上一篇类成员保护的最后有一个小小的疑问,假设 Fn 是一个类,那么 new Fn()Reflect.construct(Fn, []) 有什么区别吗?乍一看都一样,都是进行 new 操作、创建对象,而主要的区别就要从 Reflect.construct 的第三个参数 newTarget 说起了,涉及到存在继承关系时 new 操作的具体过程。

ES5 中不存在语法层面的类继承,所以 new Fn() 的过程相对简单,大致逻辑如下:

而在 ES6 中由于引入了类继承,这个过程就有了些变化。来看看之前的那个例子,如果想借助装饰器对一个由类创建的对象进行修饰,第一直觉可能是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function decorate(Target) {
function (...args) {
let obj = new Target(...args);
// do something with obj
obj.decorate = true;
return obj;
}
}

@decorate
class Human {
constructor(age = 28) {
this._age = age;
}

get age() {
return this._age;
}
}

装饰器生成一个新的类,其构造函数中生成一个原始 Human 类的对象,在对其进行操作后将这个对象作为新类构造函数的结果返回。如此这般,由装饰后的 Human 类生成的对象都会有一个 decorate 的属性。目前为止一切 OK,那就再添加一个继承试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
....

class Man extends Human {
constructor() {
super();
}

older() {
...
}
}

let man = new Man();

man 对象也有 decorate 属性,嗯,看上去 OK,但是… 但是 man 对象却没有 older 方法,man instanceof Man 会是无情的 false … 显然是原型的问题。回头看看那个装饰器,关键就在于产生的新对象完全是基于基类的,也就是那句 new Target(...args)TargetHuman),无论如何被继承,创建的对象都与子类没关系。那么如果能在父类的构造函数中创建一个基于子类的对象不就能解决这个问题了莫?而这正是 Reflect.construct 第三个参数的用武之地。

Reflect.construct 一共有三个参数,第一个参数 target 表示需要调用的构造函数,第二个参数 argumentsList 表示调用构造函数的参数,最后一个参数 newTarget 可选,表示发起操作的构造函数,也就是实际发起 new 操作的构造函数,在上面的那个例子中 newTarget 就是我们想要的子类。也就是说 Reflect.construct(target, argumentsList, newTarget) 可以基于 newTarget 产生一个对象再来调用 target 构造函数。

至于这个 newTarget 参数只能通过 Proxy 得到,所以实际的实现如下:

1
2
3
4
5
6
7
8
9
function decorate(target) {
return new Proxy(target, {
construct(target, argumentsList, newTarget) {
let obj = Reflect.construct(target, argumentsList, newTarget);
obj.decorate = true;
return obj;
}
});
}

最后再来看看 ES6new 操作的过程,大致如下:

相较 ES5new 过程,主要是区别对待了派生类与非派生类,不管如何复杂的继承关系,实际对象都是在执行根类的构造函数时基于 newTarget 构建的。同时也可以看出,在子类的构造函数中所有对 this 的操作都必须放到调用 super() 之后,因为在此之前 this 还未绑定,其值为 undefined