这里为什么把super拿出来单独讲一下呢?可能有人要说,super谁不会用,先看个例子,你就知道super不是那么简单:
class ParentA {
constructor() { this.id = "a"; }
foo() { console.log( "ParentA:", this.id ); }
}
class ParentB {
constructor() { this.id = "b"; }
foo() { console.log( "ParentB:", this.id ); }
}
class ChildA extends ParentA {
foo() {
super.foo();
console.log( "ChildA:", this.id );
}
}
class ChildB extends ParentB {
foo() {
super.foo();
console.log( "ChildB:", this.id );
}
}
var a = new ChildA();
a.foo();
// ParentA: a
// ChildA: a
var b = new ChildB();
b.foo();
// ParentB: b
// ChildB: b
b.foo.call( a );
// ParentB: a
// ChildB: a
前两个foo的调用没什么,但最后一个b.foo.call的调用结果比较奇怪是不是?为什么this.id是a,但是super仍然是ParentB呢?
答案就是this可以动态绑定,即使是class,因为class实质上还是原型继承的封装。但super不能动态绑定,super在class声明的时候就定了。以后无论你怎么调用带有super的方法,查找这个方法的原型链已经定了。
我们看一下规范8.1.1.3的Table16,函数环境记录的额外字段:
上面这两个字段就是和这节要讲的super与new.target相关。对[[HomeObject]]的描述可以清楚的看出用super调用的函数是绑定在[[HomeObject]]上的。那么这个所谓的[[HomeObject]]是怎么设置的呢?
我们一点一点在规范中找:
规范的14.5.14 Runtime Semantics: ClassDefinitionEvaluation:
20. Else, let methods be NonConstructorMethodDefinitions of ClassBody.
21. For each ClassElement m in order from methods
a. If IsStatic of m is false, then
i. Let status be the result of performing PropertyDefinitionEvaluation for m with arguments proto and false.
b. Else,
i. Let status be the result of performing PropertyDefinitionEvaluation for m with arguments F and false.
接下来14.3.9 Runtime Semantics: PropertyDefinitionEvaluation:
MethodDefinition : PropertyName ( StrictFormalParameters ) { FunctionBody }
1. Let methodDef be DefineMethod of MethodDefinition with argument object.
继续14.3.8 Runtime Semantics: DefineMethod:
MethodDefinition : PropertyName ( StrictFormalParameters ) { FunctionBody }
7. Perform MakeMethod(closure, object).
最后9.2.10 MakeMethod ( F, homeObject):
The abstract operation MakeMethod with arguments F and homeObject configures F as a method by performing the following steps:
3. Set the [[HomeObject]] internal slot of F to homeObject.
终于看到设置[[HomeObject]]的地方了。这个F大家有兴趣可以自己看规范往前推,F就是子类的原型,对于ChildB就是ChildB.prototype,对于ChildA就是ChildA.prototype。我们看整个过程都是在ClassDefinitionEvaluation的时候完成,也就是说解析类定义的时候就定了这个[[HomeObject]],运行时不会在有什么改变。即使我们动态绑定了this,但用super去调用父类方法的查找起点已经不再改变了。
那具体怎么查找这个super上的方法呢,很简单,就是循环调用Object.getPrototypeOf([[HomeObject]]),直到找到为止:
var homeObject = ChildB.prototype,superObject;
do{
superObject = Object.getPrototypeOf(homeObject);
var superMethod = superObject.foo;
if(superMethod){
var result = superMethod.call(a);
break;
}
else{homeObject = superObject}
}while(superObject != null);
这下弄明白了super的调用过程,所以尽量不要使用类上定义的方法去call或者apply动态绑定this了。
我们再来看看这个new.target是什么东西?先看个例子:
class Foo {
constructor() {
console.log( "Foo: ", new.target.name );
}
}
class Bar extends Foo {
constructor() {
super();
console.log( "Bar: ", new.target.name );
}
baz() {
console.log( "baz: ", new.target );
}
}
var a = new Foo();
// Foo: Foo
var b = new Bar();
// Foo: Bar
// Bar: Bar
b.baz();
// baz: undefined
其实new.target就是new一个对象时,对应的构造函数。并且无论在父类还是子类中都是一样的,就是new哪个类,就是那个类的构造函数。只有在new的时候有值,普通方法调用的时候new.target是undefined。
那么构造函数是怎么确定的呢?很简单,即:Foo.prototype.constructor,Bar.prototype.constructor。
*以上全部代码在Chrome 47下通过测试