JavaScript原型与原型链深度解析:从构造函数到Function对象

作者:异步漫步

时间:2026-10-23

浏览:125

本文深入解析JavaScript原型与原型链机制。通过对比传统构造函数的问题,阐述原型对象在共享方法、优化内存中的作用。详细讲解constructor、__proto__及[[Prototype]]内部属性的用法,剖析默认原型链结构。最后探讨Function构造函数动态生成函数的原理及其独特的原型链继承关系,帮助开发者理解JS面向对象的核心逻辑。

文章目录

本文通过具体实例,详细解析JavaScript中原型与原型链的实际应用方法。

简而言之,原型是一个JavaScript对象,用于存储方法。由构造函数创建的实例对象可以引用并访问原型中定义的方法。

一、传统构造函数存在的问题

参考以下代码示例:


function Foo(){
 this.sayHello = function(){
  }
}

由于对象是通过调用new Foo()创建的,因此在每次创建对象时,函数sayHello都会被重新定义一次。

这导致每个对象都拥有独立但功能逻辑完全相同的函数副本,例如:{} == {}。

在代码中为每个对象重复定义方法会消耗性能,最显著的资源浪费体现在内存占用上。

最佳的解决方案是将函数定义在构造函数外部,然后在构造函数内部引用该函数。


function sayHello () {}
function Foo () {
 this.say = sayHello;
}

这种方式在开发中可能带来维护困难:引入外部框架存在风险,且代码变得冗长难以维护。解决思路是确保外部函数不占用构造函数命名空间,并在函数名下进行组织。

每个函数在定义时,系统会自动创建一个神秘对象(即原型对象,暂且如此称呼)。

所有由该构造函数创建的对象,默认都会链接到这个神秘对象上。


var f1 = new Foo();
var f2 = new Foo();
f1.sayHello(); //如果f1没有sayHello那么就会在Foo.prototype中去找

由同一构造函数创建的多个对象共享同一个对象,即:构造函数.prototype。

只需将共享的内容、重复定义会占用额外内存的部分放入构造函数.prototype中,所有对象即可共享这些资源。


function Foo(){}
Foo.prototype.sayHello = function(){
 console.log("….");
}
var f1 = new Foo();
f1.sayHello();
var f2 = new Foo();
f2.sayHello();
console.log(f1.sayHello === f2.sayHello); // true

二、相关核心概念解析

类(class):在JavaScript中,类通常对应于构造函数。

  • 在传统面向对象语言中,使用“类”来定义模板,再通过模板创建对象。
  • 在JavaScript的构造方法中也具备类似功能,因此常将其称为类。

实例(instance)与对象(object)的区别与联系

  • 实例通常指由特定构造函数创建出来的对象,我们称之为“XXXX构造函数的实例”。
  • 实例本质上就是对象。“对象”是一个更泛化的称呼。
  • 实例与对象在概念上属于近义词。

键值对与属性和方法的关系

  • 在JavaScript中,由键值对组成的集合被称为对象。
  • 如果键对应的值是数据(非函数),则该键值对称为属性。
  • 如果键对应的值是函数(方法),则该键值对称为方法(method)。

父类与子类(基类和派生类)的概念

  • 在传统面向对象语言中,使用类来实现继承,从而产生父类和子类的概念。
  • 父类也被称为基类,子类也被称为派生类。
  • JavaScript中没有严格的“类”概念,通常称为父对象、子对象、基对象或派生对象。

三、深入认识原型

在JavaScript中,原型也是一个对象。通过原型可以实现对象的属性继承。JavaScript的每个对象内部都包含一个[[Prototype]]内部属性,该属性指向的就是该对象的原型。

[[Prototype]]作为对象的内部属性,不能直接访问。为了方便查看对象的原型,Firefox和Chrome提供了__proto__这个非标准(并非所有浏览器都支持)的访问器(ECMA标准引入了标准对象原型访问器“Object.getPrototypeOf(object)”)。

下面通过一个具体例子来理解原型相关概念:


function Person() {}
// 神秘对象就是Person.prototype
//那么只有使用构造函数才可以访问它
var o = new Person();
//以前不能直接使用o来访问神秘对象
//现在有了__proto__后,
o.__proto__也可以直接访问神秘对象
//那么o.__proto__ === Person.prototype

神秘对象(原型)中包含一个属性constructor,翻译为“构造器”。它表示该原型是与哪个构造函数相关联的。

__proto__有什么用?它可以用于访问原型。虽然在开发中除非有特殊要求,否则不建议通过实例直接修改原型的成员,导致该属性在开发中使用较少。但在调试过程中非常有用,可以轻易访问原型以查看其成员。

如果在早期浏览器中需要通过实例访问原型,该如何处理?可以使用实例对象访问构造器,然后通过构造器访问原型。


var o = new Person();
o.constructor.prototype

如果给实例继承自原型的属性赋值,会发生什么?


function Foo();
Foo.prototype.name = "test";
var o1 = new Foo();
var o2 = new Foo();
o1.name = "张三"; // 不是修改原型中的name而是自己增加了一个name属性
console.log(o1.name + ','+ o2.name); // 张三,test

四、构造、原型、实例的三角结构图

对于如下代码:


function Person(){}
var p = new Person()

console.log(Person.prototype.constructor); //function Person(){}
console.log(Person.prototype.constructor.name); //Person
console.log(typeof Person.prototype.constructor); //function

console.log(p.__prop__);
console.log(p.__prop__ === Person.prototype);//true

它们的关系图如下所示:

一、传统构造函数存在的问题

五、对象的原型链机制

凡是对象都有原型,而原型本身也是对象。因此,给定一个对象,就能找到它的原型;原型还有自己的原型,如此下去,就构成了一个对象的序列,这种结构称为原型链。

常见问题:

  1. 原型链到底以什么为终点?
  2. 一个默认的原型链结构是怎样的?
  3. 原型链结构对已知语法的修正。

5.1 原型链的结构详解

凡是使用构造函数创建对象,且没有利用赋值方式修改原型,该对象就保留了默认的原型链。

默认原型链结构具体是什么样子的?


function Person(){}
var p = new Person();
//p 具有默认的原型链

默认的原型链结构即为:当前对象 -> 构造函数.prototype -> Object.prototype -> null。

一、传统构造函数存在的问题

在实现继承时,有时会利用替换原型链结构的方式来实现原型继承,此时原型链结构会发生变化。


function DunizbCollection(){}
DunizbCollection.prototype = [];
var arr = new DunizbCollection();
// arr -> [] -> Array.prototype -> Object.prototype -> null

一、传统构造函数存在的问题

六、函数的构造函数Function

在JavaScript中,可以使用Function来实例化函数对象。也就是说,在JavaScript中,函数与普通对象一样,也是一种对象类型。函数是JavaScript中的一等公民。

  1. 函数是对象,因此可以使用对象的动态特性。
  2. 函数是对象,因此有构造函数来创建函数。
  3. 函数是对象,可以创建其他对象。
  4. 函数是唯一可以限定变量作用域的结果。

需要解决的关键问题:

  1. Function如何使用?
  2. Function与函数的关系。
  3. 函数的原型链结构。

6.1 函数是Function的实例

语法如下:


new Function( arg0,arg1,arg1,….argN, body );

Function中的参数全部是字符串。

该构造函数的作用是将参数链接起来组成函数。

  • 如果只有一个参数,则表示函数体。
  • 如果有多个参数,最后一个参数表示函数体,前面的所有参数表示函数的参数。
  • 如果没有参数,表示创建一个空函数。

举例:创建一个打印一句话的函数。


// 传统的
function foo () {
 console.log( '你好' );
}
//Function
var func = new Function( 'console.log( "你好" );' );
// 功能上,这里foo 与 func 等价

再比如,创建一个空函数。


//传统
function foo () {}
//Function
var func = new Function();
func();

传入函数内一个数字,打印该函数。


//传统
function foo ( num ) {
 console.log( num );
}
//Function
var func = new Function( "num" ,"console.log( num )" );
func();

6.2 函数的原型链结构

任意一个函数,都相当于Function的实例,类似于{}与new Object()的关系。


function foo () {}

上面的代码告诉解析器,有一个对象叫foo,它是一个函数;相当于new Function()得到一个函数对象。

  • 函数应该具备什么属性?答:__proto__。
  • 函数的构造函数是什么?答:Function。
  • 函数应该继承自Function.prototype。
  • Function.prototype继承自Object.prototype。

对于Function,我们还需要知道以下几点:

  • Object函数是Function的一个实例。
  • Object作为对象是继承自Function.prototype的,而“Function.prototype”又继承自Object.prototype。

    
    foo.prototype.__proto__ === Object.prototype // true
  • Function是自己的构造函数。
  • 在JavaScript中,任何对象的老祖宗就是Object.prototype。
  • 在JavaScript中,任何函数的老祖宗就是Function.prototype。

下面绘制出Function的构造、原型、实例三角形结构。

一、传统构造函数存在的问题

6.3 为什么要使用Function?

Function是通过字符串构建函数,因此可以在程序运行过程中动态构建函数。

以前的函数必须一开始就写好,再经过预解析,一步一步地运行。

假定从服务器拿到“[1,2,3,4,5]”,需要将数组形式的字符串转换成数组对象。


var arr = ( new Function( 'return ' + str + ' ;' ) )();

感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。

更多关于JavaScript相关内容可查看本站专题:《JavaScript常用函数技巧汇总》、《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》及《JavaScript数学运算用法总结》。

希望本文所述对大家JavaScript程序设计有所帮助。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章