原型链(Prototype Chain)与 Class:吃透 JS 对象继承的核心

原型链(Prototype Chain)与 Class:吃透 JS 对象继承的核心
Touko原型链是 JavaScript 面向对象的 “底层逻辑”—— 它让对象能 “继承” 其他对象的属性和方法,而 ES6 的
class只是这套逻辑的 “语法糖”。今天从原型链的本质讲到 Class 的应用,帮你彻底搞懂 JS 继承到底是怎么回事。
一、原型链:JS 对象继承的 “底层骨架”
1.1 什么是原型链?
简单说,原型链是 JS 实现继承的核心机制:每个对象都有一个隐藏的 “原型”([[Prototype]],可通过 Object.getPrototypeOf() 访问),这个原型本身也是一个对象,它也有自己的原型 —— 这样层层向上,就形成了一条 “原型链”。
当你访问一个对象的属性时,如果当前对象没有这个属性,JS 会自动沿着原型链向上查找,直到找到属性或走到链的尽头(null)。
看个直观的例子:
1 | // 父对象:动物,有一个共享属性 eats |
1.2 原型链的三个核心概念
要理解原型链,必须分清这三个容易混淆的概念:
prototype:只有函数才有这个属性,它是一个对象,存储着 “该函数创建的实例要继承的属性和方法”(比如Person.prototype里的方法,会被所有new Person()出来的实例继承)。[[Prototype]]:所有对象(包括函数)都有的隐藏属性,指向该对象的 “原型对象”(可通过Object.getPrototypeOf(obj)访问,旧版的__proto__已不推荐使用)。constructor:原型对象上的属性,指向 “创建该原型对应的实例的构造函数”(比如Person.prototype.constructor === Person)。
它们的关系可以用一张图表示:
graph LR A["实例对象(如 new Person())"] -->|"[[Prototype]]"| B["构造函数的 prototype(Person.prototype)"] B -->|"[[Prototype]]"| C["Object.prototype(所有对象的最终原型)"] C -->|"[[Prototype]]"| D["null(原型链的终点)"] B -->|constructor| E["构造函数(Person)"]
二、原型链是怎么 “造” 出来的?
2.1 用构造函数创建对象:原型链的常见来源
我们平时用 new 构造函数() 创建对象时,原型链会自动生成。比如:
1 | // 1. 定义构造函数(用来创建“人”实例) |
2.2 new 操作符的底层逻辑
你可能好奇:new 到底做了什么,能让实例和原型链关联起来?其实它只干了四件事:
- 创建一个空对象(
{}); - 把这个空对象的原型,设为构造函数的
prototype; - 执行构造函数,把
this指向这个空对象(给对象加属性); - 如果构造函数没有返回其他对象,就返回这个新对象(否则返回构造函数的返回值)。
我们可以手动模拟一个 new 操作符,更直观地看到这个过程:
1 | function myNew(constructor, ...args) { |
三、原型链的 “查找规则”:属性是怎么找到的?
3.1 属性查找的完整流程
当你访问 obj.prop 时,JS 会按以下步骤查找:
- 先检查
obj自身有没有prop(通过obj.hasOwnProperty('prop')可判断); - 如果没有,就找
obj的原型(Object.getPrototypeOf(obj)),检查原型有没有prop; - 如果原型也没有,就找原型的原型,以此类推;
- 直到找到
prop并返回,或者走到原型链尽头(null),返回undefined。
看个例子理解这个流程:
1 | // 1. 定义构造函数 Animal |
3.2 原型链的终点:null
所有原型链的最终尽头都是 null—— 因为 Object.prototype 的原型就是 null,它是 “所有对象的最终原型”(除了用 Object.create(null) 创建的 “纯净对象”)。
1 | const cat = new Animal('喵星人'); |
这也解释了为什么所有对象都能调用 toString()、hasOwnProperty() 等方法 —— 这些方法其实定义在 Object.prototype 上,所有对象都能通过原型链找到它们。
四、原型链的 “动态特性”:改原型会影响实例吗?
4.1 动态修改原型:已创建的实例也会受影响
JS 的原型是 “活的”—— 即使实例已经创建,后续给原型添加的属性 / 方法,实例也能访问到:
1 | function Dog() {} |
这是因为实例访问的是 “原型的引用”,而不是 “原型的副本”—— 原型变了,所有指向它的实例都会跟着变。
4.2 重写原型:旧实例不受影响
但如果是 “完全重写原型”(而不是修改原型的属性),情况就不一样了:重写后的原型是一个新对象,只有重写后创建的实例会用新原型,之前的旧实例还是指向原来的原型。
1 | function Cat() {} |
五、ES6 Class:原型链的 “语法糖”
5.1 Class 本质:还是原型继承
ES6 引入的 class 语法,看起来像其他语言的 “类”,但底层还是基于原型链实现的 —— 它只是把原型继承的写法变得更简洁、更易读,没有改变 JS 的底层逻辑。
比如下面两段代码,功能完全一样:
1 | // 写法1:ES6 Class |
5.2 Class 继承:extends 怎么工作?
Class 的 extends 关键字,本质是帮我们自动搭建了原型链。比如让 Rabbit 继承 Animal:
1 | // 子类 Rabbit 继承父类 Animal |
extends 做的核心事情:把 Rabbit.prototype 的原型,设为 Animal.prototype,从而搭建起 “bunny → Rabbit.prototype → Animal.prototype → Object.prototype → null” 的原型链。
六、原型链的实际用途:这些场景会用到
6.1 共享方法:节省内存
如果多个实例需要用同一个方法,把方法放在原型上(而不是每个实例都定义一次),能大幅节省内存 —— 因为所有实例共享同一个方法引用。
1 | function User(name) { |
如果把 sayHi 写在构造函数里(this.sayHi = function() {}),每个实例都会有一个独立的函数副本,内存占用会翻倍。
6.2 实现自定义继承(ES5 写法)
在 Class 出现前,我们用原型链手动实现继承。比如让 Circle 继承 Shape:
1 | // 父类:图形 |
6.3 扩展内置对象(谨慎使用)
我们可以给内置对象的原型添加方法,让所有该类型的对象都能使用。比如给数组加一个 sum 方法:
1 | // 给 Array.prototype 加 sum 方法,所有数组都能调用 |
⚠️ 注意:扩展内置原型有风险!可能和其他库的方法重名(比如别人也给数组加了
sum方法),导致代码冲突。非必要不推荐用。
注意: 扩展内置原型有风险!可能和其他库的方法重名(比如别人也给数组加了 `sum` 方法),导致代码冲突。非必要不推荐用。
七、原型链的坑:这些问题要注意
7.1 原型污染:修改内置原型影响所有对象
如果恶意代码(或不小心)修改了 Object.prototype,所有对象都会受到影响 —— 这就是 “原型污染”。比如:
1 | // 不小心给 Object.prototype 加了一个 hack 方法 |
解决方案:用 Object.create(null) 创建 “纯净对象”—— 这种对象没有原型([[Prototype]] 是 null),不会继承 Object.prototype 的属性,也就不会被污染:
1 | const pureObj = Object.create(null); |
7.2 实例属性 “遮蔽” 原型属性
如果实例有一个和原型同名的属性,实例属性会 “覆盖” 原型属性(这叫 “属性遮蔽”):
1 | function Person() {} |
7.3 循环引用:导致栈溢出
如果两个构造函数的原型互相指向对方,会形成 “循环原型链”,创建实例时会报错:
1 | function A() {} |
八、原型链最佳实践:写代码更安全
优先用 Class 语法:
class和extends比 ES5 手动改原型更清晰,不容易出错,比如:1
2class Animal {}
class Rabbit extends Animal {}不用
__proto__,用标准方法操作原型:- 查原型:
Object.getPrototypeOf(obj)(替代obj.__proto__) - 改原型:
Object.setPrototypeOf(obj, newProto)(替代obj.__proto__ = newProto) - 创建带原型的对象:
Object.create(proto)(比new更灵活)
- 查原型:
属性和方法分开放:构造函数里定义 “实例独有的属性”,原型上定义 “所有实例共享的方法”—— 符合内存高效利用的原则:
1
2
3
4function Person(name) {
this.name = name; // 实例独有属性
}
Person.prototype.sayName = function () {}; // 共享方法避免修改内置原型:除非有绝对必要,否则不要给
Array.prototype、Object.prototype等加方法,防止冲突。
最后小测验
看看你有没有掌握原型链的核心逻辑,下面代码会输出什么?
1 | function Foo() {} |
答案:true、true、true
f1是Foo的实例,Foo.prototype继承自Object.prototype,所以f1 instanceof Object为true;Foo.prototype是一个普通对象,继承自Object.prototype,所以Foo.prototype instanceof Object为true;new Foo()创建的实例,原型就是Foo.prototype,所以第三个判断为true。
原型链虽然是 JS 的 “底层概念”,但理解它能帮你避开很多隐藏的坑(比如原型污染、属性遮蔽),也能让你更懂 Class 的本质 —— 不是 “新的继承方式”,只是原型链的优雅包装。掌握这些,你对 JS 面向对象的理解会更上一层楼~











