第 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
22
23
function myNew(constructor, ...args) {
  // 1. 创建新对象
  const obj = {};

  // 2. 设置原型
  Object.setPrototypeOf(obj, 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

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() 而不是直接访问 __proto__,因为 __proto__ 并不是所有环境都支持的标准属性。


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 (原型)

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); // ["音乐"] ← 没有被影响!

问题:方法无法复用(每个实例都有一份方法副本)。


组合继承

组合继承结合原型链和构造函数继承:

 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

💡 为什么寄生组合式继承是最佳方式?

  1. 只调用一次 Parent 构造函数
  2. 原型链完整(instanceof 正常工作)
  3. 原型上的 constructor 正确

六种继承方式对比

方式优点缺点
原型链继承简单引用类型共享
构造函数继承属性独立方法不复用
组合继承综合优点调用两次构造函数
原型式继承简单引用类型共享
寄生式继承可扩展方法不复用
寄生组合式继承完美继承稍复杂

💡 推荐使用寄生组合式继承,或者直接使用 ES6 的 class extends


本章小结

本章我们深入理解了原型与原型链:

  1. 构造函数

    • 工厂函数:简单但方法不共享
    • 构造函数:配合 newthis 指向实例
    • new 的执行过程
  2. 原型

    • prototype 是构造函数的属性
    • __proto__ 是对象的属性,指向原型
    • 方法放 prototype 上可共享
  3. 原型链

    • 沿着 __proto__ 形成链
    • 终点是 null
    • 属性查找沿链向上
  4. 继承方式

    • 原型链继承
    • 构造函数继承
    • 组合继承
    • 原型式继承(Object.create)
    • 寄生式继承
    • 寄生组合式继承(最佳)

📊 图示:原型链结构

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 中最让人困惑的话题之一! 🔮