深入理解 JavaScript 原型链(配图超多)

从零开始彻底搞懂 JavaScript 原型和原型链,用图解和实例帮你理解 prototype、__proto__、constructor 的关系,面试必备知识点。

16 分钟阅读
小明

深入理解 JavaScript 原型链(配图超多)

「说说 JavaScript 的原型链?」

面试官轻描淡写的一句话,让无数前端候选人心跳加速。

原型链是 JavaScript 最核心也最让人困惑的概念之一。prototype__proto__constructor……这些东西到底是什么关系?

今天小明用大量的图来帮你一次搞懂。保证你看完之后,不仅能应付面试,还能真正理解 JavaScript 的面向对象。


为什么需要原型?

从一个问题说起

假设你要创建 100 个用户对象:

const user1 = {
  name: '小明',
  age: 18,
  sayHello() {
    console.log(`大家好,我是 ${this.name}`);
  }
};

const user2 = {
  name: '小红',
  age: 20,
  sayHello() {
    console.log(`大家好,我是 ${this.name}`);
  }
};

// ... 还有 98 个

问题来了:每个对象都有一个 sayHello 方法,这 100 个方法内容完全一样,但在内存中存了 100 份。

这是极大的浪费!

构造函数 + 原型 = 解决方案

// 构造函数:定义实例属性
function User(name, age) {
  this.name = name;
  this.age = age;
}

// 原型:定义共享方法
User.prototype.sayHello = function() {
  console.log(`大家好,我是 ${this.name}`);
};

// 创建实例
const user1 = new User('小明', 18);
const user2 = new User('小红', 20);

// 两个实例共享同一个 sayHello 方法
console.log(user1.sayHello === user2.sayHello);  // true

现在 100 个用户对象共享同一个 sayHello 方法,内存中只存了 1 份。

这就是原型的作用:实现属性和方法的共享。


原型三角关系

JavaScript 原型涉及三个核心概念:

  1. prototype:函数的属性,指向原型对象
  2. __proto__:对象的属性,指向创建它的构造函数的原型
  3. constructor:原型对象的属性,指向构造函数

用图来表示:

                    ┌─────────────────────┐
                    │     User 函数       │
                    │  (构造函数)         │
                    └──────────┬──────────┘
                               │
                               │ prototype
                               ▼
┌──────────────┐    ┌─────────────────────┐
│    user1     │    │  User.prototype     │
│   (实例)     │───▶│    (原型对象)       │
└──────────────┘    │                     │
   __proto__        │  sayHello: ƒ        │
                    │  constructor ───────┼───┐
┌──────────────┐    └─────────────────────┘   │
│    user2     │              ▲               │
│   (实例)     │──────────────┘               │
└──────────────┘                              │
   __proto__                                  │
                                              │
                    ┌─────────────────────────┘
                    │
                    ▼
              指回 User 函数

验证三角关系

function User(name) {
  this.name = name;
}

const user = new User('小明');

// 1. 函数有 prototype 属性
console.log(User.prototype);  // { constructor: ƒ }

// 2. 实例的 __proto__ 指向构造函数的 prototype
console.log(user.__proto__ === User.prototype);  // true

// 3. 原型的 constructor 指向构造函数
console.log(User.prototype.constructor === User);  // true

// 4. 实例可以通过 constructor 找到构造函数
console.log(user.constructor === User);  // true

小明冷笑话时间:

prototype 和 proto 有什么区别? prototype 是给孩子准备的遗产,proto 是孩子继承遗产的凭证。


原型链:一路向上

当你访问一个属性时

当你访问对象的属性时,JavaScript 会按照这个顺序查找:

  1. 先在对象自身找
  2. 找不到,去 __proto__(原型)找
  3. 还找不到,去原型的原型找
  4. 一直找到 null 为止

这条查找链路就是原型链

function Animal(name) {
  this.name = name;
}
Animal.prototype.eat = function() {
  console.log(this.name + ' is eating');
};

function Dog(name, breed) {
  Animal.call(this, name);  // 继承属性
  this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);  // 继承方法
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
  console.log('Woof!');
};

const dog = new Dog('旺财', '柴犬');

// 访问 name:在 dog 自身找到
console.log(dog.name);  // '旺财'

// 访问 bark:在 Dog.prototype 找到
dog.bark();  // 'Woof!'

// 访问 eat:在 Animal.prototype 找到
dog.eat();  // '旺财 is eating'

// 访问 toString:在 Object.prototype 找到
console.log(dog.toString());  // '[object Object]'

完整的原型链图

┌─────────────┐
│     dog     │
│  (实例)     │
│ name: '旺财' │
│ breed: '柴犬'│
└──────┬──────┘
       │ __proto__
       ▼
┌─────────────────┐
│ Dog.prototype   │
│                 │
│ bark: ƒ         │
│ constructor:Dog │
└──────┬──────────┘
       │ __proto__
       ▼
┌─────────────────┐
│ Animal.prototype│
│                 │
│ eat: ƒ          │
│ constructor:    │
│   Animal        │
└──────┬──────────┘
       │ __proto__
       ▼
┌─────────────────┐
│Object.prototype │
│                 │
│ toString: ƒ     │
│ hasOwnProperty: │
│   ƒ             │
│ ...             │
└──────┬──────────┘
       │ __proto__
       ▼
      null
     (终点)

原型链的终点

所有原型链最终都会到达 Object.prototype,而 Object.prototype.__proto__null

console.log(Object.prototype.__proto__);  // null

这就是原型链的终点。


几个容易混淆的点

1. prototype vs __proto__

prototype__proto__
谁有?只有函数有所有对象都有
作用?给实例用的模板指向原型的链接
正式名称?是标准属性非标准(应该用 Object.getPrototypeOf)
function Foo() {}
const foo = new Foo();

// 函数有 prototype
console.log(Foo.prototype);  // { constructor: ƒ }

// 对象有 __proto__
console.log(foo.__proto__);  // { constructor: ƒ }

// 它们相等
console.log(foo.__proto__ === Foo.prototype);  // true

// 普通对象没有 prototype
console.log(foo.prototype);  // undefined

2. 函数也是对象

在 JavaScript 中,函数也是对象。所以函数既有 prototype(作为构造函数),也有 __proto__(作为对象)。

function Foo() {}

// 作为构造函数,有 prototype
console.log(Foo.prototype);  // { constructor: ƒ }

// 作为对象,有 __proto__,指向 Function.prototype
console.log(Foo.__proto__ === Function.prototype);  // true

3. 内置对象的原型关系

// Array
const arr = [1, 2, 3];
console.log(arr.__proto__ === Array.prototype);  // true
console.log(Array.prototype.__proto__ === Object.prototype);  // true

// Function
function fn() {}
console.log(fn.__proto__ === Function.prototype);  // true
console.log(Function.prototype.__proto__ === Object.prototype);  // true

// 特殊:Function 自己创造自己
console.log(Function.__proto__ === Function.prototype);  // true

// 特殊:Object 也是函数
console.log(Object.__proto__ === Function.prototype);  // true

终极关系图

这是 JavaScript 原型最完整的关系图:

                              null
                                ▲
                                │ __proto__
                    ┌───────────────────────┐
                    │   Object.prototype    │
                    │                       │
                    │   toString: ƒ         │
                    │   hasOwnProperty: ƒ   │
                    └───────────┬───────────┘
                          ▲     │
                          │     │ __proto__
         ┌────────────────┼─────┴───────────────────┐
         │                │                         │
┌────────┴──────┐ ┌───────┴────────┐  ┌────────────┴──────┐
│Array.prototype│ │Function.prototype│ │  其他原型对象    │
│               │ │                  │  │                  │
│ push: ƒ       │ │ call: ƒ          │  │                  │
│ pop: ƒ        │ │ apply: ƒ         │  │                  │
│ map: ƒ        │ │ bind: ƒ          │  │                  │
└───────────────┘ └────────┬─────────┘  └──────────────────┘
        ▲                  │ ▲
        │ __proto__        │ │ __proto__
        │                  │ │
   ┌────┴────┐      ┌──────┴─┴──────┐
   │  [1,2]  │      │   Function    │◀─┐
   │  (数组) │      │   Object      │  │ __proto__
   └─────────┘      │   Array       │──┘
                    │   (构造函数)  │
                    └───────────────┘

原型的实际应用

1. 实现继承

// 父类
function Animal(name) {
  this.name = name;
}

Animal.prototype.eat = function() {
  console.log(`${this.name} is eating`);
};

// 子类
function Dog(name, breed) {
  Animal.call(this, name);  // 继承实例属性
  this.breed = breed;
}

// 继承原型方法(推荐方式)
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// 添加子类特有方法
Dog.prototype.bark = function() {
  console.log('Woof!');
};

// 使用
const dog = new Dog('旺财', '柴犬');
dog.eat();   // '旺财 is eating' (继承自 Animal)
dog.bark();  // 'Woof!' (Dog 自己的)

2. 扩展内置对象(谨慎使用)

// 给所有数组添加一个方法
Array.prototype.first = function() {
  return this[0];
};

const arr = [1, 2, 3];
console.log(arr.first());  // 1

// ⚠️ 注意:修改内置对象原型可能导致冲突
// 一般只在 polyfill 中这么做

3. 检测属性来源

const dog = new Dog('旺财', '柴犬');

// hasOwnProperty:检查是否是自身属性
console.log(dog.hasOwnProperty('name'));  // true (自身属性)
console.log(dog.hasOwnProperty('eat'));   // false (原型上的)

// in 操作符:检查自身 + 原型链
console.log('name' in dog);  // true
console.log('eat' in dog);   // true

// 遍历自身属性
Object.keys(dog);  // ['name', 'breed']

// 遍历自身 + 原型链上的可枚举属性
for (let key in dog) {
  console.log(key);  // name, breed, bark, eat, ...
}

ES6 class:语法糖的真相

ES6 的 class 只是原型的语法糖,底层还是原型链。

// ES6 class 写法
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  eat() {
    console.log(`${this.name} is eating`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
  
  bark() {
    console.log('Woof!');
  }
}

// 等价于 ES5 的原型写法
function Animal(name) {
  this.name = name;
}

Animal.prototype.eat = function() {
  console.log(`${this.name} is eating`);
};

function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
  console.log('Woof!');
};

验证一下:

class Foo {
  bar() {}
}

// class 只是语法糖
console.log(typeof Foo);  // 'function'
console.log(Foo.prototype.bar);  // ƒ bar() {}

面试常考题

题目 1:手写 instanceof

instanceof 就是沿着原型链查找。

function myInstanceof(obj, Constructor) {
  // 获取构造函数的 prototype
  const prototype = Constructor.prototype;
  
  // 获取对象的原型
  let proto = Object.getPrototypeOf(obj);
  
  // 沿着原型链查找
  while (proto !== null) {
    if (proto === prototype) {
      return true;
    }
    proto = Object.getPrototypeOf(proto);
  }
  
  return false;
}

// 测试
function Dog() {}
const dog = new Dog();

console.log(myInstanceof(dog, Dog));     // true
console.log(myInstanceof(dog, Object));  // true
console.log(myInstanceof(dog, Array));   // false

题目 2:手写 new 操作符

new 做了什么?

  1. 创建一个新对象
  2. 将新对象的 __proto__ 指向构造函数的 prototype
  3. 执行构造函数,绑定 this 为新对象
  4. 如果构造函数返回对象,则返回该对象;否则返回新对象
function myNew(Constructor, ...args) {
  // 1. 创建新对象,原型指向构造函数的 prototype
  const obj = Object.create(Constructor.prototype);
  
  // 2. 执行构造函数
  const result = Constructor.apply(obj, args);
  
  // 3. 如果构造函数返回对象,则返回该对象
  return result instanceof Object ? result : obj;
}

// 测试
function User(name, age) {
  this.name = name;
  this.age = age;
}

const user = myNew(User, '小明', 18);
console.log(user.name);  // '小明'
console.log(user instanceof User);  // true

题目 3:实现继承的几种方式

// 1. 原型链继承(问题:引用类型属性共享)
function Parent() {
  this.colors = ['red', 'blue'];
}
function Child() {}
Child.prototype = new Parent();

// 2. 构造函数继承(问题:无法继承原型方法)
function Child() {
  Parent.call(this);
}

// 3. 组合继承(问题:Parent 被调用两次)
function Child() {
  Parent.call(this);
}
Child.prototype = new Parent();

// 4. 寄生组合继承(推荐)
function Child() {
  Parent.call(this);
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

// 5. ES6 class(最推荐,语法简洁)
class Child extends Parent {
  constructor() {
    super();
  }
}

题目 4:输出什么?

function Foo() {
  getName = function() { console.log(1); };
  return this;
}

Foo.getName = function() { console.log(2); };
Foo.prototype.getName = function() { console.log(3); };
var getName = function() { console.log(4); };
function getName() { console.log(5); }

Foo.getName();           // ?
getName();               // ?
Foo().getName();         // ?
getName();               // ?
new Foo.getName();       // ?
new Foo().getName();     // ?
new new Foo().getName(); // ?

答案和解析:

Foo.getName();           // 2 (调用 Foo 的静态方法)
getName();               // 4 (函数声明提升,但被函数表达式覆盖)
Foo().getName();         // 1 (Foo() 修改了全局的 getName)
getName();               // 1 (全局 getName 已被修改)
new Foo.getName();       // 2 (new (Foo.getName)())
new Foo().getName();     // 3 ((new Foo()).getName())
new new Foo().getName(); // 3 (new ((new Foo()).getName)())

总结

原型链的核心知识点:

  1. prototype:函数的属性,存放共享方法
  2. __proto__:对象的属性,指向原型
  3. constructor:原型对象的属性,指回构造函数
  4. 原型链:通过 __proto__ 形成的查找链
  5. 终点Object.prototype.__proto__ === null

记住这个口诀:

实例的 __proto__ 等于构造函数的 prototype

instance.__proto__ === Constructor.prototype

只要记住这一条,其他的都能推导出来。


小明冷笑话收尾:

问:JavaScript 的原型链为什么这么复杂? 答:因为 JavaScript 出生太仓促了。Brendan Eich 只用了 10 天就设计出了 JavaScript。

你 10 天能做什么?我 10 天可能还在纠结用什么框架。

问:为什么 Function.__proto__ === Function.prototype? 答:因为 Function 是自己创造了自己。这就像是「先有鸡还是先有蛋」的 JavaScript 版本。

「理解原型链,你就理解了 JavaScript 的一半。」—— 小明