本文深入解析 JavaScript 原型与原型链机制,阐明 prototype 与 __proto__ 的作用。针对构造函数继承中 constructor 指向混乱及原型污染问题,提供了基于空构造函数中转的解决方案,并介绍了利用 Object.create() 实现对象间安全继承的方法,帮助开发者理解 JS 面向对象核心逻辑。
JavaScript 原型与原型链基础概念
本文通过具体实例解析 JavaScript 中原型与原型链的核心概念,旨在帮助读者深入理解并掌握相关技术细节。
在 JavaScript 语言体系中,万物皆对象。由于早期缺乏类似 Class 类的显式概念,对象之间的层级与关联关系难以直观表达。
例如,若存在对象 A 与对象 B,二者在默认状态下是相互独立的个体。修改对象 A 的属性或方法,通常不会波及对象 B。
然而,这种独立性带来了一个资源消耗问题:如果对象 A 和对象 B 都拥有完全相同的 run() 方法,且代码逻辑一致,那么每个对象都会独立存储一份完整的 run() 方法副本,这将占用大量内存资源。
当程序中创建的对象数量庞大时,这种重复存储造成的资源浪费将变得不可忽视。因此,我们需要一种机制,使对象 A 与对象 B 能够共享公共属性和方法,从而建立彼此间的联系。
我们可以设想存在一个名为 common 的公共对象,该对象负责存储所有对象共享的属性和方法。此时,对象 A 和对象 B 内部均包含一个 prototype 属性,该属性指向这个 common 对象。
在调用对象 A 或对象 B 的属性或方法时,JavaScript 引擎会遵循特定规则:首先在当前对象自身中查找;若未找到,则通过 prototype 属性指向的目标对象继续查找。
不仅如此,common 对象自身也拥有一个 prototype 属性,指向更上一级的公共对象。这种查找过程会持续向上进行,直到遇到 null 为止,此时查找停止。
这种自下而上逐级查找的路径构成了所谓的“原型链”,而路径中指向的公共对象则被称为“原型对象”。
构造函数与原型对象的关联机制
接下来,我们通过一个具体的构造函数示例来深入剖析这一机制。
function Base(name) {
this.name = name;
}
let A = new Base('A');
let B = new Base('B');
//每一个函数都有一个prototype属性,指向该函数的原型对象
console.log(Base.prototype);
//当然原型对象也是一个对象,它也有一个constructor,指向构造函数
console.log(Base.prototype.constructor === Base);
//每一个实例对象的constructor都指向创建它们的构造函数
console.log(A.constructor === Base);
console.log(B.constructor === Base);
//每一个实例对象都有一个__proto__属性,该属性指向构造函数的原型对象
console.log(A.__proto__ === Base.prototype);
console.log(B.__proto__ === Base.prototype);
首先,每个函数都具备一个 prototype 属性,该属性指向该函数对应的原型对象。
其次,原型对象本身也是一个对象,因此它拥有自己的 constructor 属性,该属性指向构造函数 Base()。这意味着,原型对象实质上也是构造函数 Base() 的一个特殊实例,其主要用途是存放公共属性和方法。
第三,通过构造函数 Base() 创建的每一个实例对象,都拥有一个 constructor 属性,该属性指向创建它的构造函数 Base()。
第四,每个对象都包含一个 __proto__ 属性,该属性指向构造函数 Base() 的原型对象。__proto__ 是将各个原型串联形成链条的关键纽带;若缺少此属性,对象 A 与对象 B 将无法访问原型对象上的公共属性和方法。
function Base(name) {
this.name = name;
}
//我们在原型对象上添加公共属性
Base.prototype.status = '开始';
//我们在原型对象上添加公共方法
Base.prototype.run = function() {
console.log(this.name + ' run ...');
};
let A = new Base('A');
let B = new Base('B');
A.run();
B.run();
console.log(A.status);
console.log(B.status);
//修改原型上的属性,则实例对象也会跟着改变
Base.prototype.status = '停止';
console.log(A.status);
console.log(B.status);
通过引入原型与原型链机制,对象与对象之间建立了明确的关联关系。
构造函数继承的实现与问题
那么,如何利用原型与原型链实现一个构造函数对另一个构造函数的继承呢?
假设我们需要让构造函数 Child 继承自构造函数 Base,直观的想法是让 Child 的 prototype 指向 Base 的原型对象。
function Base(name) {
}
Base.prototype.name = 'Base';
Base.prototype.run = function () {
console.log(this.name + ' run ...');
};
function Child() {
}
Child.prototype = Base.prototype;
//注意这个时候,Child.prototype对象的constructor属性指向了Base
//这就导致通过构造函数Child创建的实例对象,对象的constructor属性会指向Base,而不是Child,这会导致混乱。
//所以我们重新设置Child.prototype.constructor指向Child
Child.prototype.constructor = Child;
let c = new Child();
console.log(c.name);
c.run();
但这种做法存在缺陷:Child.prototype 与 Base.prototype 将指向同一个原型对象。任何对 Child.prototype 的修改都会直接反映到 Base.prototype 上,导致原型污染。
此时,Base.prototype.constructor 的指向会发生改变,转而指向 Child,这显然不符合预期,会导致类型判断混乱。
为了解决这一问题,我们需要引入一个中间的空构造函数,通过它来正确完成原型的指向与隔离。
function Base(name) {
}
Base.prototype.name = 'Base';
Base.prototype.run = function () {
console.log(this.name + ' run ...');
};
function Child() {
}
//创建一个中间的空构造函数
function Mid() {
}
//让该空构造函数的prototype指向Base的原型对象
Mid.prototype = Base.prototype;
//再让Child的prototype指向该空构造函数的一个实例
Child.prototype = new Mid();
//这样,当修改Child.prototype.constructor时,Base.prototype就不会受影响了
Child.prototype.constructor = Child;
let c = new Child();
console.log(c.name);
c.run();
//Base.prototype的constructor仍然指向Base,没有受到影响
console.log(Base.prototype.constructor);
对象间继承的安全实现方案
进一步地,如何实现一个对象对另一个对象的继承?
例如,若要让对象 B 继承自对象 A,核心目标是获取对象 A 的属性和方法。直观上,似乎只需将对象 B 的 __proto__ 指向对象 A 即可实现。
let A = {
name: 'A',
run() {
console.log(this.name + ' run ...');
}
};
console.log(A.name);
A.run();
let B = {};
//让对象B的__proto__指向对象A
B.__proto__ = A;
//当对象B调用run()方法时会在自身上找,如果没找到,则通过__proto__向上找
//由于__proto__指向对象A,所以最终会在对象A中找到run()方法
B.run();
B.__proto__.name = 'B';
console.log(A.name);
console.log(B.name);
然而,这种方法同样存在风险:当执行 B.__proto__.name = 'B' 时,由于 B.__proto__ 直接指向 A,修改操作会直接影响对象 A 的 name 属性,导致 A 的状态被意外改变。
为了解决上述直接赋值带来的副作用,可以利用 ES5 提供的 Object.create() 方法。该方法允许通过指定的原型对象创建一个新的对象,从而在保持继承关系的同时避免对原原型的直接修改。
let A = {
name: 'A',
run() {
console.log(this.name + ' run ...');
}
};
console.log(A.name);
A.run();
let B = {};
//通过Object.create()创建一个以对象A为原型对象的新对象
//让对象B的__proto__指向该新对象
//这样再操作B.__proto__中的属性就与对象A无关了。
B.__proto__ = Object.create(A);
B.run();
B.__proto__.name = 'B';
console.log(A.name);
console.log(B.name);
感兴趣的技术人员可以使用在线 HTML/CSS/JavaScript 代码运行工具:http://tools.jb51.net/code/HtmlJsRun 对上述代码的运行效果进行测试。
更多关于 JavaScript 相关内容感兴趣的读者可查看本站专题:《javascript 面向对象入门教程》、《JavaScript 错误与调试技巧总结》、《JavaScript 数据结构与算法技巧总结》、《JavaScript 遍历算法与技巧总结》及《JavaScript 数学运算用法总结》。
希望本文所述的原理与实例分析能对大家的 JavaScript 程序设计工作有所帮助。