深入理解 JavaScript 「原型与原型链」

239 阅读4分钟

在 JavaScript 的世界里,原型(Prototype)是理解对象、继承和属性查找机制的核心。本文将结合实际代码案例,带你系统梳理 JS 原型的本质、设计意义及其在开发中的实际应用。

什么是原型?

1. 显式原型(prototype)

每一个函数(包括构造函数)天生自带一个 prototype 属性,它本质上是一个对象。我们可以把一些通用的属性和方法挂载到这个原型对象上,这样所有通过该构造函数创建的实例都能访问这些属性和方法,而无需每次都在构造函数里重复定义。

示例:

Car.prototype.name = 'su7-Ultra'
Car.prototype.height = 1.4
Car.prototype.weight = 1.5
Car.prototype.long = 4800

function Car(color) {
    this.color = color
}

const car1 = new Car('orange')
const car2 = new Car('green')
console.log(car1.name) 
console.log(car2.long)

打印结果:

image.png 解读:
Car 构造函数的原型上挂载了 nameheight 等属性。car1car2 虽然自身没有这些属性,但依然可以访问到。这就是原型的威力。

2. 隐式原型(proto

每一个对象都拥有一个 __proto__ 属性,它指向该对象的构造函数的 prototype。这就是“对象原型”或“隐式原型”。

原理:
当你访问一个对象的属性时,JS 引擎会先在对象本身查找,如果找不到,就会沿着 __proto__ 指向的原型对象继续查找,直到找到或到达原型链的尽头(null)。

示例:

function Person() {
    this.name = '小华'
}
Person.prototype.say = function () {
    console.log('我太帅了')
}

const p1 = new Person()
p1.say() 

打印结果: image.png

p1 本身没有 say 方法,但它的 __proto__ 指向 Person.prototype,从而可以访问到 say

原型链与属性查找机制

1. 原型链的形成

原型链是由对象的 __proto__ 层层向上连接形成的链条。每个对象的 __proto__ 指向其构造函数的 prototype,而这个 prototype 又是一个对象,也有自己的 __proto__,如此递归,最终指向 Object.prototype,再到 null

示例:

function GrandParent() {
    this.name = 'grandParent'
    this.card = 'visa'
}

function Parent() {
    this.name = 'parent'
}
Parent.prototype = new GrandParent()

function Child() {
    this.name = 'child'
    this.age = 18
}
Child.prototype = new Parent()

const c = new Child()
console.log(c.card) 

打印结果: image.png 解读:
c 本身没有 card 属性,查找时会沿着原型链依次查找,最终在 GrandParent 的实例上找到。

2. 属性的查找顺序

  • 先查找对象自身属性
  • 找不到则查找 __proto__ 指向的原型对象
  • 依次向上查找,直到 Object.prototype
  • 还找不到则返回 undefined

new 的原理

new 关键字的底层原理其实就是:

  1. 创建一个空对象
  2. 让构造函数的 this 指向这个空对象
  3. 执行构造函数代码
  4. 将这个空对象的 __proto__ 赋值为构造函数的 prototype
  5. 返回这个对象

模拟实现:

function Person() {
    var obj = {} // 1
    Person.call(obj) // 2
    // 3.执行里面原本的代码
    obj.__proto__ = Person.prototype // 4
    return obj // 5
}
const p = new Person()
console.log(p.__proto__ === Person.prototype) // true

constructor 属性的意义

每个原型对象都有一个 constructor 属性,指回它的构造函数。这样,所有实例对象都能通过 constructor 知道自己是由哪个构造函数创建的。

示例:

function Car() {
    this.name = 'su7'
}
console.log(Car.prototype.constructor === Car) // true
const car = new Car()
console.log(car.constructor === Car) // true

注意:
如果你手动重写了原型对象(如 Car.prototype = {}),要记得把 constructor 补回来,否则会丢失原有的指向。

原型的设计意义

  • 节省内存:将通用属性和方法挂载到原型上,所有实例共享,避免重复创建。
  • 实现继承:通过原型链,子类可以继承父类的属性和方法。
  • 灵活扩展:可以随时为某个类型的所有实例添加新方法。

没有原型的对象

有时我们需要创建一个“纯净”的对象,不带任何原型链。这可以通过 Object.create(null) 实现。

const obj = Object.create(null)
console.log(obj.__proto__) // undefined

原型链图片详解

原型链.png 这张图展示了JavaScript中对象和原型链的基本结构关系。我来解析一下:

  1. Foo构造函数

    • f1f2是通过new Foo()创建的实例
    • 这些实例的__proto__指向Foo.prototype
    • Foo.prototypeconstructor指回Foo函数
  2. Object构造函数

    • o1o2是通过new Object()创建的实例
    • 这些实例的__proto__指向Object.prototype
    • Object.prototypeconstructor指回Object函数
    • Object.prototype__proto__指向null(原型链的终点)
  3. Function构造函数

    • FooObjectFunction本身都是函数对象
    • 它们的__proto__都指向Function.prototype
    • Function.prototype__proto__指向Object.prototype
  4. 原型链继承关系

    • Foo.prototype__proto__指向Object.prototype
    • Function.prototype__proto__指向Object.prototype
    • 所有对象最终都继承自Object.prototype

总结

每一个函数(包括构造函数)天生自带一个 prototype 属性(显式原型),它本质上是一个对象。每一个对象都拥有一个 __proto__ 属性(隐式原型),它指向该对象的构造函数的 prototype。而原型链则是通过对象的隐式原型向上找构造函数的显式原型,而显式原型又是对象;就这样一层一层往上找,最终指向 Object.prototype,再到 null