什么是 super ?
了解 ES6 class 的 JSer 们或多或少关注过 super
关键字。它的使用场景很简单,当存在类继承的时候,在子类构造函数中调用父类构造器,或者在子类的方法中调用被覆盖的父类同名方法。这个语义和传统的 class-based
的语言是一致的。
class Base {
constructor(){ this.a = 1;}
doSomething() { console.log(this.a);}
}
class A extends Base {
constructor() { super(); }
doSomething() {
console.log('2');
super.doSomething();
}
}
super
有两种用法:
- 在子类构造函数中直接
super()
调用父类构造器,且在子类构造器中出现 this 引用之前必须先调用super()
; - 在方法中使用
super.XXX
调用父类方法/属性
你可能不知道的 super
也许你会说,很简单明了啊,你还能讲出什么五彩斑斓的 super
吗?
你忘了,这可是 JS,class
什么的都是幻影,真正让 super
干活的是 prototype
,所以,你猜下面的代码会不会报错?不报错结果会是什么?
super
挑战
1. 给 prototype 扩充函数?
class A { a(){ return 1;} }
class B extends A{}
B.prototype.a = function(){ return super.a();}
console.log(new B().a());
2. 为什么一定要 class ?
const a = { a(){return 1;} }
const b = {
// or Object.setPrototypeOf(b, a);
__proto__: a,
a(){return super.a();}
}
console.log(b.a());
3. 听说 this
是动态绑定?
class A1{ a(){return 1;} }
class A2{ a(){return 2;} }
class B1 extends A1 {
getSuper(){ return super.a(); }
}
class B2 extends A2 {}
let b1 = new B1();
let b2 = new B2();
// b2:借你们家醋用一下,今天吃饺子
b2.getSuper = b1.getSuper;
console.log(b2.getSuper());
4. 【对象篇】听说 this
是动态绑定?
const a1 = {value: 1};
const a2 = {value: 2};
const b1 = {
__proto__: a1,
getSuper(){ return super.value;}
};
console.log(b1.getSuper());
const b2 = {
__proto__: a12,
getSuper: b1.getSuper
};
console.log(b2.getSuper());
Object.setPrototypeOf(b1, a2);
const getSuper = b1.getSuper;
// 天呐,脱离对象单独运行,
// `this` 你学着点
console.log(getSuper());
5. super 也可以当左值?
class A {}
A.prototype.v = 1;
class B extends A{
test() {
console.log(super.v);
super.v = 2;
console.log(super.v);
}
}
super 的隐藏特性
何处可以使用 super
关键字
super
除了可以在 class 定义的构造函数或者方法中使用外,还可以在对象字面量的方法定义中使用。因此
const a = { b(){ return super.b;}}
在语法上是没有问题的。但是
A.prototype.m = function(){ return super.x;}
b = {a: () => super.x};
c = {a: function(){ return super.x;}};
这些都是有问题的,本质上他们都是函数声明上下文, 等价于:
m = function(){ return super.x;};
A.prototype.m = m;
a = () => super.x;
b = {a: a};
a = function(){ return super.x;}
c = {a: a};
但是,下面这个是没有语法问题的:
class A{
b() {
const c = () => super.x;
return c;
}
}
和 this
一样,箭头函数内的 super
是引用的外部词法域的,所以只要外部域中 super
存在则箭头函数内的 super
引用即合法。
super
到底是什么
在前端的道路上,你一定和 this
指向问题打过交道,习惯了飘忽不定的 this
的 JSer 们,对固定的 super
绑定反而觉得陌生和不可思议。super
看起来像语法糖,却不仅仅是语法糖,Desugar
后的 super.xxx
近乎等价于
super.xxx
// vs
Object.getPrototypeOf(this).xxx
区别有二:
- 比起
this
,super
不存在动态绑定问题 - 做左值的时候存在特殊处理
静态绑定
super
是在方法定义时进行语义静态绑定的,这个方法最终是如何被调用的,是不会影响它的引用值的,甚至方法可以脱离原对象运行,super
一样可以拿到正确的值。
class A{ a(){return 1;} }
class B extends A {
getSuper(){ return super.a(); }
}
let b = new B();
let s = b.getSuper;
console.log(s());
getSuper
在初始化的时候就绑定了 super
的值,因此无论这个函数引用最终如何转辗,内部的 super
永远不变初心。
拨云见日
要想深入理解 super
还得研读 ECMA 规范。我们知道 JS 中存在作用域链的概念,内部函数可以引用外部作用域的变量,甚至还能形成闭包。在 ES6 Spec 中,这个是通过函数对象的槽(slot)来实现的,你可以想象成引擎内部使用的私有字段。F.[[Environment]]
这个槽存放了函数对象的外部环境引用,当 F 被调用的时候,新的词法环境会被创建,而 F.[[Environment]]
会被赋值给内部词法环境的 outer 引用,以供内部查找外部的变量之用。
相似的,函数有一个槽叫 [[HomeObject]]
, 保存的是函数作为方法定义时的主对象。
let a = {
test(){return super.x;},
notAMethod: function(){}
};
// !!! 伪代码,引擎没有提供方法获取方法的 [[HomeObject]]
a.test.[[HomeObject]] === a;
a.notAMethod.[[HomeObject]] !== a;
这两种写法是有区别的,不单单是简写而已,前者是定义方法,后者是定义一个属性值为一个函数,而只有方法才有[[HomeObject]]
,普通函数是没有的。
对于 class
的场景,方法的 [[HomeObject]]
即为 class.prototype
:
class A{
test(){ return super.x;}
}
var a = new A();
//伪代码,js 无法取到 [[HomeObject]]
a.test.[[HomeObject]] === A.prototype
不像 ThisBinding,call/apply 甚至把方法挂到其他对象下调用就能改变 this
,[[HomeObject]]
在方法定义时一次设置,再无它法修改。这也就解释了为什么 super
既可以在 class 定义中使用,也可以在对象字面量中使用了。同时,由于 [[HomeObject]]
无法修改,super
就成了静态绑定。
某种意义上来说,通过暴露[[HomeObject]]
引用到 JavaScript 用户端,JS 也可以有静态绑定的 this
用的。当然出于兼容性考虑,我们不可能贸然改变 this
的语义。
最后的揭秘
所以 super.xx
的真正内部解释为:
Object.getPrototypeOf([[HomeObject]]).xx
吗?不对,还差一点。
super.xx
作为"写"语义的时候,也许是出于保护原型的考虑,其实真正的接收者是 this.xx
。再次请出规范,ES6 规范中把 super.xx
定义为 super reference
这是一种特殊的引用类型,其 [[get]] 语义是 Object.getPrototypeOf([[HomeObject]]).xx
,而 [[set]] 语义是 this.xx = newValue
;
a = {test(n){super.x=n;}}
b = {test: a.test};
b.test(1);
console.log(a.x);
console.log(b.x);
以上就是关于 super
的全部内容了。