深入理解JavaScript原型与原型链机制

作者:代码拾荒

时间:2026-10-30

浏览:476

本文详细解析JavaScript基于原型的编程模型,阐明函数作为一等公民及prototype属性的动态继承特性。通过对比自身属性与原型属性的差异,深入剖析属性搜索流程及原型链工作原理,并结合String.prototype.trim等实例说明如何安全扩展内置对象,帮助开发者掌握JavaScript面向对象的核心机制。

文章目录

JavaScript原型基础概念

JavaScript 是一种基于原型的编程语言,这与传统的基于类的编程模式存在显著差异。其核心特征主要体现在以下几个方面:

1.函数是first class object,即函数与对象拥有同等的语言地位 2.不存在类,仅存在对象 3.函数本质上是一种对象,即函数对象 4.对象的传递方式是基于引用 那么这种基于原型(prototype based programming)的语言如何实现继承(面向对象的一大基本要素),这便是prototype概念产生的原因。

通过以下代码片段可以直观观察:

函数对象的属性观察


function foo(a, b, c)
{
return a*b*c;
}
alert(foo.length);
alert(typeof foo.constructor);
alert(typeof foo.call);
alert(typeof foo.apply);
alert(typeof foo.prototype);

在浏览器中运行上述代码后,可以观察到以下现象:

1. length 属性:提供的是函数的参数个数。 2. prototype 属性:是一个对象(object)。 3. 其余三个(call、apply、constructor)均为函数(function)。 对于任何函数的声明,它都具备上述五个属性(方法或属性)。

接下来重点分析 prototype 属性。

prototype的动态继承特性


// prototype
function Person(name, gender)
{
this.name = name;
this.gender = gender;
this.whoAreYou = function(){//这个也是所谓的closure, 内部函数可以访问外部函数的变量
var res = "I'm " + this.name + " and I'm a " + this.gender +".";
return res;
};
}
// 那么在由Person创建的对象便具有了下面的几个属性
Person.prototype.age = 24;
Person.prototype.getAge = function(){
return this.age;
};
flag = true;
if (flag)
{
var fun = new Person("Tower", "male");
alert(fun.name);
alert(fun.gender);
alert(fun.whoAreYou());
alert(fun.getAge());
}
Person.prototype.salary = 10000;
Person.prototype.getSalary = function(){
return this.name + " can earn about " + this.salary + "RMB each month." ;
};
// 下面就是最神奇的地方, 我们改变了Person的prototype,而这个改变是在创建fun之后
// 而这个改变使得fun也具有了相同的属性和方法
// 继承的意味即此
if (flag)
{
alert(fun.getSalary());
alert(fun.constructor.prototype.age);//而这个相当于你直接调用 Person.prototype.age
alert(Person.prototype.age);
}

从示例中可以发现,对于 prototype 的方法或属性,我们可以动态地增加,而由该函数创建的对象会自动继承这些新增的方法和属性。

此外,每个对象都有一个 constructor 属性,用于指向创建该对象的函数。例如在上述例子中,fun.constructor 指向的就是 Person 函数。

由此产生一个疑问:函数对象自身声明的方法和属性,与通过 prototype 声明的属性有何区别?

主要存在以下区别:

1. 自身声明的方法和属性是静态的。也就是说,在声明后,如果试图增加新方法或修改已有方法,不会对由该函数创建的对象产生影响,即继承失效。 2. 而 prototype 可以动态地增加新方法或修改已有方法,从而具有动态性。一旦父函数对象声明了相关的 prototype 属性,由其创建的对象会自动继承这些 prototype 属性。 继续上面的例子:

自身属性与原型属性的区别


flag = true;
// 函数内部声明的方法是静态的,无法传递的
Person.school = "ISCAS";
Person.whoAreYou = function(){
return "zhutao";
};//动态更改声明期的方法,并不会影响由其创建的对象的方法, 即所谓的 静态
if (flag)
{
alert(Person.school);
alert(fun.school);//输出的是 "undefined"
alert(Person.whoAreYou()); //输出 zhutao
alert(fun.whoAreYou()); // I'm Tower and I'm a male.
}
Person.prototype.getSalary = function(){
return "I can earn 1000000 USD";
};
if (flag)
{
alert(fun.getSalary());//已经继承了改变, 即所谓的 动态
}

既然函数对象既有自身属性,又有 prototype 属性,那么由该函数创建的对象是如何搜索相应属性的呢?

基本按照以下流程和顺序进行:

1. 首先搜索函数对象自身的属性,如果找到立即执行。 2. 如果自身属性未找到,则搜索 prototype 属性。此时有两种结果:找到则直接执行;否则继续搜索父对象的父对象的 prototype,直至找到属性,或者到达 prototype chain 的结尾(结尾通常是 Object 对象)。 上述机制也回答了当函数对象自身的属性与 prototype 属性重名时的解决方式:函数对象自身的属性优先。

prototype 的典型应用示例。

使用过 jQuery 或 Prototype 库的开发者可能知道,这些库中通常都包含 trim 方法。

示例代码如下:

prototype典型应用与防御性编程


String.prototype.trim = function() {
 return this.replace(/^\s+|\s+$/g, '');
};

trim 方法的用法如下:


' foo bar '.trim(); // 'foo bar'

然而,这样做存在一个缺点。因为较新版本的浏览器 JavaScript 引擎在 String 对象中本身已提供 trim 方法,我们自己定义的 trim 会覆盖内置的 trim。因此,在定义 trim 方法之前,可以进行简单检测,判断是否需要添加该方法:


if(!String.prototype.trim) {
 String.prototype.trim = function() {
 return this.replace(/^\s+|\s+$/g, '');
 };
}

原型链机制。

在 JavaScript 中,定义或实例化任何对象时,它都会被附加一个名为 __proto__ 的隐藏属性,原型链正是依靠这个属性得以形成。但请注意,不要直接访问 __proto__ 属性,因为部分浏览器不支持直接访问。另外,__proto__ 与对象的 prototype 属性并非同一概念,它们各自有特定的用途。

如何理解这一点?当我们创建 myObject 函数时,实际上是创建了一个 Function 类型的对象:

console.log(typeof myObject); // function

这里需要说明,Function 是 JavaScript 中预定义的一个对象,因此它拥有预定义的属性(如 length 和 arguments)和方法(如 call 和 apply),当然也有 __proto__,以此实现原型链。也就是说,JavaScript 引擎内部可能存在如下代码片段:

原型链与__proto__机制


Function.prototype = {
 arguments: null,
 length: 0,
 call: function() {
 // secret code
 },
 apply: function(){
 // secret code
 },
 ...
};

事实上,JavaScript 引擎的代码不可能如此简单,这里仅用于描述原型链的工作原理。

我们定义了一个函数 myObject,它还有一个参数 name,但并未给它任何其他属性,例如 length 或其他方法如 call。那么下面这段代码为何能正常执行?


console.log(myObject.length); // 结果:1,是参数的个数

这是因为我们在定义 myObject 时,同时也给它定义了一个 __proto__ 属性,并赋值为 Function.prototype(参考前面的代码片段)。因此,我们可以像访问其他属性一样访问 myObject.length,即使我们并未定义这个属性,因为它会顺着 __proto__ 原型链向上查找 length,最终在 Function 中找到。

那为什么找到的 length 属性的值是 1,而不是 0 呢?它是什么时候被赋值的?由于 myObject 是 Function 的一个实例:


console.log(myObject instanceof Function); // true
console.log(myObject === Function); // false

当实例化一个对象时,对象的 __proto__ 属性会被赋值为其构造器的原型对象。在本示例中,构造器是 Function,此时构造器会计算参数的个数,从而改变 length 的值。


console.log(myObject.__proto__ === Function.prototype); // true

而当我们使用 new 关键字创建一个新的实例时,新对象的 __proto__ 将被赋值为 myObject.prototype,因为此时的构造函数为 myObject,而非 Function。


var myInstance = new myObject('foo');
console.log(myInstance.__proto__ === myObject.prototype); // true

新对象除了能访问 Function.prototype 中继承下来的 call 和 apply 外,还能访问从 myObject 中继承下来的 getName 方法:


console.log(myInstance.getName()); // foo
 
var mySecondInstance = new myObject('bar');
 
console.log(mySecondInstance.getName()); // bar
console.log(myInstance.getName()); // foo

这相当于将原型对象视为一个蓝本,然后根据这个蓝本创建 N 个新的对象。

再看一个多重 prototype 链的例子:

多重原型链示例


// 多重prototype链的例子
function Employee(name)
{
this.name = "";
this.dept = "general";
this.gender = "unknown";
}
function WorkerBee()
{
this.projects = [];
this.hasCar = false;
}
WorkerBee.prototype = new Employee; // 第一层prototype链
function Engineer()
{
this.dept = "engineer"; //覆盖了 "父对象"
this.language = "javascript";
}
Engineer.prototype = new WorkerBee; // 第二层prototype链
var jay = new Engineer("Jay");
if (flag)
{
alert(jay.dept); //engineer, 找到的是自己的属性
alert(jay.hasCar); // false, 搜索到的是自己上一层的属性
alert(jay.gender); // unknown, 搜索到的是自己上二层的属性
}

上述示例的对象关系如下:

JavaScript原型基础概念

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章