【趣味JavaScript】一文让你读懂JavaScript原型对象与原型链的继承,探秘属性的查找机制! 《重置版》
🚀 个人主页 极客小俊
✍🏻 作者简介:web开发者、设计师、技术分享博主
🐋 希望大家多多支持一下, 我们一起学习和进步!😄
🏅 如果文章对你有帮助的话,欢迎评论 💬点赞👍🏻 收藏 📂加关注
前言
有些新手朋友可能听说过这么一句话,就是js
中存在两个链条,它们分别为:作用域链
和原型链
它们彼此的区别在于作用域链
是为了访问变量
和数据
而存在的一种链条访问机制
而原型链
是访问对象
的属性
或者方法
而存在的一种机制!
其中这里的原型链
就是今天我要说的主题!
我们学习js
必须要知道什么是原型、原型链、构成函数、实例对象
这些彼此之间的关系和应用范围!
如果你没有搞明白js
中的原型链
也就说明你没有把js
学明白!
那么接下来就跟着我一起开始学习吧!
为什么要使用原型?
原型
在js
中也称之为原型模式
, 那么为什么要有这种模式存在呢?
因为我们js面向对象
不是说过吗?面向对象编程
其实就是一种模块、一种封装
对吧!
如果你还没有完全明白javascript面向对象
建议你一定要听我讲完下面的知识点!
构造函数模式
有的时候我们会使用构造函数模式
来创建对象
虽然这种构造函数
创建对象
,看起来没什么大问题,但是并非没有缺点!
这里我们来看一段简单的构造函数模式
创建对象的案例吧!
代码
function Person(username,age,job){
this.username=username;
this.age=age;
this.job=job;
this.say=function (){
console.log('我的名字叫:'+this.username+',年龄:'+this.age+',职业:'+this.job);
}
}
var test=new Person('张三',18,'ui设计师');
test.say();
这就是一段非常简单的构造函数模式
封装对象的方法!
构造函数模式的缺点
任何一种技术的出现,都是为了弥补旧技术的不足!
那么构造函数模式
有什么缺点呢?为什么又要搞出一个原型模式
呢? 搞出这么个东西意义何在?
这里我先卖个关子,想知道的话就继续往下看吧! 😛😛😛
从上面的代码角度上看,确实感觉不出什么奇怪之处!
那我们再来看一个构造函数模式
的例子:
代码
//构造函数
function Person(){
this.say=function (num){
console.log('测试方法'+num);
}
}
//实例化
var obj_1=new Person();
var obj_2=new Person();
//调用
obj_1.say(1);
obj_2.say(2);
console.log(obj_1.say===obj_2.say); //返回false
然后我们看看下面这张图你就知道其中缺点在什么地方了!
如图
此时你会发现,这个函数居然会不相等!! 为什么呢?
从另一个角度来讲构造函数
每执行一次就会把构造函数
中的方法
也重新在内存中生成
一份相同的方法!
比如: 执行一万次构造函数,那么就会在内存中创建一万次构造函数里面的方法, 这对内存是一种非常大的消耗
要知道在ECMAScript
中只要是函数
都是对象
这句话!
之所以会返回false
就是因为函数的底层地址是不一样的! 不要以为方法名称一样就是一样! 那你就错了
如图
并且函数本身也是对象, 你定义一个函数,也就相当于实例化了一个函数对象
, 其实就会在内存中开辟一个空间,地址也会不一样!
上面的这行代码
this.say=function (num){
console.log('测试方法'+num);
}
其实从一定逻辑上讲也可以看成以下形式
this.say=new Function (num){
console.log('测试方法'+num);
}
这样子其实你更好理解每个构造函数
实例中的方法,其实是不同Function
的实例
所以说像这种在内存中无限创建很多完成同样方法的Function实例
是完全没有必要的!
那么有没有什么好的方案可以解决这个问题呢?
我们完全可以使所有的对象
共享同一个方法,那么构造函数
执行一万次,在内存中也只会存在一份相应的方法!
通常是可以把这个函数方法
转移到构造函数
外部
代码说明
function Person(username,age,city){
this.name=username;
this.age=age;
this.city=city;
this.say=say;
/*this.say=function() {
console.log(this.name+'的年龄是:'+this.age);
}*/
}
//把say方法写在全局作用域中
function say() {
//这里的this要清楚是谁在调用say这个方法 this自然就指向谁
console.log(this.name+'的年龄是:'+this.age);
}
var test1=new Person('张三',33,'北京市');
var test2=new Person('李四',66,'深圳市');
//判断test1与test2之间的方法是否是共用的
console.log(test1.say==test2.say);
如图
案例2
以下案例也是同样的道理!
//构造函数
function Person(){
this.say=say;
}
//定义到全局下
function say(num){
console.log('测试方法'+num);
}
//实例化
var obj_1=new Person();
var obj_2=new Person();
//调用
obj_1.say(1);
obj_2.say(2);
console.log(obj_1.say===obj_2.say); //返回true
目的我们是达到了,但是这样写就真的行了吗? 不会存在其他问题吗?
分析
很明显在开发当中 如果把函数这样子写在全局作用域中,在多人开发的时候那么就会出现命名冲突,或者说这样做会 污染全局作用域的命名空间, 所以说我们在项目开发的时候都是尽量地不在全局作用域中写变量和函数
那么如何来解决这样的问题呢? 就是接下来我要说的原型和原型链
,然后根据情况来解决相应的问题!
原型的定义
我们说在js
当中一切皆对象
,对吧, 那么在js
的函数对象
中都有一个内置的Prototype
属性
这个属性指向一个对象
,你可以把这个Prototype属性
想象成一个指针
, 它指向一个对象
而这个对象
就成为原函数对象
的原型
,俗称原型对象
我们来看一段简单的代码:
function Test(){
}
console.log(Test.prototype);
var T1=new Test();
var T2=new Test();
var T3=new Test();
console.log(T1.__proto__);
console.log(T2.__proto__);
console.log(T3.__proto__);
结果
这里还要给大家科普一个小知识,就是普通对象没有原型对象
的,也就是说不是函数对象
也可以说成不是通过new Function
创建的对象,那么就不会存在原型对象
我们来验证一下
代码如下
//普通对象
var json={}
console.log(json.prototype);
//元素对象
var oDiv = document.getElementById("connent");
console.log(oDiv.prototype);
//函数对象
function test(){
}
console.log(test.prototype);
结果如下
所以说首先只有函数对象
才会有原型对象
理解构造函数、实例化对象、原型对象彼此之间的关系
这样就形成了每个函数对象
其实都有一个指向另一个对象的指针
这里我们要说明一点的就是,只有函数
才有一个prototype属性
,这个prototype属性
就是我们的原型对象
同时从图中也可以看到原型
当中含有一个constructor 属性
,而这个属性指向的就是当前原型对象
的构造函数
我们一般会拿这个构造函数
通过new
创建出来实例对象
,而实例对象
是没有prototype
属性的!
如果你在一个实例对象
上调用prototype
必然返回undefined
, 而一个实例对象
靠的是使用__proto__
的隐式属性,进行访问原型对象
!
如图
如果按照这个逻辑推理的话,你可以使用以下代码进行验证一下,是否正确:
console.log(T1.__proto__==Test.prototype); //返回true
console.log(Test.prototype.constructor==Test); //返回true
结果证明的确是这样子,实例对象
自己会有一个指针属性为__proto__
, 用它来指向构造函数
的原型对象
其实你也可以使用isPrototypeOf方法
来判断当前这个实例对象
中的__proto__指针
到底是不是能够指向到本身构造函数
的原型对象
中!
例如
//构造函数1
function Person(name,age) {
}
var p1=new Person();
//构造函数2
function Test(){
}
var test=new Test();
console.log(Person.prototype.isPrototypeOf(p1)); //返回true
console.log(Person.prototype.isPrototypeOf(test)); //返回false
以上我们用了isPrototypeOf方法
来监测一个实例对象
中的__proto__指针
是否指向对应的原型对象
在js
中我们还可以使用一个叫Object.getPrototypeOf()
的方法来监测实例对象
和原型对象
之间的关系
例如
console.log(Object.getPrototypeOf(p1)==Person.prototype); //返回true
console.log(Object.getPrototypeOf(p1)==Test.prototype); //返回false
所以从结果上看
使用这个Object.getPrototypeOf方法
返回的就是当前实例对象
中__proto__属性
所指向的原型对象
同时constructor
属性也的确指向了本身的构造函数
这样原型对象
和实例对象
之间就通过__proto__
连接在一起,形成了一个链条
, 而所谓的原型链
也就是实例对象
和原型对象
之间的链条关系, __proto__
这条线,也就是原型链
的关键
并且这条链条,从图中我们也可以看到,还可以往上走到一个叫Object.prototype
的地方!
实例对象属性和方法搜索的优先级
上面说了,有了原型对象
,那么实例对象
可以共享原型对象
中的属性
和方法
那么问题来了,这些实例对象
又是如何进行查找属性和方法的呢?
举个栗子
function createPerson(name,age) {
this.name=name;
this.age=age;
this.say=function (){
console.log('2.构造函数中定义的say方法!');
}
}
createPerson.prototype.say=function () {
console.log('我的名字叫:'+this.name);
}
var a=new createPerson('张三','33');
var b=new createPerson('李四','55');
var c=new createPerson('王武','66');
c.say=function(){
console.log('1.实例对象c 定义的say方法!');
}
a.say();
b.say();
c.say();
结果如下:
代码分析
按照这个查找逻辑上来看的话,调用的查找方式如下:
先在实例对象
上查找定义的属性
和方法
,优先级最高,如果找不到的情况下,然后再是构造函数
中进行查找我们定义的属性
和方法
, 最后如果也找不到的情况下,就到原型对象
中去寻找!
注意:这并不是把原型对象
中的属性
和方法
覆盖了,只是优先调用的顺序而已!
例如
function Person(){
}
Person.prototype.username='张三';
Person.prototype.age=30;
Person.prototype.job='设计师';
Person.prototype.say=function (){
console.log('我是'+this.username);
}
var p1=new Person();
var p2=new Person();
p1.username='李四';
console.log(p1.username);
console.log(p2.username);
代码分析
首先这里构造函数中我们什么都没有定义的情况下,这里就是先搜索实例对象本身
,如果在实例对象
中找到了具有给定的属性
或者方法
则进行返回!
如果没有找到,则会根据一个叫__proto__
的指针
到原型对象
中去寻找,如果找到就返回! 如果最终都没有找到则返回undefined
那么这个案例中,则执行了两次搜索!先询问了实例对象本身
是否具有, 然后顺着指针到原型对象
中去询问
所以说我们在实例对象
上调用属性
和方法
的时候,都会出现以上相同的搜索过程!
而有了这个搜索模式的帮助下,多个实例对象
则可以共享原型对象
所定义的属性
和方法
就是这个原理!
如图
这里我再次提一嘴,前面不是使用到了constructor
属性吗, 这个属性也默认是共享的,也就是所有实例对象
默认情况下,都可以通过访问这个属性来确定构造函数
是谁!
所以大家也应该注意一下,就是如果你在实例对象上定义了一个属性或者方法,而且原型对象中也定义了同名的属性或者方法,依照查找的顺序会依次搜索实例对象--->构造函数--->原型对象
, 即便是同名也是优先调用最先找到的位置!
也就是说当你在实例对象
上定义一个与原型对象
中同名属性
和方法
的时候,会自动屏蔽
原型对象中的同名属性和方法,注意这里也仅仅是屏蔽,而不是覆盖! 当然换句话说也可以理解为你在实例对象上添加同名属性和方法的时候,会阻止访问原型对象中的同名属性和方法,明白这个意思吧!
即便是你在实例对象
上把某个属性和方法的值定义为null
, 那么访问的时候也只会停留在实例对象
这个层面,而不会恢复其指向构造函数
和原型对象
的链接!
但是如果你使用delete操作符
是可以完全删除实例属性
同时也删除构造函数
中的同名属性,从而让我们能够访问原型对象
中的同名属性!
例如
function Person(){
this.username='李四';
}
Person.prototype.username='王五';
Person.prototype.age=30;
Person.prototype.job='设计师';
Person.prototype.say=function (){
console.log('我是'+this.username);
}
var p1=new Person();
p1.username=null;
delete p1.username;
console.log(p1.username); //这里输出的结果来自于 原型对象
其实我们就可以按照这个查找逻辑,来修改原型对象
从而实现父子继承
的关系 这个我们后面再说!
__proto__的真正含义!
那么实例对象
到底底层是如何查到原型对象
中去的呢?
这其实就要说到刚刚我们提及到的__proto__
这个东西了! 嘿嘿
我们来看一张图:
如图
每个实例对象
都会有一个 __ proto__
属性,这个属性是自动生成的, __ proto__
属性指向自己的原型对象
而且实例对象
也就是通过这一条__ proto__
线路,找到原型对象
中的属性
和方法
的
这就是我马上要提到的原型链
特别注意
这里我提醒一下,可能你以前看到的也的确是叫__proto__
这个
但是目前Chrome
打印出来之后效果提示的是[[Prototype]]
如图
这里只是显示变了而已,代码层面上,实例对象
还是可以继续使用__proto__
这个属性的
然而__proto__
的真正意义也就在于两个字:查找
也就是接下来要说的原型链
因为原型链
就是通过__proto__
属性形成的,任何对象普通对象
和函数对象
都有__proto__
属性
prototype与__proto__的区别
其实我们在上面的图中也能看出来彼此的一个很明显的区别:
__proto__
是实例对象
指向原型对象
的指针,我们俗称隐式原型
,并且是每个实例对象
都会有的一个属性!
prototype
是构造函数/函数
才有的原型对象
,我们俗称为显式原型
这里我特别提一下,其实prototype
就是一个用来设置原型
,而另外一个__proto__
则用来查找数据,如果你还不明白,那么就看下面的原型链
解释就清楚了!
所以说大家不要再把__proto__
与函数的 func.prototype
属性混淆了!
原型链
理解了以上这些是什么之后,那么接下来,我们就可以来研究一下什么是原型链
了!
并且js
中实现继承
主要是依靠原型链
来实现! 所以我们才需要学习原型链
的原理!
在学习原型链
之前,你先记住一个概念,就是原型链
: 其实是实例对象
和原型对象
之间的链条!
我们之前不是说了 实例对象
在搜索属性
或者方法
的优先级吗 ?
就是说如果当你调用一个属性或者方法时, 其实首先是会在当前实例对象
上进行搜索, 如果没有那么就到构造函数中去进行搜索,如果还是没有,那么就到原型对象当中去是进行搜索
那么有人就会问了,如果 当前构造函数的原型对象中也没有呢? 接下来js
会干什么呢?
揭晓谜底吧,请看下图:
如图
分析
以上这张图就是默认情况下,如果说person
这个实例对象
如果顺着__proto__
这个链条没有在Person.prototype
中找到想找的方法或数据,那么又会继续顺着__proto__
这个链条往上走,继续寻找,
那么找谁呢? 这是js
设计者规定的,默认就会找到一个Object.prototype原型对象
当中去, 那么这样子的查找链条
也就形成了原型链
并且既然这里有一个所谓的Object.prototype原型对象
那么这个对象也会有一个属性叫constructor
的属性,来返指向其它的构造函数Object
, 这里的Object.prototype原型对象
自身其实也有一个__proto__
, 因为我们说了,实例对象
才有这个属性,所以还可以向上访问,不过这里再往上就只能返回null
了,因为没有了!
小结
js
其实在面向对象的设计之初就是依照近原则,当我们要使用一个方法和数据时,js
会优先查找自身,如果没有就查找离自己最近的,这里也就指的是构造函数,如果自己没有,他就会沿着原型链__proto__
这个链条,向上查找,如果还没有找到,它还会沿着原型链
继续向上查找,直到找到Object.prototype原型对象
Object.prototype原型对象
默认也是会有一些方法在里面的
如图
所以这里 其实就是解释了js
中每个函数都存在原型对象
属性prototype
并且在js
中所有函数
的默认的原型对象
都是Object
的实例对象
,而且默认还形成一个层层嵌套的形式,这也就是默认原型链
那我们要Object
的原型对象
干嘛,它里面又没几个方法
和属性
来满足我们日常开发的需求对吧,
所以我们需要扩大
我们的原型链条
, 这就要谈论到继承
了
原型对象中的this指向
当我们使用new
操作符来执行一个函数的时候,这个时候,构造函数
中的this
会指向到该实例对象
上
而原型对象
中如果函数方法
出现的this
那么也是指向的该实例对象
举个栗子
function Test(name,age,company,salary){
this.username=name;
this.age=age;
this.company=company;
this.salary=salary;
console.log(this);//打印this
}
Test.prototype.say=function (){
console.log(this); //打印this
}
var test=new Test('张三',18,'重庆科技','9K');
//打印实例对象
console.log(test);
结果如下
由此可见构造函数
中的this
与原型对象
中方法中的this
其实就是当前new
出来的实例对象
原型链继承的实现
在js
中继承
就是通过原型链
来实现的,那么到底如何实现呢?
我们废话不多说,直接看个案例!
代码
//猫类
function Cat(){
this.username='小猫';
}
//狗类
function Dog(){
this.username='小狗';
}
//老虎类
function tiGer(){
this.username='老虎';
}
//猫类的原型对象中有一个方法
Cat.prototype.behavior=function (){
console.log('【'+this.username+'】 这种动物真的会要咬人!!....');
console.log(this);//谁调用this归谁!
}
//实例化猫类
var cat=new Cat();
//把狗类的原型对象指向猫
Dog.prototype=cat;
//实例化狗类
var dog=new Dog();
//把老虎的原型对象指向狗
tiGer.prototype=dog;
//实例化老虎类
var tiger=new tiGer();
//调用方法
tiger.behavior();
dog.behavior();
这里我们修改了原型对象
的指向, 也就是修改了构造函数
的prototype
属性值,对吧!
那么这样一来会造就什么样的情况呢?
简单一点说, 我们就会顺着一个: C实例−>C原型(B实例)−>B原型(A实例)−>A原型
这样一个过程来进行查找!
也就是实例tiger-->Tiger原型对象(dog实例)--->Dog原型对象(cat实例)--->Cat原型对象
进行查找
这里也很明显,tiGer
的实例
和原型对象
都没有一个叫behavior
的方法, 那么就会顺着一条线路,一直往上寻找
如图
当然你也可以通过修改__proto__
来实现!
代码如下
//猫类
function Cat() {
this.username = '小猫';
}
//狗类
function Dog() {
this.username = '小狗';
}
//老虎类
function tiGer() {
this.username = '老虎';
}
//猫类的原型对象中有一个方法
Cat.prototype.behavior = function () {
console.log('【' + this.username + '】 这种动物真的会要咬人!!....');
console.log(this);//谁调用this归谁!
}
//实例化猫类
var cat = new Cat();
//实例化狗类
var dog = new Dog();
//实例化老虎类
var tiger = new tiGer();
//修改原型链指针
tiger.__proto__= dog;
dog.__proto__= cat;
//console.log(tiger);
tiger.behavior();
原理分析
首先,定义了三个构造函数:Cat
、Dog
和Tiger
,每个构造函数都有一个属性username
, 分别赋值为小猫"、"小狗"和"老虎
接下来,在Cat
类的原型对象中定义了一个方法behavior
,该方法用于打印出动物的名字以及调用该方法的对象信息。
然后,通过实例化Cat
类创建了一个名为cat
的实例对象
,并将Dog
类的原型对象指向了cat
。
这样就建立了一个继承关系
,即Dog
类会继承Cat
类的属性和方法。
接着,通过实例化Dog
类创建了一个名为dog
的实例对象
,并将Tiger
类的原型对象指向了dog
。同样地,这也建立了一个继承关系,即Tiger
类会继承Dog
类的属性和方法。
最后,通过实例化Tiger
类创建了一个名为tiger
的对象,并调用了它的behavior
方法。
由于原型链上的继承关系,调用这个dog.behavior()
和tiger.behavior()
都会查找到最终的原型对象
也就是Cat.prototype
中的behavior方法
进行调用!
当然如果这里再调用Object.prototype.__proto__
往上就没有了,就会返回null
这样就形成了一个父子级别
的关系,因为我们通过修改prototype
或者__proto__
形成了一个链条
毕竟原型对象
,其实也是一个Object
的实例
,所以它也有一个__proto__属性
,本身它在一个普通原型对象
下的指向为Object.Prototype原型对象
,也就是说所有函数
的默认原型对象
都是Object的实例
, 但是这里我们把它修改了!
通过__proto__
相连接, 每个继承父函数
的实例对象
都包含一个__proto__
指针
最后会指向我们指定父函数
的prototype原型对象
这样一直可以以此类推
,进行迭代父函数
的原型对象
, 利用__proto__
属性一直可以再往上一层继承。
在这个程中就形成了原型链
我们也可以使用Chrome
并且打印一下实例对象
来进行查看这个链条的走向!
console.log(tiger);
如图
这里如果眼尖的朋友可能已经注意到了一个问题,那就是constructor
这个属性显示不见了, 构造函数
的指向也不对了、原型的显示也不对了, 全部都指向了Cat构造函数
, 当然从继承的效果上是不影响的!
我们可以用以下代码测试一下:
console.log(tiGer.prototype.constructor);
console.log(Dog.prototype.constructor);
如图
原因:
简单点说因为修改原型对象
的时候,指向了另一个新的实例对象
,所以把 constructor
给丢失了!
如果你想看上去比较合理一点,加入以下代码
解决方案
Dog.prototype.constructor=Dog;
tiGer.prototype.constructor=tiGer;
修改之后如图
这就是原型链查找
的关系,一层一层的链接关系就是:原型链
有些实例对象
能够直接调用Object.prototype
中的方法
也是因为存在原型链
的机制!
所以说JavaScript
中原型链
用于实现继承
就是这样实现的!
给大家专门准备了一张通用默认原型链
原理图,拿去背吧!!
如图
基于原型链的继承
看了以上的案例和图例之后,我们应该就对javascript
中的继承有个深入的理解了!
JavaScript
的对象
其实都会有一个指向一个原型对象
的链条
, 当我们试图访问一个对象的属性时,它不仅仅在该对象
本身上去进行搜寻,还会搜寻该对象的原型
,以及原型的原型
依次层层向上搜索,直到找到一个名字匹配的属性
为止, 或到原型链
的末尾!
对象属性的继承
但是有一点我觉得值得注意,就是修改原型链
也就是使用{ __proto__: ... }
和 obj.__proto__
有点不同,前者是标准且未被弃用的一种方式!
举个栗子
var obj={
a: 值1,
b: 值2,
__proto__: c
}
比如在像这样的对象字面量
中,c
的值必须为 null
或者指向另一个对象
用来当做字面量
所表示的对象
的 原型链
,而其他的如a
和 b
将变成对象
的自有属性
, 这种语法读起来非常自然,并且兼容性也比较不错!
我们来看个实际的小案例
代码如下
const obj = {
a: '张三',
b: '李四',
__proto__: {
b: "王五",
c: "绿巨人",
},
};
console.log(obj);
console.log(obj.a);
delete obj.b;
console.log(obj.b);
console.log(obj.c);
分析
当前obj的原型链中具有属性 b和c两个属性
如果obj.__proto__.__proto__
依照之前的图例肯定是访问到Object.prototype
最后obj.__proto__.__proto__.__proto__
则是 null
, 这里就是原型链的末尾,值为null
完整的原型链看起来像这样:
{ a: 张三, b: 李四 } ---> { b:王五, c: 绿巨人 } ---> Object.prototype ---> null
那么要说继承关系的话,那就是__proto__
设置了原型链
,也就是说它在这里的原型链被指定为另一个对象字面量!
即便是这里我使用了delete obj.b
删除了属性b
,也会从__proto__
这个链条找到原型链
中所继承来的属性b
如图
但是注意了如果这里我没有使用delete obj.b
来删除了属性b
那么,当我们调用obj.b
返回的则是李四
而不是王五
,这里其实叫做属性遮蔽(Property Shadowing)
,意思是虽然没有访问到王五
,但这只是被遮住了而已,并不是被覆盖和删除的意思!
当然我们也可以根据这个原理来创建更长的原型链
,并在原型链
上查找一个属性
代码如下
const obj = {
a: 1,
b: 2,
// __proto__ 设置了原型链。它在这里被指定为另一个对象字面量。
__proto__: {
b: 3,
c: 4,
// __proto__ 设置了原型链。它在这里被指定为另一个对象字面量。
__proto__: {
d: 5,
},
},
};
console.log(obj.d); // 输出5
那么它的原型链就是如下这样:
{ a: 1, b: 2 } ---> { b: 3, c: 4 } ---> { d: 5 } ---> Object.prototype ---> null
其实就是这样就可以嵌套很多层出来,让对象看起来更加有层次结构,也方便管理一些特殊的数据!
对象方法的继承
方法
或者函数
的继承
在js
中其实和属性
的继承
也没有差别!
特别要说明的其实也就是this
的指向,当继承的方法被调用时,this
值指向的是当前继承的对象
,而不是拥有该函数属性的对象
代码说明
const parent = {
username: '张三',
age:35,
method() {
return '我的年龄是'+(this.age + 1)+'岁';
}
}
console.log(parent.method()); //输出36
//然后我们通过child继承了parent的对象
const child = {
__proto__: parent,
}
console.log(child.method());//输出36
child.age = 5;
console.log(child.method());
代码分析
当调用 parent.method
时,this
指向了parent
所以按照正常逻辑执行 所以输出36
然后我们通过child
继承了parent的对象
现在调用 child.method
时,this
虽然指向了child
,但是又因为 child
继承的是 parent
的方法,
首先在 child
上寻找有没有method
方法, 但由于child
本身没有名为method
方法,则会根据原型链__proto__
找上去,最后找到即 parent.method
方法来执行!
然后我们在 child
添加一个age属性
赋值为5
, 这会就会遮蔽parent
上的age属性
child对象
现在的看起来是如下这样的:
{ age: 5, __proto__: { username: '张三', age: 35, method: [Function] } }
最后输出:6
, 是因为child
现在拥有age属性
,就不会去找parent对象
中的age属性
了, 但是方法还是会去找parent
中的method方法
, 而方法中的this.age
现在表示 child.age
然后再这个基础上+1
结果就是这样了!
更多参考案例
function Test() {
this.username = '张三';
this.age = 33;
this.job='软件开发';
}
Test.prototype.say=function (){
return '我的名字叫:'+this.username+',我的年龄是:'+this.age+'我的职业是:'+this.job;
}
var test=new Test();
//新建一个对象,并且修改原型链
var obj = {
username:'李四',
age:'35',
__proto__:test
}
console.log(obj);
console.log(obj.username);
console.log(obj.age);
console.log(obj.say());
另类继承实现方法
修改构造函数this指向从而实现继承
我们有时候可以借助call方法
来实现简单的继承效果!
举个栗子
function Animal(name,age,food){
this.username=name;
this.age=age;
this.eat=function (){
console.log('这只['+this.username+']动物要吃['+food+']');
}
}
Animal.prototype.color='黑色';
Animal.prototype.say=function (){
console.log('我的名字叫'+this.name);
}
function Panda(name,age,eat){
this.like='玩耍';
Animal.call(this,name,age,eat); //借用一下
}
var p1=new Panda('熊猫盼盼',18,'竹叶');
//打印输出
console.log(p1);
p1.eat();
结果
这个案例中,应用了call
修改this
指向来达到一个共享的目的!
但是这种使用call
等方法来修改this
严格意义上来讲,只能算借用!
因为这种方式有一个很大的缺点,就是不能继承
所谓父类
中原型
里面的属性
和方法
,不然你看上图,打印的结果当中并没有出现Animal类
原型对象中的color属性
和say方法
所以这种继承
方式如何和prototype
修改原型方式结合一起使用就会有意想不到的效果,并且参数的传递也会更加灵活多变!
举个栗子
//定义构造函数
function Person(userename,age,sex){
this.name=userename;
this.age=age;
this.sex=sex;
this.type='人类';
}
Person.prototype.say=function(){
console.log("hello world");
}
function Student(username,age,sex,score){
//借用Person构造函数
Person.call(this,username,age,sex);
//定义属性
this.score=score
}
//改变原型指向
Student.prototype=new Person();//不传值
Student.prototype.behavior=function(){
console.log("英语学习!!");
}
var s1=new Student("张三",15,"男","100分")
//打印结果看看
console.log(s1);
console.log(s1.type);
console.log(s1.name);
console.log(s1.age);
console.log(s1.sex);
console.log('考试得分:'+s1.score);
s1.behavior();
s1.say();
代码分析
从上面的代码中,我们可以看到构造函数Student
中我们借用了Person构造函数
, 然后在通过prototype
修改原型指向,这样一来,不仅可以获取到父类构造函数
中的属性和方法
也可以获取到父类原型对象
中的属性和方法
这时都可以通过__proto__
这个链条拿到!
如图
通过循环复制实现继承
我们的原型对象prototype
既然是一个对象
,那么我们也可以通过循环复制
的手法把父级原型对象
里面的属性和方法拷贝到目标原型对象
下,同时也可以结合call方法
借用构造函数
中的属性和方法
代码如下
function Person(username,age) {
this.name=username;
this.age=age;
}
Person.prototype.type = "人类";
Person.prototype.nationality = "中国";
Person.prototype.job = '软件开发';
Person.prototype.like = '足球,篮球,游戏';
Person.prototype.test = 123;
function Student(username,age) {
Person.call(this,username,age);
}
var per = Person.prototype;
var stu = Student.prototype;
//过滤不需要的属性
var arr=['type','nationality','test'];
for (k in per) {
if(arr.indexOf(k)==-1){
stu[k] = per[k];
}
}
var s1=new Student('李四',18);
var s2=new Student('王五',25);
console.log(s1);
console.log(s2);
如图
__proto__
的兼容性
根据MDN
官方的建议,其实__proto__
是被弃用了的!
那么到底我们平常使用什么来修改原型的指针呢?
在 JavaScript
中,你的确是可以通过直接修改实例对象
的 __proto__
属性来达到目的!
__proto__
是一个非标准的属性
,它在大多数的js
环境中都可以使用,包括浏览器
和Node.js
但是由于这个属性是非标准
的,它在一些环境中可能不可用,或者在未来的标准中可能会被弃用,也就是说虽然一些浏览器仍然支持__proto__
,但也许已从相关的web标准
中移除,也许正准备移除或者出于兼容性而保留!
__proto__浏览器兼容性如下表
所以如果可以的话我们尽量不使用__proto__
而改成其他,例如:Object.setPrototypeOf方法
举个栗子
让我们使用Object.setPrototypeOf
来实现一个简单的继承
代码如下
function Test(){
}
Test.prototype.company='重庆科技';
function Test2(){
}
Test2.prototype.num=100;
function Test3(){
}
Test3.prototype.username='张三';
//实现继承
Object.setPrototypeOf(Test2.prototype, Test.prototype);
Object.setPrototypeOf(Test3.prototype, Test2.prototype);
var t3=new Test3();
console.log(t3);
效果
再看一个案例!
const a = { company : '重庆科技' };
const b = { age: 33 };
const c = { username: "张三" };
//实现继承
Object.setPrototypeOf(a, b);
Object.setPrototypeOf(b, c);
console.log(a);
console.log(a.username);
console.log(a.age);
console.log(a.company);
效果如下
所以我觉得可以的情况下,尽量使用标准的Object.setPrototypeOf方法
来实现继承
因为Object.setPrototypeOf方法
基本上被所有现代浏览器引擎所支持, 并且也允许动态地修改对象
的原型
原型链与继承查找机制
当你访问一个对象的属性
或方法
时,如果这个对象本身没有这个属性
或方法
,那么js
会在这个对象的原型中寻找这个属性或方法,如果找到了,就会使用它, 如果还是找不到,就会在原型的原型中寻找,以此类推,直到找到为止, 而继承
的关键,也就在于自定义修改原型
的指向!
所以当你把之前的原型链
图分析透彻,你就会知道原型链
就是通过__proto__
属性形成的,任何对象普通对象
和函数对象
都有__proto__
属性,并且其核心思想
也就是通过__proto__
这个链条来进行查找数据
!
DOM原型链的形成
这其实也很好的解释了我们javascript
中DOM属性和方法
也是这样子进行查找的
html
<div id="connent"></div>
js
var oDiv = document.getElementById("connent");
console.log(oDiv.__proto__);
console.log(oDiv.__proto__.__proto__);
console.log(oDiv.__proto__.__proto__.__proto__);
console.log(oDiv.__proto__.__proto__.__proto__.__proto__);
console.log(oDiv.__proto__.__proto__.__proto__.__proto__.__proto__);
console.log(oDiv.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__);
console.log(oDiv.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__);
结果如下
其实这样你就会知道当一个DOM
元素在使用某个属性和方法的时候,是怎么进行查找的它的链条
也是通过__proto__
来进行查找的,对吧! 其中顺序如下
↓HTMLDivElement
↓HTMLElement、
↓Element、
↓Node、
↓EventTarget
↓Object
通过__proto__
就形成了JavaScript
中与DOM文档对象模型
相关的概念了
这里也给大家简单介绍一下,方便理解!
HTMLDivElement
: 这是一个代表 HTML<div>
元素的类, 它继承了HTMLElement
的属性和方法,包括可以用来改变元素样式的属性和方法,当然这里我只是举个栗子,不一定就是div元素
根据你打印的情况决定!HTMLElement
: 这是一个基础类,代表任何 HTML 元素, 所有的HTML
元素都继承了HTMLElement
的属性和方法Element
: 这是一个基础类,代表任何HTML 或 XML
元素, 它定义了所有元素共享的属性
和方法
,例如getAttribute()
和setAttribute()
Node
: 这是所有DOM
节点的基类,包括元素、文本节点、注释
等, 它定义了一些通用的属性
和方法
,如parentNode
和childNodes
。EventTarget
: 这个接口表示可以添加或删除事件监听器的事件目标
Object
: 这个也就是顶层的Object构造函数
在w3c也有这些属性和方法的详细解释
如图
而且这些继承
关系这样子一直走下来查找的属性
和方法
的关键就是原型链
, 可以说没有原型链
就没有现在的javascript
当查找对象
的某个属性
或方法
的时候,首先在当前对象
中查找,如果没有去对象的__proto__
中去查找, 这样子一直到最顶层null
,而这样的__proto__
形成的一条查找链条
就是原型链
现在你可以感受一下是不是如此呢!
并且继承
也就是修改原型的指向,即__proto__
或prototype
以上这些类
和接口
一起构成了JavaScript
的 DOM API应用程序编程接口
, 这样来允许我们以代码编程方式操作网页中的元素内容、结构和样式。
DOM
中所有的属性
和方法
你都可以看做为一个原型链的继承关系!
其实你可以去通过js
创建一些xml、svg、普通元素
以及文档模型!
代码如下
// 创建一个新的XML文档
var xmlDoc = document.implementation.createDocument(null, null);
// 创建根元素
var root = xmlDoc.createElement("root");
xmlDoc.appendChild(root);
// 创建一个子元素
var child = xmlDoc.createElement("child");
// 设置子元素的内容
var childText = xmlDoc.createTextNode("This is a child element");
child.appendChild(childText);
// 将子元素添加到根元素
root.appendChild(child);
// 打印XML文档
console.log(xmlDoc.__proto__);
console.log(xmlDoc.__proto__.__proto__);
console.log(xmlDoc.__proto__.__proto__.__proto__);
console.log(xmlDoc.__proto__.__proto__.__proto__.__proto__);
console.log(xmlDoc.__proto__.__proto__.__proto__.__proto__.__proto__);
console.log("------------------------------------------------------------");
var oDiv = document.getElementById("oDiv");
console.log(oDiv.__proto__);
console.log(oDiv.__proto__.__proto__);
console.log(oDiv.__proto__.__proto__.__proto__);
console.log(oDiv.__proto__.__proto__.__proto__.__proto__);
console.log(oDiv.__proto__.__proto__.__proto__.__proto__.__proto__);
console.log("------------------------------------------------------------");
console.log(document.__proto__);
console.log(document.__proto__.__proto__);
console.log(document.__proto__.__proto__.__proto__);
console.log(document.__proto__.__proto__.__proto__.__proto__);
console.log(document.__proto__.__proto__.__proto__.__proto__.__proto__);
console.log("------------------------------------------------------------");
// 创建一个新的SVG文档
var svgNS = "http://www.w3.org/2000/svg";
var svgDoc = document.implementation.createDocument(svgNS, "svg", null);
// 添加根元素
var root = svgDoc.documentElement;
// 添加一个矩形元素
var rect = svgDoc.createElementNS(svgNS, "rect");
rect.setAttribute("x", 10);
rect.setAttribute("y", 10);
rect.setAttribute("width", 100);
rect.setAttribute("height", 100);
rect.setAttribute("fill", "blue");
root.appendChild(rect);
// 添加一个圆形元素
var circle = svgDoc.createElementNS(svgNS, "circle");
circle.setAttribute("cx", 120);
circle.setAttribute("cy", 120);
circle.setAttribute("r", 50);
circle.setAttribute("fill", "red");
root.appendChild(circle);
// 将SVG文档添加到HTML文档中
document.body.appendChild(root);
console.log(rect.__proto__);
console.log(rect.__proto__.__proto__);
console.log(rect.__proto__.__proto__.__proto__);
console.log(rect.__proto__.__proto__.__proto__.__proto__);
console.log(rect.__proto__.__proto__.__proto__.__proto__.__proto__);
console.log(rect.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__);
console.log(rect.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__);
console.log(rect.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__);
然后看看他们的__proto__
原型链的指针你就明白了!
如图
使用原型和原型链的好处
到这里学了那么多,我们使用原型链
到底有什么好处呢?
其实原型链
也可以适当的帮助我们优化代码,减少代码冗余,提高程序代码的复用性!
举个栗子
现在有一个属性
应该出现在每一个实例上,那我们就可以重用它,尤其是对于方法
或者函数
这种类型的属性!
比如说现在有多个实例字面量对象
,每一个对象都是一个容器,而里面都包含一个 getValue方法
可以用来访问的值对象本身内部的某值!
代码如下
const arr = [
{ value: '张三', getValue() { return this.value; } },
{ value: '李四', getValue() { return this.value; } },
{ value: '王五', getValue() { return this.value; } },
];
但是你可以想一下,这样子做好吗? 每一个对象都基本上有同样的代码, 这就是冗余
且不必要的代码,并且我以前也说过,一个函数在一个对象中,就会开辟一块内存空间,如果代码巨大的情况下,这样子做非常耗费内存资源!
所以你可以尝试优化一下,当然优化的办法有很多,这里我们重点讨论的就是原型链
你可以试想一下将 getValue方法
移动到所有盒子的原型链[[prototype]]
上!
那么我们加以修改一下,变成如下形式
代码
//公共使用
const _ObjPublic={
getValue() {
return this.value;
}
}
const arr = [
{ value: '张三', __proto__:_ObjPublic },
{ value: '李四', __proto__:_ObjPublic },
{ value: '王五', __proto__:_ObjPublic },
];
console.log(arr[0].getValue());
console.log(arr[1].getValue());
console.log(arr[2].getValue());
效果如下
这样一来所有对象中的 getValue方法
都会根据原型链
的原理找到并引用相同的函数,降低了内存使用率!
但是上面这样一个一个手动去捆绑原型链
太麻烦了, 如果代码多了这样一个一个的去修改也是一件很大工程量的事情! 那么怎么办呢?
这时,我们就可以使用构造函数
方式创建实例对象
因为当我们使用构造函数
来构造的实例对象
它会自动为实例对象
设置 原型链(__proto__)
属性
构造函数
是使用 new
调用的函数
还记得吗!
其实在js
的设计之初,就给我们考虑过这些问题, 大致我分为以下几个用途:
- 优化和简化代码,实现代码重用
- 根据
__proto__
的链条实现属性和方法
的继承
,只要是这个__proto__
链条上的东西,都可以被调用到
所以在js
中就有了这样一个说法:js
是基于对象的脚本语言,但是也有人说js
是基于原型的脚本语言!
我们单纯的来说一下prototype原型对象
用它来实现代码重用与属性和方法
的共享!
所以为了避免了代码冗余,公共使用的属性
和方法
,我们是可以设置到原型对象
中的!
方法
构造函数名.prototype.属性=值;
构造函数名.prototype.方法=function(){
..代码段..
}
然后通过构造函数
来实例化
的所有实例对象
都可以使用该构造函数
对应原型对象
中的属性
和方法
!
也就是说这个类型的实例对象
就都会共享
这些属性
和方法
也就是通过原型链(__proto__)
在进行查找!
这样做的一个好处是:减少了内存占用, 并且也实现了代码重用!
也是使用原型
对象的一大优点
代码如下
function createPerson(name,age) {
this.name=name;
this.age=age;
}
createPerson.prototype.say=function () {
console.log('我的名字叫【'+this.name+'】, 我的年龄是:'+this.age);
}
var a=new createPerson('张三','33');
var b=new createPerson('李四','55');
a.say();
b.say();
console.log(a.say===b.say);
如图
大家可以看到,say方法
,我没有定义到实例对象
上,也没有定义到构造函数
当中,而是定义到了原型对象
里面!
并且这样子做就相当于所有的实例对象
都共享一个方法,那么它们的地址都是相等的了!
这样做的好处,在于节约内存开销
为什么这样说呢?
我们来看下面这张图:
如图
这就是让公用的方法
或者属性
在内存中只存在一份,所以prototype
就是这样来实现数据的共享, 不然的话你每一次new
都会在内存中创建一份属性
或者方法
出来
而不管我们实例化多少次对象出来,原型对象
里面的属性和方法
只生成一次,所以会节省内存, 同时提高代码的可重用性和可维护性 。
也就是说只要是通过 new
创建的实例对象
,无论多少次,它们的__proto__
都是指向构造函数的prototype
如图
所以我们给构造函数
的原型对象
添加一些方法,就能让创建的多个实例对象
共享同一个方法,减少内存的使用。
当然你也可以把所有的属性和方法
都添加到原型对象当中,构造函数
中就不用再去定义了,看情况来决定!
相当于构造函数
创建的每一个实例
都会自动将构造函数
的prototype
属性作为其 原型链__proto__
你完全可以使用Object.getPrototypeOf
方法来进行验证以下
console.log(Object.getPrototypeOf(a) === createPerson.prototype); //返回true
console.log(Object.getPrototypeOf(b) === createPerson.prototype); //返回true
console.log(createPerson.prototype.constructor === createPerson); //返回true
字面量与原型链之间的关系
在JavaScript
中的一些字面量语法
会隐式的创建原型链__proto__
这里我给大家举几个案例就会明白了~~
举栗
对于使用对象字面量创建的对象,__proto__
返回的是:Object的原型对象
你也可以理解为对象字面量没有 __proto__
的情况下,自动将Object.prototype
作为它们的__proto__
值
代码
var obj = {}
console.log(obj.__proto__);
console.log(Object.getPrototypeOf(obj) === Object.prototype); //返回true
对于使用数组字面量创建的对象,__proto__
返回的是:数组的原型对象
也就是说数组字面量
会自动将 Array.prototype
作为它们的 __proto__
值
代码
var arr = [];
console.log(arr.__proto__);
console.log(Object.getPrototypeOf(arr) === Array.prototype); //返回true
如果是正则表达式字面量
,则会自动将 RegExp.prototype
作为这些字面量的 __proto__
值
const regexp = /abc/;
console.log(Object.getPrototypeOf(regexp) === RegExp.prototype) // true
对于使用字符串字面量
方式创建的字符串对象,__proto__
返回的是:字符串的原型对象,也就是说
如果是字符串字面量
,则会自动将 String.prototype
作为这些字面量的 __proto__
值
var str = "";
console.log(str.__proto__);
console.log(Object.getPrototypeOf(str) === String.prototype) // true
如果是使用数字字面量
方式创建的数值对象,__proto__
返回的是:数值的原型对象,也就数值字面量
,则会自动将 Number.prototype
作为这些字面量的 __proto__
值
var num = 100;
console.log(num.__proto__);
console.log(Object.getPrototypeOf(num) === Number.prototype) // true
那么如果是函数呢,一个函数名称
其实也算是一种函数字面量
的形式!
那__proto__
返回的是:函数的原型对象
,也就是Function.prototype
也就是会自动将 Function.prototype
作为这些函数字面量
的 __proto__
值!
var fn = function () {
}
console.log(fn.__proto__);
console.log(Object.getPrototypeOf(fn) === Function.prototype) // true
所以说这又解释了为什么有些属性和方法
只是在特定的构造函数
上定义的, 而它们又自动在所有特定的实例对象
上才可以使用,对吧!
比如像 map()
这样的数组方法
只是在 Array.prototype
上定义的方法,而它又只会自动在所有数组实例
上可用,就是因为这个原因!
性能与原型链
了解原型继承
的模型是使用javascript
编写复杂代码的重要基础,另外我们还要注意代码中原型链的长度,在必要时可以将其分解,以避免潜在的性能问题!
因为原型链
上较深层的属性
和方法
的查找, 在时间上可能会对性能产生负面影响,这在性能至关重要的代码中可能会格外明显, 因为如果尝试访问不存在的属性
始终会遍历整个原型链
,也就是原型链中的每个
可枚举属性都将被枚举, 那么层次多了反而不好!
所以说我们在遍历对象的属性时,最好先判断一下 要检查对象是否具有在其自身
上是否有定义的属性,而不是让__proto__
自动的去搜索其原型链
上的某个地方! 必要的情况下可以使用hasOwnProperty()
判断
举个栗子
function Graph() {
this.vertices = [];
this.edges = [];
}
Graph.prototype.addVertex = function (v) {
this.vertices.push(v);
};
const g = new Graph();
// 当前原型链为: g ---> Graph.prototype ---> Object.prototype ---> null
//检查对象自身是否有vertices属性 返回true
console.log(g.hasOwnProperty("vertices"));
//检查对象自身是否有nope属性 返回false
console.log(g.hasOwnProperty("nope"));
//检查对象自身是否有addVertex属性 返回false
console.log(g.hasOwnProperty("addVertex"));
//检查原型对象自身是否有addVertex属性 返回true
console.log(Object.getPrototypeOf(g).hasOwnProperty("addVertex"));
最后总结
原型链
其实是一种关系的链条, 它是让实例对象
和原型对象
之间产生关系一种链条!
而这个关系是通过原型([[Prototype]])
也就是__proto__
来进行关联的!
而也只有实例对象
才有这个__proto__
不标准的属性,当然这里的意思是有的游览器并不支持这个属性!
那么有了原型链
我们实例对象
在进行查找属性
的时候则按照以下规则:
首先在实例对象
上查找,如果有则使用自身带有的属性或方法,如果没有则通过__proto__
指向的原型对象
进行 查找,找到则使用, 如果找不到则继续向原型对象
的__proto__
进行查找, 找到则使用,以此类推, 如果最终未找到则会报错!
同时,我们也对构造函数
有了一个深入的了解,也就是构造函数
在实例化
的时候会生成一个叫prototype
的属性,它就是构造函数
的原型对象
, 这个对象中还有一个默认存在的属性constructor
用来指向原型对象
所在的构造函数
的指针!
实例对象
的__proto__
属性指向的是构造函数
的原型对象
,这个对象的指向是可以被修改的,从而实现层层继承
也就是说原型
的指向
是可以被改变
的, 也不管你是修改prototype
还是__proto__
最终原来本身指向的原型会指向到一个新的原型对象上从而通过__proto__
查找链条来实现继承
关系!
"👍点赞" "✍️评论" "💙收藏"