"在JavaScript中,对象是原型的奴隶,而原型是对象的祖先。" ------ 无名开发者
引言:JavaScript的OOP独特之道
在JavaScript的世界里,理解和掌握面向对象编程(OOP)的概念是构建复杂、可维护应用的关键。尽管JavaScript最初设计时并没有类的概念,但通过其独特的原型机制,它能够实现强大的OOP功能。本文将探索JavaScript的原型世界,揭示其OOP实现的精妙之处。
一、从对象字面量到构造函数
1.1 对象字面量的局限性
当我们需要创建多个具有相同结构的对象时,对象字面量方式显得笨拙且低效:
javascript
// 低效的对象创建方式
const person1 = {
name: '张三',
age: 20,
say() {
console.log(`你好,我是${this.name}`);
}
};
const person2 = {
name: '李四',
age: 22,
say() {
console.log(`你好,我是${this.name}`);
}
};
为了解决这个问题,JavaScript允许我们定义构造函数并使用new
关键字实例化对象。这不仅提高了代码复用性,还让我们能够更好地组织代码结构。
javascript
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function() {
console.log(`你好,我是${this.name}`);
};
const p1 = new Person('王五', 25);
const p2 = new Person('赵六', 28);
p1.say(); // "你好,我是王五"
p2.say(); // "你好,我是赵六"
二、深入原型三要素
2.1 构造函数(Constructor)
- 命名通常以大写字母开头(约定)
- 通过
new
关键字调用时创建新对象 - 函数内部
this
指向新实例
javascript
function Animal(name) {
this.name = name; // 实例属性
}
2.2 原型对象(Prototype)
- 每个函数自动获得
prototype
属性 - 存放所有实例共享的属性和方法
- 包含
constructor
属性指向构造函数
javascript
Animal.prototype.eat = function() {
console.log(`${this.name}正在进食`);
};
2.3 实例(Instance)
- 通过
new
操作符创建的对象 - 包含隐藏属性
__proto__
指向构造函数的原型 - 可以访问原型链上的属性和方法
javascript
const cat = new Animal('咪咪');
console.log(cat.__proto__ === Animal.prototype); // true
三、图解原型链:JavaScript的继承机制

原型链关键点:
- 所有实例的
__proto__
指向其构造函数的prototype
- 原型对象的
__proto__
指向Object.prototype
Object.prototype.__proto__
为null
,是原型链终点- 属性查找沿原型链向上进行
四、ES6之前的原型式继承
在ES6之前,JavaScript没有class
关键字,但可以通过多种方式模拟类的行为和继承机制。
4.1 原型链继承
这是最基础也是最原始的继承方式:
javascript
function Parent() {
this.parentProp = '父类属性';
}
Parent.prototype.parentMethod = function() {
console.log('父类方法');
};
function Child() {}
Child.prototype = new Parent();
const child = new Child();
child.parentMethod(); // 父类方法
缺点:
- 无法传递参数给父类构造函数
- 所有子类实例共享父类的引用类型属性(共享状态问题)
4.2 借用构造函数(call/apply)
为了解决上述问题,可以使用call()
或apply()
来"借用"父类构造函数:
javascript
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
function Child(name, age) {
Parent.call(this, name); // 借用构造函数
this.age = age;
}
const c1 = new Child('小明', 10);
c1.colors.push('green');
const c2 = new Child('小红', 12);
console.log(c2.colors); // ["red", "blue"],不共享颜色数组
优点:
- 可传参
- 避免引用类型共享问题
缺点:
- 方法必须定义在构造函数内,不能复用
4.3 组合继承(原型链 + 构造函数)
结合前两种方式,形成组合继承:
javascript
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent(); // 设置原型链
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(this.age);
};
const c = new Child('Tom', 5);
c.sayName(); // Tom
c.sayAge(); // 5
优点:
- 支持向父类传参
- 实现了属性独立、方法共享
缺点:
- 调用了两次父类构造函数(性能略差)
五、ES6及之后的class语法
ES6引入了class
关键字,使JavaScript的OOP写法更接近其他经典OOP语言,但本质上仍然是基于原型的实现。
5.1 class基本语法
javascript
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
say() {
console.log(`你好,我是${this.name}`);
}
}
const p = new Person('Alice', 25);
p.say(); // 你好,我是Alice
特点:
- 使用
class
声明类 constructor
为构造函数- 方法定义在类体内,自动添加到原型上
- 不支持变量提升(必须先定义后使用)
5.2 类的继承
javascript
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类构造函数
this.breed = breed;
}
bark() {
console.log('Woof!');
}
}
const d = new Dog('Buddy', 'Golden Retriever');
d.speak(); // Buddy makes a noise.
d.bark(); // Woof!
关键点:
- 使用
extends
关键字继承 - 子类构造函数中必须调用
super()
初始化父类 super.methodName()
用于调用父类方法
六、class vs 原型继承:本质与差异
特性 | ES5原型继承 | ES6+ class |
---|---|---|
核心机制 | 原型链 | 原型链(语法糖) |
语法风格 | 函数 + prototype | 类风格 |
继承方式 | 手动设置原型链 | extends 关键字 |
构造函数 | 自定义函数 | constructor 方法 |
方法共享 | 定义在prototype上 | 自动绑定到原型 |
私有成员 | 无法直接定义 | 使用# 符号定义私有字段 |
子类调用父类 | Parent.call(this) |
super() |
6.1 class是语法糖
虽然class
看起来像是真正的类系统,但其底层仍然是基于原型的机制。例如:
javascript
class Person {}
console.log(typeof Person); // "function"
说明class
本质上是一个函数,只是增加了新的语法结构。
6.2 class的优势
- 更清晰的代码结构
- 更好的封装性和可读性
- 支持静态方法、getter/setter等特性
- 支持私有字段(
#name
) - 更容易实现继承逻辑(
super
、extends
)
结语
JavaScript的OOP机制经历了从原型继承到class语法的演变,但其核心始终是基于原型的机制。无论是使用传统的原型方式还是现代的class
语法,理解原型链和继承原理都是写出高质量JavaScript代码的关键。
"不要被语法迷惑,要理解背后的机制。"
掌握这两种方式的异同,有助于我们在不同场景下做出合适的选择,并写出更加优雅、高效、可维护的代码。