JavaScript 原型探秘:揭开 "类" 的面纱与继承的真相
JavaScript 的面向对象编程(OOP)机制与其他经典 OOP 语言(如 Java、C++)有着本质区别。它没有真正的 class
(在 ES6 之前),而是基于原型(Prototype) 来实现封装、继承和多态。理解原型链是掌握 JS OOP 的核心钥匙。
一、JS 的 "类" 从何而来?
- 对象字面量的局限
创建多个结构相似的对象时,字面量方式效率低下且冗余:
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');
-
问题:
greet
方法在每个实例中都是独立的副本,浪费内存。 -
构造函数的双重职责
JS 中的函数身兼两职:- 普通函数:直接调用执行逻辑。
- "类" :当使用
new
操作符调用时,它负责构造对象。
二、原型登场:共享的力量
JS 通过 原型对象(Prototype) 解决共享属性和方法的问题。
- 每个函数都有一个
prototype
属性
当函数被声明时,JS 自动为其创建一个prototype
属性(一个空对象)。这个对象是构造函数的"原型对象" 。
function Person(name) {
this.name = name;
}
console.log(Person.prototype); // {} (一个空对象,但存在!)
- 实例的
__proto__
指向构造函数的原型对象
使用new
创建实例时,实例内部会包含一个属性[[Prototype]]
(在浏览器环境中通常可通过__proto__
访问)。这个__proto__
指向其构造函数的prototype
对象。
const alice = new Person('Alice');
console.log(alice.__proto__ === Person.prototype); // true
- 共享方法:定义在原型上
将需要共享的方法(或属性)添加到构造函数的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! 同一个函数引用
三、原型链:继承的基石
__proto__
的链式连接
对象的__proto__
属性指向其构造函数的原型对象 (prototype
)。这个原型对象本身也是一个对象,它也有自己的__proto__
,指向创建它的构造函数的原型对象。这就形成了一条链:原型链(Prototype Chain) 。- 默认的链:指向
Object.prototype
构造函数Person
本身也是由Function
构造的,但Person.prototype
是一个普通对象,它是由Object
构造函数创建的。因此:console.log(Person.prototype.__proto__ === Object.prototype); // true
- 链的终点:
null
Object.prototype
是绝大多数对象的原型链顶端:
console.log(Object.prototype.__proto__); // null
-
访问机制:沿着链查找
当试图访问一个对象的属性/方法时:- JS 引擎首先在对象自身查找。
- 如果没找到,则沿着对象的
__proto__
向上查找其构造函数的原型对象 (prototype
)。 - 如果还没找到,则继续沿着原型对象的
__proto__
向上查找 (Object.prototype
)。 - 直到找到该属性/方法或到达链的终点 (
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')
在底层执行了以下关键步骤:
-
创建空对象:
const obj = {};
-
链接原型:
obj.__proto__ = Person.prototype;
(建立原型链) -
绑定
this
并执行构造函数:Person.call(obj, 'Alice');
(构造函数内部的this
指向这个新对象obj
) -
返回新对象:
- 如果构造函数没有显式返回一个对象 (
return someObject;
),则自动返回obj
。 - 如果构造函数返回了一个非对象(基本类型如
string
,number
),则忽略返回值,仍然返回obj
。 - 如果构造函数返回了一个对象,则返回该对象(此时步骤 1 创建的
obj
可能被丢弃)。
- 如果构造函数没有显式返回一个对象 (
总结:JS 面向对象的精髓
- 没有真正的类: JS 的 "类" 本质是函数(构造函数)。
- 构造函数双重角色: 普通函数 / 对象创建器 (
new
)。 - 原型对象 (
prototype
): 构造函数的属性,用于存储共享的属性和方法。 - 实例连接 (
__proto__
): 实例通过__proto__
指向其构造函数的prototype
对象,形成访问共享成员的桥梁。 - 原型链: 对象通过
__proto__
层层连接形成的链式结构,是实现继承(查找属性和方法)的机制。链的终点是Object.prototype.__proto__
(即null
)。 new
的魔法: 创建空对象、链接原型、绑定this
执行构造函数、返回对象。
理解原型链是掌握 JavaScript 面向对象编程、继承机制和高级特性的关键。ES6 的 class
语法本质上是基于原型的语法糖,让写法更接近传统 OOP 语言,但其底层原理仍然是本文所阐述的原型机制。