← 返回博客

接原型链的四种写法:分水岭是「替换」还是「原位改」

看这段代码:

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.createnew f()
类别原位改原位改替换替换
ES 版本ES6非标准 → ES6 Annex BES5ES3
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:

GroupActionKey consequence
Mutatekeep the object, move its [[Prototype]] pointerconstructor survives, order does not matter
Replacebuild a new object, assign it to Employee.prototypethe 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

OptionGroupStanding
new f()replacesince ES3, available everywhere
Object.createreplaceES5
Object.setPrototypeOfmutateES6
Employee.prototype.__proto__ =mutatede 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:

OptionGroupPointer wiredconstructorOrder-sensitiveRuns a function
Object.setPrototypeOfmutate✓Employee ✓NoNo
__proto__ =mutate✓Employee ✓NoNo
Object.createreplace✓Person ❌YesNo
new f()replace✓Person ❌YesYes

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, no constructor repair
  • ES5 environment → Employee.prototype = Object.create(Person.prototype) plus the constructor repair. setPrototypeOf is 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.createnew f()
Groupmutatemutatereplacereplace
SinceES6de facto → ES6 Annex BES5ES3
constructorpreservedpreservedlost, repair itlost, repair it
Orderanyanywire firstwire first
Runs a functionNoNoNoYes
Extra trapsnonesilent no-op on null-prototype objects, literal special case, browser-only spec—f's return value hijacks the result
Todayfirst choice by handdo not useuse on ES5obsolete

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.