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.prototype | employee 是什么、实例方法、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 Person | Object.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 ofEmployeewhose value is an object. Instances fromnew Employee()point at itObject.getPrototypeOf(Employee)— the prototype ofEmployeeitself, 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:
| Chain | Starts at | Handles |
|---|---|---|
| Instance chain | Employee.prototype | what employee is, instance methods, instanceof |
| Constructor chain | Employee itself | whether 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.prototypeis the blueprint. Everyemployeefromnew Employee()is built to it.Employeeitself 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 Personasks whether the product is on the parent's product lineEmployee.createasks 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 chain | Constructor chain | |
|---|---|---|
| How to write it | Employee.prototype = Object.create(Person.prototype) | Object.setPrototypeOf(Employee, Person) |
| What it touches | the Employee.prototype object | Employee itself |
| What it handles | instance methods, instanceof | static methods, static getters, Symbol.species |
| If you miss it | throws immediately | throws only on the first static call |
| How to verify | employee instanceof Person | Object.getPrototypeOf(Employee) === Person |
| Who does it in class | extends, automatically | extends, 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.