本文深入解析JavaScript中的原型与原型链机制。从构造函数内存浪费问题入手,详解prototype原型对象、__proto__对象原型及constructor构造函数的关系。通过原型链追溯Object.prototype,并探讨原型中this指向及内置对象扩展应用,帮助开发者彻底理解JS面向对象核心原理。
在ES6语法规范普及之前,JavaScript中实现面向对象编程主要依赖构造函数模式。这种模式的核心思路是将对象共有的属性与方法封装在构造函数内部。
具体实现方式如下所示:
function student(uname,age) {
this.uname = uname;
this.age = age;
this.school = function() {
console.log('深圳理工大学');
}
}
var stu1 = new student('小红',18);
var stu2 = new student('小紫',20);尽管构造函数模式使用便捷,但它存在明显的内存效率缺陷。以示例为例,构造函数内部定义的函数属于复杂数据类型,每次实例化时都会在内存中开辟独立空间。若创建多个实例,同一函数会被重复存储,导致不必要的内存浪费。
1. 构造函数原型(prototype)
通过原型分配的函数由所有对象实例共享。
JavaScript规范规定,每个构造函数均包含一个prototype属性,该属性指向另一个独立对象。需注意的是,prototype本身即为一个对象,其包含的所有属性和方法均归构造函数所有。
我们可以将那些不会随实例变化的公共方法直接定义在prototype对象上,从而使所有对象实例共享这些方法。
因此,在前述示例中,将公共函数移至原型对象内部定义,即可有效避免内存浪费问题。
function student(uname,age) {
this.uname = uname;
this.age = age;
}
student.prototype.school = function() {
console.log('深圳理工大学');
}
var stu1 = new student('小红',18);
var stu2 = new student('小紫',20);至此,我们明确了原型的概念:它本质上是一个对象,通常被称为原型对象。
原型的核心作用可概括为四个字:共享方法。
2. 对象原型(__proto__)
每个对象实例均拥有一个__proto__属性,该属性指向创建该实例的构造函数的prototype原型对象。正是由于__proto__的存在,对象才能访问构造函数原型对象中的属性和方法。
系统在对象实例上自动添加__proto__属性,使其指向构造函数的原型对象。因此,__proto__(对象原型)与prototype(原型对象)在指向关系上是等价的。
我们可以通过以下代码验证这一关系:
console.log(stu1.__proto__ === student.prototype);
最终输出结果为true,证实了对象原型__proto__与原型对象prototype在指向上是等价的。
因此,方法查找遵循以下规则:首先检查stu1对象自身是否包含school方法,若存在则直接执行该对象上的school;若不存在,则通过__proto__属性,前往构造函数原型对象prototype中查找school方法。
若上述解释仍显抽象,可通过下图直观理解:

3. constructor构造函数
对象原型(__proto__)与构造函数原型(prototype)内部均包含一个constructor属性,该属性指向构造函数本身,因此被称为构造函数。
我们打印student.prototype和stu1.__proto__以检查constructor属性的存在:
console.log(student.prototype);
console.log(stu1.__proto__);
结果显示两者均包含constructor属性,那么该属性的具体作用是什么呢?
constructor主要用于记录对象引用的构造函数,它使原型对象能够重新指向原始的构造函数。
我们可以在原型对象中定义多个公共方法。若方法数量较多,使用对象字面量的形式进行存储将更加便捷:
student.prototype = {
school:function() {},
location:function() {}
}接下来,我们输出原型对象的constructor以观察其变化:
console.log(student.prototype.constructor);
console.log(stu1.__proto__.constructor);最终输出结果如下:

为何constructor不再指向我们的student构造函数?
原因在于,当我们通过student.prototype = {...}这种赋值方式重写原型对象时,原有的prototype内容被完全覆盖,导致student.prototype中不再包含constructor属性。由于缺少constructor,原型对象便无法再指回student构造函数。
此时,我们需要手动利用constructor属性将原型对象指回原来的构造函数。
student.prototype = {
constructor:student,
school:function() {},
location:function() {}
}再次输出原型对象的constructor:

这样我们就能明确识别出该对象是由哪个构造函数创建出来的。
现在,我们更新构造函数、实例与原型对象三者之间的关系图:

4. 原型链
由于student原型对象本身也是一个对象,根据前文所述,任何对象都具备__proto__属性。
我们打印该原型对象以查看其内部是否包含原型:
function student(uname,age) {
this.uname = uname;
this.age = age;
}
student.prototype.school = function() {
console.log('深圳理工大学');
}
var stu1 = new student('小红',18);
console.log(student.prototype);输出结果如下:

可以看到,原型对象内部也存在一个原型。由于原型指向原型对象,那么student.prototype内部的__proto__具体指向何处?
我们进行打印测试:
console.log(student.prototype.__proto__);结果显示,它指向的是Object原型对象。

Object原型对象由谁创建?毫无疑问,是由Object构造函数创建。继续推导,Object原型对象也是一个对象,因此它同样拥有原型,该原型又指向何处?
我们输出查看:
console.log(Object.prototype.__proto__);最终结果为null(空):

至此到达原型链的最顶层。将这些环节串联起来,便形成了原型链:

5. 原型对象中的this指向
我们知道,在构造函数内部,this指向的是对象实例。那么,定义在原型对象中的函数,其内部的this指向谁呢?
我们声明一个全局变量that,将原型对象中的this赋值给that,以验证that是否指向实例对象:
function student(uname,age) {
this.uname = uname;
this.age = age;
}
var that;
student.prototype.school = function() {
that = this;
console.log('深圳理工大学');
}
var stu1 = new student('小红',18);
stu1.school();
console.log(that === stu1);输出结果如下:

由此可见,原型对象函数内部的this同样指向实例对象stu1。
6. 扩展内置对象(原型对象的应用)
我们可以利用原型对象对原有的内置对象进行扩展,添加自定义方法。例如,为数组增加自定义的求和功能。
我们输出数组的原型对象,查看其中包含的方法:
console.log(Array.prototype);
此处尚未为数组定义求和函数,因此我们向数组的原型对象中添加该方法:
Array.prototype.sum = function() {
var sum = 0;
for(var i = 0;i<this.length;i++)
{
sum += this[i];
}
return sum;
}
var ss = new Array(4,5,3,6);
console.log(ss.sum());在自定义的sum方法中,this.length指的是调用该方法的数组长度。基于前文结论,原型对象函数中的this指向实例对象,因此this.length即代表调用该方法的数组实例的长度。
我们通过new关键字创建一个数组实例对象。由于已向数组的原型对象添加sum方法,该实例对象即可直接调用此方法。
输出结果如下:

再次打印数组的原型对象,确认sum方法是否已成功添加:

结果显示sum方法已成功添加到数组的原型对象中。此后,在需要使用数组求和时,可直接调用sum方法。
至此,关于JavaScript中原型与原型链的详细介绍已完成。如需了解更多JS原型与原型链相关内容,请搜索本站往期文章或继续浏览后续相关文章,希望大家今后多多支持本站!