← 返回博客

extends 修的是两条原型链,不是一条

看这段代码:

class Person {
  static create() { return new this(); }
}
class Employee extends Person {}

Employee.create();   // 正常

把 extends 手写成 ES5 时,漏掉某一步,Employee.create 就没了。

为什么?先看一个大多数人没留意过的事实。

函数是「双面」的

写下一个普通函数,JS 悄悄塞了两样东西:

function Employee() {}

Employee.prototype;                 // {} —— 函数自带的中转对象,给实例用
Object.getPrototypeOf(Employee);    // Function.prototype —— 函数自己也是对象

这两个是完全无关的东西:

  • Employee.prototype —— 是 Employee 的一个属性,值是个对象。new Employee() 造出来的实例会指向它
  • Object.getPrototypeOf(Employee) —— 是 Employee 自己作为对象的原型

平时只用第一个,第二个几乎没人提。但 extends 两个都动。

于是一个类有两条链

class Employee extends Person {}
实例链(给 employee 用的)
employee ──▶ Employee.prototype ──▶ Person.prototype ──▶ Object.prototype ──▶ null

构造链(给 Employee 自己用的)
Employee ──▶ Person ──▶ Function.prototype ──▶ Object.prototype ──▶ null

两条链各管各的:

链起点管什么
实例链Employee.prototypeemployee 是什么、实例方法、instanceof
构造链Employee 自己Employee.xxx 这类静态成员能不能借到

手写 ES5 对应两行

// Link the instance chain
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;

// Link the constructor chain ← the line people forget
Object.setPrototypeOf(Employee, Person);

两行动的是两个不同的对象(Employee.prototype 这个对象,和 Employee 本身)。所以先后无关,换顺序也对。

第一条链漏了会立刻炸

const employee = new Employee();
employee.describe();   // 找不到 → 立刻发现

实例链断了,所有实例方法消失,代码当场报错。所以没人会漏。

第二条链漏了,是安静地坏

Employee.create;      // undefined
Employee.create();    // TypeError: Employee.create is not a function

不报错、不提示,new Employee() 一切照常。只有第一次调静态成员时才炸。

而大多数类根本没有静态方法 —— 所以这条链漏了能潜伏很久,直到某天有人写了个 static。

通俗版:原型管产品,构造链管工厂

  • Employee.prototype 是产品图纸。new Employee() 出来的每个 employee 都照着它长。
  • Employee 本身是个工厂。工厂也有师傅。

Object.setPrototypeOf(Employee, Person) 就是让 Employee 这个工厂拜 Person 为师。拜师之后,工厂自己手里没有的工具,就去问师傅借。

class Person {
  static create(n) { return new this(n); }   // 师傅手里的工具
  constructor(n) { this.name = n; }
}

class Employee extends Person {}   // 拜师

Employee.create('alice');            // Employee 自己没有 create,去 Person 那儿借到

区分开:

  • employee instanceof Person 问的是产品在不在师傅的产品线上
  • Employee.create 问的是工厂有没有这件工具

两码事,走的是两条链。

静态继承真有用吗:new this

上面 create 里写的是 new this,不是 new Person。这是关键。

class Person {
  static create(n) { return new this(n); }
  constructor(n) { this.name = n; }
}
class Employee extends Person {}
class Manager extends Person {}

Employee.create('alice') instanceof Employee;   // true
Manager.create('bob') instanceof Manager;   // true

一份代码,子类各造各的 —— this 是调用时那个类。

ES5 里这句特别容易写死:

Person.createHard = function (n) { return new Person(n); };   // hardcoded

结果:

Employee.createHard('alice') instanceof Employee;   // false —— 造出来的是 Person

工厂拜了师,但工具本身写死了要造 Person,借过来也没用。

最常踩的现场:继承内建类型

Array 上挂了一堆静态方法。子类想用,全靠第二条链:

class MyArray extends Array {}

MyArray.from;                    // function —— 从 Array 借到了
MyArray.from([1, 2]) instanceof MyArray;   // true
new MyArray(1, 2).map(x => x) instanceof MyArray;   // true

MyArray.from 只是「不报错」那层。map 那个更隐蔽 —— 返回什么类型由 Symbol.species 决定,而 Symbol.species 走的也是构造链。链断了,map 会悄悄退化成返回普通 Array,不报错。

ES5 手写漏掉 setPrototypeOf:

MyArray2.from;   // undefined

别拿 instanceof 验证第二条链

这是最容易混的一点:

const employee = new Employee();
employee instanceof Person;   // true

这条加不加 setPrototypeOf 都是 true —— instanceof 只看实例链。手写 ES5 时漏掉静态继承,employee instanceof Person 照样 true,所以它给不了任何信号。

要验证第二条链,得直接看:

Object.getPrototypeOf(Employee) === Person;   // true

注意写成 Employee.getPrototypeOf(...) 是错的 —— 这个方法根本不存在。因为 Object.getPrototypeOf 是 Object 的静态方法,而 Employee 的链到不了 Object:

Employee → Person → Function.prototype → Object.prototype
                                        ↑ 终点是这里,不是 Object 本身

完整手写版

function Person(name) {
  this.name = name;
}
Person.prototype.role = '人';
Person.prototype.describe = function () {
  return this.name;
};
Person.create = function (name) {          // static
  return new this(name);
};

function Employee(name) {
  Person.call(this, name);                 // super(name)
}

Employee.prototype = Object.create(Person.prototype);   // instance chain
Employee.prototype.constructor = Employee;              // restore the back-reference
Object.setPrototypeOf(Employee, Person);                // constructor chain ← this line

Employee.prototype.role = '员工';
Employee.prototype.describe = function () {             // override
  return Person.prototype.describe.call(this) + ',' + this.role;
};

const employee = Employee.create('alice');
employee instanceof Employee;      // true
employee instanceof Person;        // true
employee.describe();               // "alice,员工"

接实例链那行还有几种等价写法(setPrototypeOf 原位改、__proto__ 赋值、new f()),各自的坑和 ES 版本见 接原型链的四种写法。

小结

实例链构造链
怎么写Employee.prototype = Object.create(Person.prototype)Object.setPrototypeOf(Employee, Person)
动谁Employee.prototype 这个对象Employee 自己
管什么实例方法、instanceof静态方法、静态 getter、Symbol.species
漏了会怎样立刻报错调静态成员时才炸
怎么验证employee instanceof PersonObject.getPrototypeOf(Employee) === Person
class 里谁做extends 自动做extends 自动做

一句话:extends 不是一条链,是两条 —— 一条接产品(实例),一条认师傅(构造函数自己)。手写 ES5 时第二条最容易漏,因为漏了不报错,只在你第一次调静态方法时才炸。

Consider this:

class Person {
  static create() { return new this(); }
}
class Employee extends Person {}

Employee.create();   // works

Translate extends into ES5 by hand, miss one step, and Employee.create is gone.

Why? Start with a fact most people never notice.

Functions are two-faced

Write down a plain function and JS quietly hands you two things:

function Employee() {}

Employee.prototype;                 // {} — the relay object, used by instances
Object.getPrototypeOf(Employee);    // Function.prototype — the function is an object too

These two are completely unrelated:

  • Employee.prototype — a property of Employee whose value is an object. Instances from new Employee() point at it
  • Object.getPrototypeOf(Employee) — the prototype of Employee itself, as an object

You use the first one constantly. The second one is rarely mentioned. extends moves both.

So a class has two chains

class Employee extends Person {}
Instance chain (used by employee)
employee ──▶ Employee.prototype ──▶ Person.prototype ──▶ Object.prototype ──▶ null

Constructor chain (used by Employee itself)
Employee ──▶ Person ──▶ Function.prototype ──▶ Object.prototype ──▶ null

Each chain handles its own business:

ChainStarts atHandles
Instance chainEmployee.prototypewhat employee is, instance methods, instanceof
Constructor chainEmployee itselfwhether Employee.something can borrow a static member

Two lines of hand-written ES5

// Link the instance chain
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;

// Link the constructor chain ← the line people forget
Object.setPrototypeOf(Employee, Person);

The two lines touch two different objects (the Employee.prototype object, and Employee itself). So order does not matter — swap them and it still works.

Miss the first chain and it explodes immediately

const employee = new Employee();
employee.describe();   // not found → you find out right away

Break the instance chain and every instance method disappears, so the code throws on the spot. Nobody forgets this one.

Miss the second chain and it fails quietly

Employee.create;      // undefined
Employee.create();    // TypeError: Employee.create is not a function

No error, no warning, and new Employee() works perfectly. It only blows up the first time you call a static member.

And most classes have no static methods at all — so this can sit broken for a long time, until the day someone writes a static.

The plain-language version: prototypes make products, the constructor chain finds a parent

  • Employee.prototype is the blueprint. Every employee from new Employee() is built to it.
  • Employee itself is a factory. And factories can have a parent.

Object.setPrototypeOf(Employee, Person) makes the Employee factory a child of the Person factory. After that, any tool the factory does not hold itself, it borrows from its parent.

class Person {
  static create(n) { return new this(n); }   // a tool the parent holds
  constructor(n) { this.name = n; }
}

class Employee extends Person {}   // now a child factory

Employee.create('alice');            // Employee has no create — it borrows Person's

Keep the two apart:

  • employee instanceof Person asks whether the product is on the parent's product line
  • Employee.create asks whether the factory holds that tool

Different questions, different chains.

Does static inheritance ever actually matter: new this

That create above uses new this, not new Person. That is the whole point.

class Person {
  static create(n) { return new this(n); }
  constructor(n) { this.name = n; }
}
class Employee extends Person {}
class Manager extends Person {}

Employee.create('alice') instanceof Employee;   // true
Manager.create('bob') instanceof Manager;   // true

One definition, each subclass builds its own. this is whichever class was called.

In ES5 this is very easy to hardcode:

Person.createHard = function (n) { return new Person(n); };   // hardcoded

And then:

Employee.createHard('alice') instanceof Employee;   // false — it built an Person

The factory joined the family, but the tool itself is hardcoded to build an Person, so borrowing it buys you nothing.

Where you actually hit this: subclassing built-ins

Array carries a pile of static methods. A subclass gets them only through the second chain:

class MyArray extends Array {}

MyArray.from;                    // function — borrowed from Array
MyArray.from([1, 2]) instanceof MyArray;   // true
new MyArray(1, 2).map(x => x) instanceof MyArray;   // true

MyArray.from is only the "throws or not" layer. The map case is sneakier — the returned type is decided by Symbol.species, which also travels the constructor chain. Break the chain and map silently degrades to returning a plain Array, with no error at all.

Hand-written ES5 without setPrototypeOf:

MyArray2.from;   // undefined

Do not use instanceof to check the second chain

This is the easiest thing to get backwards:

const employee = new Employee();
employee instanceof Person;   // true

That is true with or without setPrototypeOf — instanceof only walks the instance chain. Hand-write ES5, skip static inheritance, and employee instanceof Person is still true. So it gives you no signal whatsoever.

To check the second chain, look at it directly:

Object.getPrototypeOf(Employee) === Person;   // true

Note that Employee.getPrototypeOf(...) is wrong — that method does not exist. Object.getPrototypeOf is a static method of Object, and Employee's chain never reaches Object:

Employee → Person → Function.prototype → Object.prototype
                                        ↑ the chain ends here, not at Object itself

The complete hand-written version

function Person(name) {
  this.name = name;
}
Person.prototype.role = 'person';
Person.prototype.describe = function () {
  return this.name;
};
Person.create = function (name) {          // static
  return new this(name);
};

function Employee(name) {
  Person.call(this, name);                 // super(name)
}

Employee.prototype = Object.create(Person.prototype);   // instance chain
Employee.prototype.constructor = Employee;              // restore the back-reference
Object.setPrototypeOf(Employee, Person);                // constructor chain ← this line

Employee.prototype.role = 'employee';
Employee.prototype.describe = function () {             // override
  return Person.prototype.describe.call(this) + ', ' + this.role;
};

const employee = Employee.create('alice');
employee instanceof Employee;      // true
employee instanceof Person;        // true
employee.describe();               // "alice, employee"

That instance-chain line has several equivalent spellings (setPrototypeOf in place, a __proto__ assignment, new f()) — their traps and ES versions are covered in Four Ways to Wire a Prototype Chain.

Summary

Instance chainConstructor chain
How to write itEmployee.prototype = Object.create(Person.prototype)Object.setPrototypeOf(Employee, Person)
What it touchesthe Employee.prototype objectEmployee itself
What it handlesinstance methods, instanceofstatic methods, static getters, Symbol.species
If you miss itthrows immediatelythrows only on the first static call
How to verifyemployee instanceof PersonObject.getPrototypeOf(Employee) === Person
Who does it in classextends, automaticallyextends, automatically

One line: extends is not one chain but two — one for the products (instances), one that finds the factory a parent (the constructor itself). The second is the one hand-written ES5 forgets, because forgetting it throws no error until the first time you call a static method.