深入理解 JavaScript 原型链(配图超多)
从零开始彻底搞懂 JavaScript 原型和原型链,用图解和实例帮你理解 prototype、__proto__、constructor 的关系,面试必备知识点。
深入理解 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 原型涉及三个核心概念:
- prototype:函数的属性,指向原型对象
- __proto__:对象的属性,指向创建它的构造函数的原型
- 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 会按照这个顺序查找:
- 先在对象自身找
- 找不到,去
__proto__(原型)找 - 还找不到,去原型的原型找
- 一直找到
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 做了什么?
- 创建一个新对象
- 将新对象的
__proto__指向构造函数的prototype - 执行构造函数,绑定
this为新对象 - 如果构造函数返回对象,则返回该对象;否则返回新对象
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)())
总结
原型链的核心知识点:
- prototype:函数的属性,存放共享方法
- __proto__:对象的属性,指向原型
- constructor:原型对象的属性,指回构造函数
- 原型链:通过
__proto__形成的查找链 - 终点:
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 的一半。」—— 小明