接原型链的四种写法:分水岭是「替换」还是「原位改」
看这段代码:
function Person(name) {
this.name = name;
}
Person.prototype.role = '人';
function Employee(name, salary) {
this.salary = salary;
Person.call(this, name);
}
// ← 这里写什么?
Employee.prototype.role = '员工';
Employee.prototype.describe = function () {
console.log(`${this.name},薪资 ${this.salary}`);
};
空白处有四种填法,四种都能跑通。选哪个?
先分清:这里接的是哪条链
四种写法接的都是实例链。构造链是另一句 —— 而且用的是同一个 setPrototypeOf:
Object.setPrototypeOf(Employee.prototype, Person.prototype); // 实例链
Object.setPrototypeOf(Employee, Person); // 构造链
同一个函数,目标不同,作用完全不同。名字撞车是很多人绕不过来的原因。这篇只讲实例链,构造链那条见 extends 修的是两条原型链。
归成两类就清楚了
四种写法做的事,只有两类:
| 类别 | 动作 | 关键后果 |
|---|---|---|
| 原位改 | 不换对象,只改已有 Employee.prototype 的 [[Prototype]] 指针 | constructor 保住,顺序无关 |
| 替换 | 造一个新对象,赋给 Employee.prototype | 旧的连同 constructor 一起丢,顺序敏感 |
constructor 丢不丢、顺序敏不敏感,全都由这一条决定。ES 版本也是分开的。
原位改(两类)
Object.setPrototypeOf — ES6
Object.setPrototypeOf(Employee.prototype, Person.prototype);
动作:造任何东西都不造,直接改已有 Employee.prototype 对象的指针。
对象没被换掉,函数自带的 constructor: Employee 原封不动保留,不用补。意图最直白,现代首选。
ES6(ES2015)才进规范。ES5 环境里没有它 —— 这点下面还会回来。
Employee.prototype.__proto__ = … — 非标准 → ES6 Annex B
Employee.prototype.__proto__ = Person.prototype;
和上面是同一个操作,只是换了个入口。__proto__ 是 Object.prototype 上的一个 accessor(configurable: true,有 get/set),内部读写的就是 [[Prototype]]。
同样是原位改,同样保住 constructor,顺序同样无所谓。
规范地位有讲究:ES6 之前它不在任何规范里,是各引擎的事实标准;ES6 把它收进了 Annex B —— 规范性附录,只对 Web 浏览器宿主有规定。所以非浏览器环境(老版本 Node、嵌入式 JS 引擎)不保证有。
但它的问题不是"各浏览器不一致" —— 现代引擎都支持。真正的坑在下面那节。
替换(两类)
Object.create — ES5
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee; // 必须补
动作:造一个新空对象,原型指向 Person.prototype,赋给 Employee.prototype。
旧对象被整个换掉,自带的 constructor: Employee 随之丢失,新对象沿链摸到 Person.prototype.constructor = Person。所以第二行必须写。
ES5 起可用,最常用,也是面试标准答案。
new f() — ES3 起(上古 polyfill)
var f = function () {};
f.prototype = Person.prototype;
Employee.prototype = new f();
动作:造一个空函数 f,把 f.prototype 指向 Person.prototype,再 new f() —— 产出的对象原型就是 Person.prototype。
等价于 Object.create(Person.prototype),在 Object.create 普及之前就是它的 polyfill。
也是替换型,同样要补 constructor,同样顺序敏感。现在没有理由新写。
ES 版本总览
| 写法 | 类别 | 规范地位 |
|---|---|---|
new f() | 替换 | ES3 起,任何版本都有 |
Object.create | 替换 | ES5 |
Object.setPrototypeOf | 原位改 | ES6 |
Employee.prototype.__proto__ = | 原位改 | 非标准事实标准 → ES6 Annex B |
表格只收这四种手写写法。class / extends 不在其中 —— 它属于另一层,见下面「构造链」那节。
ES5 里想"原位改",实际只有 __proto__ 一条路 —— 而它当时不在规范里。这是个真实的历史取舍:要么用一个非标准特性,要么用 Object.create 走替换型、承担 constructor 和顺序的代价。
实测对比
四种写法对同一个 Employee 做同样的事:
| 写法 | 动作 | 指针接上了 | constructor | 顺序敏感 | 会执行函数 |
|---|---|---|---|---|---|
Object.setPrototypeOf | 原位改 | ✓ | Employee ✓ | 否 | 否 |
__proto__ = | 原位改 | ✓ | Employee ✓ | 否 | 否 |
Object.create | 替换 | ✓ | Person ❌ | 是 | 否 |
new f() | 替换 | ✓ | Person ❌ | 是 | 是 |
四种都"继承成功"了 —— instanceof 全对,方法全能调。唯一当场可见的差别是 constructor。
坑一:替换型丢 constructor
replace 之后,补之前: Employee.prototype.constructor === Person
补 constructor 之后: Employee.prototype.constructor === Employee
constructor 被 instanceof 之外的代码依赖(克隆、this.constructor.name、工厂里 new this.constructor())。丢了不一定马上报错。
原位改没有这个问题。
坑二:替换型对顺序敏感
替换会把已经写在旧对象上的东西全丢掉:
function Employee5(name, salary) { this.salary = salary; Person.call(this, name); }
Employee5.prototype.role = '员工';
Employee5.prototype.describe = function () { return this.role; };
Employee5.prototype = Object.create(Person.prototype); // ← 晚了一步
Employee5.prototype.describe; // undefined —— 方法没了
Employee5.prototype.role; // '人' —— 退回父类的值
正确顺序是先接线、后挂方法:
Employee5.prototype = Object.create(Person.prototype); // 先接线
Employee5.prototype.constructor = Employee5;
Employee5.prototype.role = '员工'; // 后挂方法
Employee5.prototype.describe = function () { return this.role; };
原位改随便什么顺序都对。
坑三:new f() 真的会执行构造函数
f 是空的所以看起来没事。但只要 f 返回一个对象,new 就会用那个返回值 —— 继承链悄悄接错:
var g = function () { return { hijacked: true }; };
g.prototype = Person.prototype;
Employee4.prototype = new g();
Object.getPrototypeOf(Employee4.prototype) === Person.prototype; // false
Employee4.prototype; // { hijacked: true }
Employee4.prototype 变成 { hijacked: true },跟 Person 半点关系没有。不报错,直到你调 d.describe() 发现是 undefined。
Object.create 不执行任何函数,没这个可能。
__proto__ 特有的三个坑
既然它是"ES5 里唯一的原位改",值得单独说清楚为什么不推荐。
一、它不是自有属性
__proto__ 只是 Object.prototype 上的一个 accessor。链上没有 Object.prototype 时,它就是个普通属性名:
const o = Object.create(null);
o.__proto__ = { a: 1 };
Object.getPrototypeOf(o); // null ← 原型没被设置!
Object.getOwnPropertyNames(o); // ['__proto__'] ← 变成普通自有属性了
静默失败,不报错。同理,任何人 delete Object.prototype.__proto__(不推荐但合法)之后,全场的 __proto__ 写法一起失效。
二、对象字面量里它是特例
const lit = { __proto__: { a: 1 } }; // 设置原型
const computed = { ['__proto__']: { a: 1 } }; // 创建普通自有属性
Object.getPrototypeOf(lit); // { a: 1 } —— 原型被改了
Object.getOwnPropertyNames(lit); // []
Object.getOwnPropertyNames(computed); // ['__proto__'] —— 就是个普通属性
Object.getPrototypeOf(computed) === Object.prototype; // true
{ __proto__: x } 是语法特例(规范单独特判),而 ['__proto__'] 计算属性名不是。同一个"键",一个改原型一个建属性 —— 看代码很容易看漏。
三、规范地位
如上:ES6 才进 Annex B,只对 Web 宿主有规定。加上它是遗留特性,ESLint 的 no-proto 专门禁它。
所以不推荐的理由不是兼容性,是:会在 null 原型对象上静默失效、字面量里有反直觉特例、规范地位只覆盖浏览器。 同样的操作,Object.setPrototypeOf(ES6)没有这三个问题。
什么时候用哪个
- ES6+ 新写继承链 →
class extends,它两条链都管 - 必须手写、且要最小副作用 →
Object.setPrototypeOf(Employee.prototype, Person.prototype)。原位改,不用重排顺序、不用补constructor - ES5 环境 →
Employee.prototype = Object.create(Person.prototype)加补constructor。setPrototypeOf不可用,__proto__非标准 - 读老代码 / polyfill → 认识
new f()和__proto__ =就行,别新写
性能上有个常被提起的点:改已有对象的 [[Prototype]] 会让引擎放弃针对该对象的优化。但原型链初始化时设置一次就完事,无所谓 —— 真正要避免的是在循环里对实例改原型。
四种手写写法都漏掉的:构造链
上面四种只接了实例链,Object.getPrototypeOf(Employee) 都还是 Function.prototype:
Employee.create; // undefined —— 父类的静态方法一个都拿不到
想完整等价 class extends,得再写一句(setPrototypeOf 的另一个用法):
Object.setPrototypeOf(Employee, Person); // 构造链
Employee.create('小明') instanceof Employee; // true
class extends 之所以省事,恰恰因为它不属于上面任何一类 —— 它和那四种不同层:那四种是"你手动接一条链",它是"语言两条链一起接",你一行都不用写。
class Employee extends Person {
constructor(name, salary) {
super(name); // = Person.call(this, name)
this.salary = salary;
}
describe() { /* … */ }
}
ES6 起可用,现代代码直接用这个。上面四种只在读老代码或纯 ES5 环境里才需要。
完整版
ES6+ 环境:
class Employee extends Person {
constructor(name, salary) {
super(name);
this.salary = salary;
}
describe() { /* … */ }
}
必须手写时(构造链那行需要 ES6):
function Person(name) {
this.name = name;
}
Person.prototype.role = '人';
Person.create = function (name) { // static
return new this(name);
};
function Employee(name, salary) {
this.salary = salary;
Person.call(this, name); // super(name)
}
// instance chain — pick one
Object.setPrototypeOf(Employee.prototype, Person.prototype); // ES6, in place
// Employee.prototype = Object.create(Person.prototype); // ES5, replace
// Employee.prototype.constructor = Employee;
// constructor chain — the line everyone forgets (ES6)
Object.setPrototypeOf(Employee, Person);
Employee.prototype.role = '员工'; // shadows Person.prototype.role
Employee.prototype.describe = function () {
console.log(`${this.name},薪资 ${this.salary}`);
};
const d1 = new Employee('小明', 8000);
d1 instanceof Employee; // true
d1 instanceof Person; // true
Employee.create('小明') instanceof Employee; // true ← 靠第二句
纯 ES5 环境(没有 setPrototypeOf,构造链只能缺席或用 __proto__ 顶替):
Employee.prototype = Object.create(Person.prototype); // 替换型
Employee.prototype.constructor = Employee; // 必须补
// 顺序必须在这里之后才挂 role / describe
小结
Object.setPrototypeOf | __proto__ = | Object.create | new f() | |
|---|---|---|---|---|
| 类别 | 原位改 | 原位改 | 替换 | 替换 |
| ES 版本 | ES6 | 非标准 → ES6 Annex B | ES5 | ES3 |
constructor | 保留 | 保留 | 丢,要补 | 丢,要补 |
| 顺序 | 无所谓 | 无所谓 | 先接线 | 先接线 |
| 会执行函数 | 否 | 否 | 否 | 是 |
| 额外坑 | 无 | null 原型上失效、字面量特例、规范只覆盖浏览器 | — | f 返回值劫持结果 |
| 现状 | 手写首选 | 别用 | ES5 环境用 | 淘汰 |
一句话:四种写法归成两类就够了 —— 原位改(setPrototypeOf / __proto__ =)保住 constructor 且顺序无关,替换(Object.create / new f())正好相反;ES5 里想要原位改只有非标准的 __proto__ 一条路,setPrototypeOf 要等到 ES6;而四种全都只接实例链,构造链永远要另写一句 Object.setPrototypeOf(Employee, Person),这也是 class extends 唯一真正省事的地方。
Consider this:
function Person(name) {
this.name = name;
}
Person.prototype.role = 'person';
function Employee(name, salary) {
this.salary = salary;
Person.call(this, name);
}
// ← what goes here?
Employee.prototype.role = 'employee';
Employee.prototype.describe = function () {
console.log(`${this.name}, salary ${this.salary}`);
};
There are four things you could put in the blank. All four work. Which one?
First: which chain are we wiring?
All four wire the instance chain. The constructor chain is a separate line — and it uses the same setPrototypeOf:
Object.setPrototypeOf(Employee.prototype, Person.prototype); // instance chain
Object.setPrototypeOf(Employee, Person); // constructor chain
Same function, different target, completely different effect. The name collision is exactly why this trips people up. This post covers the instance chain only — for the constructor chain, see extends Rewires Two Prototype Chains.
Two groups make it click
The four options do only two kinds of things:
| Group | Action | Key consequence |
|---|---|---|
| Mutate | keep the object, move its [[Prototype]] pointer | constructor survives, order does not matter |
| Replace | build a new object, assign it to Employee.prototype | the old one goes, constructor with it, order matters |
Whether constructor survives, and whether attaching order matters, follows from this one distinction. The ES versions split the same way.
Mutate (two options)
Object.setPrototypeOf — ES6
Object.setPrototypeOf(Employee.prototype, Person.prototype);
What it does: builds nothing. It changes the pointer of the existing Employee.prototype object.
The object is never swapped, so the free constructor: Employee survives untouched. Nothing to repair. Clearest intent, the modern first choice.
Only standardised in ES6 (ES2015). ES5 has no such method — more on that below.
Employee.prototype.__proto__ = … — non-standard → ES6 Annex B
Employee.prototype.__proto__ = Person.prototype;
The same operation, different entry point. __proto__ is an accessor on Object.prototype (configurable: true, with a getter and setter) that reads and writes [[Prototype]].
Also a mutate, so it also preserves constructor and also does not care about order.
Its standing in the spec is odd: before ES6 it was in no specification at all, just a de facto convention every engine implemented. ES6 folded it into Annex B — a normative annex that only applies to web browser hosts. So non-browser hosts (older Node, embedded engines) are not required to have it.
But its problem is not "browsers disagree" — modern engines all support it. The real traps are in the section below.
Replace (two options)
Object.create — ES5
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee; // required
What it does: creates a new empty object whose prototype is Person.prototype, then assigns it to Employee.prototype.
The old object is swapped out entirely, taking the free constructor: Employee with it — the new object resolves constructor up the chain to Person.prototype.constructor, i.e. Person. Hence the required second line.
Available since ES5. Most common, and the standard interview answer.
new f() — ES3 (the ancient polyfill)
var f = function () {};
f.prototype = Person.prototype;
Employee.prototype = new f();
What it does: creates an empty function f, points f.prototype at Person.prototype, then does new f() — producing an object whose prototype is Person.prototype.
Equivalent to Object.create(Person.prototype). Before Object.create was widely available, this was the polyfill.
Also a replace, so it also needs the constructor repair and is also order-sensitive. There is no reason to write it today.
ES version overview
| Option | Group | Standing |
|---|---|---|
new f() | replace | since ES3, available everywhere |
Object.create | replace | ES5 |
Object.setPrototypeOf | mutate | ES6 |
Employee.prototype.__proto__ = | mutate | de facto → ES6 Annex B |
The table covers the four hand-written options only. class / extends is not one of them — it sits at a different level, covered in the constructor-chain section below.
In ES5, mutating in place meant __proto__ or nothing — and at the time it was not in any spec. That was a real historical trade-off: use a non-standard feature, or use Object.create and accept the constructor and ordering costs.
Measured comparison
All four do the same thing to the same Employee:
| Option | Group | Pointer wired | constructor | Order-sensitive | Runs a function |
|---|---|---|---|---|---|
Object.setPrototypeOf | mutate | ✓ | Employee ✓ | No | No |
__proto__ = | mutate | ✓ | Employee ✓ | No | No |
Object.create | replace | ✓ | Person ❌ | Yes | No |
new f() | replace | ✓ | Person ❌ | Yes | Yes |
All four "inherit successfully" — instanceof is correct across the board and every method is callable. The only difference visible on the spot is constructor.
Trap 1: replace loses constructor
after replace, before repair: Employee.prototype.constructor === Person
after repairing constructor: Employee.prototype.constructor === Employee
Code beyond instanceof depends on constructor — cloning, this.constructor.name, new this.constructor() in factories. Losing it does not necessarily throw right away.
Mutating in place does not have this problem.
Trap 2: replace is order-sensitive
A replace throws away everything already written on the old object:
function Employee5(name, salary) { this.salary = salary; Person.call(this, name); }
Employee5.prototype.role = 'employee';
Employee5.prototype.describe = function () { return this.role; };
Employee5.prototype = Object.create(Person.prototype); // ← one step too late
Employee5.prototype.describe; // undefined — the method is gone
Employee5.prototype.role; // 'person' — falls back to the parent's value
The correct order is wire first, then attach:
Employee5.prototype = Object.create(Person.prototype); // wire first
Employee5.prototype.constructor = Employee5;
Employee5.prototype.role = 'employee'; // attach after
Employee5.prototype.describe = function () { return this.role; };
Mutating in place works in any order.
Trap 3: new f() really does run a constructor
f is empty, so it looks harmless. But if f returns an object, new uses that return value instead — and the chain silently wires wrong:
var g = function () { return { hijacked: true }; };
g.prototype = Person.prototype;
Employee4.prototype = new g();
Object.getPrototypeOf(Employee4.prototype) === Person.prototype; // false
Employee4.prototype; // { hijacked: true }
Employee4.prototype becomes { hijacked: true }, with no relationship to Person at all. No error — you find out when d.describe() comes back undefined.
Object.create executes no function and cannot do this.
Three traps specific to __proto__
Since it is "the only in-place mutation ES5 had", it is worth being precise about why it is not recommended.
1. It is not an own property
__proto__ is only an accessor on Object.prototype. Where Object.prototype is not on the chain, it is just an ordinary property name:
const o = Object.create(null);
o.__proto__ = { a: 1 };
Object.getPrototypeOf(o); // null ← the prototype was NOT set!
Object.getOwnPropertyNames(o); // ['__proto__'] ← it became a plain own property
A silent failure, no error. By the same token, anyone who runs delete Object.prototype.__proto__ (legal, if unwise) breaks every __proto__ assignment in the program.
2. Inside an object literal it is special-cased
const lit = { __proto__: { a: 1 } }; // sets the prototype
const computed = { ['__proto__']: { a: 1 } }; // creates an ordinary own property
Object.getPrototypeOf(lit); // { a: 1 } — the prototype changed
Object.getOwnPropertyNames(lit); // []
Object.getOwnPropertyNames(computed); // ['__proto__'] — just a normal property
Object.getPrototypeOf(computed) === Object.prototype; // true
{ __proto__: x } is a syntactic special case written into the spec; ['__proto__'] with a computed key is not. Same "key", one changes the prototype and the other creates a property — very easy to miss when reading code.
3. Spec standing
As above: ES6 Annex B only, web hosts only. Plus it is a legacy feature that ESLint's no-proto bans outright.
So the reason to avoid it is not compatibility — it is that it fails silently on null-prototype objects, has a counter-intuitive special case in literals, and is only specified for browsers. Object.setPrototypeOf (ES6) does the same operation without those three problems.
Which one to use
- New code on ES6+ →
class extends; it covers both chains - Hand-written, minimal side effects →
Object.setPrototypeOf(Employee.prototype, Person.prototype). Mutates in place, no reordering, noconstructorrepair - ES5 environment →
Employee.prototype = Object.create(Person.prototype)plus theconstructorrepair.setPrototypeOfis unavailable and__proto__is non-standard - Reading old code / polyfills → recognise
new f()and__proto__ =, do not write them
One performance note that comes up often: mutating the [[Prototype]] of an existing object makes the engine give up on optimisations for that object. But a prototype chain is set once at initialisation, so it does not matter — what actually hurts is changing the prototype of instances inside a loop.
What all four hand-written options miss: the constructor chain
All four above wire the instance chain only. With any of them, Object.getPrototypeOf(Employee) is still Function.prototype:
Employee.create; // undefined — not one of the parent's statics is reachable
To be fully equivalent to class extends, you write one more line (the other use of setPrototypeOf):
Object.setPrototypeOf(Employee, Person); // constructor chain
Employee.create('alice') instanceof Employee; // true
class extends is convenient precisely because it is not one of the four at all — it sits at a different level: those four are "you wire one chain by hand", while it is "the language wires both", with no line written by you.
class Employee extends Person {
constructor(name, salary) {
super(name); // = Person.call(this, name)
this.salary = salary;
}
describe() { /* … */ }
}
Available since ES6. Modern code just uses this; the four above matter only when reading old code or working in a pure ES5 environment.
The complete version
On ES6+:
class Employee extends Person {
constructor(name, salary) {
super(name);
this.salary = salary;
}
describe() { /* … */ }
}
When you must hand-write it (the constructor-chain line needs ES6):
function Person(name) {
this.name = name;
}
Person.prototype.role = 'person';
Person.create = function (name) { // static
return new this(name);
};
function Employee(name, salary) {
this.salary = salary;
Person.call(this, name); // super(name)
}
// instance chain — pick one
Object.setPrototypeOf(Employee.prototype, Person.prototype); // ES6, in place
// Employee.prototype = Object.create(Person.prototype); // ES5, replace
// Employee.prototype.constructor = Employee;
// constructor chain — the line everyone forgets (ES6)
Object.setPrototypeOf(Employee, Person);
Employee.prototype.role = 'employee'; // shadows Person.prototype.role
Employee.prototype.describe = function () {
console.log(`${this.name}, salary ${this.salary}`);
};
const d1 = new Employee('alice', 8000);
d1 instanceof Employee; // true
d1 instanceof Person; // true
Employee.create('alice') instanceof Employee; // true ← thanks to that second line
Pure ES5 (no setPrototypeOf; the constructor chain is either absent or faked with __proto__):
Employee.prototype = Object.create(Person.prototype); // replace
Employee.prototype.constructor = Employee; // required
// role / describe must be attached only after this point
Summary
Object.setPrototypeOf | __proto__ = | Object.create | new f() | |
|---|---|---|---|---|
| Group | mutate | mutate | replace | replace |
| Since | ES6 | de facto → ES6 Annex B | ES5 | ES3 |
constructor | preserved | preserved | lost, repair it | lost, repair it |
| Order | any | any | wire first | wire first |
| Runs a function | No | No | No | Yes |
| Extra traps | none | silent no-op on null-prototype objects, literal special case, browser-only spec | — | f's return value hijacks the result |
| Today | first choice by hand | do not use | use on ES5 | obsolete |
One line: four options collapse into two groups — mutating in place (setPrototypeOf / __proto__ =) preserves constructor and does not care about order, replacing (Object.create / new f()) is the exact opposite; in ES5, mutating in place meant the non-standard __proto__ or nothing, since setPrototypeOf only arrived with ES6; and all four wire the instance chain only, so the constructor chain always needs its own Object.setPrototypeOf(Employee, Person) — which is the one thing class extends genuinely saves you from.