从字面量到原型链:JavaScript 面向对象的完整进化史
JavaScript 是一门基于对象(Object-based) 的语言------你几乎接触到的一切都是对象,甚至连 1、'hello' 这样的原始值,在需要时也会被自动包装成 Number、String 对象。然而,JavaScript 并不是传统意义上的"面向对象语言"(如 Java 或 C++),它没有类(class)的概念(直到 ES6 才引入语法糖形式的 class),其核心机制是原型(Prototype) 。
那么,在没有 class 的年代,我们如何用 JavaScript 实现面向对象编程(OOP)?本文将带你从最原始的对象字面量出发,逐步演进到成熟的原型继承模式,理解 JS OOP 的本质。
一、起点:对象字面量(Object Literal)
最简单的创建对象方式:
javascript
const cat1 = {
name: '小白',
color: '白色',
type: '猫科动物',
eat() {
console.log('喜欢 Jerry');
}
};
const cat2 = {
name: '小黑',
color: '黑色',
type: '猫科动物',
eat() {
console.log('喜欢 Jerry');
}
};
✅ 优点 :简单直观。
❌ 缺点:
- 代码重复;
- 无法批量生成相似对象;
- 实例之间没有"类型"关联,无法判断
cat1是否属于"猫"。
这只是"数据容器",不是"类"的实例。
二、进化:构造函数模式(Constructor Pattern)
为了解决复用问题,我们封装一个函数来生成对象:
ini
function Cat(name, color) {
this.name = name;
this.color = color;
this.type = '猫科动物';
this.eat = function() {
console.log('喜欢 Jerry');
};
}
const cat1 = new Cat('小白', '白色');
const cat2 = new Cat('小黑', '黑色');
new 调用时发生了什么?
- 创建一个空对象
{}; - 将
this指向这个空对象; - 执行函数体,给
this添加属性和方法; - 返回这个新对象(除非显式返回其他对象)。
✅ 优点:
- 可批量创建实例;
- 实例可通过
instanceof Cat判断类型; - 实例间有了"关系"。
❌ 致命缺点:
- 每个实例都拥有自己的一份
eat方法 → 内存浪费; - 公共属性(如
type)也无法共享。
三、优化:原型模式(Prototype Pattern)
JavaScript 的核心思想:把不变的、公共的部分放到原型上。
ini
function Cat(name, color) {
this.name = name;
this.color = color;
}
Cat.prototype.type = '猫科动物';
Cat.prototype.eat = function() {
console.log('喜欢 Jerry');
};
const cat1 = new Cat('小白', '白色');
const cat2 = new Cat('小黑', '黑色');
console.log(cat1.eat === cat2.eat); // true ✅ 共享同一个方法
原型链机制
- 每个函数都有一个
prototype属性(指向原型对象); - 每个实例都有一个内部链接
[[Prototype]](在浏览器中可通过__proto__访问),指向其构造函数的prototype; - 当访问属性时,JS 会先查实例自身,再沿原型链向上查找。
✅ 优点:
- 方法和公共属性共享,节省内存;
- 支持动态扩展:
Cat.prototype.sleep = ...所有实例立即可用。
四、进阶:继承(Inheritance)
OOP 的三大特性之一是继承 。在 JS 中,如何让 Cat 继承 Animal?
1. 借用构造函数(继承实例属性)
ini
function Animal() {
this.species = '动物';
}
function Cat(name, color) {
// 关键:让 Animal 的 this 指向当前 Cat 实例
Animal.apply(this, arguments);
this.name = name;
this.color = color;
}
Animal.apply(this):借用父类构造函数 ,将species添加到子类实例上。- ✅ 继承了实例属性;
- ❌ 未继承原型方法 (如
Animal.prototype.move)。
2. 设置原型链(继承原型方法)
javascript
// 让 Cat.prototype 的原型指向 Animal.prototype
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat; // 修复 constructor 指向
// 补充 Cat 自己的方法
Cat.prototype.purr = function() {
console.log('呼噜呼噜~');
};
现在,Cat 实例既能访问 species(来自 Animal 构造函数),也能调用 move()(来自 Animal.prototype)。
五、现代写法:ES6 class(语法糖)
ES6 引入了 class 语法,但底层仍是原型:
javascript
class Animal {
constructor() {
this.species = '动物';
}
move() {
console.log('I can move!');
}
}
class Cat extends Animal {
constructor(name, color) {
super(); // 等价于 Animal.apply(this)
this.name = name;
this.color = color;
}
purr() {
console.log('呼噜呼噜~');
}
}
extends→ 设置原型链;super()→ 借用父类构造函数;- 一切更清晰,但本质仍是原型继承。
六、总结:JS OOP 的演进路径
| 阶段 | 方式 | 核心思想 | 缺陷 |
|---|---|---|---|
| 1 | 对象字面量 | 直接写对象 | 无法复用,无类型关系 |
| 2 | 构造函数 | 封装生成过程 | 方法不共享,内存浪费 |
| 3 | 原型模式 | 公共部分放原型 | 属性/方法分离,需手动管理 |
| 4 | 原型继承 | apply + Object.create |
写法繁琐,易出错 |
| 5 | ES6 class |
语法糖封装 | 底层仍是原型,需理解本质 |
💡 关键认知 :
JavaScript 的 OOP 不是"类继承",而是"对象委托 "------通过原型链,对象可以委托给另一个对象处理自己不会的事情。
掌握从字面量到原型继承的全过程,你才算真正理解了 JavaScript 面向对象的精髓。即使今天使用 class,也要明白:class 只是原型的语法糖,而原型才是 JS OOP 的灵魂。