为什么 super() 之前不能用 this?聊聊 JS 继承里这个反直觉的设计

2 阅读6分钟

为什么 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) 内部大致做了:

  1. 找到父类 Animal
  2. 调用父类的构造函数,把 this 的创建权交给它
  3. 父类构造函数里创建出 this
  4. 把 this 返回给子类构造函数
  5. 从这一刻起,子类构造函数里才有 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。

之所以这么设计,是因为:

  1. 父类可能创建特殊对象(如 Array、Error)
  2. 父类构造函数可能返回不同对象
  3. 语义上,"先有父类部分,再加子类部分"更清晰

结语

super() 必须在 this 之前调用,这个规定刚开始看起来有点反直觉,但理解了"this 是父类创建的"这一点之后,一切都顺理成章了。

下次再看到这个报错,不要急着 Google,回想一下:

"我还没从父类那儿领到 this 呢。"


如果这篇文章帮你搞懂了这个知识点,欢迎点赞收藏,也欢迎在评论区聊聊你踩过的 class 继承的坑。

关注我,一起把 JS 的边角知识啃透。