JavaScript - 原型及原型链
#原型体系概述
JavaScript 是一门基于 原型
而非基于类的面向对象编程语言。尽管 ES6 引入了 class
语法,但这只是语法糖,底层实现仍然基于原型系统。理解 JavaScript 的原型及原型链是掌握这门语言的关键所在。
JavaScript 的原型体系中有三个关键概念:
__proto__
(隐式原型)prototype
(显式原型)原型链
(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); // trueconsole.log(cloudy.__proto__ === Weather.prototype); // true
#constructor 属性
prototype
对象默认有一个 constructor
属性,指向关联的构造函数。这种循环引用使得我们可以从实例找到其构造函数,也可以从构造函数找到其原型对象。
constructor属性JavaScript
function Weather(condition) {this.condition = condition;}// 原型的 constructor 指向构造函数console.log(Weather.prototype.constructor === Weather); // trueconst 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); // trueconsole.log(obj.__proto__.__proto__ === null); // true// 原型链示意图:// obj --> Object.prototype --> null
#数组的原型链
数组的原型链JavaScript
const arr = [];console.log(arr.__proto__ === Array.prototype); // trueconsole.log(arr.__proto__.__proto__ === Object.prototype); // trueconsole.log(arr.__proto__.__proto__.__proto__ === null); // true// 原型链示意图:// arr --> Array.prototype --> Object.prototype --> null
#函数的原型链
函数的原型链JavaScript
function foo() {}console.log(foo.__proto__ === Function.prototype); // trueconsole.log(foo.__proto__.__proto__ === Object.prototype); // trueconsole.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); // trueconsole.log(summerWeather.__proto__.__proto__ === Climate.prototype); // trueconsole.log(summerWeather.__proto__.__proto__.__proto__ === Object.prototype); // trueconsole.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. 扩展内置对象
虽然可以通过原型为内置对象(如 Array
、String
)添加新方法,但这可能导致命名冲突和意外行为。
最佳实践:考虑使用辅助函数或工具库,而不是直接扩展内置对象 的原型。
#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); // trueconsole.log(Weather.prototype.__proto__ === Climate.prototype); // true
#总结
JavaScript 的原型系统是该语言面向对象编程的核心机制,主要由以下几个部分组成:
-
__proto__
(隐式原 型):每个对象都有的内部属性,指向其构造函数的prototype
,用于实现属性和方法的查找。 -
prototype
(显式原型):每个函数都有的属性,用于定义构造函数创建的实例共享的属性和方法。 -
原型链
:通过__proto__
连接多个对象形成的链条,实现对象属性和方法的继承。
通过这三个概念,JavaScript 实现了灵活而强大的 基于原型的继承
机制,使得对象能够共享和继承其他对象的属性和方法。这种机制虽然与基于类的继承有所不同,但提供了同样强大甚至更为灵活的继承能力。
深入理解原型和原型链对于掌握 JavaScript 这门语言、理解现有框架和库的内部实现,以及编写高质量的 JavaScript 代码都是至关重要的。