Skip to content

JavaScript 原型与原型链深度解析

1. 核心思想:JS 是一门基于原型的语言

1.1 设计初衷

  • 设计者: 布兰登·艾克 (Brendan Eich)。
  • 灵感来源: 借鉴了 SelfSmalltalk 这两门基于原型的语言。
  • 设计选择:
    • 为何选择原型而非类?
      • JS 最初的设计目标是为非专业的网页设计者提供一个简单的脚本工具,而非像 Java/C++ 那样的专业编程语言。
      • 设计者初期不打算引入复杂的“类” (Class) 概念。
    • 结果: JS 的对象系统是基于原型的,而非传统的面向对象(基于类)。

1.2 基本概念

  • JS 是其最重要的语言特性之一,也是面试中的高频考点(例如:美团2019年面试题)。
  • 在 JS 中,对象并非从类实例化而来,而是从原型对象 (Prototype Object) 构建而来

2. 原型模式的直接体现:Object.create()

Object.create() 是 ES5 提供的一个方法,它可以让我们直接体验基于原型的对象创建过程。

2.1 基本用法

通过克隆一个“原型”对象来创建一个新对象。

javascript
// 1. 定义一个原型对象 person
const person = {
    arms: 2,
    legs: 2
};

// 2. 使用 Object.create() 创建一个新对象 zhangsan,并指定 person 为其原型
const zhangsan = Object.create(person);

console.log(zhangsan); // {} -> 输出一个空对象
console.log(zhangsan.arms); // 2
console.log(zhangsan.legs); // 2

// 验证原型关系
console.log(zhangsan.__proto__ === person); // true
  • 解释:
    • zhangsan 对象本身是空的。
    • 当我们试图访问 zhangsan.arms 时,JS 引擎在 zhangsan 自身上找不到该属性。
    • 于是,引擎会沿着 zhangsan 的原型链向上查找,找到了其原型对象 person,并返回 person.arms 的值。
    • 我们可以通过非标准的 __proto__ 属性访问一个对象的原型。

2.2 Object.create() 的第二个参数

该方法接收第二个可选参数,用于定义新对象自身的属性(通过属性描述符)。

javascript
const zhangsan = Object.create(person, {
    // 每个键是一个新属性,值是该属性的描述符对象
    name: {
        value: '张三',
        enumerable: true // 设置为 true 才能在 console.log 中直接看到
    },
    age: {
        value: 18,
        enumerable: true
    }
});

console.log(zhangsan); // { name: '张三', age: 18 }
console.log(zhangsan.name); // 张三
console.log(zhangsan.arms); // 2 (从原型 person 继承)

2.3 构建原型链

我们可以通过 Object.create() 链接多个对象,形成一条原型链。

javascript
// zhangsan 继承自 person
const zhangsan = Object.create(person, { /* ...属性... */ });

// zhangxiaosan 继承自 zhangsan
const zhangxiaosan = Object.create(zhangsan, {
    born: {
        value: '北京',
        enumerable: true
    }
});
  • 形成的链条: zhangxiaosan -> zhangsan -> person

2.4 原型链上的属性查找规则

总结:当查找一个对象的属性时,如果该对象自身没有这个属性,则会去该对象的原型对象 (__proto__) 上查找。如果原型对象上还没有,则会继续沿着原型对象的原型向上查找,直到找到该属性或到达原型链的终点 (null)。如果最终没有找到,则返回 undefined

javascript
// 1. 查找自身属性
console.log(zhangxiaosan.born); // '北京'

// 2. 自身没有,去原型 zhangsan 上查找
console.log(zhangxiaosan.name); // '张三'

// 3. zhangsan 也没有,去 zhangsan 的原型 person 上查找
console.log(zhangxiaosan.arms); // 2

// 4. 整条原型链上都没有
console.log(zhangxiaosan.gender); // undefined

// 验证原型链
console.log(zhangxiaosan.__proto__ === zhangsan); // true
console.log(zhangxiaosan.__proto__.__proto__ === person); // true

3. 模拟类:构造函数

随着 JS 的发展,开发者希望能像传统面向对象语言一样,通过“类”来批量生产对象。在 ES6 之前,这是通过构造函数来模拟的。

3.1 基本实现

javascript
// 1. 定义一个构造函数 (首字母通常大写)
function Computer(name, price) {
    // `new` 会创建一个新对象,并把 this 指向它
    this.name = name;
    this.price = price;
}

// 2. 使用 new 关键字创建实例
const apple = new Computer('苹果', 15000);
const huawei = new Computer('华为', 12000);

console.log(apple); // Computer { name: '苹果', price: 15000 }
console.log(huawei); // Computer { name: '华为', price: 12000 }

3.2 方法存放的问题与优化

  • 问题: 如果将方法直接定义在构造函数内部,每个实例都会创建并持有一份独立的方法函数,造成内存浪费。
javascript
function Computer(name, price) {
    this.name = name;
    this.price = price;
    // 每次 new 都会创建一个新的函数,浪费内存
    this.showPrice = function() { /*...*/ };
}
  • 优化: 将共享的方法挂载到构造函数的原型对象上。

核心原则:属性(通常是每个实例独有的值)放在构造函数里,方法(所有实例共享的功能)放在原型对象上。

javascript
function Computer(name, price) {
    this.name = name;
    this.price = price;
}

// 将方法挂载到 Computer 的原型对象上
Computer.prototype.showPrice = function() {
    console.log(`${this.name}的电脑价格为${this.price}`);
};

const apple = new Computer('苹果', 15000);
const huawei = new Computer('华为', 12000);

apple.showPrice(); // 苹果的电脑价格为15000
huawei.showPrice(); // 华为的电脑价格为12000
  • 解释: applehuawei 实例自身没有 showPrice 方法,它们会通过原型链在 Computer.prototype 上找到并共享同一个 showPrice 方法。

4. 关键三角关系:prototype, __proto__, constructor

这是理解原型链的基石,必须掌握。

  • 构造函数 (Constructor): 如 Computer
  • 原型对象 (Prototype Object): Computer.prototype
  • 实例对象 (Instance): 如 apple

4.1 三大定律

  1. 实例.__proto__ === 构造函数.prototype

    • 实例对象的 __proto__ 属性,指向其构造函数的 prototype 属性所引用的那个对象(即原型对象)。
  2. 构造函数.prototype.constructor === 构造函数

    • 原型对象天生自带一个 constructor 属性,指回其关联的构造函数。
  3. 实例的 constructor 来自原型

    • 实例对象本身没有 constructor 属性,当访问 实例.constructor 时,它会通过原型链找到 实例.__proto__.constructor,也就是其构造函数。

4.2 代码验证

javascript
function Computer(name, price) { /* ... */ }

const apple = new Computer('苹果', 15000);

// 验证定律 1
console.log(apple.__proto__ === Computer.prototype); // true

// 验证定律 2
console.log(Computer.prototype.constructor === Computer); // true

// 验证定律 3
console.log(apple.constructor === Computer); // true

这个三角关系同样适用于 JS 内置的构造函数,如 Array, Object, Function 等。

javascript
const arr = []; // 等价于 new Array()

console.log(arr.__proto__ === Array.prototype); // true
console.log(Array.prototype.constructor === Array); // true

5. 完整的原型链图谱

这张图揭示了 JS 世界中几乎所有对象和函数的最终联系。

5.1 链条的终点

  • 任何普通对象的原型链,最终都会指向 Object.prototype
  • Object.prototype 是一个特殊的对象,它的原型是 null
  • null 是整个原型链的终点。

代码验证

javascript
const apple = new Computer('苹果', 15000);

// apple -> Computer.prototype -> Object.prototype -> null
console.log(apple.__proto__ === Computer.prototype); // true
console.log(apple.__proto__.__proto__ === Object.prototype); // true
console.log(apple.__proto__.__proto__.__proto__ === null); // true

// Object.prototype 的原型是 null
console.log(Object.prototype.__proto__); // null

5.2 Function 的特殊地位

  • 所有函数都是 Function 的实例。这包括自定义构造函数 (Computer)、内置构造函数 (Object, Array),甚至 Function 自身。
  • 因此,任何函数的 __proto__ 都指向 Function.prototype
  • Function.prototype 也是一个对象,它的原型是 Object.prototype

代码验证

javascript
// Computer 函数的原型是 Function.prototype
console.log(Computer.__proto__ === Function.prototype); // true

// Object 构造函数的原型是 Function.prototype
console.log(Object.__proto__ === Function.prototype); // true

// Function.prototype 的原型是 Object.prototype
console.log(Function.prototype.__proto__ === Object.prototype); // true

6. 回答面试题

Q1: 谈谈你对JS中原型和原型链的理解?

  1. 原型 (Prototype):

    • JS 是基于原型的语言。每个对象都有一个内部链接指向另一个对象,这个对象就是它的“原型”。
    • 可以通过 __proto__ 访问一个对象的原型。
    • 构造函数有一个 prototype 属性,它指向一个对象,这个对象将成为由该构造函数创建的所有实例的原型。
    • 原型对象有一个 constructor 属性,指回构造函数本身,形成一个循环引用(三角关系)。
  2. 原型链 (Prototype Chain):

    • 当试图访问一个对象的属性时,如果在对象本身找不到,JS 引擎就会去它的原型对象上找。
    • 原型对象本身也是一个对象,它也有自己的原型。这样一层一层链接起来,就形成了一条“原型链”。
    • 属性查找会沿着这条链一直向上,直到找到属性或到达链的终点 null
    • 这种机制是 JS 实现继承的核心。

Q2: 对一个构造函数实例化后,它的原型链指向什么?

假设有一个构造函数 MyConstructor 和一个实例 const myInstance = new MyConstructor()

它的原型链是:

  1. myInstance 的原型 (myInstance.__proto__) 指向 MyConstructor.prototype
  2. MyConstructor.prototype 本身是一个普通对象,所以它的原型 (MyConstructor.prototype.__proto__) 指向 Object.prototype
  3. Object.prototype 的原型 (Object.prototype.__proto__) 指向 null,链条结束。

完整链条: myInstance -> MyConstructor.prototype -> Object.prototype -> null