JavaScript 原型探秘:揭开 "类" 的面纱与继承的真相

51 阅读3分钟

JavaScript 原型探秘:揭开 "类" 的面纱与继承的真相

JavaScript 的面向对象编程(OOP)机制与其他经典 OOP 语言(如 Java、C++)有着本质区别。它没有真正的 class(在 ES6 之前),而是基于原型(Prototype)  来实现封装、继承和多态。理解原型链是掌握 JS OOP 的核心钥匙。


一、JS 的 "类" 从何而来?

  1. 对象字面量的局限
    创建多个结构相似的对象时,字面量方式效率低下且冗余:
    const person1 = { name: 'Alice', greet() { console.log(`Hi, I'm ${this.name}`) } };
    const person2 = { name: 'Bob', greet() { console.log(`Hi, I'm ${this.name}`) } };
// ... 重复劳动,方法在每个对象中都复制一份

构造函数的诞生
JS 通过函数来模拟类的概念。约定:首字母大写的函数被视为"构造函数",用于创建对象。

function Person(name) {
  this.name = name;
  this.greet = function() { console.log(`Hi, I'm ${this.name}`); };
}
const alice = new Person('Alice');
const bob = new Person('Bob');
  1. 问题:greet 方法在每个实例中都是独立的副本,浪费内存。

  2. 构造函数的双重职责
    JS 中的函数身兼两职:

    • 普通函数:直接调用执行逻辑。
    • "类" :当使用 new 操作符调用时,它负责构造对象。

二、原型登场:共享的力量

JS 通过 原型对象(Prototype)  解决共享属性和方法的问题。

  1. 每个函数都有一个 prototype 属性
    当函数被声明时,JS 自动为其创建一个 prototype 属性(一个空对象)。这个对象是构造函数的"原型对象"
function Person(name) {
  this.name = name;
}
console.log(Person.prototype); // {} (一个空对象,但存在!)
  1. 实例的 __proto__ 指向构造函数的原型对象
    使用 new 创建实例时,实例内部会包含一个属性 [[Prototype]](在浏览器环境中通常可通过 __proto__ 访问)。这个 __proto__ 指向其构造函数的 prototype 对象
const alice = new Person('Alice');
console.log(alice.__proto__ === Person.prototype); // true
  1. 共享方法:定义在原型上
    将需要共享的方法(或属性)添加到构造函数的 prototype 对象上:
Person.prototype.greet = function() {
  console.log(`Hi, I'm ${this.name}`);
};
const alice = new Person('Alice');
const bob = new Person('Bob');
alice.greet(); // "Hi, I'm Alice"
bob.greet(); // "Hi, I'm Bob"
console.log(alice.greet === bob.greet); // true! 同一个函数引用

三、原型链:继承的基石

  1. __proto__ 的链式连接
    对象的 __proto__ 属性指向其构造函数的原型对象 (prototype)。这个原型对象本身也是一个对象,它也有自己的 __proto__,指向创建它的构造函数的原型对象。这就形成了一条链:原型链(Prototype Chain)
  2. 默认的链:指向 Object.prototype
    构造函数 Person 本身也是由 Function 构造的,但 Person.prototype 是一个普通对象,它是由 Object 构造函数创建的。因此:
    console.log(Person.prototype.__proto__ === Object.prototype); // true
    
  3. 链的终点:null
    Object.prototype 是绝大多数对象的原型链顶端:
console.log(Object.prototype.__proto__); // null
  1. 访问机制:沿着链查找
    当试图访问一个对象的属性/方法时:

    1. JS 引擎首先在对象自身查找。
    2. 如果没找到,则沿着对象的 __proto__ 向上查找其构造函数的原型对象 (prototype)。
    3. 如果还没找到,则继续沿着原型对象的 __proto__ 向上查找 (Object.prototype)。
    4. 直到找到该属性/方法或到达链的终点 (null),此时返回 undefined
    alice.toString(); // "[object Object]"
// 查找路径:
// 1. alice 自身? -> 无
// 2. alice.__proto__ (Person.prototype) ? -> 无
// 3. Person.prototype.__proto__ (Object.prototype) ? -> 找到 toString!

图示简化原型链 (alice -> Person.prototype -> Object.prototype -> null)

alice (实例)
  ├── name: 'Alice'            <--- 自身属性
  └── __proto__: ------------> Person.prototype
                  ├── greet: function() {...} <--- 共享方法
                  └── __proto__: -----------> Object.prototype
                                      ├── toString: function() {...} <--- 继承的方法
                                      ├── hasOwnProperty: function() {...}
                                      └── __proto__: null

四、new 操作符的幕后工作

new Person('Alice') 在底层执行了以下关键步骤:

  1. 创建空对象:  const obj = {};

  2. 链接原型:  obj.__proto__ = Person.prototype; (建立原型链)

  3. 绑定 this 并执行构造函数:  Person.call(obj, 'Alice'); (构造函数内部的 this 指向这个新对象 obj)

  4. 返回新对象:

    • 如果构造函数没有显式返回一个对象 (return someObject;),则自动返回 obj
    • 如果构造函数返回了一个非对象(基本类型如 stringnumber),则忽略返回值,仍然返回 obj
    • 如果构造函数返回了一个对象,则返回该对象(此时步骤 1 创建的 obj 可能被丢弃)。

总结:JS 面向对象的精髓

  • 没有真正的类:  JS 的 "类" 本质是函数(构造函数)。
  • 构造函数双重角色:  普通函数 / 对象创建器 (new)。
  • 原型对象 (prototype):  构造函数的属性,用于存储共享的属性和方法
  • 实例连接 (__proto__):  实例通过 __proto__ 指向其构造函数的 prototype 对象,形成访问共享成员的桥梁。
  • 原型链:  对象通过 __proto__ 层层连接形成的链式结构,是实现继承(查找属性和方法)的机制。链的终点是 Object.prototype.__proto__ (即 null)。
  • new 的魔法:  创建空对象、链接原型、绑定 this 执行构造函数、返回对象。

理解原型链是掌握 JavaScript 面向对象编程、继承机制和高级特性的关键。ES6 的 class 语法本质上是基于原型的语法糖,让写法更接近传统 OOP 语言,但其底层原理仍然是本文所阐述的原型机制。