JavaScript 是一门基于对象 的语言,但它早期的面向对象实现方式与传统的类继承语言(如 Java、C++)大相径庭。在没有 class 关键字的年代,JavaScript 通过原型(prototype) 机制实现了面向对象编程的核心特性:封装、继承与多态。即便 ES6 引入了 class 语法,其底层依然是原型继承的"语法糖"。理解 JavaScript 的原型系统,是深入理解这门语言的关键。
一、从对象字面量到构造函数:封装的第一步
最初,我们使用对象字面量来创建对象:
javascript
var cat1 = { name: '小白', color: '白色' };
这种方式简单直接,但问题也很明显:如果我们需要创建多个结构相同的对象,就会产生大量重复代码。于是,我们尝试用函数封装对象的创建过程:
javascript
function createCat(name, color) {
return { name: name, color: color };
}
但这仍不够"面向对象"。真正的转折点在于构造函数的出现:
javascript
function Cat(name, color) {
this.name = name;
this.color = color;
}
const cat1 = new Cat('小白', '白色');
这里的 new 关键字做了几件重要的事:
- 创建一个空对象
{} - 将这个空对象的
__proto__指向Cat.prototype - 将构造函数内部的
this绑定到这个新对象 - 执行构造函数内部的代码,为对象添加属性
- 返回这个新对象(除非构造函数显式返回其他对象)
这就是 JavaScript 中实例化的基本过程。通过构造函数,我们实现了对"猫"这一类对象的封装,每个实例都拥有相同的属性结构,但属性值可以不同。
二、原型:共享属性和方法的智慧
如果我们在构造函数内部定义方法,每个实例都会创建自己的方法副本,造成内存浪费:
javascript
function Cat(name, color) {
this.name = name;
this.color = color;
this.eat = function() { console.log('吃鱼'); }; // 每个实例都会创建这个函数
}
为了解决这个问题,JavaScript 引入了原型(prototype) 机制。每个函数都有一个 prototype 属性,指向一个对象。当我们通过 new 创建实例时,实例内部的 [[Prototype]](可通过 __proto__ 访问)会指向构造函数的 prototype 对象。
javascript
Cat.prototype.eat = function() { console.log('吃鱼'); };
Cat.prototype.type = '猫科动物';
现在,所有 Cat 的实例都共享同一个 eat 方法和 type 属性。当我们访问 cat1.eat() 时,JavaScript 引擎会先在 cat1 自身上查找 eat 属性,如果找不到,就会沿着原型链向上查找,直到找到为止。
这种机制体现了 JavaScript 的原型继承思想:对象可以从其他对象继承属性和方法,而不是从类继承。
三、原型链与继承的实现
JavaScript 的继承是通过原型链实现的。当我们试图访问一个对象的属性时,引擎会:
- 先在对象自身上查找
- 如果找不到,则通过
__proto__找到它的原型对象并查找 - 继续沿着原型链向上,直到找到属性或到达
null
javascript
function Animal() {
this.species = '动物';
}
Animal.prototype.sayHi = function() {
console.log('hi');
};
function Cat(name, color) {
Animal.apply(this, arguments); // 继承自有属性
this.name = name;
this.color = color;
}
// 设置原型链
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat; // 修复构造函数指向
const cat1 = new Cat('Tom', '黑色');
cat1.sayHi(); // 可以调用父类方法
这里的继承分为两步:
- 自有属性继承 :通过
Animal.apply(this)在子类构造函数中调用父类构造函数,将父类的自有属性复制到子类实例上。 - 原型方法继承 :通过
Object.create(Animal.prototype)创建一个以父类原型为原型的新对象,将其赋值给子类的prototype,这样子类实例就能访问父类原型上的方法。
四、ES6 类语法:优雅的语法糖
ES6 引入的 class 关键字让 JavaScript 的面向对象编程更加直观:
javascript
class Animal {
constructor() {
this.species = '动物';
}
sayHi() {
console.log('hi');
}
}
class Cat extends Animal {
constructor(name, color) {
super(); // 调用父类构造函数
this.name = name;
this.color = color;
}
eat() {
console.log('吃鱼');
}
}
但这只是语法糖。通过 babel 等工具转译后,你会发现底层仍然是基于原型的实现。extends 关键字自动建立了原型链,super() 调用了父类构造函数,class 方法被添加到原型上。
五、深入理解原型相关方法
JavaScript 提供了一系列方法用于操作和检查原型链:
instanceof:检查对象的原型链中是否包含某个构造函数的prototypeisPrototypeOf():检查一个对象是否在另一个对象的原型链上hasOwnProperty():检查属性是否是对象自身的(非继承)in操作符:检查属性是否在对象或其原型链上Object.getPrototypeOf():获取对象的原型(推荐替代__proto__)
结语:JavaScript 的原型式面向对象
JavaScript 的面向对象实现是独特而强大的。它没有采用传统的类继承模型,而是通过原型链实现了对象的继承和共享。这种设计使得 JavaScript 更加灵活:对象可以在运行时修改其原型,实现动态继承;也可以轻松实现对象组合等模式。
理解原型不仅是为了应付面试题,更是为了编写更高效、更优雅的 JavaScript 代码。当你理解了 __proto__、prototype、constructor 这三者的关系,当你能够自如地操作原型链,你就能真正掌握 JavaScript 面向对象编程的精髓。
即使在今天,虽然我们可以使用 class 语法,但理解其背后的原型机制仍然至关重要。因为 JavaScript 的本质没有变:它依然是一门基于原型的语言,而原型,正是这门语言最具特色和最强大的特性之一。