JavaScript 原型链与原型对象详解
一、为什么需要原型
假设用构造函数创建多个对象。如果给每个对象都准备一份自己的方法,会产生重复的内存开销:
js
// 方法定义在构造函数内部:每个实例都复制一份新函数
function Person(name) {
this.name = name;
this.sayHello = function () {
console.log(`你好,我是 ${this.name}`);
};
}
const p1 = new Person('小明');
const p2 = new Person('小红');
console.log(p1.sayHello === p2.sayHello); // false,每个实例的方法都是独立的函数
两个实例的 sayHello 逻辑完全相同,却没有共享,白白占用内存。
原型(prototype)要解决的就是这个问题:把需要共享的属性与方法放到一个公共对象上,让所有实例通过原型链引用同一个对象,而不是各自复制一份。原型机制还构成了 JavaScript 实现继承的基础。
二、构造函数、实例、原型对象
以 Person 为例,先认识三个互相联系的对象:
| 角色 | 指代 | 说明 |
|---|---|---|
| 构造函数 | Person |
一个函数,用 new 调用时负责创建实例 |
| 实例对象 | p1、p2 |
new Person() 得到的结果 |
| 原型对象 | Person.prototype |
Person 上名为 prototype 的属性所指向的对象 |
2.1 prototype:显式原型
只有函数才有 prototype 属性 (箭头函数没有,也不能用作构造函数)。每个函数都自带一个 prototype 属性,它指向一个普通对象:
js
function Person(name) {
this.name = name;
}
// 把共享方法挂到原型对象上
Person.prototype.sayHello = function () {
console.log(`你好,我是 ${this.name}`);
};
console.log(typeof Person.prototype); // "object"
2.2 proto:隐式原型
任何对象都有 __proto__ 属性 ,它指向该对象的原型对象。这个属性对应的是对象内部的原型槽位 [[Prototype]]。__proto__ 是历史上浏览器实现的访问器,标准写法推荐用 Object.getPrototypeOf() 读取。
js
function Person(name) {
this.name = name;
}
const p1 = new Person('小明');
console.log(p1.__proto__ === Person.prototype); // true
console.log(Object.getPrototypeOf(p1) === Person.prototype); // true
2.3 构造函数创建实例时发生了什么
用 new 调用一个函数时,引擎大致执行以下四步:
- 创建一个新的空对象。
- 把这个新对象的
__proto__指向构造函数的prototype(即建立原型关联)。 - 把构造函数体内的
this绑定到新对象并执行构造函数。 - 若构造函数显式返回一个对象,则返回该对象;否则返回第 1 步创建的对象。
因此 new Person() 得到的实例,其隐式原型自动等于 Person.prototype。
2.4 三者关系
三者之间需要记住的关系只有两条:
-
Person.prototype.constructor === Person:原型对象上的constructor属性指回构造函数。 -
p1.__proto__ === Person.prototype:new时把实例的隐式原型连到构造函数的prototype。三、属性查找规则与原型链
3.1 查找规则
访问
对象.属性时,JavaScript 的执行顺序是:- 先查找对象自身的属性,找到就返回。
- 找不到,就沿
__proto__去原型对象上找。 - 还找不到,就继续沿上一级的
__proto__往上找。 - 直到
Object.prototype,它的__proto__是null,再往上没有对象,返回undefined。
jsfunction Person(name) { this.name = name; // name 是实例自身属性 } Person.prototype.sayHello = function () { console.log(`你好,我是 ${this.name}`); }; const p1 = new Person('小明'); console.log(p1.name); // "小明",来自自身属性 p1.sayHello(); // "你好,我是 小明",自身没有,沿原型链找到 // p1 自身没有 toString,但 Object.prototype 上有,所以仍能调用 console.log(p1.toString()); // "[object Object]"
一条不断通过 __proto__ 向上串联的链路称为原型链。p1 的原型链为:
text
p1 ──► Person.prototype ──► Object.prototype ──► null
Person.prototype 本身是一个普通对象,它的 __proto__ 指向 Object.prototype;而 Object.prototype 是原型链的顶端,其 __proto__ 为 null,查找到此终止。
3.2 hasOwnProperty 与 in
hasOwnProperty():只检查属性是否存在于对象自身。in:检查属性是否能被访问到,包括沿原型链找到的继承属性。
js
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {};
const p1 = new Person('小明');
console.log(p1.hasOwnProperty('name')); // true,自身属性
console.log(p1.hasOwnProperty('sayHello')); // false,继承自原型
console.log('sayHello' in p1); // true,可以访问到
3.3 instanceof
obj instanceof Constructor 的原理是:沿着 obj 的原型链查找,看 Constructor 的 prototype 是否出现在这条链上。只要出现就返回 true。
js
function Person() {}
const p1 = new Person();
console.log(p1 instanceof Person); // true,Person.prototype 在 p1 的原型链上
console.log(p1 instanceof Object); // true,Object.prototype 也在链上
3.4 数组等内置对象的原型链
数组方法没有定义在数组实例上,而是定义在 Array.prototype 上,通过原型链共享:
js
const arr = [1, 2, 3];
console.log(arr.hasOwnProperty('map')); // false
console.log('map' in arr); // true
// arr 的原型链
// arr ──► Array.prototype ──► Object.prototype ──► null
四、函数的原型链:函数也是对象
函数既可以作为构造器,本身也是对象,因此函数也有 __proto__。所有函数(普通函数、构造函数、Object、Function 本身)都是 Function 的实例,它们的 __proto__ 都指向 Function.prototype。
js
function Person() {}
const p1 = new Person();
// 普通函数的隐式原型是 Function.prototype
console.log(Person.__proto__ === Function.prototype); // true
// 内置构造函数 Object 本身也是函数,同样指向 Function.prototype
console.log(Object.__proto__ === Function.prototype); // true
// 唯一的特殊情况:Function 的隐式原型也是 Function.prototype
console.log(Function.__proto__ === Function.prototype); // true
这里要注意区分两个对象:
Person.prototype:由new Person()生成的实例的原型,存共享方法。Person.__proto__:指向Function.prototype,因为它本身是一个函数对象。
它们不是同一个东西:
js
console.log(Person.__proto__ === Person.prototype); // false
Function.prototype 本身是一个可调用的函数对象,它的隐式原型再指向 Object.prototype:
js
console.log(Function.prototype.__proto__ === Object.prototype); // true
以上各对象的 __proto__ 指向可以列表核对:
| 节点 | 该节点的 __proto__ |
依据 |
|---|---|---|
p(new Person() 的实例) |
Person.prototype |
由 new 建立 |
Person.prototype |
Object.prototype |
原型对象是普通对象,其原型是 Object.prototype |
Person |
Function.prototype |
函数都是 Function 的实例 |
Object |
Function.prototype |
同上,Object 本身是函数 |
Function |
Function.prototype |
全局唯一闭环,可作结论记忆 |
Function.prototype |
Object.prototype |
Function.prototype 本身也是一个函数对象 |
Object.prototype |
null |
原型链顶端 |
要点归纳:
Object.prototype位于绝大多数原型链的顶端,它的__proto__是null。Object、Function、所有普通函数都是Function的实例,因此它们的__proto__都是Function.prototype。Function.__proto__ === Function.prototype,这是全局关系中的唯一闭环,可作为结论记住,不必深究成因。
五、获取与设置对象原型
5.1 读取原型
__proto__ 在代码中可以直接访问,但它不是规范推荐的正式 API。读取原型应使用 Object.getPrototypeOf():
js
function Person() {}
const p1 = new Person();
const proto1 = p1.__proto__; // 不推荐但可读
const proto2 = Object.getPrototypeOf(p1); // 标准写法
console.log(proto1 === Person.prototype); // true
console.log(proto2 === Person.prototype); // true
5.2 改变已有对象的原型
使用 Object.setPrototypeOf() 可以修改一个对象的原型。该操作会触发引擎重排继承关系,性能开销较大,正式代码中应尽量少用:
js
const a = { x: 1 };
const b = { y: 2 };
console.log(b.x); // undefined,b 的原型上没有 x
Object.setPrototypeOf(b, a);
console.log(b.x); // 1,通过原型链从 a 上找到
console.log(Object.getPrototypeOf(b) === a); // true
5.3 Object.create:创建一个指定原型的对象
Object.create(proto) 创建一个新对象,并把它的原型直接设为 proto。这是创建"继承自某个对象"的对象最直接的方式:
js
const personProto = {
sayHello() {
console.log(`你好,我是 ${this.name}`);
},
};
// 创建的原型为 personProto,自身再添加 name
const p1 = Object.create(personProto);
p1.name = '小明';
p1.sayHello(); // "你好,我是 小明"
console.log(Object.getPrototypeOf(p1) === personProto); // true
Object.create(null) 可以创建没有任何原型 的对象,它无法访问 toString 等方法:
js
const empty = Object.create(null);
console.log(empty.toString); // undefined,Object.prototype 不在其原型链上
六、用原型实现继承
6.1 原型链继承
把子类构造函数的 prototype 替换为父类的一个实例,子类的实例就能通过链式查找访问父类原型上的方法:
js
function Animal(name) {
this.name = name;
}
Animal.prototype.say = function () {
console.log(`${this.name} 发出叫声`);
};
function Dog(name) {
this.name = name;
}
// 让 Dog.prototype 成为一个 Animal 实例,从而继承 Animal.prototype
Dog.prototype = new Animal('占位');
const d = new Dog('旺财');
d.say(); // "旺财 发出叫声",沿原型链找到 Animal.prototype.say
纯原型链继承的缺点:父类的实例属性会被所有子类实例共享,属性写在同一处会互相影响,所以实际中通常配合下面的"借用构造函数"使用。
6.2 组合继承:借用构造函数 + 原型
先用 call 在子类构造函数里执行父类构造函数,让每个子实例有各自的实例属性;再把子类的原型指向父类原型的副本,继承方法:
js
function Animal(name) {
this.name = name;
}
Animal.prototype.say = function () {
console.log(`${this.name} 发出叫声`);
};
function Dog(name, breed) {
Animal.call(this, name); // 借用父构造函数,name 成为每个实例的自身属性
this.breed = breed;
}
// 把子类原型替换为父类原型的副本,替换后 constructor 会丢失,需要补回
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 补回 constructor 指向(见易错点 7.2)
const d = new Dog('旺财', '金毛');
console.log(d.name); // "旺财",自身属性
d.say(); // "旺财 发出叫声",继承的方法
console.log(d instanceof Dog); // true
console.log(d instanceof Animal); // true
console.log(Dog.prototype.constructor === Dog); // true
七、常见易错点
7.1 混淆 proto 与 prototype
__proto__存在于所有对象,指"我这个对象从哪里继承"。prototype只存在于函数,指"用我 new 出来的实例从哪里继承"。
Person.__proto__(函数作为对象的原型)与 Person.prototype(作为构造器时为实例准备的原型)是两个不同的东西,见第四节。
7.2 重写 prototype 后 constructor 丢失
constructor 是原型对象上的属性,本身不是自动维护的。用新对象整体替换 prototype 后,新对象上没有 constructor,此时沿原型链会找到 Object.prototype,导致 constructor 指向 Object:
js
function Person() {}
const p = new Person();
console.log(p.constructor === Person); // true
// 用新对象整体替换 prototype,新对象没有 constructor 属性
Person.prototype = {
sayHello() {},
};
console.log(p.constructor === Person); // false,指向了 Object
console.log(p.constructor === Object); // true
解决办法是替换后手动补回:
js
Person.prototype.sayHello = function () {};
// 或者
Person.prototype = {
constructor: Person,
sayHello() {},
};
注意:替换 prototype 只影响之后 new 出来的实例。上面代码中旧实例 p 的隐式原型仍是旧的 Person.prototype 对象。
7.3 重写 prototype 之后才创建的实例才受新原型影响
js
function Person() {}
const p1 = new Person();
Person.prototype.sayHi = function () { console.log('hi'); };
console.log(p1.sayHi); // 函数存在,原型对象被修改,实例立刻能访问到
Person.prototype = { other() {} };
const p2 = new Person();
console.log(p1.other); // undefined,p1 的原型仍是旧的 Person.prototype
console.log(p2.other); // 函数存在,p2 使用新的 prototype
给已有原型追加 方法,对所有实例即时生效;整体替换原型,只影响替换之后创建的实例。
7.4 不要修改内置对象的原型
往 Array.prototype、Object.prototype 上挂自定义方法是许多旧代码的常见写法,会污染全局,可能与第三方库冲突,也可能破坏依赖枚举的代码。除非做 Polyfill 且有规范约束,否则应避免。
7.5 for...in 会遍历继承来的可枚举属性
js
const proto = { inherited: 1 };
const obj = Object.create(proto);
obj.own = 2;
for (const key in obj) {
console.log(key); // "own"、"inherited",继承属性也会被遍历
}
// 只想要自身属性时用 hasOwnProperty 过滤
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key); // 只输出 "own"
}
}
八、总结
| 概念 | 说明 |
|---|---|
prototype |
函数才有的显式属性,指向该函数作为构造函数时给实例准备的原型对象 |
__proto__ |
所有对象都有的隐式原型,指向它的原型对象;标准读取用 Object.getPrototypeOf() |
constructor |
原型对象上的属性,默认指回该函数;整体替换 prototype 后需要手动补回 |
Object.getPrototypeOf / setPrototypeOf |
读取 / 修改一个对象的原型 |
Object.create(proto) |
创建指定原型的对象;Object.create(null) 创建无原型对象 |
| 原型链 | 通过 __proto__ 逐级上溯的链路,终点是 null |
| 属性查找 | 先查自身,再沿原型链向上找,hasOwnProperty 只判断自身 |
instanceof |
判断右侧构造函数的 prototype 是否在左侧对象的原型链上 |
class extends |
语法糖,底层仍通过原型链实现继承 |
使用建议:
- 共享方法挂到
prototype上,实例状态放在构造函数内部,不要给每个实例复制方法。 - 读取原型用
Object.getPrototypeOf(),不要依赖__proto__。 - 整体替换
prototype后记得补回constructor,并注意它只影响替换之后创建的实例。 - 不要修改
Object.prototype、Array.prototype等内置原型。 - 现代代码做继承优先使用
class extends。