引言:万物皆对象的困惑
在 JavaScript 的世界里,我们习惯了“万物皆对象”。但与 Java、C++ 等传统面向对象语言不同,JavaScript 并没有类(ES6 之前的语法糖之下)的概念,而是基于原型(Prototype)构建的。
初学者往往对 this、prototype 和 __proto__ 感到困惑:为什么方法要写在 prototype 上?实例是如何访问到构造函数之外的方法的?今天,我们就通过几个简单的代码片段,带你彻底搞懂 JS 的原型式面向对象。
1. 构造函数:对象的“工厂”
在 ES5 时代,我们使用首字母大写的函数作为构造函数来创建对象。构造函数解决了我们需要批量生产相似对象的问题。
看下面这段代码:
function Car(color) {
// this 指向新创建的实例
this.color = color;
}
// 共享属性
Car.prototype = {
drive() { console.log('drive, 下赛道'); },
name: 'su7'
}
const car1 = new Car('霞光紫');
car1.drive(); // "drive, 下赛道"
核心点: 构造函数内部的 this 指向新创建的实例,用于定义每个实例独有的属性(如 color)。
2. 原型(Prototype):共享的“基因库”
如果把所有方法都写在构造函数里,每次 new 一个对象,内存中就会多一份方法的副本,这非常浪费资源。
JavaScript 的解决方案是 prototype。正如文档 8.md 所述:
“prototype 属性的值是一个对象,它上面的属性和方法会被所有实例共享。”
我们来看一个经典的 Person 案例:
function Person(name, age) {
this.name = name;
this.age = age;
}
// 将属性挂载到原型上
Person.prototype.speci = '人类';
const person1 = new Person('张三', 18);
console.log(person1.speci); // "人类"
关键机制: 实例对象内部有一个私有属性 __proto__(现在标准推荐使用 Object.getPrototypeOf()),它指向构造函数的 prototype 对象。当访问 person1.speci 时,如果实例上没有,引擎就会去 Person.prototype 上找。
3. 原型链继承:模拟“血缘关系”
传统的 Class 面向对象是“血缘关系”,而 JS 是“原型式”的。如何实现继承?答案是原型链。
我们可以利用 prototype 指向另一个构造函数的实例,来实现属性的层层继承。:
var obj = { species: '动物' };
function Animal() { }
Animal.prototype = obj; // Animal 继承了 obj 的属性
function Person() { }
Person.prototype = new Animal(); // Person 继承了 Animal
var su = new Person();
console.log(su.species); // "动物"
继承逻辑:
su的__proto__指向Person.prototype(即new Animal())。new Animal()的__proto__指向Animal.prototype(即obj)。- 当查找
su.species时,引擎会沿着这条链一直找到obj上的species。
4. 原型链的终点:Object.prototype
所有的对象,最终都会指向 Object.prototype。这也是为什么我们所有的对象都能调用 .toString() 方法的原因。
// 6.html
console.log(su.toString()); // 能调用,因为原型链最终指向了 Object.prototype
console.log(su.__proto__.__proto__); // 指向 Object.prototype
注意: Object.prototype 的 __proto__ 指向 null,标志着原型链的结束。
5. 雷点和实践
在使用原型时,有一个容易踩的坑:
// 5.html
Person.prototype.species = '人类';
var su = new Person();
su.species = 'LOL达人'; // 这是在实例上新建了一个属性,而不是修改原型
解释: 当你给实例设置一个与原型同名的属性时,JS 引擎会在实例上直接创建该属性(遮蔽效应),而不会修改原型上的值。如果你删除了实例的这个属性,它依然会回到原型上取值。
结语
理解构造函数、实例与原型三者的关系,是掌握 JavaScript 面向对象的基石。
- 构造函数是模版(Constructor)。
- 实例是具体的对象。
- 原型是所有实例共享的属性和方法的容器。
- 原型链是实现继承的机制。
最后用一张图总结下 助你更好理解原型和构造函数
图的上半部分主要展示了自定义构造函数 Person 的内部关系:
-
构造函数
Person:- 它是一个函数,用来通过
new关键字创建实例(如new Person())。 - 它有一个指向原型对象的属性:
prototype。
- 它是一个函数,用来通过
-
原型对象
Person.prototype:- 这是构造函数的“原型”,它是一个对象。
- 它有一个指向构造函数的属性:
constructor。 - 关系:
Person.prototype.constructor指向Person。这是一个循环引用,确保原型知道是谁构造了它。
-
实例对象
person:- 这是通过
new Person()创建出来的具体对象。 - 它有一个内部指针:
__proto__(注意:这是非标准但广泛支持的属性,标准中对应[[Prototype]])。 - 关系:
person.__proto__指向Person.prototype。这是原型链的核心:实例通过这个指针去原型对象上查找方法和属性。
- 这是通过
小结:实例的
__proto__指向构造函数的prototype,而prototype的constructor又指回构造函数。
2. 继承的终点(下半部分)
图的下半部分展示了所有对象的最终归宿——Object:
-
Object():- 这是 JS 内置的顶级构造函数。
- 同样,
Object.prototype的constructor指向Object。
-
连接点:
- 注意看中间那条向下的箭头:
Person.prototype的__proto__指向了Object.prototype。 - 这意味着:
Person的原型对象本身也是一个对象(它是由Object构造出来的),所以它也要遵循对象的规则,去继承Object.prototype上的通用方法(如toString、hasOwnProperty等)。
- 注意看中间那条向下的箭头: