JavaScript prototype(原型对象):理解对象继承的底层机制
在 JavaScript 的世界里,一切皆对象。但你有没有想过,为什么一个对象能“借用”另一个对象的方法?为什么我们能调用 Array.prototype.push?这背后的核心机制,就是 JavaScript prototype(原型对象)。
如果你是初学者,刚接触构造函数和对象创建,可能对 prototype 这个概念感到陌生。如果你是中级开发者,可能知道它存在,但不清楚它在内存中是如何运作的。这篇文章,就是为你准备的——从零开始,一步步揭开 JavaScript prototype 的神秘面纱。
什么是原型对象?一个“模板”式的共享机制
在 JavaScript 中,每个函数都有一个名为 prototype 的属性,它是一个对象。这个对象就是所谓的“原型对象”。当你用 new 关键字创建一个对象时,新对象会自动继承这个原型对象上的所有属性和方法。
想象一下:你有一张“设计图纸”(原型对象),每次用这张图纸打印出一个产品(实例对象)。这些产品虽然独立存在,但都共享同一个设计模板。这就是原型链的运作逻辑。
// 定义一个构造函数
function Animal(name) {
this.name = name; // 实例属性
}
// 在原型对象上添加方法
Animal.prototype.speak = function () {
console.log(`${this.name} 发出了声音`);
};
// 创建两个实例
const dog = new Animal("旺财");
const cat = new Animal("咪咪");
dog.speak(); // 旺财 发出了声音
cat.speak(); // 咪咪 发出了声音
注释:
Animal.prototype.speak是在原型对象上定义的方法。dog和cat都能访问它,但方法只在内存中存在一份,节省了资源。
原型链:对象如何“找爸爸”?
当你访问一个对象的属性或方法时,JavaScript 不是直接去对象本身查找,而是先在对象自身找,找不到就去它的原型对象找,再找不到就去原型的原型找……直到找到 null 为止。
这个链条,就叫 原型链(prototype chain)。
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
console.log(`你好,我是 ${this.name}`);
};
const john = new Person("约翰");
// 访问实例属性(直接找)
console.log(john.name); // 约翰
// 访问原型方法(通过原型链查找)
john.greet(); // 你好,我是 约翰
// 查看原型链
console.log(john.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
注释:
__proto__是对象内部的链接,指向其构造函数的prototype。虽然不推荐在生产中使用,但用于理解原型链非常直观。
构造函数 vs 原型对象:谁在“造”对象?
构造函数是创建对象的“工厂”,而原型对象是这个工厂提供的“通用工具库”。构造函数负责初始化实例的私有属性,原型对象则负责提供公共方法。
function Car(brand, color) {
this.brand = brand; // 实例属性,每个对象都有自己的副本
this.color = color;
}
// 公共方法放在原型上
Car.prototype.start = function () {
console.log(`${this.brand} 的 ${this.color} 车启动了`);
};
Car.prototype.stop = function () {
console.log(`${this.brand} 的车熄火了`);
};
const bmw = new Car("宝马", "黑色");
const benz = new Car("奔驰", "白色");
bmw.start(); // 宝马 的 黑色 车启动了
benz.stop(); // 奔驰 的车熄火了
// 每个实例的 brand 和 color 都是独立的
console.log(bmw.brand); // 宝马
console.log(benz.brand); // 奔驰
注释:
brand和color是每个实例独有的,所以放在构造函数里。而start和stop是所有车共用的,放在原型上,避免重复定义。
原型对象的动态性:修改随时生效
原型对象是动态的!你在运行时修改原型,所有已创建的实例都能立刻访问到新内容。
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function () {
console.log(`${this.name} 汪汪叫`);
};
const puppy = new Dog("小黄");
// 调用方法
puppy.bark(); // 小黄 汪汪叫
// 动态添加新方法
Dog.prototype.wagTail = function () {
console.log(`${this.name} 摇尾巴`);
};
// 即使实例已创建,也能调用新方法
puppy.wagTail(); // 小黄 摇尾巴
注释:JavaScript 的原型是“活”的。你修改了
Dog.prototype,所有基于Dog创建的对象都会继承这些变化。这在开发中非常有用,比如给某个类动态添加工具方法。
原型与继承:实现“类式”行为的基石
虽然 JavaScript 没有传统类(class),但通过原型,我们可以模拟出类似类的行为。这正是 class 语法糖的背后原理。
// 基类
function Shape(color) {
this.color = color;
}
Shape.prototype.getArea = function () {
return "面积未知";
};
// 子类
function Rectangle(width, height, color) {
Shape.call(this, color); // 继承属性
this.width = width;
this.height = height;
}
// 建立原型链:Rectangle.prototype 继承自 Shape.prototype
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
// 重写父类方法
Rectangle.prototype.getArea = function () {
return this.width * this.height;
};
// 创建实例
const rect = new Rectangle(5, 3, "红色");
console.log(rect.getArea()); // 15
console.log(rect.color); // 红色
注释:
Object.create()创建一个新对象,其原型指向Shape.prototype。这样Rectangle实例就能通过原型链访问getArea方法。Shape.call(this, color)是为了在子类中调用父类构造函数。
常见误区与最佳实践
误区一:误以为 prototype 是实例的属性
function Test() {}
console.log(Test.prototype); // 是一个对象
console.log(new Test().prototype); // undefined!实例没有 prototype 属性
注释:
prototype是函数的属性,不是对象的属性。实例只能通过__proto__访问原型。
误区二:直接修改 prototype 但忘记 constructor
function Foo() {}
Foo.prototype = { method: function () {} };
const f = new Foo();
console.log(f.constructor === Foo); // false!constructor 指向了新对象
注释:推荐显式修复构造函数指向:
Foo.prototype = { method: function () {} };
Foo.prototype.constructor = Foo;
最佳实践建议:
- 将公共方法放在
prototype上,避免重复创建 - 使用
Object.create()建立原型链,更清晰 - 不要直接修改
prototype的constructor,除非你清楚后果
总结:掌握 prototype,才能真正理解 JavaScript
JavaScript prototype(原型对象)并不是一个“高级技巧”,而是理解整个语言机制的核心。它决定了对象如何共享行为、如何实现继承、如何优化内存。
当你能清晰地说出“对象通过原型链查找方法”时,你就真正迈入了 JavaScript 的深层世界。它不像 Java 的 extends 那样显式,但它更灵活、更强大。
无论你是初学者还是中级开发者,花时间理解 prototype,都能让你写出更高效、更优雅的代码。
下一次,当你看到 Array.prototype.map 或 String.prototype.trim 时,不再只是调用,而是能理解:这一切,都源于一个简单的原型对象机制。