为什么 super() 之前不能用 this?聊聊 JS 继承里这个反直觉的设计
前言
写 React 或者 Vue 的 class 组件时,你一定见过这样的代码:
js
class Dog extends Animal {
constructor(name, breed) {
super(name); // 必须先调用
this.breed = breed; // 之后才能用 this
}
}
如果你手滑把 this.breed = breed 写到 super() 前面,控制台会毫不留情地给你一巴掌:
text
ReferenceError: Must call super constructor in derived class
before accessing 'this' or returning from derived constructor
第一次遇到这个问题时,我心想: "this 不就是一个对象吗?我自己造一个不行吗?凭啥非得让父类先造?"
直到我搞懂了背后的语言设计,才发现这个看似反直觉的规定,其实非常合理。
这篇文章就来聊聊:为什么在继承中,this 必须由父类创建,super() 到底做了什么。
一、先看现象
先看不继承的情况:
js
class Animal {
constructor(name) {
// 这里 this 已经存在了
this.name = name;
}
}
const a = new Animal('猫');
console.log(a.name); // '猫'
没毛病,this 直接用。
再看继承的情况:
js
class Dog extends Animal {
constructor(name, breed) {
super(name); // 必须先调
this.breed = breed;
}
}
如果你把 this.breed = breed 挪到前面:
js
class Dog extends Animal {
constructor(name, breed) {
this.breed = breed; // ❌ ReferenceError
super(name);
}
}
直接报错。
问题来了:同样是构造函数,为什么继承的时候 this 就不能提前用?
二、真相:继承时,this 是 super() 返回的
这里有个很多人没注意到的细节:
在不继承的类里,
this是new的时候由 JS 引擎创建的。
在继承的类里,this是super()调用父类构造函数后,由父类创建并返回的。
也就是说,this 不是凭空存在的,它是 super() 的返回值。
super(name) 内部大致做了:
- 找到父类
Animal - 调用父类的构造函数,把
this的创建权交给它 - 父类构造函数里创建出
this - 把
this返回给子类构造函数 - 从这一刻起,子类构造函数里才有
this
所以在 super() 之前,this 处于未初始化状态,你访问它就报错。
三、为什么不让子类自己造 this?
这是最核心的问题。答案是:因为子类不知道父类需要什么样的对象。
例子1:内置类 Array
js
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
const arr = new MyArray(1, 2, 3);
console.log(arr); // [1, 2, 3]
console.log(arr instanceof MyArray); // true
console.log(Array.isArray(arr)); // true
Array 创建出来的不是普通对象,而是一个真正的数组对象,带有特殊的内部结构(length、索引、迭代器等)。
如果子类自己造一个 {}:
js
this = {}; // 普通对象,没有数组的内部结构
那它就不是数组了,arr[0]、arr.length 全都不正常。
只有 Array 自己知道怎么造一个数组,所以必须让 Array 来造。
例子2:内置类 Error
js
class MyError extends Error {
constructor(msg) {
super(msg);
}
}
const e = new MyError('出错了');
console.log(e.message); // '出错了'
console.log(e instanceof Error); // true
Error 的实例带有栈信息(stack)等特殊结构,子类造不出来。
例子3:父类构造函数可能返回完全不同的对象
js
class Animal {
constructor() {
return { custom: true }; // 父类返回一个完全不同的对象
}
}
class Dog extends Animal {
constructor() {
super(); // this 就是这个 { custom: true }
}
}
如果子类自己造了 this,父类又返回了别的对象,到底用哪个?会彻底混乱。
所以规范规定:父类说了算,父类返回什么,this 就是什么。
四、规范里怎么说
在 ECMAScript 规范中:
- 每个函数都有一个内部方法
[[Construct]] - 基类的
[[Construct]]:创建一个新对象作为this,然后执行构造函数体 - 派生类的
[[Construct]]:不创建对象,执行构造函数体,等super()来创建
派生类在 super() 之前,this 处于 TDZ(暂时性死区) 类似的状态,一访问就报错。
text
ReferenceError: Must call super constructor in derived class
before accessing 'this' or returning from derived constructor
注意后半句:or returning from derived constructor。
意思是:如果你在构造函数里既不调 super(),还想直接 return,也不行:
js
class Dog extends Animal {
constructor() {
// 没调 super(),也没返回对象
return; // ❌ ReferenceError
}
}
除非你返回一个对象:
js
class Dog extends Animal {
constructor() {
return { custom: true }; // ✅ 合法,直接返回对象,不依赖 this
}
}
五、为什么这么设计?三个理由
理由1:父类可能创建特殊对象
Array、Error、Map、Set 这些内置类的实例都有特殊内部槽位,只有它们自己能创建。子类若自己造 this,就会破坏这些内部结构。
理由2:语义更清晰
"子类是基于父类的扩展"——那就应该先有父类的部分,再有子类的部分。
js
constructor(name, breed) {
super(name); // 先建父类的部分:this.name
this.breed = breed; // 再加子类自己的部分
}
理由3:父类构造函数不一定返回标准对象
父类构造函数可以 return 一个任意对象,此时 this 就是那个对象。如果子类提前造了 this,就会冲突。
六、一个类比帮助记忆
想象盖房子:
- 父类 = 地基施工队
- 子类 = 装修队
你不能在打好地基之前就开始装修。super() 就是让地基施工队先干活,干完把房子(this)交给你,你才能装修。
如果地基还没打,你就想装修,那装修什么呢?没有房子可装。
七、常见疑问
Q1:不写 constructor 会怎样?
不写的话,JS 默认帮你加:
js
class Dog extends Animal {
// 等价于:
// constructor(...args) {
// super(...args);
// }
}
所以默认情况下 super() 是自动调用的,你就不用操心。只有你自己写了 constructor,才需要手动写 super()。
Q2:super() 可以放在构造函数中间的任意位置吗?
可以,但 必须在所有使用 this 的语句之前:
js
class Dog extends Animal {
constructor(name, breed) {
console.log('准备创建'); // ✅ 用不到 this,可以在 super 前
super(name);
this.breed = breed; // ✅ super 之后
}
}
Q3:为什么箭头函数里的 this 不受影响?
箭头函数没有自己的 this,它用的是外层的 this。所以在构造函数里的箭头函数,只要等到被调用时 super() 已经执行过了,就没问题。
Q4:不在构造函数里的方法能用 this 吗?
可以。方法(如 speak())是在实例已经创建完之后调用的,那时 this 早就存在了。只有构造函数里才有这个限制。
八、一张表总结
| 情况 | this 谁创建 | 能否在 super() 前用 this |
|---|---|---|
| 不继承 | JS 引擎创建 | 没有 super(),直接用 |
| 继承 | 父类构造函数创建 | ❌ 不能,必须先 super() |
九、核心记忆点
不继承时,
this由引擎造。
继承时,this由父类造,super()就是"领取"这个this的仪式。
领回来之前,你没资格用this。
之所以这么设计,是因为:
- 父类可能创建特殊对象(如
Array、Error) - 父类构造函数可能返回不同对象
- 语义上,"先有父类部分,再加子类部分"更清晰
结语
super() 必须在 this 之前调用,这个规定刚开始看起来有点反直觉,但理解了"this 是父类创建的"这一点之后,一切都顺理成章了。
下次再看到这个报错,不要急着 Google,回想一下:
"我还没从父类那儿领到
this呢。"
如果这篇文章帮你搞懂了这个知识点,欢迎点赞收藏,也欢迎在评论区聊聊你踩过的 class 继承的坑。
关注我,一起把 JS 的边角知识啃透。