在前端开发中,JavaScript 的面向对象编程(OOP)一直是一个绕不开的话题。虽然 JS 是一种基于对象的语言,但它不像 Java 或 C++ 那样天生支持类(class),而是通过原型链(prototype chain)实现了一套独特的 OOP 机制。今天我们就来一起回顾一下 JS 中面向对象编程的发展历程,从最原始的对象字面量,到构造函数、原型模式,再到 ES6 引入的 class 语法糖。
🧱 1. 原始模式:对象字面量
最开始我们可能这样写一个“猫”:
var Cat = {
name: "",
color: ""
};
var cat1 = {};
cat1.name = "加菲猫";
cat1.color = "橘色";
var cat2 = {};
cat2.name = "黑猫警长";
cat2.color = "黑色";
这种方式虽然直观,但存在明显问题:
- 每次创建实例都要手动赋值,重复代码多;
- 实例之间毫无关联,无法共享行为;
- 不利于维护和扩展。
💡 小结:这是面向对象思想的雏形,但不够优雅,也不够“面向对象”。
🔧 2. 构造函数模式:封装实例化过程
为了解决上述问题,我们引入了构造函数模式:
function Cat(name, color) {
this.name = name;
this.color = color;
}
const cat1 = new Cat('加菲猫', '橘色');
const cat2 = new Cat('黑猫警长', '黑色');
✅ new 关键字做了什么?
- 创建一个空对象
{}; - 将这个空对象的
__proto__指向构造函数的prototype; - 执行构造函数内部代码,给
this添加属性; - 返回这个新对象。
此时我们可以通过 instanceof 判断类型:
console.log(cat1 instanceof Cat); // true
⚠️ 但是!每个实例都拥有自己的方法副本,比如你给
Cat.prototype.eat = function(){...},那所有实例都能调用,但如果直接在构造函数里定义方法,就会造成内存浪费。
🔄 3. 原型模式:共享方法与属性
为了优化性能,我们将公共方法放到 prototype 上:
function Cat(name, color) {
this.name = name;
this.color = color;
}
Cat.prototype.type = '猫科动物';
Cat.prototype.eat = function() {
alert('喜欢jerry');
};
const cat1 = new Cat('tom', '黑色');
console.log(cat1.type); // 猫科动物
cat1.eat(); // 调用原型上的方法
🔍 如何判断属性来源?
console.log(cat1.hasOwnProperty('name')); // true(实例自有)
console.log(cat1.hasOwnProperty('type')); // false(来自原型)
console.log('type' in cat1); // true(可访问)
💡 原型链查找机制:当访问某个属性时,先查实例,再查原型,直到找到或到达顶层。
📚 4. 继承:让子类复用父类功能
假设我们要实现“猫是动物的一种”,可以使用 apply 和原型链结合的方式:
function Animal() {
this.species = '动物';
}
Animal.prototype.sayHi = function() {
console.log('哪哪哪啦');
};
function Cat(name, color) {
Animal.apply(this); // 绑定 this,继承属性
this.name = name;
this.color = color;
}
// 设置原型链
Cat.prototype = new Animal(); // 注意:这会丢失 constructor
然后就可以:
const cat = new Cat('加菲猫', '橘色');
cat.sayHi(); // 哪哪哪啦
❗️ 问题来了:
Cat.prototype.constructor变成了Animal,需要手动修复;- 这种方式容易出错,逻辑复杂。
🤔 所以早期开发者很痛苦,继承机制不直观。
✨ 5. ES6 class:语法糖登场!
终于,在 ES6 中,JavaScript 引入了 class 关键字,让我们可以用更清晰的语法写出面向对象代码:
class Cat {
constructor(name, color) {
this.name = name;
this.color = color;
}
eat() {
console.log('喜欢jerry');
}
}
const cat1 = new Cat('tom', '黑色');
cat1.eat();
✅ 优点:
- 语义清晰,符合传统 OOP 思维;
- 支持继承(
extends)、静态方法、getter/setter 等; - 底层依然是原型链,只是包装得更好用了。
class Animal {
constructor() {
this.species = '动物';
}
sayHi() {
console.log('哪哪哪啦');
}
}
class Cat extends Animal {
constructor(name, color) {
super(); // 调用父类构造函数
this.name = name;
this.color = color;
}
}
🧩 总结:JS OOP 的演变之路
| 阶段 | 方式 | 特点 | 缺点 |
|---|---|---|---|
| 初始 | 对象字面量 | 直观简单 | 无封装,无共享 |
| 第一代 | 构造函数 | 封装实例化 | 方法重复,浪费内存 |
| 第二代 | 原型模式 | 共享方法 | 复杂,难理解 |
| 第三代 | 继承 + prototype | 实现继承 | 逻辑混乱,易错 |
| 当代 | ES6 class | 清晰简洁 | 本质仍是原型 |
🌟 核心结论:
class是语法糖,底层依然是原型链。它让 JS 更接近传统的面向对象语言,降低了学习成本,提升了可读性。
🚀 写在最后
作为开发者,我们不仅要会写 class,更要理解它的背后——原型链才是 JS 面向对象的根基。当你看到 cat.__proto__.constructor === Cat 时,你就真正懂了 JS 的灵魂。