JavaScript 面向对象编程(OOP):从原始模式到原型继承
本文系统梳理 JavaScript 中面向对象编程的核心思想与实现方式,涵盖封装、原型链、继承等关键概念,并对比 ES5 与 ES6 的不同写法。适合初学者理解 JS OOP 的底层机制,也帮助进阶开发者巩固基础。
一、JavaScript 是"基于对象"的语言
JavaScript 虽然常被归类为"面向对象语言",但它没有传统意义上的类(Class) (ES6 之前)。它是一种 基于原型(Prototype-based) 的语言,几乎所有值(包括基本类型)在运行时都能表现为对象。
- 早期 JS 没有
class关键字,但可以通过函数模拟"构造器"。 - ES6 引入的
class只是语法糖,底层依然依赖原型链机制。
二、生成实例对象的原始模式
最朴素的方式是使用对象字面量:
css
var cat1 = {
name: "加菲猫",
color: "橘色"
};
var cat2 = {
name: "coke",
color: "黑色"
};
问题:
- 代码重复;
- 无法批量创建;
- 实例之间无关联,难以判断"是否属于同一类"。
三、构造函数模式:封装实例化过程
通过函数 + new 关键字实现"类"的效果:
ini
function Cat(name, color) {
this.name = name;
this.color = color;
}
const cat1 = new Cat('加菲猫', '橘色');
const cat2 = new Cat('coke', '黑色');
console.log(cat1 instanceof Cat); // true
console.log(cat1.constructor === Cat); // true
instanceof:
- 作用 :检测构造函数的
prototype是否出现在对象的原型链上。 - 返回值:布尔值。
- 用途:判断对象是否为某构造函数的实例。
new 的执行过程(关键!):
- 创建一个空对象
{}; - 将该对象的
__proto__指向Cat.prototype; - 执行构造函数,
this指向新对象; - 返回该对象(若构造函数无显式返回)。
⚠️ 若直接调用
Cat()(不加new),this指向全局对象(如window),造成污染。
四、原型(Prototype)模式:解决方法重复问题
如果在构造函数中定义方法,每个实例都会拥有独立副本,浪费内存:
ini
function Cat(name, color) {
this.name = name;
this.color = color;
// ❌ 每个实例都新建一个 eat 函数
this.eat = function() { console.log('吃鱼'); };
}
解决方案:将公共属性/方法挂载到 prototype 上:
ini
function Cat(name, color) {
this.name = name;
this.color = color;
}
Cat.prototype.type = '猫';
Cat.prototype.eat = function() {
console.log('喜欢 eat jerry');
};
const cat1 = new Cat('tom', '黑色');
const cat2 = new Cat('加菲猫', '橘色');
console.log(cat1.type); // '猫'
cat1.eat(); // 共享方法
原型链查找机制:
- 访问属性时,先查实例自身(
hasOwnProperty为true); - 若未找到,则沿
__proto__向上查找prototype; - 直到
Object.prototype,最终为null。
arduino
console.log(cat1.hasOwnProperty('name')); // true(自有属性)
console.log(cat1.hasOwnProperty('type')); // false(来自 prototype)
console.log('type' in cat1); // true(可访问,无论来源)
Object.prototype.hasOwnProperty(prop):
- 作用 :判断对象自身(不包括原型链)是否拥有指定属性。
- 返回值:布尔值。
- 用途:区分自有属性和继承属性。
五、继承:让子类拥有父类的能力
1. 借用构造函数(仅继承实例属性)
ini
function Animal() {
this.species = '动物';
}
function Cat(name, color) {
Animal.apply(this); // 绑定 this,继承实例属性
this.name = name;
this.color = color;
}
apply:
- 作用 :调用一个函数,并显示指定其内部
this的值,同时传入参数数组(或类数组)。 - 常用于:在子类构造函数中"借用"父类构造函数,实现属性继承。
✅ 优点:可传承,支持多继承。
❌ 缺点:无法继承父类原型上的方法 (如 Animal.prototype.sayHi)。
2. 原型链继承(继承原型方法)
ini
function Animal() {
this.species = '动物';
}
Animal.prototype.sayHi = function() {
console.log('你好,我是动物');
};
function Cat(name, color) {
Animal.apply(this); // 继承实例属性
this.name = name;
this.color = color;
}
// 关键:让 Cat.prototype 指向 Animal 实例
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat; // 修复 constructor 指向
const cat = new Cat('加菲猫', '橘色');
cat.sayHi(); // ✅ 成功继承原型方法
这是经典的 组合继承(Combination Inheritance) :
- 构造函数继承属性(支持传参);
- 原型链继承方法(共享、高效)。
六、ES6 Class:语法糖,更清晰的 OOP 写法
javascript
class Animal {
constructor() {
this.species = '动物';
}
sayHi() {
console.log('你好,我是动物');
}
}
class Cat extends Animal {
constructor(name, color) {
super(); // 等价于 Animal.call(this)
this.name = name;
this.color = color;
}
eat() {
console.log('喜欢 eat jerry');
}
}
const cat1 = new Cat('tom', '黑色');
cat1.sayHi(); // 继承自 Animal
cat1.eat(); // 自身方法
注意:
class本质仍是基于原型;super()必须在子类constructor中调用;- 方法自动绑定到
prototype,无需手动设置。
七、总结:JS OOP 的演进路径
| 方式 | 特点 | 适用场景 |
|---|---|---|
| 对象字面量 | 简单直接 | 单例、配置对象 |
| 构造函数 | 封装实例化 | 需要多个相似对象 |
| 原型模式 | 共享方法,节省内存 | 大量实例 + 公共行为 |
| 组合继承 | 属性 + 方法完整继承 | 传统 ES5 继承方案 |
| ES6 Class | 语法简洁,语义清晰 | 现代项目首选 |
八、延伸思考
-
为什么 JS 不直接用类?
因其动态性与灵活性,原型链更适合运行时修改行为(如 monkey patching)。
-
__proto__vsprototype?prototype是函数的属性,用于构建原型链;__proto__是对象 的内部属性,指向其构造函数的prototype。
-
如何判断继承关系?
javascriptcat1 instanceof Cat; // true Cat.prototype.isPrototypeOf(cat1); // true
📌 记住 :无论用
function还是class,JavaScript 的 OOP 核心始终是 原型链 。理解prototype、__proto__和constructor的关系,是掌握 JS 面向对象的关键。
参考资料:
- 《JavaScript 高级程序设计》(红宝书)
- MDN Web Docs: Inheritance and the prototype chain