JavaScript 是一门基于原型(Prototype-based)的面向对象语言,它不像 Java 或 C++ 那样使用类(class)来实现继承和共享行为。相反,它通过 原型对象(prototype object) 和 原型链(prototype chain) 机制来实现对象之间的属性继承和方法共享。
对于刚入门 JavaScript 的开发者来说,"原型"和"原型链"常常是一个难以理解的概念。本文将从底层出发,详细讲解原型对象、原型链的工作原理,并结合代码示例帮助你建立清晰的理解框架。
一、JavaScript 对象的本质
在深入了解原型之前,我们需要先明确一个核心概念:在 JavaScript 中,一切皆为对象(Object) 。函数是对象,数组是对象,甚至 null
和 undefined
虽然不是对象,但很多操作都围绕对象模型展开。
每个对象都有一个 [[Prototype]]
在 JavaScript 内部,每个对象都有一个隐藏的内部属性 [[Prototype]]
,它指向另一个对象,这个对象就是该对象的原型。你可以把它理解为对象的"父级"。
当访问一个对象的属性或方法时,如果该对象本身没有这个属性,JavaScript 引擎会自动去它的 [[Prototype]]
上查找,这个过程就构成了所谓的原型链。
javascript
let obj = {};
console.log(obj.toString()); // [object Object]
虽然我们并没有给 obj
定义 toString()
方法,但它依然可以调用这个方法,原因就在于它继承自 Object.prototype
。
二、构造函数与 prototype 属性
在 JavaScript 中,函数也是对象 。而所有函数(除了箭头函数)都有一个特殊的属性:prototype
,它是一个对象,默认包含一个 constructor
属性,指向该函数本身。
构造函数的 prototype 是实例的原型
当我们使用构造函数创建对象时,新对象的 [[Prototype]]
会被设置为构造函数的 prototype
属性所指向的对象。
例如:
javascript
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
};
let p1 = new Person('Alice');
p1.sayHello(); // Hello, my name is Alice
在这个例子中:
Person.prototype
是一个对象。p1.__proto__
(即[[Prototype]]
)指向Person.prototype
。- 所以
p1
可以访问到定义在Person.prototype
上的sayHello
方法。
⚠️ 注意:
__proto__
是非标准但广泛支持的属性,建议使用Object.getPrototypeOf(obj)
来获取对象的原型。
三、原型链是如何工作的?
原型链的核心思想是:对象之间可以通过原型链接起来,形成一条链式结构,在访问属性时沿着这条链向上查找。
示例说明
让我们来看一个更复杂的例子:
javascript
function Animal() {}
Animal.prototype.eat = function() {
console.log('Animal is eating.');
};
function Dog() {}
Dog.prototype = Object.create(Animal.prototype); // 设置原型链
Dog.prototype.bark = function() {
console.log('Woof!');
};
let dog = new Dog();
dog.eat(); // Animal is eating.
dog.bark(); // Woof!
在这段代码中:
Dog.prototype
被设置为Animal.prototype
的副本(通过Object.create()
创建)。- 所以
Dog
实例的[[Prototype]]
指向Dog.prototype
,而Dog.prototype
的[[Prototype]]
又指向Animal.prototype
。 - 当调用
dog.eat()
时,JavaScript 引擎发现dog
自身没有eat
方法,于是顺着原型链找到Dog.prototype
,也没有,继续找到Animal.prototype
,最终找到了eat
方法并执行。
这就是原型链的基本工作方式。
四、原型链的终点 ------ null
原型链并不是无限延伸的。它的终点是 null
。也就是说,原型链的最顶端通常是 Object.prototype
,而 Object.prototype
的原型是 null
。
我们可以验证一下:
javascript
console.log(Object.getPrototypeOf(Object.prototype)); // null
这说明 Object.prototype
是原型链的尽头。一旦到达这里还没有找到对应的属性或方法,就会返回 undefined
。
五、构造函数、原型、实例之间的关系图解
为了更直观地理解它们的关系,我们可以画出一张图:
lua
构造函数: Person
|
v
Person.prototype(原型对象)
|
v
实例 p1, p2, ... 的 [[Prototype]]
- 构造函数
Person
有一个prototype
属性,指向其原型对象。 - 所有通过
new Person()
创建的实例,其[[Prototype]]
都指向Person.prototype
。 - 原型对象本身也有自己的
[[Prototype]]
,构成完整的原型链。
六、原型链中的继承机制
JavaScript 的继承本质上就是原型链的构建过程。要实现继承,通常的做法是让子类的原型指向父类的一个实例或者其原型对象。
经典继承方式:借用构造函数 + 原型链
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 = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
let child = new Child('Tom', 5);
child.sayName(); // Tom
在这个例子中:
- 使用
Parent.call(this, name)
在子类中调用父类构造函数,实现属性继承。 - 使用
Object.create(Parent.prototype)
将子类的原型指向父类原型,从而继承方法。
七、原型链的性能与内存优化
由于原型上的属性和方法是被多个实例共享的,因此非常适合用来存储不依赖于具体实例的方法(如工具函数),这样可以节省内存。
例如:
javascript
function Rectangle(width, height) {
this.width = width;
this.height = height;
}
Rectangle.prototype.getArea = function() {
return this.width * this.height;
};
上面的例子中,getArea
方法只在 Rectangle.prototype
上定义一次,所有实例都可以访问,避免了重复定义方法造成的内存浪费。
八、常见误区与注意事项
❌ 误区一:直接修改内置对象的 prototype
虽然技术上可行,但修改 Object.prototype
、Array.prototype
等内置对象的原型是非常危险的行为,可能导致与其他库冲突或破坏现有代码。
✅ 正确做法:
如果你希望扩展某些功能,可以通过模块化的方式封装成工具函数,而不是污染原型链。
javascript
// 不推荐
Array.prototype.myMethod = function() { ... }
// 推荐
function myArrayMethod(arr) { ... }
❌ 误区二:把所有方法都写在构造函数里
有些开发者习惯在构造函数中定义方法:
javascript
function Person(name) {
this.name = name;
this.sayHello = function() {
console.log('Hello, ' + this.name);
};
}
这种做法会导致每次创建实例时都重新定义一次 sayHello
方法,造成资源浪费。正确做法应是将其定义在原型上。
九、ES6 类语法糖下的原型本质
ES6 引入了 class
语法,使得 JavaScript 的面向对象编程更加接近传统 OOP 语言的风格。但实际上,class
只是原型机制的语法糖。
例如:
javascript
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
console.log('Hello, ' + this.name);
}
}
等价于:
javascript
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log('Hello, ' + this.name);
};
所以,即使使用 class
,原型链依然是 JavaScript 实现继承和共享方法的核心机制。
十、总结
原型对象和原型链是 JavaScript 面向对象编程的基础,也是区别于其他语言的重要特性。它们不仅决定了对象如何继承属性和方法,也影响着程序的性能和可维护性。
通过本文的学习,你应该已经掌握了以下内容:
- 原型对象(prototype) :每个函数都有一个
prototype
属性,用于存放可以被其实例共享的属性和方法。 - 原型链(prototype chain) :对象通过
[[Prototype]]
形成的一条链,在属性查找时逐层向上查找。 - 继承机制:通过设置子类的原型为父类的原型或实例,实现继承。
- 性能优化:将公共方法放在原型上,避免重复定义。
- ES6 class 的本质:只是对原型机制的封装,底层依然是原型链。
掌握这些知识,不仅能让你写出更高效、更优雅的代码,也能帮助你在阅读源码或调试复杂问题时游刃有余。
参考资料:
- MDN Web Docs - Inheritance and the prototype chain
- 《JavaScript高级程序设计》第四版
- 《你不知道的JavaScript(上卷)》
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!更多 JavaScript 进阶内容将持续更新。