在前端开发的世界里,JavaScript 是一门"看似简单、实则深邃"的语言。尤其在面向对象编程(OOP)方面,它既不像 Java 那样有清晰的类结构,也不像 Python 那样支持多重继承。然而,正是这种灵活性和独特性,使得 JavaScript 的 OOP 模型成为开发者必须深入理解的核心内容之一。
本文将带你从最原始的对象字面量出发,逐步深入构造函数、原型链、继承机制,最终揭开 ES6 class 背后的本质------基于原型的面向对象编程。
一、万物皆对象?JavaScript 的"伪 OOP"之谜
JavaScript 常被称作"基于对象的语言"。你几乎遇到的所有东西------字符串、数字、数组、函数------背后都有对应的对象包装(如 String、Number、Array、Function)。但严格来说,JavaScript 并不是传统意义上的面向对象语言。
- 它没有原生的"类"(直到 ES6 才引入
class语法糖) - 没有真正的构造器(constructor)概念
- 继承不是通过类层级,而是通过原型链(Prototype Chain)
这导致很多初学者困惑:"我到底是在写面向对象,还是在操作一堆对象?"
二、从对象字面量开始:最原始的"实例化"
让我们看一段来自 1.js 的代码:
ini
var Cat = { name: "", color: "" };
var cat1 = {};
cat1.name = "加菲猫";
cat1.color = "橘色";
var cat2 = {};
cat2.name = "黑猫警长";
cat2.color = "黑色";
这段代码创建了两个"猫"对象。虽然能用,但问题显而易见:
- 重复代码多:每次都要手动赋值
- 无模板约束 :无法保证每个实例都有
name和color - 实例之间毫无关系 :
cat1和cat2是完全独立的,无法共享方法
这就像手工作坊造车------每辆车都单独打造,效率低、难维护。
三、构造函数:封装"实例生成"的过程
为了解决上述问题,我们引入构造函数:
ini
function Cat(name, color) {
this.name = name;
this.color = color;
}
var cat1 = new Cat("加菲猫", "橘色");
var cat2 = new Cat("黑猫警长", "黑色");
当你使用 new 调用函数时,JavaScript 引擎会:
- 创建一个空对象
{} - 将
this指向这个新对象 - 执行函数体,给
this添加属性 - 默认返回这个新对象
这样,我们就有了"类模板"的雏形。但仍有缺陷:
- 方法无法复用 :如果给每个实例添加
sayHello方法,会重复创建函数,浪费内存
ini
function Cat(name, color) {
this.name = name;
this.color = color;
this.sayHello = function() { console.log("Meow!"); }; // ❌ 每个实例都新建函数
}
四、原型(Prototype):共享方法的终极方案
JavaScript 的精妙之处在于 原型机制。
每个函数都有一个 prototype 属性,指向一个对象;每个实例都有一个内部属性 [[Prototype]](可通过 __proto__ 访问),指向其构造函数的 prototype。
于是我们可以这样优化:
javascript
function Cat(name, color) {
this.name = name;
this.color = color;
}
Cat.prototype.sayHello = function() {
console.log(`${this.name} says Meow!`);
};
现在,所有 Cat 实例共享同一个 sayHello 方法,内存高效,逻辑清晰。
关键理解:
- 实例属性放在构造函数中(如
name,color)- 公共方法放在
prototype上(如sayHello)
这就是经典的 "构造函数 + 原型" 模式,也是早期 JS OOP 的标准实践。
五、继承:如何让"猫"继承"动物"?
假设我们有一个父类 Animal:
javascript
function Animal(species) {
this.species = species;
}
Animal.prototype.breathe = function() {
console.log("I am breathing...");
};
如何让 Cat 继承 Animal?
方案1:借用构造函数(不完整继承)
kotlin
function Cat(name, color) {
Animal.call(this, "猫科"); // 绑定 this,继承实例属性
this.name = name;
this.color = color;
}
✅ 优点:能继承父类的实例属性
❌ 缺点:无法继承父类原型上的方法 (如 breathe)
方案2:原型链继承(经典方案)
ini
Cat.prototype = new Animal("猫科");
Cat.prototype.constructor = Cat; // 修复 constructor 指向
现在 cat1 instanceof Animal 为 true,且能调用 breathe()。
但问题又来了:所有子类实例共享父类实例属性,若父类有引用类型属性(如数组),会互相污染。
方案3:组合继承(推荐)
结合前两种方式:
javascript
function Cat(name, color) {
Animal.call(this, "猫科"); // 继承属性
this.name = name;
this.color = color;
}
Cat.prototype = Object.create(Animal.prototype); // 继承方法
Cat.prototype.constructor = Cat;
Cat.prototype.sayHello = function() {
console.log(`${this.name} says Meow!`);
};
这才是真正意义上的"继承":属性私有,方法共享,结构清晰。
六、ES6 的 class:语法糖还是革命?
ES6 引入了 class 语法:
javascript
class Animal {
constructor(species) {
this.species = species;
}
breathe() {
console.log("I am breathing...");
}
}
class Cat extends Animal {
constructor(name, color) {
super("猫科");
this.name = name;
this.color = color;
}
sayHello() {
console.log(`${this.name} says Meow!`);
}
}
看起来像 Java/Python?但请记住:
class只是原型继承的语法糖!底层仍是基于原型链。
你可以验证:
javascript
console.log(Cat.prototype.__proto__ === Animal.prototype); // true
所以,理解原型,才是掌握 JS OOP 的钥匙。
七、总结:JS OOP 的演进脉络
| 阶段 | 特点 | 问题 |
|---|---|---|
| 对象字面量 | 简单直接 | 无法复用,无模板 |
| 构造函数 | 封装实例化 | 方法不能共享 |
| 原型模式 | 方法共享 | 实例属性需在构造函数中定义 |
| 组合继承 | 属性+方法完美分离 | 稍显冗长 |
| ES6 class | 语法简洁 | 仍是原型,需理解底层 |
写在最后
JavaScript 的面向对象不是"残缺",而是"另辟蹊径"。它的原型系统赋予了语言极大的动态性和灵活性------你可以随时修改原型、扩展内置对象、实现 mixin 等高级模式。
不要被 class 迷惑,也不要畏惧 prototype。
真正理解原型链,你才能写出高性能、可维护、可扩展的 JavaScript 代码。