ES5原型深度解析:从构造函数到原型链的底层逻辑
在ES6 class 语法出现之前,JavaScript并没有真正意义上的“类”,但它通过原型链完美实现了面向对象编程。它不像传统类式面向对象那样搞“血缘继承”,而是靠“共享借用”实现复用——本文结合代码示例,用最通俗的方式拆解原型的核心逻辑,新手也能轻松看懂。
一、先搞懂:ES5没有class,怎么模拟“类”?
ES5没有专门的class关键字,但我们可以用【构造函数+原型对象】的组合,实现和“类”一样的效果。比如想创建“小米SU7”,核心就3步:
- 写一个首字母大写的构造函数(比如
Car),负责初始化实例的“私有属性”; - 用
new关键字创建实例(比如car1、car2),每个实例都是独立的; - 在构造函数的
prototype上挂载“共享属性/方法”,让所有实例共用。
代码实操:创建小米SU7
// 构造函数:初始化实例私有属性(每个实例独一份)
function Car(color) {
this.color = color; // 比如霞光紫、海湾蓝,每个车颜色不同
}
// 原型对象:存储共享属性/方法(所有实例共用一份)
Car.prototype = {
drive() {
console.log('drive,下赛道');
},
name: 'su7', // 所有Car实例都是su7车型
height: 1.4, // 共享高度参数
weight: 1.5, // 共享重量参数
long: 4800 // 共享长度参数
}
// 创建2个实例
const car1 = new Car('霞光紫');
const car2 = new Car('海湾蓝');
// 访问私有属性(各自独立)
console.log(car1.color); // 霞光紫
console.log(car2.color); // 海湾蓝
// 访问共享属性/方法(共用一份)
console.log(car1.name); // su7
console.log(car2.weight); // 1.5
car1.drive(); // drive,下赛道
关键区别:
- 构造函数内的
this.color:实例私有,改一个不影响另一个; Car.prototype上的name、drive():所有实例共享,内存中只存一份,节省空间。
二、核心概念:构造函数的2个核心角色
构造函数是原型体系的“主角”,它要干两件大事:
1. 作为constructor:给实例“造私有属性”
当用new调用构造函数时,JS会自动做4件事:
- 新建一个空对象;
- 让
this指向这个空对象; - 执行构造函数代码,给对象加私有属性;
- 返回这个对象(不用手动写
return)。
比如new Person('张三', 18),会生成一个{name: '张三', age: 18}的实例,每个实例的name和age都是独立的:
function Person(name, age) {
this.name = name; // 私有属性
this.age = age; // 私有属性
}
Person.prototype.speci = '人类'; // 共享属性
const person1 = new Person('张三', 18);
const person2 = new Person('金总', 19);
console.log(person1.name); // 张三(独立)
console.log(person2.name); // 金总(独立)
console.log(person1.speci); // 人类(共享)
2. 作为原型载体:通过prototype共享内容
每个函数(包括构造函数)都天生带一个prototype属性,它的值是一个对象——我们叫它“原型对象”。
这个原型对象的作用很简单:存储所有实例都能用的共享内容。不管是属性(比如speci: '人类')还是方法(比如sayHi),只要挂在prototype上,所有实例都能访问到。
这就是JS原型式的规则:不搞“血缘继承”,只搞“共享借用”——实例不用自己带所有属性方法,需要时从原型上“借用”就行。
三、关键关联:实例、原型对象、构造函数的三角关系
要吃透原型,必须记住3个核心关联,结合代码一看就懂:
1. 实例 ↔ 原型对象:__proto__是“桥梁”
所有对象(包括实例)都有一个私有属性__proto__(ES5标准方法是Object.getPrototypeOf()),它直接指向创建这个实例的构造函数的prototype。
简单说:实例.proto === 构造函数.prototype
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = { speci: '人类' }
const su = new Person('舒老板', 19);
console.log(su.__proto__ === Person.prototype); // true
console.log(Object.getPrototypeOf(su) === Person.prototype); // 同样true(推荐用这个)
这就是实例能访问原型属性的原因:实例自己没有,就通过__proto__去原型对象上找。
2. 原型对象 ↔ 构造函数:constructor是“反向指针”
原型对象上有个constructor属性,它会指向对应的构造函数。
简单说:原型对象.constructor === 构造函数
// 接上面的代码
console.log(Person.prototype.constructor === Person); // true
注意:如果直接覆盖prototype(比如Person.prototype = { ... }),会弄丢默认的constructor,需要手动恢复,否则原型链会断:
function Person(name, age) {
this.name = name;
this.age = age;
}
// 直接覆盖prototype,手动恢复constructor
Person.prototype = {
speci: '人类',
sayHi: function() {
console.log(`你好,我是${this.name}`);
},
constructor: Person // 关键:恢复指向
}
const su = new Person('舒老板', 19);
console.log(Person.prototype.constructor === Person); // true(恢复成功)
3. 三角关系总结
如图所示:
- 构造函数 → prototype → 原型对象
- 原型对象 → constructor → 构造函数
- 实例 →
__proto__→ 原型对象
四、原型访问规则:自身有就用,没有就找原型
当你访问实例的某个属性时,JS会按“就近原则”查找:
- 先查实例自身有没有这个属性;
- 没有的话,通过
__proto__找原型对象; - 原型没有,就继续找原型的原型(这就是原型链);
- 直到找到
Object.prototype,还没有就返回null。
代码验证:实例属性“屏蔽”原型属性
function Person(){}
// 原型上的属性
Person.prototype.species = '人类';
Person.prototype.sayHi = function() {
console.log(`你好,我是${this.name}`);
}
const su = new Person();
su.species = 'LOL达人'; // 给实例加同名属性
console.log(su.species); // LOL达人(用实例自己的)
console.log(su.__proto__.species); // 人类(原型上的没被改)
这里要注意:给实例加和原型同名的属性,不是修改原型,而是在实例上新增属性,从而“屏蔽”原型上的同名属性。
五、原型链:最终指向Object.prototype
所有对象的原型链,最终都会指向Object.prototype,而Object.prototype的原型是null(原型链的终点)。
1. 为什么实例能调用toString()?
我们从没在Person或Car的原型上定义toString(),但实例却能调用——因为Person.prototype本身是对象,它的__proto__指向Object.prototype,而toString()是Object.prototype的内置方法。
function Person(){}
const su = new Person();
console.log(su.toString()); // [object Object](来自Object.prototype)
console.log(su.__proto__.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null(原型链终点)
2. 手动模拟原型链
我们可以通过修改prototype的指向,手动构建原型链:
// 1. 创建Object实例,作为Animal的原型
var obj = new Object();
obj.species = '动物';
// 2. Animal的原型指向obj
function Animal() {}
Animal.prototype = obj;
// 3. Person的原型指向Animal实例
function Person() {}
Person.prototype = new Animal();
// 4. Person实例的原型链:su → Person.prototype → Animal.prototype → Object.prototype → null
const su = new Person();
console.log(su.species); // 动物(从Animal.prototype找到)
console.log(su.toString()); // [object Object](从Object.prototype找到)
在这个代码例子中,su访问species的查找路径是: su(无)→ Person.prototype(无)→ Animal.prototype(有,返回“动物”)
这里附上一张原型链图便于理解:
六、核心总结
- ES5无
class,靠“构造函数+prototype”模拟类; - 构造函数管“私有属性”,prototype管“共享属性/方法”;
- 实例
__proto__→ 构造函数prototype→ 原型对象constructor→ 构造函数(三角关系); - 属性查找:自身 → 原型 → 原型链 →
Object.prototype→null; - JS原型式核心:共享借用,而非血缘继承。
理解了ES5构造函数+原型,再去看ES6的class你就会豁然开朗——class只是原型机制的语法糖,但底层还是基于“构造函数+原型”那一套。掌握原型,才算真正走进JavaScript面向对象的大门。希望这篇文章能帮你看清背后的原型链本质,在未来的开发中写出更扎实、更高效的 JavaScript 代码。