JavaScript - 原型及原型链

#原型体系概述

JavaScript 是一门基于 原型 而非基于类的面向对象编程语言。尽管 ES6 引入了 class 语法,但这只是语法糖,底层实现仍然基于原型系统。理解 JavaScript 的原型及原型链是掌握这门语言的关键所在。

JavaScript 的原型体系中有三个关键概念:

  1. __proto__(隐式原型)
  2. prototype(显式原型)
  3. 原型链(prototype chain)

#隐式原型(proto

__proto__ 是每个 JavaScript 对象(除了 null)都拥有的一个内部属性,它指向该对象的构造函数的原型对象(即构造函数的 prototype 属性)。

__proto__ 属性在现代浏览器中可以被直接访问,但它实际上是一个非标准属性。ES6 标准化了访问这个内部属性的方法:Object.getPrototypeOf()Object.setPrototypeOf()

__proto__ 决定了对象的 原型链,是 JavaScript 实现继承的核心机制。当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 引擎会沿着 __proto__ 指向的原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(null)。

#示例解析

隐式原型示例
JavaScript
// 创建普通对象
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
// 创建数组
const arr = [];
console.log(arr.__proto__ === Array.prototype); // true
// 自定义构造函数和实例
function Weather(condition) {
this.condition = condition;
}
const sunny = new Weather('晴天');
console.log(sunny.__proto__ === Weather.prototype); // true

#显式原型(prototype)

prototype 是函数对象特有的属性。每当我们定义一个函数时,JavaScript 引擎会为这个函数自动创建一个 prototype 属性,该属性是一个对象(默认情况下只包含一个 constructor 属性,指向这个函数本身)。

prototype 属性用于实现 JavaScript 的 基于原型的继承。当一个函数被用作构造函数(通过 new 关键字调用)来创建对象时,所有通过该构造函数创建的实例对象都将共享这个 prototype 对象上定义的属性和方法。

简而言之,构造函数的 prototype 属性会成为其所有实例的 __proto__ 属性的指向对象,它是该构造函数所有实例共享的原型。

#示例解析

显式原型示例
JavaScript
function Weather(condition) {
this.condition = condition;
}
// 在原型上添加方法,所有实例共享
Weather.prototype.forecast = function() {
console.log(`今天的天气是 ${this.condition}`);
};
const rainy = new Weather('下雨');
const cloudy = new Weather('多云');
// 两个实例共享原型上的方法
rainy.forecast(); // "今天的天气是 下雨"
cloudy.forecast(); // "今天的天气是 多云"
// 验证实例与原型的关系
console.log(rainy.__proto__ === Weather.prototype); // true
console.log(cloudy.__proto__ === Weather.prototype); // true

#constructor 属性

prototype 对象默认有一个 constructor 属性,指向关联的构造函数。这种循环引用使得我们可以从实例找到其构造函数,也可以从构造函数找到其原型对象。

constructor属性
JavaScript
function Weather(condition) {
this.condition = condition;
}
// 原型的 constructor 指向构造函数
console.log(Weather.prototype.constructor === Weather); // true
const windy = new Weather('大风');
// 实例通过 __proto__ 访问原型的 constructor 属性
console.log(windy.__proto__.constructor === Weather); // true
// 简写形式(通过原型链查找)
console.log(windy.constructor === Weather); // true

#原型链(Prototype Chain)

原型链 是由对象通过 __proto__ 属性连接形成的链条。当访问一个对象的属性或方法时,如果这个对象本身没有该属性或方法,JavaScript 引擎会沿着 __proto__ 指向的原型链向上查找,直到找到该属性或方法,或到达原型链的顶端 null

原型链是 JavaScript 实现 继承 的核心机制。通过原型链,一个对象可以继承另一个对象的属性和方法,实现代码的复用和层次化组织。

#不同类型对象的原型链

#普通对象的原型链
普通对象的原型链
JavaScript
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
console.log(obj.__proto__.__proto__ === null); // true
// 原型链示意图:
// obj --> Object.prototype --> null
#数组的原型链
数组的原型链
JavaScript
const arr = [];
console.log(arr.__proto__ === Array.prototype); // true
console.log(arr.__proto__.__proto__ === Object.prototype); // true
console.log(arr.__proto__.__proto__.__proto__ === null); // true
// 原型链示意图:
// arr --> Array.prototype --> Object.prototype --> null
#函数的原型链
函数的原型链
JavaScript
function foo() {}
console.log(foo.__proto__ === Function.prototype); // true
console.log(foo.__proto__.__proto__ === Object.prototype); // true
console.log(foo.__proto__.__proto__.__proto__ === null); // true
// 原型链示意图:
// foo --> Function.prototype --> Object.prototype --> null

#基于原型链的继承实现

JavaScript 中实现继承主要是通过原型链来完成的。通过将一个构造函数的 prototype 设置为另一个构造函数的实例,可以创建一条原型链,从而实现继承关系。

基于原型链的继承
JavaScript
// 父构造函数
function Climate(season) {
this.season = season;
}
// 在父原型上添加方法
Climate.prototype.getSeason = function() {
console.log(`当前季节是 ${this.season}`);
};
// 子构造函数
function Weather(condition, season) {
// 调用父构造函数
Climate.call(this, season);
this.condition = condition;
}
// 建立原型链,实现继承
Weather.prototype = Object.create(Climate.prototype);
// 修复 constructor 指向
Weather.prototype.constructor = Weather;
// 在子原型上添加方法
Weather.prototype.getCondition = function() {
console.log(`${this.season}的天气是 ${this.condition}`);
};
// 创建实例
const summerWeather = new Weather('炎热', '夏季');
// 测试继承的属性和方法
console.log(summerWeather.season); // "夏季"
summerWeather.getSeason(); // "当前季节是 夏季"
summerWeather.getCondition(); // "夏季的天气是 炎热"
// 验证原型链
console.log(summerWeather.__proto__ === Weather.prototype); // true
console.log(summerWeather.__proto__.__proto__ === Climate.prototype); // true
console.log(summerWeather.__proto__.__proto__.__proto__ === Object.prototype); // true
console.log(summerWeather.__proto__.__proto__.__proto__.__proto__ === null); // true
// 原型链示意图:
// summerWeather --> Weather.prototype --> Climate.prototype --> Object.prototype --> null

#常见问题与最佳实践

#1. 原型污染

当在原生对象原型(如 Object.prototype)上添加或修改属性时,会影响所有通过原型链继承这些属性的对象,这称为 原型污染

原型污染示例
JavaScript
// 不推荐的做法
Object.prototype.customMethod = function() {
console.log('这会影响所有对象!');
};
const temp = {};
temp.customMethod(); // "这会影响所有对象!"

最佳实践:避免修改原生对象的原型,可以使用类、闭包或模块模式来封装功能。

#2. 原型与实例属性的区分

当实例和原型上存在同名属性时,访问时会优先获取实例上的属性,这称为 属性遮蔽

属性遮蔽示例
JavaScript
function Temperature() {}
Temperature.prototype.value = '原型上的温度';
const temp = new Temperature();
console.log(temp.value); // "原型上的温度"
temp.value = '实例上的温度';
console.log(temp.value); // "实例上的温度"

最佳实践:使用 hasOwnProperty 方法区分属性是来自实例还是原型。

hasOwnProperty使用
JavaScript
console.log(temp.hasOwnProperty('value')); // true,属性在实例上

#3. 扩展内置对象

虽然可以通过原型为内置对象(如 ArrayString)添加新方法,但这可能导致命名冲突和意外行为。

最佳实践:考虑使用辅助函数或工具库,而不是直接扩展内置对象的原型。

#ES6 中的类与原型

ES6 引入的 class 语法为基于原型的继承提供了更清晰的语法糖,但底层实现仍然基于原型系统。

ES6类与原型
JavaScript
class Climate {
constructor(season) {
this.season = season;
}
getSeason() {
console.log(`当前季节是 ${this.season}`);
}
}
class Weather extends Climate {
constructor(condition, season) {
super(season);
this.condition = condition;
}
getCondition() {
console.log(`${this.season}的天气是 ${this.condition}`);
}
}
const winterWeather = new Weather('寒冷', '冬季');
winterWeather.getSeason(); // "当前季节是 冬季"
winterWeather.getCondition(); // "冬季的天气是 寒冷"
// 验证,仍然基于原型系统
console.log(winterWeather.__proto__ === Weather.prototype); // true
console.log(Weather.prototype.__proto__ === Climate.prototype); // true

#总结

JavaScript 的原型系统是该语言面向对象编程的核心机制,主要由以下几个部分组成:

  1. __proto__(隐式原型):每个对象都有的内部属性,指向其构造函数的 prototype,用于实现属性和方法的查找。

  2. prototype(显式原型):每个函数都有的属性,用于定义构造函数创建的实例共享的属性和方法。

  3. 原型链:通过 __proto__ 连接多个对象形成的链条,实现对象属性和方法的继承。

通过这三个概念,JavaScript 实现了灵活而强大的 基于原型的继承 机制,使得对象能够共享和继承其他对象的属性和方法。这种机制虽然与基于类的继承有所不同,但提供了同样强大甚至更为灵活的继承能力。

深入理解原型和原型链对于掌握 JavaScript 这门语言、理解现有框架和库的内部实现,以及编写高质量的 JavaScript 代码都是至关重要的。