基本概念
JavaScript 使用原型链来实现继承机制。
每个对象内部有一个 [[Prototype]] 对象(可以通过 __proto__
进行访问),指向它的原型对象。
关键概念
- 构造函数:用于创建对象的函数
- prototype:函数特有的属性
- proto:对象实例指向原型对象的属性
- constuctor:原型对象指向构造函数的属性
原型链关系
javascript
function Person() {}
const p = new Person();
// 关系链
p.__proto__ === Person.prototype
Person.prototype.constructor === Person
Person.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
一些讨论
2.1 我们需要『继承』这种特性
"继承"是人类构建复杂事物的关键能力。
这种需求或许源于两重限制:
- 认知层面,人脑处理能力有限,所以衍生出分类、拆解、解构此类需求
- 时间层面,生命有限性促使我们将知识代际传递以延续文明。
2.2 编程语言中的继承是什么?
继承本质上源于人类对现实世界的抽象。------通过分层归纳来简化认知与记忆。
例如,要描述鸟、老鹰:
- 先定义鸟的通用特征。
- 再基于鸟扩展老鹰、大鹏的【特有属性】
有趣的是,现实世界的认知过程往往是相反的。
人类先观察到鸟、老鹰等具体实例,而后才抽象出鸟这一通用概念。
这种差异解释了早期编程语言的诸多设计局限:受限于当时的认知范式,导致了一些不符合直觉的实现方式。
2.3 ES6之前,如何实现继承
ES6之前的JS没有现成语法可用,人们另辟蹊径,间接实现继承:
- JS创建函数时,在对象的内部会创建一个
prototype
对象 - 使用 new 关键字调用函数时,实例对象的
__proto__
属性指向构造函数的prototype
- 访问一个对象的属性,如果没找到该属性,会顺着
__proto__
继续向上查找,如还没找到,则继续向上,直到为null
基于以上3条原理,前人【发现】了旧JS实现继承的路径。
- 原型链的尽头是 null。
- 一切JS对象均有原型

2.4 一些无需记忆的旧实现
javascript
function F(name, age) {
this.name = name
this.age = age
}
F.prototype.say = function() {
console.log(this.name)
}
function S(name, age, sex) {
F.call(this, name, age)
this.sex = sex
}
// 此方法是ES6之前的实现方式
// S.prototype = new F()
// ES6之后可以这样实现
S.prototype = Object.create(F.prototype)
const son = new S('chp', 18, 'male')
son.say() // chp
上述代码是经典的 组合构造模式 的继承。
缺陷也比较明显,我们发现我们调用了 F.call,ES6之前我们第二次调用了 new F()
不过ES6之后弥补了缺陷
2.5 Class语法糖的出现,ES6之后如何实现
新时代的继承
scala
class F {
constructor(name, age) {
this.name = name
this.age = age
}
say() {
console.log(this.name)
}
}
class S extends F {
constructor(name, age, sex) {
super(name, age)
this.sex = sex
}
}
const son = new S('chp', 18, 'male')
son.say() // chp
2.6 所以什么是原型链?
- 原型链是 JS 实现继承的机制。
- 当访问一个属性时,如果对象自身有该属性,直接返回。
- 如果没有,则沿着 [[Prototype]] (
__proto__
)向上查找,直到找到对象的属性、或到达 null。 - 这条自底向上,逐级查找的链路,就叫做【原型链】。
原型链的终点是 null
注意事项
- 不要随意覆盖函数的原型
prototype
,例如对Function.prototype
进行赋值。比较好的方式是只对Function.prototype.attr
进行赋值。 - 原型链的终点是
null
继承模式
发展顺序:
原型链继承 -> 构造函数继承 -> 组合 -> 原型 -> 寄生式
原型链继承
javascript
function Parent() {}
function Child() {}
Child.prototype = new Parent();
【子类原型 = 父类实例】
直接修改 prototype。
- ✅简单直接
- ❌所有子类实例共享父类引用属性
- ❌无法向父类构造函数传参
构造函数继承
csharp
function Parent() {}
function Child() {
Parent.call(this);
}
在函数内,借用调用。
在子类函数中使用 call 方法。
- ✅解决引用属性共享问题
- ✅可以向父类传参
- ❌无法继承父类原型上的方法
组合继承(最常用)
javascript
function Parent() {}
function Child() {
Parent.call(this);
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
- ✅实例属性独立
- ✅方法共享
- ❌父类被调用了2次【为了修复指向,会连续2次调用
Parent()
】
原型式继承
本质是克隆
ini
const obj = Object.create(parent);
- ✅简单对象继承方便
- ❌引用属性共享问题
寄生式继承
优化版的克隆
ini
function inheritPrototype(child, parent) {
const prototype = Object.create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
- ✅ 避免组合继承的两次调用问题
- ✅ 保持原型链完整
总结 ES6 最终实现
ES6最后采用的是 【寄生组合式】继承。
这是最理想的继承方式。
scala
class Parent {}
class Child extends Parent {}
// 转译后的核心代码:
function _inherits(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype); // 寄生式继承
subClass.prototype.constructor = subClass; // 修复构造函数
Object.setPrototypeOf(subClass, superClass); // 继承静态属性
}
它有以下优势:
- 只调用一次父类函数 【性能最优,同时正确修复了 Child
constructor``prototype
的指向】 - 原型链纯净无冗余属性【内存友好】
- 完整继承静态属性 + 实例方法【功能完整】
ES6 的 class 继承是语法糖,其底层采用 寄生组合式继承,解决了传统继承方式的缺陷,成为 JavaScript 中最完善、最高效的继承实现方案。这也是为什么现代 JavaScript 开发推荐始终使用 class 语法而非手动实现继承。
目的主要是:
- 方法共享
- 属性独立
相关案例
🔥 超经典的题目
javascript
function F(){}
function O(){}
var obj = new O();
O.prototype = new F();
// 如果此时有个 obj2
var obj2 = new O();
obj.__proto__
是否指向O.prototype
? // ❌obj2.__proto__.__proto__ === F.prototype
// ✅obj.__proto__.constructor === O
// ✅
答案:
obj.__proto__
并不指向 O.prototpye
,因为后者被修改了。
由于对象本身 this 上没挂在东西,所以此时它指向了【实例对象】
{constructor: O(), [[prototype]]: Object}

var obj = new O()
时O.prototype
=> 默认的原型对象,constructor
=> O。- 重定向
O.prototype
,其被赋值为F()
的实例。 - 【此操作不影响已创建的 obj,因为 obj 内部
[[prototype]]
仍然指向旧的O.prototype
】
- obj instanceof O ? // ❌
instanceof 的本质是,【一个对象】的【原型链上】,是否存在某个函数的 prototype
属性。
obj.__proto__
此时指向了一个实例对象。
对于实例对象的 __proto__
,只会指向 Object.prototype
。
由于 O.prototype
已经被改变,他们注定无缘。
- obj instanceof F ? // ❌ 同理。
- obj intanceof Object ? // ✅,因为
obj.__proto__.__proto__ === Object.prototype
一切对象都是 Object 的继承,原型链的终点是 null
javascript
let o = new Object()
o.__proto__ // Object.prototype
o.__proto__.__proto__ // null
Object.__proto__ // Function.prototype,Object和Function的关系
Object.__proto__.__proto__ === Object.prototype // 函数也是对象
Object.__proto__.__proto__.__proto__ // null
手写 instanceof
instanceof 的本质是,【一个对象】的【原型链上】,是否存在某个函数的 prototype
属性。
javascript
export const myInstanceof = (ins, targetCls) => {
let p = ins.__proto__
while (p) {
if (p === targetCls.prototype) {
return true
}
p = p.__proto__
}
return false
}
不断寻找自己的 __proto__
实例,判断该属性是否与【指定类型的原型prototype】相等。
自底向上,直到 null
。
由于 __proto__
是一个非标准属性,更推荐使用 Object.getPrototypeOf
javascript
export const myInstanceof = (ins, targetCls) => {
if (typeof targetCls !== 'function') {
return TypeError('第二个参数必须是函数')
}
let p = Object.getPrototypeOf(ins)
while (p) {
if (p === targetCls.prototype) {
return true
}
p = Object.getPrototypeOf(p)
}
return false
}
多层继承
javascript
function A() { this.x = 1; }
function B() { this.y = 2; }
function C() { this.z = 3; }
B.prototype = new A();
C.prototype = new B();
const c = new C();
console.log(c.x); // 1,从 A.prototype 继承
console.log(c.y); // 2,从 B.prototype 继承
console.log(c.z); // 3,从 C.prototype 继承
console.log(c.w); // undefined,未定义
c -> C.prototype (B的实例) -> B.prototype (A的实例) -> A.prototype -> Object.prototype -> null
动态修改原型的影响
javascript
function Person() {}
const p1 = new Person();
Person.prototype = {
name: 'Alice'
};
const p2 = new Person();
console.log(p1.name); // undefined,还是指向原来的引用
console.log(p2.name); // Alice
console.log(p1 instanceof Person); // false
console.log(p2 instanceof Person); // true
/*
解释:
- p1创建时使用的是原始的Person.prototype
- 之后Person.prototype被完全替换,但p1的[[Prototype]]仍然指向旧的原型
- p2创建时使用的是新的Person.prototype
- instanceof检查当前构造函数的prototype是否在对象的原型链上
*/
构造函数返回对象的影响
javascript
function Foo() {
this.name = 'Foo';
return { name: 'Bar' };
}
function Bar() {
this.name = 'Bar';
}
Bar.prototype = new Foo();
const b = new Bar();
console.log(b.name); // 'Bar'
console.log(b instanceof Bar); // true
console.log(b instanceof Foo); // false
console.log(b instanceof Object); // true
/*
原型链分析:
- Foo构造函数返回了一个新对象,所以Bar.prototype = new Foo()
实际上使Bar.prototype指向{name: 'Bar'}这个对象
- b的原型链:b -> Bar.prototype ({name: 'Bar'}) -> Object.prototype -> null
- Foo.prototype不在b的原型链上
*/
如果 Foo 没有 return 一个对象,则:
b instanceof Foo // true
b instanceof Bar // true
但是因为 Foo return 了一个对象。所以这里的链条断掉了。
原型链和静态属性
ini
function Parent() {}
Parent.staticProp = 'Parent Static';
Parent.prototype.protoProp = 'Parent Proto';
function Child() {}
Child.staticProp = 'Child Static';
Child.prototype = Object.create(Parent.prototype);
Child.prototype.protoProp = 'Child Proto';
const c = new Child();
console.log(c.protoProp); // 'Child Proto'
console.log(c.staticProp); // undefined
console.log(Child.staticProp); // 'Child Static'
console.log(Child.prototype.protoProp); // 'Child Proto'
/*
静态属性不会被实例继承,它们只是构造函数的属性
只有添加到prototype上的属性才会被实例继承
*/
复杂原型链与属性屏蔽
ini
const grandparent = { a: 1 };
const parent = Object.create(grandparent);
parent.a = 2;
parent.b = 3;
const child = Object.create(parent);
child.b = 4;
child.c = 5;
console.log(child.a); // 2 屏蔽了父级的 a
console.log(child.b); // 4 屏蔽了 parent 的b
console.log(child.c); // 5
console.log(child.d); // undefined
delete child.b;
console.log(child.b); // 3 解除了屏蔽
循环原型链
javascript
function A() {}
function B() {}
A.prototype = new B();
B.prototype = new A();
const a = new A();
const b = new B();
console.log(a instanceof A); // true
console.log(a instanceof B); // true
console.log(b instanceof A); // true
console.log(b instanceof B); // true
/*
这种设计会导致原型链循环引用:
A.prototype -> B实例 -> B.prototype -> A实例 -> A.prototype -> ...
浏览器处理:
现代浏览器能检测这种循环引用,不会导致无限循环
但这是非常糟糕的设计,会导致难以预测的行为和性能问题
*/
综合原型链分析
javascript
function Foo() {}
Foo.prototype.bar = function() { return 1; };
const foo = new Foo();
Foo.prototype = {
bar: function() { return 2; },
baz: function() { return 3; }
};
const foo2 = new Foo();
console.log(foo.bar()); // 1
console.log(foo2.bar()); // 2
console.log(foo.baz); // // undefined
console.log(foo2.baz()); // 3
/*
原型链差异:
- foo的原型指向最初的Foo.prototype
- foo2的原型指向替换后的新Foo.prototype
替换原型对象不会影响已存在的实例
*/
原型链污染+防御
javascript
Object.prototype.hack = function() {
console.log('Prototype polluted!');
};
const obj = {};
// 问题1: obj.hack() 输出什么? 'Prototype polluted!'
// 问题2: 如何防御这种原型污染?
// * Object.freezee 冻结
// * 创建无原型对象Object.create(null)
// 问题3: Object.create(null) 创建的对象会受到污染吗?
// 不会,因为它没有原型链,不继承 Object.prototype
ES6 class 与原型链的对应关系
javascript
class A {
static staticMethod() { return 'A static'; }
instanceMethod() { return 'A instance'; }
}
class B extends A {
static staticMethod() { return 'B static'; }
instanceMethod() { return 'B instance'; }
}
const b = new B();
// 问题1: 用 ES5 语法重写上述 class 定义
// 问题2: console.log(b.instanceMethod()) 输出什么?'B instance'
// 问题3: console.log(B.staticMethod()) 输出什么?'B static'
// 问题4: 如何通过原型链访问父类的实例方法?通过 super 调用父类方法
使用 ES5 去实现。
javascript
function A() {}
A.staticMethod = function () {
return 'A static'
}
A.prototype.instanceMethod = function () {
return 'A instance'
}
function B() {}
const prototype = Object.create(A.prototype)
prototype.constructor = B
B.prototype = prototype
B.staticMethod = function () {
return 'B static'
}
B.prototype.instanceMethod = function () {
return 'B instance'
}
const b = new B()
console.log('🍀🍀🍀🍀', b.instanceMethod())
console.log('🍀🍀🍀🍀', B.staticMethod())
console.log('🍀🍀🍀🍀', b instanceof B) // true
console.log('🍀🍀🍀🍀', b instanceof A) // true
原型链与访问优先级
javascript
function Foo() {
this.name = 'Foo';
}
Foo.prototype.name = 'Foo Prototype';
Object.prototype.name = 'Object Prototype';
const foo = new Foo();
console.log(foo.name); // 'Foo'
console.log(foo.__proto__.name); // 'Foo Prototype'
console.log(foo.__proto__.__proto__.name); // 'Object Prototype'
console.log(foo.toString()); // 输出什么?'[object Object]'
delete foo.name;
console.log(foo.name); // 输出什么?'Foo Prototype'
原型链与性能优化
javascript
// 现有以下性能较差的代码
function HeavyComponent() {
this.id = Math.random();
this.data = new Array(10000).fill('data');
}
HeavyComponent.prototype.render = function() {
console.log('Rendering:', this.id);
};
// 问题:如何优化创建大量HeavyComponent实例时的内存使用?
// 要求:
// 1. 保持render方法的可用性
// 2. 避免每个实例都创建大数组
// 3. 写出优化后的代码实现
// 4. 解释优化原理
javascript
function F() {
this.id = Math.random()
}
F.prototype.data = new Array(10000).fill('data')
F.prototype.render = function () {
console.log('Rendering:', this.id)
}
let list = []
for (let i = 0; i < 100; i++) {
list.push(new F())
}
- 将占用内存大的都移动到原型上,所有实例共享
- 每个实例只保留独有的 id
- render放在原型上共享
- 保留原来 api