第 17 章 原型与原型链
第 17 章 原型与原型链
原型与原型链是 JavaScript 面向对象的基石。如果你理解了原型链,就能理解"为什么 a 对象有 sayHi 方法",就能理解 JavaScript 的继承机制。这可能是 JavaScript 中最难理解的概念之一,但也是最强大的!
17.1 构造函数
工厂函数
在 ES6 class 出现之前,工厂函数是一种创建对象的常用方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| function createPerson(name, age) {
return {
name: name,
age: age,
greet: function() {
console.log("你好,我是" + this.name);
}
};
}
const person1 = createPerson("小明", 18);
const person2 = createPerson("小红", 20);
person1.greet(); // "你好,我是小明"
person2.greet(); // "你好,我是小红"
|
问题:每个对象都有一份 greet 方法的副本,浪费内存!
1
| console.log(person1.greet === person2.greet); // false(方法不是共享的)
|
构造函数:首字母大写,用 new 调用
构造函数(Constructor)是专门用来创建对象的函数,配合 new 关键字使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
| function Person(name, age) {
this.name = name;
this.age = age;
this.greet = function() {
console.log("你好,我是" + this.name);
};
}
const person1 = new Person("小明", 18);
const person2 = new Person("小红", 20);
person1.greet(); // "你好,我是小明"
console.log(person1 instanceof Person); // true
|
构造函数的特点:
- 函数名首字母大写(约定)
- 使用
new 关键字调用 this 指向新创建的对象- 隐式返回
this(如果显式返回对象,会覆盖默认返回)
1
2
3
4
5
6
7
8
9
10
| // 构造函数
function Person(name) {
this.name = name;
this.greet = function() {
console.log("你好,我是" + this.name);
};
}
const p = new Person("小明");
console.log(p); // Person { name: "小明", greet: ƒ }
|
💡 构造函数是 JavaScript 实现"类"的基础,虽然 JavaScript 没有真正的类(ES6 之前),但构造函数让我们可以"模拟"面向对象编程。
new 关键字做了什么
new Person(...) 实际上做了以下事情:
1
2
3
4
5
6
7
8
9
10
11
12
| // new Person("小明") 相当于执行:
// 1. 创建一个新对象
const obj = {};
// 2. 设置原型
obj.__proto__ = Person.prototype;
// 3. 调用构造函数(并绑定 this)
Person.call(obj, "小明"); // 或者 Person.apply(obj, arguments)
// 4. 返回对象
// return obj;
|
可以用一个函数来模拟这个过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| function myNew(constructor, ...args) {
// 1 + 2. 创建新对象并指定它的原型
// Object.create 一步完成,等价于「新建空对象 + 把 __proto__ 指向构造函数的 prototype」
const obj = Object.create(constructor.prototype);
// 3. 调用构造函数
const result = constructor.apply(obj, args);
// 4. 如果构造函数显式返回对象,就返回那个对象
// 否则返回新创建的对象
return typeof result === "object" && result !== null ? result : obj;
}
// 使用
function Person(name) {
this.name = name;
}
const p = myNew(Person, "小明");
console.log(p.name); // "小明"
console.log(p instanceof Person); // true
|
几个容易忽略的细节:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 1. 构造函数的 prototype 不是对象时,会用 Object.prototype 兜底
function Weird() {}
Weird.prototype = 123; // 赋成原始值会被忽略
console.log(Object.getPrototypeOf(new Weird()) === Object.prototype); // true
// 2. 构造函数显式返回「原始值」不会生效,只有返回「对象」才会覆盖默认结果
function A() { this.name = "A"; return 42; }
console.log(new A().name); // "A"(数字被忽略)
function B() { this.name = "B"; return { name: "被返回的对象" }; }
console.log(new B().name); // "被返回的对象"
// 3. new 的目标必须是可构造的;箭头函数、对象方法简写都没有 [[Construct]]
const Arrow = () => {};
// new Arrow(); // TypeError: Arrow is not a constructor
|
17.2 原型
prototype 属性
每个函数都有一个 prototype 属性,它是一个对象。这个对象的用途是: 当用这个函数作为构造函数创建对象时,该对象会共享这个 prototype 上的属性和方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| function Person(name) {
this.name = name;
}
// 在 Person.prototype 上添加方法
Person.prototype.greet = function() {
console.log("你好,我是" + this.name);
};
Person.prototype.species = "人类";
const person1 = new Person("小明");
const person2 = new Person("小红");
console.log(person1.greet()); // "你好,我是小明"
console.log(person2.greet()); // "你好,我是小红"
console.log(person1.species); // "人类"
console.log(person2.species); // "人类"
// 验证:方法共享(同一个引用)
console.log(person1.greet === person2.greet); // true(不像工厂函数那样每个对象都有自己的方法副本!)
|
💡 把方法放在 prototype 上而不是构造函数内部,可以大大节省内存!
原型对象
prototype 指向的那个对象叫做原型对象。原型对象上通常会有一个 constructor 属性,指回构造函数:
1
2
3
4
5
6
7
8
| function Person(name) {
this.name = name;
}
console.log(Person.prototype.constructor === Person); // true
const p = new Person("小明");
console.log(p.__proto__ === Person.prototype); // true
|
关系图:
┌─────────────────────────────────────────────────────────┐
│ Person(构造函数) │
│ - prototype ────────┐ │
└───────────────────────┼─────────────────────────────────┘
↓
┌─────────────────┐
│ Person.prototype │
│ - constructor │──→ Person
│ - greet │
│ - species │
└────────┬─────────┘
│
┌─────────────┴─────────────┐
↓ ↓
┌─────────────────┐ ┌─────────────────┐
│ person1 实例 │ │ person2 实例 │
│ - name │ │ - name │
│ __proto__ ──────┼────────┼─ __proto__ │
└─────────────────┘ └─────────────────┘
constructor 属性
每个实例(通过 new 创建的对象)都有 constructor 属性,指向创建它的构造函数:
1
2
3
4
5
6
7
8
9
10
| function Person(name) {
this.name = name;
}
const p = new Person("小明");
console.log(p.constructor === Person); // true
console.log(p.constructor === Person.prototype.constructor); // true
// 可以用 constructor 判断类型(但不完全可靠,因为 prototype 可以被替换)
console.log(p instanceof Person); // true
|
⚠️ 注意:不要随意修改 prototype,否则 constructor 会失准:
1
2
| Person.prototype = {}; // 这样会丢失 constructor
p.constructor === Person; // false!
|
17.3 原型链
proto 与 prototype
prototype:构造函数上的属性,指向原型对象__proto__:对象的属性,指向其构造函数的 prototype(即该对象的原型)
1
2
3
4
5
6
7
8
9
10
11
12
13
| function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log("你好,我是" + this.name);
};
const p = new Person("小明");
console.log(p.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null(原型链的终点)
|
原型链的终点:Object.prototype.proto === null
原型链是有尽头的,最终指向 null:
1
2
| console.log(Object.prototype.__proto__); // null
console.log(Object.prototype.__proto__ === null); // true
|
完整的原型链:
person 对象
↓ __proto__
Person.prototype
↓ __proto__
Object.prototype
↓ __proto__
null
属性查找机制:顺着原型链向上搜索
当你访问对象的属性时,JavaScript 会沿着原型链向上查找:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log("你好,我是" + this.name);
};
Person.prototype.species = "人类";
const p = new Person("小明");
// p.name 在 p 对象本身找到
console.log(p.name); // "小明"
// p.greet 在 Person.prototype 上找到
p.greet(); // "你好,我是小明"
// p.species 也在 Person.prototype 上找到
console.log(p.species); // "人类"
// p.toString 在 Object.prototype 上找到
console.log(p.toString); // ƒ toString() { [native code] }
// p.xxx 不存在,原型链上都没有,返回 undefined
console.log(p.xxx); // undefined
|
Object.getPrototypeOf():获取对象的原型
Object.getPrototypeOf() 是获取对象原型的标准方法(代替 __proto__):
1
2
3
4
5
6
7
8
| function Person(name) {
this.name = name;
}
const p = new Person("小明");
console.log(Object.getPrototypeOf(p) === Person.prototype); // true
console.log(Object.getPrototypeOf(p).constructor === Person); // true
|
💡 推荐使用 Object.getPrototypeOf() / Object.setPrototypeOf() 而不是 __proto__。原因不是「不支持」——__proto__ 早已被各浏览器实现并被标准附录(Annex B)收录——而是它属于历史遗留的访问器属性:
- 它的行为在「对象字面量里写
__proto__: x」和「用 obj.__proto__ = x 赋值」两种情况下语义不同,容易写出意外结果; - 如果对象是用
Object.create(null) 创建的,它根本没有 __proto__; - 一旦有人在数据里塞了一个名为
__proto__ 的键,就可能引发原型污染问题。
一句话:读原型用 Object.getPrototypeOf,建对象用 Object.create,改原型用 Object.setPrototypeOf(且尽量避免在运行时频繁改)。
hasOwnProperty:区分自身属性与原型属性
hasOwnProperty 用于检查属性是对象自身的还是从原型链继承的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| function Person(name) {
this.name = name; // 自身属性
}
Person.prototype.greet = function() { // 原型属性
console.log("你好");
};
const p = new Person("小明");
console.log(p.hasOwnProperty("name")); // true(构造函数中定义的)
console.log(p.hasOwnProperty("greet")); // false(在 prototype 上定义的)
console.log(p.hasOwnProperty("toString")); // false(Object.prototype 上的)
// 可以用 for...in 遍历所有可枚举属性
for (let key in p) {
console.log(key, p.hasOwnProperty(key) ? "(自身)" : "(原型)");
}
// name (自身)
// greet (原型)
|
关于 hasOwnProperty 有两个需要注意的地方:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // 1. 它本身是 Object.prototype 上的方法,可以被覆盖,也会被 Object.create(null) 挡住
const weird = Object.create(null);
weird.name = "小明";
// weird.hasOwnProperty("name"); // TypeError: weird.hasOwnProperty is not a function
// ✅ 现代写法(ES2022):Object.hasOwn 不受对象原型影响
console.log(Object.hasOwn(weird, "name")); // true
console.log(Object.prototype.hasOwnProperty.call(weird, "name")); // true(旧写法)
// 2. 用「谁的属性」做判断时,注意三种遍历的差别
const demo = Object.create({ inherited: 1 });
demo.own = 2;
console.log(Object.keys(demo)); // ["own"] 只列自有可枚举(字符串键)
console.log("inherited" in demo); // true in 会查原型链
console.log(demo.propertyIsEnumerable("inherited")); // false(是原型上的)
|
另外,p.hasOwnProperty('name') 为 true 还有一个隐含前提:name 是在构造函数里通过 this.name = ... 加到自己身上的。如果只是读到了原型上的同名属性,hasOwnProperty 仍然返回 false——这正是区分「自己有的」和「继承来的」的可靠手段。
instanceof:检查原型链
instanceof 运算符用于检查对象的原型链上是否存在某个构造函数的 prototype:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| function Person(name) {
this.name = name;
}
function Student(name, grade) {
Person.call(this, name);
this.grade = grade;
}
// 设置原型链继承
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
const s = new Student("小明", 3);
console.log(s instanceof Student); // true
console.log(s instanceof Person); // true(因为 Student.prototype.__proto__ === Person.prototype)
console.log(s instanceof Object); // true
|
原型链的引用共享问题
原型上的属性是被所有实例共享的!这意味着如果你在原型上定义了一个引用类型(数组、对象),所有实例都会共享这个引用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| function Person(name) {
this.name = name;
}
Person.prototype.hobbies = ["编程", "阅读"]; // 引用类型!
const p1 = new Person("小明");
const p2 = new Person("小红");
console.log(p1.hobbies); // ["编程", "阅读"]
console.log(p2.hobbies); // ["编程", "阅读"]
// 修改 p1 的 hobbies
p1.hobbies.push("游戏");
console.log(p1.hobbies); // ["编程", "阅读", "游戏"]
console.log(p2.hobbies); // ["编程", "阅读", "游戏"] ← p2 也被影响了!
|
⚠️ 这是一个经典的陷阱!解决方法是在构造函数中定义引用类型属性:
1
2
3
4
| function Person(name) {
this.name = name;
this.hobbies = []; // 在构造函数中定义,每个实例都有独立的数组
}
|
17.4 继承实现
原型链继承
原型链继承是最基本的继承方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| function Parent() {
this.name = "Parent";
}
Parent.prototype.sayHi = function() {
console.log("你好,我是" + this.name);
};
function Child() {
this.name = "Child";
}
// 原型链继承
Child.prototype = new Parent();
Child.prototype.constructor = Child; // 修复 constructor
const child = new Child();
child.sayHi(); // "你好,我是Child"
console.log(child instanceof Child); // true
console.log(child instanceof Parent); // true
|
问题:引用类型的属性被所有实例共享。
构造函数继承(借用构造函数 / call)
借用构造函数可以解决引用类型共享问题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| function Parent(name, hobbies) {
this.name = name;
this.hobbies = hobbies; // 引用类型在构造函数中定义
}
function Child(name, hobbies, grade) {
Parent.call(this, name, hobbies); // 借用 Parent 构造函数
this.grade = grade;
}
const child1 = new Child("小明", ["阅读"], 3);
const child2 = new Child("小红", ["音乐"], 5);
child1.hobbies.push("游戏");
console.log(child1.hobbies); // ["阅读", "游戏"]
console.log(child2.hobbies); // ["音乐"] ← 没有被影响!
|
问题:借用构造函数只复制了「构造函数里写的实例属性」,父类原型上的方法一个都没继承过来。想在子类里用父类的方法,只能把方法也写进构造函数,那样每个实例又会各持一份副本——既费内存,instanceof Parent 也不成立。
1
2
3
| const child = new Child("小明", ["阅读"], 3);
// child.sayHi(); // Parent.prototype 上的方法根本不在它的原型链上
console.log(child instanceof Parent); // false
|
组合继承
组合继承结合原型链和构造函数继承:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| function Parent(name) {
this.name = name;
this.colors = ["红", "绿"];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, grade) {
Parent.call(this, name); // 继承实例属性
this.grade = grade;
}
Child.prototype = new Parent(); // 继承原型方法
Child.prototype.constructor = Child;
Child.prototype.sayGrade = function() {
console.log(this.grade);
};
const c1 = new Child("小明", 3);
const c2 = new Child("小红", 5);
c1.colors.push("蓝");
console.log(c1.colors); // ["红", "绿", "蓝"]
console.log(c2.colors); // ["红", "绿"]
|
原型式继承:Object.create()
Object.create() 创建一个新对象,使用现有对象作为新对象的原型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| const person = {
name: "小明",
hobbies: ["阅读", "音乐"]
};
const p1 = Object.create(person);
const p2 = Object.create(person);
p1.name = "小红";
console.log(p1.name); // "小红"
console.log(p2.name); // "小明"(不受影响)
p1.hobbies.push("游戏");
console.log(p1.hobbies); // ["阅读", "音乐", "游戏"]
console.log(p2.hobbies); // ["阅读", "音乐", "游戏"] ← 引用类型还是共享的!
|
寄生式继承
寄生式继承在原型式继承的基础上添加额外的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| function createPerson(original) {
const clone = Object.create(original);
clone.greet = function() {
console.log("你好,我是" + this.name);
};
return clone;
}
const person = {
name: "小明",
age: 18
};
const p = createPerson(person);
p.greet(); // "你好,我是小明"
|
寄生组合式继承(最佳方式)
寄生组合式继承是 JavaScript 最理想的继承方式,避免了其他方式的缺点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| function Parent(name) {
this.name = name;
this.colors = ["红"];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, grade) {
Parent.call(this, name); // 继承实例属性
this.grade = grade;
}
// 寄生组合继承的关键:用 Object.create 创建 Parent.prototype 的副本
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.sayGrade = function() {
console.log(this.grade);
};
const c = new Child("小明", 3);
c.sayName(); // "小明"
c.sayGrade(); // 3
console.log(c instanceof Child); // true
console.log(c instanceof Parent); // true
|
💡 为什么寄生组合式继承是最佳方式?
- 只调用一次 Parent 构造函数
- 原型链完整(instanceof 正常工作)
- 原型上的 constructor 正确
六种继承方式对比
| 方式 | 优点 | 缺点 |
|---|
| 原型链继承 | 简单 | 引用类型共享 |
| 构造函数继承 | 属性独立 | 方法不复用 |
| 组合继承 | 综合优点 | 调用两次构造函数 |
| 原型式继承 | 简单 | 引用类型共享 |
| 寄生式继承 | 可扩展 | 方法不复用 |
| 寄生组合式继承 | 完美继承 | 稍复杂 |
💡 推荐使用寄生组合式继承,或者直接使用 ES6 的 class extends。
17.5 常见误区与进阶细节
prototype 只属于「可构造的函数」
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| function normal() {}
console.log(typeof normal.prototype); // "object"
const arrow = () => {};
console.log(arrow.prototype); // undefined(箭头函数没有 prototype)
// new arrow(); // TypeError:箭头函数不能构造
const obj = {
method() {}, // 对象方法简写同样没有 prototype
};
console.log(obj.method.prototype); // undefined
class Person {}
console.log(typeof Person.prototype); // "object"(class 有 prototype)
|
函数自己也在原型链上:Fn.__proto__ === Function.prototype
1
2
3
4
5
6
| function Person() {}
console.log(Person.__proto__ === Function.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true(Function 自己指向自己)
|
「实例的原型」和「构造函数自己的原型」是两条不同的链,这一点非常容易混淆。
修改原型会影响所有已存在的实例
1
2
3
4
5
6
7
8
9
| function Person() {}
const p = new Person();
Person.prototype.greet = function () { return "你好"; };
console.log(p.greet()); // "你好":实例是「查」到原型的,不是复制
Person.prototype = {}; // ❌ 直接替换整个 prototype
console.log(p.greet); // 仍然能读到(p 的原型还是旧对象)
console.log(new Person().greet); // undefined(新实例用的是新对象)
|
第二条也解释了为什么「替换 prototype 对象」是个坏习惯:已经创建的实例还连着旧原型,新旧实例的行为会不一致。要扩展功能,就在原原型对象上添加方法。
属性遮蔽(shadowing)
1
2
3
4
5
6
7
8
9
10
11
12
13
| function Person() {}
Person.prototype.name = "原型上的名字";
const p = new Person();
console.log(p.name); // "原型上的名字"(读的是原型)
p.name = "自己的名字"; // 写操作会创建「自有属性」,不会改原型
console.log(p.name); // "自己的名字"
console.log(Object.hasOwn(p, "name")); // true
console.log(Person.prototype.name); // 仍是 "原型上的名字"
delete p.name; // 删掉自有属性后,又「露出」原型上的值
console.log(p.name); // "原型上的名字"
|
原型属性的可枚举性:class 与手动赋值不同
1
2
3
4
5
6
7
8
9
10
11
12
| // 手动赋值:属性是可枚举的,会被 for...in 遍历到
function Old() {}
Old.prototype.method = function () {};
for (const k in new Old()) console.log(k); // "method"
// class 里定义的方法:不可枚举,不会出现在 for...in 中
class NewCls {
method() {}
}
for (const k in new NewCls()) console.log(k); // 什么都不打印
console.log(Object.keys(NewCls.prototype)); // [](方法不可枚举)
console.log(Object.getOwnPropertyDescriptor(NewCls.prototype, "method").enumerable); // false
|
instanceof 的两个局限
1
2
3
4
5
6
7
8
9
10
| // 1. 跨 Realm(iframe、Node 的 vm 模块)时原型不同,instanceof 会失败
// 这时候用 Array.isArray、typeof 等自带判断更可靠
// 2. instanceof 可以被 Symbol.hasInstance 改写
class Even {
static [Symbol.hasInstance](value) {
return typeof value === "number" && value % 2 === 0;
}
}
console.log(2 instanceof Even); // true(跟原型链毫无关系)
|
Object.create(null):没有原型的「纯净字典」
1
2
3
4
5
6
7
8
9
| const dict = Object.create(null);
dict.key = "value";
console.log(Object.getPrototypeOf(dict)); // null
console.log(Object.hasOwn(dict, "key")); // true
// dict.toString(); // TypeError:没有继承任何方法
// 适合当作纯粹的键值容器,用来避免「用户输入的键名撞上 Object.prototype」
// 例如:把用户提供的 key 直接存进 {} 时,key 为 "__proto__" 可能造成原型污染
|
关于性能
- 不要在运行时频繁调用
Object.setPrototypeOf,它会让引擎放弃针对该对象的优化,规范也明确指出这是「慢操作」;原型应该在创建对象时就确定好。 - 原型链越短,属性查找越快。绝大多数业务代码不必为此操心,但如果在极热的循环里访问深层原型属性,可以先把值取到局部变量再用。
class 语法在语义上就是原型继承,性能上没有额外魔法;不要为了「性能」而放弃 class。
本章小结
本章我们深入理解了原型与原型链:
构造函数:
- 工厂函数:简单但方法不共享
- 构造函数:配合
new,this 指向实例 new 的执行过程(创建对象 → 关联原型 → 绑定 this 调用 → 返回对象)
原型:
prototype 是构造函数的属性__proto__ 是对象的属性,指向原型- 方法放 prototype 上可共享
- 属性查找是「自上而下」:自有属性优先,找不到才沿原型链向上找(属性遮蔽)
原型链:
- 沿着
__proto__ 形成链 - 终点是
null - 属性查找沿链向上
hasOwnProperty / Object.hasOwn / in / for...in 的差别
继承方式:
- 原型链继承
- 构造函数继承
- 组合继承
- 原型式继承(Object.create)
- 寄生式继承
- 寄生组合式继承(最佳)
常见误区:
- 箭头函数没有
prototype,不能 new - 「函数自己的原型链」与「实例的原型链」是两条不同的链
- 替换整个
prototype 会让新旧实例行为不一致 - 不要在运行时频繁
Object.setPrototypeOf(性能代价高) - 原型上的引用类型属性会被所有实例共享
📊 图示:原型链结构
graph TD
A[实例<br/>obj] -->|__proto__| B[原型<br/>Constructor.prototype]
B -->|constructor| C[构造函数<br/>Constructor]
B -->|__proto__| D[Object.prototype]
D -->|__proto__| E[null]
D -->|constructor| F[Object]
下章预告:下一章我们将学习 this 指向——JavaScript 中最让人困惑的话题之一! 🔮