超级深入理解 JS 中的 super: A super deep dive into JS super

4,436 阅读6分钟

什么是 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 A1a(){return 1;} }
class A2a(){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 = {afunction(){ 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

区别有二:

  • 比起 thissuper 不存在动态绑定问题
  • 做左值的时候存在特殊处理

静态绑定

super 是在方法定义时进行语义静态绑定的,这个方法最终是如何被调用的,是不会影响它的引用值的,甚至方法可以脱离原对象运行,super 一样可以拿到正确的值。

class Aa(){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 的全部内容了。