Appearance
原型、原型链
难度等级:🟡 中级 分类:JavaScript 归档日期:2026-09-17
函数的
prototype:函数的原型属性,它保存着一个"原型对象"对象的
[[Prototype]]:对象的内部原型,表示该对象委托给哪个对象
text
cat
↓ [[Prototype]]
Animal.prototype
↓ [[Prototype]]
Object.prototype
↓
null一、基本概念
JS 是没有类的概念的(ES6 其实是语法糖)。原型、原型链就是 JS 对象实现属性共享、继承机制的一条委托链。
JS 函数除了自身是对象以外,通常[1]还拥有一个 prototype 属性,prototype 属性内部会有一个 constructor 构造函数指向 JS 函数本身。当这个 JS 函数当作构造函数被 new[2] 的时候,新创建出的对象会带一个内部属性 [[Prototype]],这个属性指向构造函数的 prototype 属性。这样,新对象就与构造函数的原型对象建立原型关联。[[Prototype]] 本身也是一个对象,它也可能拥有自己的内部原型(是另外一个函数的原型对象),所以它也会有一个内部的属性 [[Prototype]] 指向另一个函数的原型对象。那 JS 对象在查找属性的时候,会先从对象本身查找,如果没找到,就从对象的原型上找,还没有就从对象的原型的原型上查找。
二、常用 API
| API | 说明 | 推荐程度 |
|---|---|---|
Object.create(proto) | 以指定对象作为原型,创建一个新对象 | 推荐 |
Object.getPrototypeOf(obj) | 获取对象的原型(标准方式) | 推荐 |
Object.setPrototypeOf(obj, proto) | 设置对象的原型(有性能开销,不推荐频繁使用)[3] | 不推荐 |
Object.hasOwn(obj, key) | 判断自身属性 | 推荐 |
Object.prototype.hasOwnProperty(prop) | 判断属性是否是对象自身的,不沿原型链查找 | 注意覆盖和无原型对象 |
Object.prototype.isPrototypeOf(obj) | 判断某对象是否在另一个对象的原型链上(是不是祖先原型) | 推荐 |
prop in obj | 判断属性是否存在于对象自身或其原型链上(会向上查找) | 常用 |
对象 instanceof 构造函数 | 构造函数的 prototype,是否存在于对象的原型链上 | 常用, 数组推荐 Array.isArray([]) [4] |
Object.keys(obj) / Object.getOwnPropertyNames(obj) | 只返回对象自身的属性,不含原型链上的 | 常用 |
三、原型链继承有什么问题
js
function Parent() {
this.colors = ['red', 'blue']
}
Parent.prototype.sayHello = function () {
console.log('hello')
}
function Child() {}
Child.prototype = new Parent()
const child1 = new Child()
const child2 = new Child()1.引用类型属性会被所有实例共享(对于基本类型,如果重新赋值,通常会在实例自身创建属性)
2. 创建子类实例时,无法给父构造函数传参
js
function Parent(name) {
this.name = name
}
function Child() {}
Child.prototype = new Parent('固定名字')
const child1 = new Child()
const child2 = new Child()
child1.name // "固定名字"
child2.name // "固定名字"
//不能自然地做到:
new Child('张三')
new Child('李四')3. 父构造函数只在设置原型时执行一次
js
Child.prototype = new Parent() // 在此时 Parent 就已经运行了
// 之后不会再次调用 Parent,因此父构造函数中的实例初始化逻辑无法为每个子实例分别执行。
new Child()
new Child()4. 父构造函数的副作用会在定义继承时发生
仅仅为了建立继承关系,就执行了父构造函数。如果父构造函数中有:网络请求、定时器、DOM 操作、事件监听、数据库或资源初始化,这些副作用都会在设置原型时提前发生。
5. constructor 指向会发生变化
js
function Parent() {}
function Child() {}
Child.prototype = new Parent()
Child.prototype.constructor === Parent // true
Child.prototype.constructor === Child // false
// 原因是重新赋值后,新的 Child.prototype 本质上是一个 Parent 实例,它沿着原型链找到的是:
Parent.prototype.constructor
// 通常需要手动修复:
Child.prototype.constructor = Child
// 更严谨的方式是控制它的可枚举属性:
Object.defineProperty(
Child.prototype,
'constructor',
{
value: Child,
writable: true,
configurable: true,
enumerable: false
}
)6. 无法区分属性应该共享还是独立
js
//原型方法适合共享:
Parent.prototype.sayHello = function () {}
//因为所有实例共用同一个函数通常是合理的。
//但是父构造函数中的实例数据:
this.colors = []
this.profile = {}
//本应属于每个实例。使用原型链继承后,它们却被放在了 Child.prototype 上,导致所有实例共享。
//问题本质是:原型链继承把父实例上的数据,也变成了子实例共享的原型属性。四、改进方案
改进一:借用构造函数
在子构造函数中调用父构造函数:
js
function Parent(name) {
this.name = name
this.colors = ['red', 'blue']
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}这样每个子实例都有独立数据:
js
const child1 = new Child('张三', 18)
const child2 = new Child('李四', 20)
child1.colors.push('green')
console.log(child1.colors)
// ['red', 'blue', 'green']
console.log(child2.colors)
// ['red', 'blue']但这种方式不能继承 Parent.prototype 上的方法。
改进二:组合继承
js
function Parent(name) {
this.name = name
this.colors = ['red', 'blue']
}
Parent.prototype.sayHello = function () {
console.log(`你好,我是${this.name}`)
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
Child.prototype = new Parent()
Child.prototype.constructor = Child它解决了参数传递和引用属性共享的问题,但 Parent 会执行两次:
js
Child.prototype = new Parent() // 第一次
const child = new Child()
// Parent.call(this, name) // 第二次推荐:寄生组合式继承
js
function Parent(name) {
this.name = name
this.colors = ['red', 'blue']
}
Parent.prototype.sayHello = function () {
console.log(`你好,我是${this.name}`)
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
Child.prototype = Object.create(Parent.prototype)
Object.defineProperty(Child.prototype, 'constructor', {
value: Child,
writable: true,
configurable: true
})这里没有执行:
js
new Parent()而是直接建立关系:
text
Child.prototype → Parent.prototype同时通过:
js
Parent.call(this, name)让每个子实例拥有独立的父类实例属性。
现代代码一般直接使用:
js
class Parent {
constructor(name) {
this.name = name
this.colors = ['red', 'blue']
}
sayHello() {
console.log(`你好,我是${this.name}`)
}
}
class Child extends Parent {
constructor(name, age) {
super(name)
this.age = age
}
}一句话回答面试官:
传统原型链继承通常通过
Child.prototype = new Parent()实现。它的主要问题是,父实例上的引用类型属性会成为子实例共享的原型属性;创建子实例时不能方便地向父构造函数传参;建立继承关系时还会提前执行父构造函数,并导致constructor指向错误。后来通常使用寄生组合式继承,通过Parent.call(this)初始化实例属性,再用Object.create(Parent.prototype)继承原型方法;ES6 中则一般使用class extends。
五、寄生组合式继承 vs class 继承
用一个简单的 动物 → 猫 → 灰灰实例,分别演示两种写法。两段代码请分别运行。
1. 寄生组合式继承
js
function Animal(name) {
this.name = name
}
// 方法放在原型上,供实例共享
Animal.prototype.introduce = function () {
return `我叫${this.name}`
}
function Cat(name, personality) {
// ① 调用父构造函数,初始化实例属性
Animal.call(this, name)
this.personality = personality
}
// ② 连接原型链,继承父级方法
Cat.prototype = Object.create(Animal.prototype)
// ③ 修复 constructor 指向
Object.defineProperty(Cat.prototype, 'constructor', {
value: Cat,
writable: true,
configurable: true,
enumerable: false,
})
// 重写方法,并调用父级方法
Cat.prototype.introduce = function () {
const text = Animal.prototype.introduce.call(this)
return `${text},性格${this.personality}`
}
const huhui = new Cat('灰灰', '安静')
console.log(huhui.introduce())
// 我叫灰灰,性格安静这里有两件核心工作:
Animal.call(this, name):执行父构造函数,把name放到灰灰实例上。Object.create(Animal.prototype):连接原型链,让灰灰能够访问父级方法,且不执行父构造函数。
2. class 继承
js
class Animal {
constructor(name) {
this.name = name
}
// 自动放在 Animal.prototype 上
introduce() {
return `我叫${this.name}`
}
}
class Cat extends Animal {
constructor(name, personality) {
// 调用父构造函数,初始化 this 和实例属性
super(name)
this.personality = personality
}
// 重写方法,并调用父级方法
introduce() {
const text = super.introduce()
return `${text},性格${this.personality}`
}
}
const huhui = new Cat('灰灰', '安静')
console.log(huhui.introduce())
// 我叫灰灰,性格安静extends 自动连接原型链,并设置正确的 constructor。super(name) 调用父构造函数,super.introduce() 调用父级方法,执行时的 this 仍然是灰灰。
3. 两种写法怎么对应?
| 作用 | 寄生组合式继承 | class 继承 |
|---|---|---|
| 定义构造逻辑 | function Animal(name) {} | constructor(name) {} |
| 定义原型方法 | Animal.prototype.introduce = ... | 类体中的 introduce() {} |
| 初始化父级实例属性 | Animal.call(this, name) | super(name) |
| 连接实例原型链 | Object.create(Animal.prototype) | extends Animal 自动完成 |
修复 constructor | 手动定义 | 自动完成 |
| 调用父级方法 | Animal.prototype.introduce.call(this) | super.introduce() |
两种写法中,灰灰的结构都是:
- 自身属性:
name、personality。 Cat.prototype:重写后的introduce。Animal.prototype:父级的introduce。
寄生组合式继承手动完成“属性初始化”和“原型链连接”;class 用 super 和 extends 表达这两件事。
不过,它们并非完全等价:extends 还会连接构造函数自身的原型链以继承静态成员;类方法默认不可枚举,类内部采用严格模式,类构造函数必须通过 new 调用。
- 箭头函数不能作为构造函数,没有
prototype属性。2.{ fn(){} }对象的简写没有。3. async 函数没有。4.function Animal() {} const BoundAnimal = Animal.bind(null);绑定后的函数没有。
- 箭头函数不能作为构造函数,没有
- 创建一个新对象。2. 将新对象的
[[Prototype]]指向 Animal.prototype。3. 以新对象作为 this 执行 Animal。4. 如果构造函数没有显式返回一个对象,就返回这个新对象。
- 创建一个新对象。2. 将新对象的
Object.setPrototypeOf()会在运行时修改对象的原型链,破坏 JavaScript 引擎对对象结构和属性访问路径的稳定性假设,可能导致隐藏类变化、内联缓存失效以及优化代码退优化。它的影响不仅是修改操作本身,后续属性访问也可能变慢。因此通常应该在创建对象时,通过Object.create()、构造函数或 class 确定原型关系,避免频繁动态修改原型。 ↩︎在浏览器中,不同的 iframe 拥有各自的内置构造函数。如果数组来自另一个 iframe:
otherWindowArray instanceof Array// 可能是 false ↩︎