一 前言
作为一个面向对象的语言,理解语言中的对象概念是十分重要的,而要讨论js中的对象,就离不开js的原型。这篇文章会先从理解对象、构造函数开始,进而帮助大家理解原型和原型链,希望可以帮助到正在学习js的小伙伴。(PS:博主也是在学习中发布的这篇博客,不足之处欢迎指正)
二 什么是对象
对象是什么呢?简单的来说——对象是一种数据结构,他拥有属性和方法。
他往往以这样的形式存在
{
name:"老赵",
age:10
getAge:function(){
console.log(name)
}
}
上述代码中的对象有两个属性 name、age ,有一个方法,getAge(),那么我们如何创建一个对象呢,不同的语言有不同的方法,我们熟知的JAVA是通过实例化类的形式来获得对象的,而js是通过原型,和构造函数来创建对象的。
这里我们要先有这样的一个概念,原型和构造函数用于创建对象
三 什么是构造函数
那~什么是构造函数呢?我们可以将构造函数理解为一个生产对象的工厂,构造的意思就是构造对象,其实就是对象的模板。在JAVA中我们通过实例化类来创建对象,那么在JS中,我们就是通过实例化构造函数来创建对象。
我们可以举一个栗子。
//这里创建了一个构造函数
function Dog (name,age){
this. name=name;
this.age=age;
}
//我们通过new关键字创建一个对象
let dog = new Dog("老赵",2);
//输出这个对象的结构
document.write(JSON.stringify(dog))
//构造出的对象
{"name":"老赵","age":2}
在上述的代码中,我们创建了一个构造函数,然后使用new关键字调用这个构造函数,创建了一个对象,通过对比我们可以很清楚的看到构造函数和对象结构的对应关系。那么构造函数创建对象的详细流程是什么呢?
(1)在内存中创建一个新对象。
(2)构造函数内部的this被赋值为这个新对象(即this指向新对象)。
(3)执行构造函数内部的代码(给新对象添加属性)。
(4)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
结合列出的创建对象的步骤,我们再去看上面的代码,是不是又更清晰了呢。
那么构造函数是不是特殊的呢?JS该如何判断一个函数是不是构造函数呢。其实构造函数不是特殊的,任意一个函数,我们都可以把他当作构造函数来用,用它来创建一个对象。至于构造出的对象到底有什么属性,我们根据上面列出的对象创建过程就可以分析出来了。
比如我们这里随意使用一个函数来创建一个对象。
function Show(){
console.log("hello")
}
let show = new Show();
document.write(JSON.stringify(show));
这个函数的功能很简单,就是打印一个hello,我们按照上面的创建规则来分析
第一步 先创建一个新对象,现在我们有一个新对象了
第二步 将this关键字的的指向改为这个新对象,我们没有this关键字,所以忽略
第三步 执行构造函数内部的代码,这里只会打印一个hello
第四步 如果构造函数有非空对象,就返回,否则使用之前创建的对象,我们并没有,所以最终的创建结果就是
{}
一个没有任何属性的对象。
如果大家还有兴趣可以拿别的函数来实验,然后使用上面的流程来分析结果,建议大家实验一下函数直接返回,很有意思的 (😉),这里给大家几个例子。
function Test1(){
this.name="老牛"
return {name :"老赵"}
}
function Test2(){
this.name="老牛"
return "老赵"
}
let test1 = new Test1();
let test2 = new Test2();
document.write(JSON.stringify(test1));
document.write(JSON.stringify(test2));
//大家可以尝试着自己分析一下结果的原因,很好玩的
四 原型和原型的作用
看到这里可能就会有小伙伴疑惑了,我们之前不是说“原型和构造函数用于创建对象”,可上面创建对象的步骤中并没有提及到原型啊?,其实没有提及到原型是为了方便大家理解,我把有关原型的步骤省略了,那么接下来就请出我们今天讨论的主角——“原型”
4.1 什么是原型
我们用最简单的方式理解原型,原型其实就是一个对象,里面存有一些信息。
那么原型和函数的关系是什么呢?其实原型和函数的关系就像双胞胎。
每个函数都有一个prototype属性,这个属性指向一个对象,也就是原型。
我们可以使用上述的图示来描述两者之间的关系,一个函数被创建的时候,这个这个函数对应的原型也会被创建,同时这个函数会有一个属性prototype指向自己的原型,而这个函数的原型,如上图Dog的原型——Dog.prototype中也会有一个属性constructor指向这个函数。两者就像双胞胎一样,每一个函数必定对应一个原型,每一个原型也都有对应的函数,我们在创建一个函数的时候,默认就会创建这个函数对应的原型。
4.2 原型的作用
那么原型有什么作用呢?官方的回答是这样的:“原型中会储存同一构造函数的实例所共享的属性和方法”,听起来可能很抽象,让我们来探寻一下他的原理吧。
其实上面提出的创建对象的流程被我省略了一步,实际的创建过程是这样的。
(1)在内存中创建一个新对象。
(2)这个对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性。
(3)构造函数内部的this被赋值为这个新对象(即this指向新对象)。
(4)执行构造函数内部的代码(给新对象添加属性)。
(5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
我们省略了第二步,那么接下来就让我们来分析一下第二步的内容吧。这个对象(指的就是我们在内存中划出的新对象)内部的[[Prototye]]特性(所谓特性就是不暴露给开发者的属性,但是其实这个属性在某些实现中已经被暴露了,也就是__proto__ 属性)赋值为构造函数的prototype属性。(也就是指对象中的__proto__属性指向构造函数的原型)
我们用下面这张图来描述这个关系
上图中,Dog()这个构造函数的实例 dog 有一个__proto__属性指向原型。也就是说,每一个对象,都有一个__proto__属性,指向自己构造函数的原型。这其实也是一个很有趣的现象,构造函数的对象其实和对象之间没有直接关联,对象直接能访问到的是构造函数的原型。
因为对象可以直接访问到原型,所以对象也就可以访问到原型中的属性。因为同一构造器的所有实例都指向同一个原型,所以放在原型中的属性可以被所有的实例公用。
function Person(){
}
Person.prototype.comment="test";
let person1 = new Person();
let person2 = new Person();
document.write(JSON.stringify(person1)+" ");
document.write(JSON.stringify(person2)+" ");
document.write(person1.comment+" ");
document.write(person1.comment===person2.comment);
{} {} test true
我们可以看到,虽然person1 和 person2 中都没有comment这个属性,但是两者都可以访问到这个属性,同时两者的comment属性完全相同。实际上当我们在person1中去寻找comment属性的时候,当person1中不存在这个属性的时候,js就会去寻找person1所指向的原型,在原型中去寻找。所以我们可以在原型中储存一些公共元素,可以让相同构造器下的所有实例都访问到这些属性。
原型中属性的公共性其实主要体现在引用数据类型上,如果我们通过构造函数在实例中创造出一个方法
function Person(){
this. show = function(){}
}
let person1 = new Person();
let person2 = new Person();
document.write(person1.show==person2.show);
false
我们可以看到,不同实例的相同方法,即使名称相同,但是两者并不相同,因为两者存在与不同的内存空间中,相当于我们把同一个函数创造了两遍,这样无疑会浪费空间,所以我们一般把这些公共的方法放在protortyoe中。
function Person(){
}
Person.prototype.show = function(){}
let person1 = new Person();
let person2 = new Person();
document.write(person1.show==person2.show);
true
这样就可以创造出一个真正的单例公共函数。
4.3 实例修改原型属性
实例是无法修改原型中的属性的,实例对原型的修改会在实例中创造出一个同名的属性,并不会修改原型中的属性。
function Person(){
}
//我们在原型中创造一个公用属性
Person.prototype.comment="test";
let person1 = new Person();
let person2 = new Person();
//试图在person1中修改共有属性
person1.comment="mytest";
//表面上看,person1好像修改了在原型中的属性
document.write(person1.comment+" ") //mytest
//其实只是在person1中创造了一个同名的属性,所以调用后的结果为mytest
document.write(JSON.stringify(person1)+" ") //{comment:mytest}
//我们使用person2调用prototype中的属性,发现并没有被修改
document.write(person2.comment+" ") //test
但是实例是可以修改在prototype中对象的属性的,因为修改对象的属性并没有修改prototype属性的指向,所以不会被认为是修改了propertype的属性。
function Person(){
}
//我们在原型中创造一个公用对象
Person.prototype.obj={};
let person1 = new Person();
let person2 = new Person();
//给公共对象添加属性
person1.obj.name="老赵";
//我们可以用person2查看到修改
document.write(JSON.stringify(person2.obj)) //{"name":"老赵"}
//person1 中也没有新建同名属性
document.write(JSON.stringify(person1))//{}
同时这样是原型的缺点,强大的共享能力,使得原型中不适合放入对象。
4.4 总结
我们上面主要讲明了对象,原型,构造函数之间的关系——构造函数和原型用于创建函数,构造函数中的语句会作为对象的模板,“指导”对象在内存中创建相应的属性,而原型中储存了所有实例对象共享的属性,实例对象可以通过指向原型的指针来访问原型中的公共属性。
此外,原型和构造函数是一对双生子,每一个构造函数都有对应的原型,两者可以分别通过.prototype 和 .constructor来访问彼此,形象点的解释,构造函数和原型就是对象的爹和妈。两者分别以不同的方式让实例对象(孩子)获得属性。
一旦一个构造函数被创造,就会形成这样的链条。
一旦一个对象被实例化出来,就会在链条的末尾添上一个对象。
好了,基础知识咱们就讲完了,接下来就是咱们的重头戏,原型链了。
五 原型链
在如果你对前面的知识已经理解的差不多了,那么恭喜你,因为你再去学习原型链将会非常容易,如果你对前面的知识并不是十分理解,那么建议你停下来好好理解一下,再来进行原型链的学习,因为原型链其实就是多个原型拼接起来的链条而已。
原型链(一)
我们前面提到过,原型是一个对象,那么根据我们之前的理论,每个对象都是由一个构造函数和构造函数原型来进行创造的,那么是谁创造了原型呢,我们用一个图来解释。
其实就是Object这个这个构造器(没错,Object其实是个函数)和Object.prototype 创造了构造函数的prototype。那么是谁创造了Object.prototype呢。
对,就是null(😂)这个对象是js预设好的,我们可以理解为他就是原型链的终点,万物的伊始,类似于JAVA中的Object。
这样我们就形成了一个链条
对象实例可以沿着链条逐步向前访问各prototype中的内容。
function Person(){}
//我们在原型链上各个prototyp中添加内容
Person.prototype.add1="add1";
Object.prototype.add2="add2";
//实例化一个对象
let person = new Person();
//可以访问到原型链上的内容
document.write(person.add1); //add1
document.write(person.add2); //add2
这样就形成了一条对象的原型链了。
原型链(二)
我们知道,其实在js中函数也是一种对象,那么既然是对象就一定会有构造函数和对应的原型,那么,构造函数的原型是什么呢。
其实就是Function,这个其实好理解,我们创建的每一个函数对象,都是Function的实例,所以Dog的.__proto__属性就指向Function.prototype,Functon就是最初的构造函数。
那么~~,Function这个构造函数的原型是什么呢?(🤣)
没错,就是Function,既然Function这个构造函数他是个函数,那么他的__proto__指向所有函数的原型Function.function也挺合情合理的吧(都是JS底层规定好的)
接下来我们继续寻找,Function.prototype的原型。既然他是个对象(我们前面说过函数的prototype是一个对象)那么他的原型就应该指向Object.prototype,所以最终我们的原型链就完成了。
是不是有些理解了,其实核心思路就是对象的创建需要构造函数和原型,对象指向原型,所有的没有经过显式构造器创建的对象默认通过Object构造创建,指向Object.prototype,所有的函数默认通过Function构造器构造,指向Function.Prototype,原型和构造函数之间可以通过constructor和prototye相互访问。(划重点😁)
我自己画的图并不好,这里给大家提供另一个博主画的图,方便大家理解
六 写在后面
原型链的内容就给大家讲完了,本篇文章的主要目的是让大家对原型和原型链有一个初步的认识,奠定一定的基础,如果大家想对原型链有更深入的认识和理解,建议去看一下其他的教程,博主自己是通过看JavaScript高级程序设计(第四版)这本书来进行学习的,书内的第八章的主要内容就是对象和原型链,书内还有很多好玩的内容,比如说通过原型链来实现类与继承。鼓励大家多使用原型链的知识去尝试解释一些js的语法,会有更好的理解,比如js的数据类型。
好了,这篇博客到这里就结束了,欢迎大家在评论区进行讨论。