Skip to content

原型、原型链 ​

难度等级:🟡 中级 分类: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 调用。


    1. 箭头函数不能作为构造函数,没有 prototype 属性。2. { fn(){} } 对象的简写没有。3. async 函数没有。4. function Animal() {} const BoundAnimal = Animal.bind(null); 绑定后的函数没有。
    ↩︎
    1. 创建一个新对象。2. 将新对象的 [[Prototype]] 指向 Animal.prototype。3. 以新对象作为 this 执行 Animal。4. 如果构造函数没有显式返回一个对象,就返回这个新对象。
    ↩︎
  1. Object.setPrototypeOf() 会在运行时修改对象的原型链,破坏 JavaScript 引擎对对象结构和属性访问路径的稳定性假设,可能导致隐藏类变化、内联缓存失效以及优化代码退优化。它的影响不仅是修改操作本身,后续属性访问也可能变慢。因此通常应该在创建对象时,通过 Object.create()、构造函数或 class 确定原型关系,避免频繁动态修改原型。 ↩︎

  2. 在浏览器中,不同的 iframe 拥有各自的内置构造函数。如果数组来自另一个 iframe:otherWindowArray instanceof Array // 可能是 false ↩︎

个人面试题归档,持续更新中