JavaScript原型继承体系详解:从原型链到constructor修正

作者:拾光

时间:2026-10-22

浏览:315

本文深入解析JavaScript基于原型的继承体系,阐述构造器、原型属性与原型对象的关系。通过对比null与空对象,厘清原型链终点。重点讲解如何通过修改prototype实现继承,并针对手动指定原型后constructor指向偏移的问题,介绍ECMAScript 3中修正constructor属性的标准方法,以正确串联内部原型链与构造器原型链。

文章目录

在近期进行的Web项目开发中,虽然jQuery等框架的使用带来了极大的便利,但为了深入掌握JavaScript的核心机制,笔者对JS的原型继承体系进行了梳理与总结。本文旨在分享对原型继承的理解,如有疏漏之处,恳请指正。

构造器的原型属性与原型对象

一、构造器的原型属性与原型对象

初学者在接触JavaScript时,往往只是机械地使用函数通过new关键字创建实例,并不清楚其背后的原理,仅知“函数即对象”这一概念。实际上,JavaScript并未采用Java等语言中的类继承体系,而是基于原型对象(prototype)来构建继承关系。其核心机制是利用“构造器”来模拟类的功能。

在理解原型继承之前,需要明确两个核心概念:原型属性(prototype)与原型对象(实例)。

在JavaScript的对象系统中,每个创建的函数(即构造器)都拥有一个名为prototype的原型属性。同时,通过该构造器创建的每个对象实例,内部都包含一个_proto_属性。prototype和_proto_这两个属性本质上都是指针,它们均指向同一个原型对象。普通函数与构造函数的关键区别在于,构造函数的prototype属性指向一个具有实际意义的对象,而普通函数则不然。

原型属性prototype所指向的原型,本质上是一个对象实例(Object instance)。如图1所示,如果构造器Animal()拥有一个原型对象B,那么由该构造器创建的所有实例都将复制自B。这意味着,实例a1的_proto_属性会指向原型对象B,从而使得a1能够继承B所拥有的所有属性、方法及其他特性。

构造器的原型属性与原型对象

图1 js对象实例化实现

空的对象与null的区别

二、空的对象

在JavaScript中,“空的对象”构成了整个原型继承体系的基石,它是所有对象的基础。在深入探讨“空的对象”之前,有必要先区分“空对象(null)”这一概念。

关于空对象null

null并非“空的对象”,它是JavaScript中的一个保留字,具体含义如下:

(1)它属于对象类型;

(2)其值为空。

作为一个对象类型,可以使用for…in循环尝试列举其属性。然而,由于它是一个空值,null没有任何方法和属性(包括constructor、_proto_等),因此循环内部无法列举出任何内容。如下例所示:


var num=0;
  for(var propertyName in null)
  {
  num++;
  }

Alert(num);//显示值为0

最关键的一点是,null没有原型。它不是由Object()构造器(或其任何子类)实例化而来的对象,因此对null进行instanceof运算将返回false。

2. “空的对象”

“空的对象”指的是一个标准的、通过Object()构造器创建的对象实例。例如:


obj=new Object();或 obj={};

“空的对象”具备“对象”的所有特性,因此可以访问toString()、valueOf等预定义的属性和方法。

3. “空的对象”与null的关系

如图2中红线所示路径,当通过“Object.prototype._proto_”获取Object原型对象的_proto_属性时,结果将是“null”。由于null对象没有任何属性,这意味着“Object {}”的原型对象就是原型链的终点。

原型对象就是原型链的终点了。

构造器的原型属性与原型对象

图2 js类继承体系

Javascript继承的实现及原型链维护

三、Javascript继承的实现以及原型链维护

(1)继承的实现

前文提到,JavaScript中的类继承是通过修改构造函数的原型属性prototype来实现的。具体代码实现如下:


function Animal() {
this.name = 'Animal';
};
function Dog() {
};
  Dog.prototype = new Animal();
var d = new Dog();
console.log(d.name);//'Animal'

通过创建一个Animal类型的实例,并将其赋值给构造函数Dog()的prototype属性,从而实现了类型继承,即Animal成为Dog的父类。此时,Dog类型的实例d也能访问到Animal类型的name属性。

(2)原型链

在JS对象继承体系中,存在两种原型链:“内部原型链”和“构造器原型链”。如图3所示,黑色箭头指示的路径是通过构造函数的prototype属性维持的“构造器原型链”。红色箭头指示的路径则是通过对象实例的_proto_属性维持的“内部原型链”。

构造器的原型属性与原型对象

图3 原型链

(3)原型链维护

图3表明,构造器通过显式的prototype构建了一条原型链,而对象实例则通过_proto_属性构建了另一条原型链。由于_proto_是一个不可直接访问的内部属性(尽管在Chrome中可以查看其值,但无法直接修改),因此无法直接从子类(Dog)的实例dog1开始访问整个原型链。为此,我们需要在图3中的“内部原型链”和“构造器原型链”之间找到一个连接点,使得在无法直接访问obj._proto_的情况下,仍能通过构造器访问内部原型链,从而将两种原型链串联起来。

若要从子类的实例开始访问整个原型链,必须利用实例的constructor属性来维护原型链。

实际上,JavaScript已经为构造器自动维护了原型属性。根据以下测试代码,当我们自定义一个构造器时,其原型对象是一个Object()类型的实例,但该原型对象的constructor属性默认总是指向构造器自身,而非其父类Object。如图4中构造器实例里蓝色框所示的constructor属性,该属性继承自原型对象。由此可知,自定义构造器产生的实例,其constructor属性默认指向该构造器本身。


function Animal() {
};
var a = new Animal();
console.log(Animal.prototype);//Object(){}
console.log(Animal.prototype.constructor === Animal);//true//true

  构造器的原型属性与原型对象

图4

因此,在_proto_属性不可访问时,可以通过a1.constructor.prototype来获取实例a1的原型对象。然而,当我们自定义构造函数Dog(),并手动将其prototype属性值指定为Animal(即指定Dog的父类为Animal)时,访问d1.constructor的值将变为Animal,而非Dog。从图5可以看出,Dog的原型对象和dog实例分别由Animal()和Dog()两个不同的构造器产生,但它们的constructor属性都指向了相同的构造器(Animal)。这与利用constructor属性串联两种原型链的初衷相冲突。

构造器的原型属性与原型对象

图5

是构造器出了问题还是原型对象出了问题?从图5可以看出,原型继承所要求的“复制行为”已正确实现,子类实例能够访问原型对象的属性。问题在于,在为子类构造器Dog()赋予原型对象时,应当“修正”该原型对象的constructor属性值(constructor)。ECMAScript 3标准提供的方法是:保持原型的构造器属性,并在子类构造器中初始化其实例对象的constructor属性。代码如下:


function Dog () {
  //初始化constructor属性
   this.constructor=Dog; //或 this.constructor=arguments.callee;
  };
  Dog.prototype = new Animal();//赋予原型对象,实现继承

构造器的原型属性与原型对象

图6

对constructor属性进行“修正”后的效果如图6所示。在子类构造器Dog中初始化其实例对象的constructor属性后,Dog的实例对象的constructor均指向Dog,而Dog的原型对象的constructor仍指向父类型构造器Animal。这样便实现了利用constructor属性串联原型链,使得可以从子类实例开始回溯整个原型链。

总结

以上所述是小编给大家介绍的javascript继承体系的相关知识,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对本站网站的支持!

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章