JavaScript 原型与原型链深度解析
1. 核心思想:JS 是一门基于原型的语言
1.1 设计初衷
- 设计者: 布兰登·艾克 (Brendan Eich)。
- 灵感来源: 借鉴了
Self和Smalltalk这两门基于原型的语言。 - 设计选择:
- 为何选择原型而非类?
- JS 最初的设计目标是为非专业的网页设计者提供一个简单的脚本工具,而非像 Java/C++ 那样的专业编程语言。
- 设计者初期不打算引入复杂的“类” (Class) 概念。
- 结果: JS 的对象系统是基于原型的,而非传统的面向对象(基于类)。
- 为何选择原型而非类?
1.2 基本概念
- JS 是其最重要的语言特性之一,也是面试中的高频考点(例如:美团2019年面试题)。
- 在 JS 中,对象并非从类实例化而来,而是从原型对象 (Prototype Object) 构建而来。
2. 原型模式的直接体现:Object.create()
Object.create() 是 ES5 提供的一个方法,它可以让我们直接体验基于原型的对象创建过程。
2.1 基本用法
通过克隆一个“原型”对象来创建一个新对象。
// 1. 定义一个原型对象 person
const person = {
arms: 2,
legs: 2
};
// 2. 使用 Object.create() 创建一个新对象 zhangsan,并指定 person 为其原型
const zhangsan = Object.create(person);
console.log(zhangsan); // {} -> 输出一个空对象
console.log(zhangsan.arms); // 2
console.log(zhangsan.legs); // 2
// 验证原型关系
console.log(zhangsan.__proto__ === person); // true- 解释:
zhangsan对象本身是空的。- 当我们试图访问
zhangsan.arms时,JS 引擎在zhangsan自身上找不到该属性。 - 于是,引擎会沿着
zhangsan的原型链向上查找,找到了其原型对象person,并返回person.arms的值。 - 我们可以通过非标准的
__proto__属性访问一个对象的原型。
2.2 Object.create() 的第二个参数
该方法接收第二个可选参数,用于定义新对象自身的属性(通过属性描述符)。
const zhangsan = Object.create(person, {
// 每个键是一个新属性,值是该属性的描述符对象
name: {
value: '张三',
enumerable: true // 设置为 true 才能在 console.log 中直接看到
},
age: {
value: 18,
enumerable: true
}
});
console.log(zhangsan); // { name: '张三', age: 18 }
console.log(zhangsan.name); // 张三
console.log(zhangsan.arms); // 2 (从原型 person 继承)2.3 构建原型链
我们可以通过 Object.create() 链接多个对象,形成一条原型链。
// zhangsan 继承自 person
const zhangsan = Object.create(person, { /* ...属性... */ });
// zhangxiaosan 继承自 zhangsan
const zhangxiaosan = Object.create(zhangsan, {
born: {
value: '北京',
enumerable: true
}
});- 形成的链条:
zhangxiaosan->zhangsan->person
2.4 原型链上的属性查找规则
总结:当查找一个对象的属性时,如果该对象自身没有这个属性,则会去该对象的原型对象 (
__proto__) 上查找。如果原型对象上还没有,则会继续沿着原型对象的原型向上查找,直到找到该属性或到达原型链的终点 (null)。如果最终没有找到,则返回undefined。
// 1. 查找自身属性
console.log(zhangxiaosan.born); // '北京'
// 2. 自身没有,去原型 zhangsan 上查找
console.log(zhangxiaosan.name); // '张三'
// 3. zhangsan 也没有,去 zhangsan 的原型 person 上查找
console.log(zhangxiaosan.arms); // 2
// 4. 整条原型链上都没有
console.log(zhangxiaosan.gender); // undefined
// 验证原型链
console.log(zhangxiaosan.__proto__ === zhangsan); // true
console.log(zhangxiaosan.__proto__.__proto__ === person); // true3. 模拟类:构造函数
随着 JS 的发展,开发者希望能像传统面向对象语言一样,通过“类”来批量生产对象。在 ES6 之前,这是通过构造函数来模拟的。
3.1 基本实现
// 1. 定义一个构造函数 (首字母通常大写)
function Computer(name, price) {
// `new` 会创建一个新对象,并把 this 指向它
this.name = name;
this.price = price;
}
// 2. 使用 new 关键字创建实例
const apple = new Computer('苹果', 15000);
const huawei = new Computer('华为', 12000);
console.log(apple); // Computer { name: '苹果', price: 15000 }
console.log(huawei); // Computer { name: '华为', price: 12000 }3.2 方法存放的问题与优化
- 问题: 如果将方法直接定义在构造函数内部,每个实例都会创建并持有一份独立的方法函数,造成内存浪费。
function Computer(name, price) {
this.name = name;
this.price = price;
// 每次 new 都会创建一个新的函数,浪费内存
this.showPrice = function() { /*...*/ };
}- 优化: 将共享的方法挂载到构造函数的原型对象上。
核心原则:属性(通常是每个实例独有的值)放在构造函数里,方法(所有实例共享的功能)放在原型对象上。
function Computer(name, price) {
this.name = name;
this.price = price;
}
// 将方法挂载到 Computer 的原型对象上
Computer.prototype.showPrice = function() {
console.log(`${this.name}的电脑价格为${this.price}`);
};
const apple = new Computer('苹果', 15000);
const huawei = new Computer('华为', 12000);
apple.showPrice(); // 苹果的电脑价格为15000
huawei.showPrice(); // 华为的电脑价格为12000- 解释:
apple和huawei实例自身没有showPrice方法,它们会通过原型链在Computer.prototype上找到并共享同一个showPrice方法。
4. 关键三角关系:prototype, __proto__, constructor
这是理解原型链的基石,必须掌握。
- 构造函数 (Constructor): 如
Computer。 - 原型对象 (Prototype Object):
Computer.prototype。 - 实例对象 (Instance): 如
apple。
4.1 三大定律
实例.__proto__ === 构造函数.prototype- 实例对象的
__proto__属性,指向其构造函数的prototype属性所引用的那个对象(即原型对象)。
- 实例对象的
构造函数.prototype.constructor === 构造函数- 原型对象天生自带一个
constructor属性,指回其关联的构造函数。
- 原型对象天生自带一个
实例的
constructor来自原型- 实例对象本身没有
constructor属性,当访问实例.constructor时,它会通过原型链找到实例.__proto__.constructor,也就是其构造函数。
- 实例对象本身没有
4.2 代码验证
function Computer(name, price) { /* ... */ }
const apple = new Computer('苹果', 15000);
// 验证定律 1
console.log(apple.__proto__ === Computer.prototype); // true
// 验证定律 2
console.log(Computer.prototype.constructor === Computer); // true
// 验证定律 3
console.log(apple.constructor === Computer); // true这个三角关系同样适用于 JS 内置的构造函数,如 Array, Object, Function 等。
const arr = []; // 等价于 new Array()
console.log(arr.__proto__ === Array.prototype); // true
console.log(Array.prototype.constructor === Array); // true5. 完整的原型链图谱
这张图揭示了 JS 世界中几乎所有对象和函数的最终联系。
5.1 链条的终点
- 任何普通对象的原型链,最终都会指向
Object.prototype。 Object.prototype是一个特殊的对象,它的原型是null。null是整个原型链的终点。
代码验证
const apple = new Computer('苹果', 15000);
// apple -> Computer.prototype -> Object.prototype -> null
console.log(apple.__proto__ === Computer.prototype); // true
console.log(apple.__proto__.__proto__ === Object.prototype); // true
console.log(apple.__proto__.__proto__.__proto__ === null); // true
// Object.prototype 的原型是 null
console.log(Object.prototype.__proto__); // null5.2 Function 的特殊地位
- 所有函数都是
Function的实例。这包括自定义构造函数 (Computer)、内置构造函数 (Object,Array),甚至Function自身。 - 因此,任何函数的
__proto__都指向Function.prototype。 Function.prototype也是一个对象,它的原型是Object.prototype。
代码验证
// Computer 函数的原型是 Function.prototype
console.log(Computer.__proto__ === Function.prototype); // true
// Object 构造函数的原型是 Function.prototype
console.log(Object.__proto__ === Function.prototype); // true
// Function.prototype 的原型是 Object.prototype
console.log(Function.prototype.__proto__ === Object.prototype); // true6. 回答面试题
Q1: 谈谈你对JS中原型和原型链的理解?
原型 (Prototype):
- JS 是基于原型的语言。每个对象都有一个内部链接指向另一个对象,这个对象就是它的“原型”。
- 可以通过
__proto__访问一个对象的原型。 - 构造函数有一个
prototype属性,它指向一个对象,这个对象将成为由该构造函数创建的所有实例的原型。 - 原型对象有一个
constructor属性,指回构造函数本身,形成一个循环引用(三角关系)。
原型链 (Prototype Chain):
- 当试图访问一个对象的属性时,如果在对象本身找不到,JS 引擎就会去它的原型对象上找。
- 原型对象本身也是一个对象,它也有自己的原型。这样一层一层链接起来,就形成了一条“原型链”。
- 属性查找会沿着这条链一直向上,直到找到属性或到达链的终点
null。 - 这种机制是 JS 实现继承的核心。
Q2: 对一个构造函数实例化后,它的原型链指向什么?
假设有一个构造函数 MyConstructor 和一个实例 const myInstance = new MyConstructor()。
它的原型链是:
myInstance的原型 (myInstance.__proto__) 指向MyConstructor.prototype。MyConstructor.prototype本身是一个普通对象,所以它的原型 (MyConstructor.prototype.__proto__) 指向Object.prototype。Object.prototype的原型 (Object.prototype.__proto__) 指向null,链条结束。
完整链条: myInstance -> MyConstructor.prototype -> Object.prototype -> null