在 JavaScript 中,构造函数、实例对象和原型对象三者互相引用,形成一条完整的关系链。理解这个“铁三角”,是真正掌握原型与继承的起点。
一张图看清三角关系
先看一个最简单的例子:
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log('Hi, I am ' + this.name);
};
const p1 = new Person('李四');
执行完以上代码后,内存中存在三个关键对象:
- 构造函数
Person - 实例对象
p1 - 原型对象
Person.prototype
它们之间的引用关系如下:
构造函数 Person
/ \
.prototype new 调用
/ \
原型对象 Person.prototype ←—— 实例对象 p1
↑ |
|__p1.__proto__ ______|
同时:Person.prototype.constructor === Person
用代码验证这些关系:
// 构造函数 -> 原型对象
console.log(Person.prototype); // {sayHi: f, constructor: f}
console.log(Object.getOwnPropertyNames(Person.prototype)); // ['constructor', 'sayHi']
// 原型对象 -> 构造函数
console.log(Person.prototype.constructor === Person); // true
// 实例对象 -> 原型对象
console.log(p1.__proto__ === Person.prototype); // true
// 实例对象 -> 构造函数(间接)
console.log(p1.__proto__.constructor === Person); // true
每个角色的职责
构造函数(Constructor)
构造函数本质上就是一个普通函数,只是通过 new 运算符调用。它的职责有三:
- 创建新的空对象。
- 将该对象的
[[Prototype]](即proto)指向构造函数的prototype属性所引用的对象。 - 将该对象作为
this的指向,执行函数体内的代码(通常用来初始化实例属性)。 - (隐式)返回这个新对象。
构造函数上的 prototype 属性 是一个对象,它会被自动赋予一个 constructor 属性,指向构造函数自身。这个 prototype 对象就是所有实例共享的“原型池”。
实例对象(Instance)
通过 new 构造函数() 创建的每个实例都是独立的对象。实例上存放的是实例自己的属性和方法(比如 p1.name)。这些属性在不同的实例之间互不影响。实例并不复制原型对象上的属性,而是通过内部的 proto 指针动态查找。
原型对象(Prototype)
原型对象是实例和构造函数之间的“共享空间”。定义在原型对象上的属性和方法,可以被所有实例访问到(因为实例的 proto 指向它)。这正是 JavaScript 实现方法共享的方式,而不需要每个实例都存一份方法代码。
三角关系带来的核心行为
1. 属性查找:自己→原型→更上层原型
访问 p1.name 时,引擎先在 p1 自身上查找,找到了直接返回。访问 p1.sayHi 时,p1 自身上没有这个方法,引擎会沿着 proto 找到 Person.prototype,在上面找到 sayHi 方法并执行。
如果 Person.prototype 上也没有,就会继续向上查找 Person.prototype.proto(也就是 Object.prototype),直到 null。这就是原型链查找的全过程。
2. 原型上的属性是所有实例共享的
这是难点也是重点:
Person.prototype.type = '人类';
console.log(p1.type); // '人类'
const p2 = new Person('王五');
console.log(p2.type); // '人类'
// 通过实例修改共享属性会发什么?
p1.type = '兽人'; // 不是修改原型,而是给 p1 添加了自身属性
console.log(p1.type); // '兽人' (来自自身)
console.log(p2.type); // '人类' (来自原型)
delete p1.type; // 删除自身属性
console.log(p1.type); // '人类' (重新走原型链查找)
读取操作会沿着原型链向上搜索,但赋值操作默认只影响实例自身,不会改动原型。除非显式地通过 Person.prototype.type = xxx 去修改原型对象。
3. constructor 的意义
Person.prototype.constructor 指向 Person,通常用来从实例回溯到构造函数。例如在继承场景中,子类的原型需要重置 constructor 指向自身,以保证正确的引用关系。但需要注意 constructor 是一个普通属性,可以被篡改,因此不是 100% 可靠的类型判断手段。
用验证代码加深理解
不妨在浏览器控制台亲手跑一下这些语句,观察输出:
function Dog(breed) {
this.breed = breed;
}
Dog.prototype.bark = function() { console.log('Woof!'); };
const d = new Dog('Golden');
// 三角关系验证
console.log(d.constructor === Dog); // true(因为 d.__proto__.constructor === Dog)
console.log(Dog.prototype.isPrototypeOf(d)); // true(原型检测)
console.log(Object.getPrototypeOf(d) === Dog.prototype); // true(ES6推荐的获取方式)
// 遍历自身属性
console.log(d.hasOwnProperty('breed')); // true
console.log(d.hasOwnProperty('bark')); // false(bark 在原型上)
// 所有实例共享原型方法
const d2 = new Dog('Husky');
console.log(d.bark === d2.bark); // true(同一个函数引用)
三角关系的实用价值
理解这一条关系链之后,以下问题自然有了答案:
- 为什么在
Array.prototype上新增一个方法,所有数组实例都能使用?—— 因为所有数组实例的proto都指向Array.prototype。 - 为什么方法要定义在原型上,而数据定义在实例内?—— 原型方法被共享,节省内存;实例数据各自独立,互不影响。
- 在使用
for...in遍历对象时,为什么有时会冒出预期之外的属性?—— 因为它会沿着原型链遍历所有的可枚举属性(需要配合hasOwnProperty过滤)。
当你遇到任何涉及原型的问题时,冷静地画出“构造函数 → prototype → 原型对象 ← proto ← 实例”这条三角链,答案就会清晰呈现。