从问题入手,深入了解JavaScript中原型与原型链

前言

开篇之前,我想提出3个问题:

  1. 新建一个不添加任何属性的对象为何能调用toString方法?
  2. 如何让拥有相同构造函数的不同对象都具备相同的行为?
  3. instanceof关键字判断对象类型的依据是什么?

要是这3个问题都能回答上来,那么接下来的内容不看也罢。但若是对这些问题还存在疑虑和不解,相信我,下面的内容将正是你所需要的。

正文

新建一个不添加任何属性的对象为何能调用toString方法?

我在深入了解JavaScript中基于原型(prototype)的继承机制一文中提到过,JavaScript使用的是基于原型的继承机制,它的引用类型与其对应的值将都存在着__proto__[1]属性,指向继承的原型对象[2]。当访问对象属性无果时,便会在其原型对象中继续查找,倘若其原型对象中还是查询无果,那便接着去其原型对象的原型中去查找,直到查找成功或原型为null时[3]才会停止查找。

let obj = {
}
obj.toString();//"[object Object]"

这段代码就是在obj对象中查找toString方法,查询无果,继而在其原型[4]中查找toString方法,正好其原型中含有toString方法,故而得以输出"[object Object]"。

如何让拥有相同构造函数的不同对象都具备相同的行为?

下面是一段实现了发布订阅模式的代码:

let _indexOf = Array.prototype.indexOf;
let _push = Array.prototype.push;
let _slice = Array.prototype.slice;
let _concat = Array.prototype.concat;
let _forEach = Array.prototype.forEach; function Publish(){
this.subList; this.indexOf = function(sub){
let index = -1;
if(typeof this.subList === 'undefined' || this.subList === null){
this.subList = [];
}
if(typeof sub !== 'undefined' && sub !== null){
index = _indexOf.call(this.subList,sub);
}
return index;
} this.addSub = function(sub){
let index = this.indexOf(sub);
index > -1 ?
'' :
_push.call(this.subList,sub);
}; this.removeSub = function(sub){
let index = this.indexOf(sub);
index > -1 ?
index === 0 ?
this.subList = _slice.call(this.subList,1) :
this.subList = _concat.call(_slice.call(this.subList,0,index),_slice.call(this.subList,index + 1)) :
'';
}; this.notifySingle = function(sub,msg){
let index = this.indexOf(sub);
index > -1 ?
(typeof sub.onReceive === 'function' ?
sub.onReceive(msg) :
'') :
'';
}; this.notifyAll = function(msg){
if(typeof this.subList !== 'undefined' && this.subList !== null){
_forEach.call(this.subList,(sub)=>{
if(typeof sub !== 'undefined' && sub !== null){
typeof sub.onReceive === 'function' ?
sub.onReceive(msg) :
'';
}
})
}
};
} function Subscription(name){
this.name = name;
this.onReceive = function(msg){
console.log(this.name + ' 收到消息 : ' + msg);
};
} let pub = new Publish();
let sub1 = new Subscription('sub1');
let sub2 = new Subscription('sub2');
let sub3 = new Subscription('sub3');
let sub4 = new Subscription('sub4'); pub.addSub(sub1);
pub.addSub(sub1);
pub.addSub(sub2);
pub.addSub(sub3);
pub.addSub(sub4); pub.notifyAll('这是一条全部推送的消息');
// sub1 收到消息 : 这是一条全部推送的消息
// sub2 收到消息 : 这是一条全部推送的消息
// sub3 收到消息 : 这是一条全部推送的消息
// sub4 收到消息 : 这是一条全部推送的消息 pub.notifySingle(sub2,"这是一条单独推送的消息");
// sub2 收到消息 : 这是一条单独推送的消息 pub.removeSub(sub3); pub.notifyAll('这是一条全部推送的消息');
// sub1 收到消息 : 这是一条全部推送的消息
// sub2 收到消息 : 这是一条全部推送的消息
// sub4 收到消息 : 这是一条全部推送的消息

此代码中拥有同一构造函数的所有对象都含有不同的方法。

sub1.onReceive === sub2.onReceive;//false
sub1.onReceive === sub3.onReceive;//false
sub1.onReceive === sub4.onReceive;//false
sub2.onReceive === sub3.onReceive;//false
sub2.onReceive === sub4.onReceive;//false
sub3.onReceive === sub4.onReceive;//false

这样会导致:

1.浪费内存;

2.不易于对方法进行批量操作。

接下来是改进版本,使用原型达到代码复用的效果

let _indexOf = Array.prototype.indexOf;
let _push = Array.prototype.push;
let _slice = Array.prototype.slice;
let _concat = Array.prototype.concat;
let _forEach = Array.prototype.forEach; function Publish(){
this.subList;
} Publish.prototype.indexOf = function(sub){
let index = -1;
if(typeof this.subList === 'undefined' || this.subList === null){
this.subList = [];
}
if(typeof sub !== 'undefined' && sub !== null){
index = _indexOf.call(this.subList,sub);
}
return index;
} Publish.prototype.addSub = function(sub){
let index = this.indexOf(sub);
index > -1 ?
'' :
_push.call(this.subList,sub);
}; Publish.prototype.removeSub = function(sub){
let index = this.indexOf(sub);
index > -1 ?
index === 0 ?
this.subList = _slice.call(this.subList,1) :
this.subList = _concat.call(_slice.call(this.subList,0,index),_slice.call(this.subList,index + 1)) :
'';
}; Publish.prototype.notifySingle = function(sub,msg){
let index = this.indexOf(sub);
index > -1 ?
(typeof sub.onReceive === 'function' ?
sub.onReceive(msg) :
'') :
'';
}; Publish.prototype.notifyAll = function(msg){
if(typeof this.subList !== 'undefined' && this.subList !== null){
_forEach.call(this.subList,(sub)=>{
if(typeof sub !== 'undefined' && sub !== null){
typeof sub.onReceive === 'function' ?
sub.onReceive(msg) :
'';
}
})
}
}; function Subscription(name){
this.name = name; } Subscription.prototype.onReceive = function(msg){
console.log(this.name + ' 收到消息 : ' + msg);
}; let pub = new Publish();
let sub1 = new Subscription('sub1');
let sub2 = new Subscription('sub2');
let sub3 = new Subscription('sub3');
let sub4 = new Subscription('sub4'); pub.addSub(sub1);
pub.addSub(sub1);
pub.addSub(sub2);
pub.addSub(sub3);
pub.addSub(sub4); pub.notifyAll('这是一条全部推送的消息');
// sub1 收到消息 : 这是一条全部推送的消息
// sub2 收到消息 : 这是一条全部推送的消息
// sub3 收到消息 : 这是一条全部推送的消息
// sub4 收到消息 : 这是一条全部推送的消息 pub.notifySingle(sub2,"这是一条单独推送的消息");
// sub2 收到消息 : 这是一条单独推送的消息 pub.removeSub(sub3); pub.notifyAll('这是一条全部推送的消息');
// sub1 收到消息 : 这是一条全部推送的消息
// sub2 收到消息 : 这是一条全部推送的消息
// sub4 收到消息 : 这是一条全部推送的消息
sub1.onReceive === sub2.onReceive;//true
sub1.onReceive === sub3.onReceive;//true
sub1.onReceive === sub4.onReceive;//true
sub2.onReceive === sub3.onReceive;//true
sub2.onReceive === sub4.onReceive;//true
sub3.onReceive === sub4.onReceive;//true

改进版本与之前的版本相比有一个特点:拥有同一构造函数的对象,属性是唯一的,行为是一致的[5]。所有对象都拥有独立于其它对象的属性,却存在相同的行为。这正是因为在改进版本中,方法存在于构造函数的prototype属性值上,其将被其创建的对象所继承。也正是因为如此,尽管此时的sub1、sub2、sub3、sub4中都不包含onReceive方法,但也可以通过继承的原型对象Subscription.prototype去达到调用onReceive的目的。而且修改Subscription.prototype上的onReceive方法是可以马上作用到sub1、sub2、sub3、sub4上的。将方法定义到构造函数的prototype属性值上,就可以让拥有相同构造函数的不同对象都具备相同的行为以达到代码复用目的。

instanceof关键字判断对象类型的依据是什么?

我在深入了解JavaScript中基于原型(prototype)的继承机制中声明了函数Person,并以它为构造函数创建了person对象

function Person(){

}
let person = new Person();

person对象的继承Person函数的prototype属性值,而Person函数的prototype属性值又继承Object函数的prototype属性值,这种一层一层继承的关系构成了原型链。

instanceof关键字判断对象类型的依据便是判断函数的prototype属性值是否存在于对象的原型链上。

正如Person函数的prototype属性值和Object函数的prototype属性值都存在于person对象的原型链上,所以使用instanceof判断两者都为true。

person instanceof Person;//true
person instanceof Object;//true

而Function函数的prototype属性值不存在于person对象的原型链上,所以使用instanceof判断Function函数为false。

person instanceof Function;//false

最后,完成一个instanceof。

/**
* obj 变量
* fn 构造函数
*/
function myInstanceof(obj,fn){
let _prototype = Object.getPrototypeOf(obj);
if(null === _prototype){
return false;
}
let _constructor = _prototype.constructor;
if(_constructor === fn){
return true;
}
return myInstanceof(_prototype,fn);
} //测试代码
myInstanceof({},Object);//true
myInstanceof([],Array);//true
myInstanceof(window,Window);//true
myInstanceof(new Map(),Map);//true
myInstanceof({},Array);//false
myInstanceof({},Function);//false

大功告成。

结尾

这3个问题的解答分别对原型和原型链的含义以及它们在JavaScript中起到了什么作用进行了阐述。不过由于本人才疏学浅,难免会遇到一些我个人理解亦或是表达存在错误的地方,还望各位遇到之时,能不吝指出。


  1. 虽然__proto__已经被不推荐使用,但是为了更直观,我在此文中获取对象原型的方法都将通过对象的__proto__属性,还望悉知。

  2. Object.prototype继承的原型指向null。

  3. Object.prototype的原型为null,它是原型链的顶点,查到Object.prototype的原型时还找不到便会报找不到了。

  4. 对象obj的原型为obj的构造函数的prototype属性,也就是Object.prototype。

  5. 这里的属性意指除方法外的属性,行为意指方法。

从问题入手,深入了解JavaScript中原型与原型链的更多相关文章

  1. 深入理解Javascript中构造函数和原型对象的区别

    在 Javascript中prototype属性的详解 这篇文章中,详细介绍了构造函数的缺点以及原型(prototype),原型链(prototype chain),构造函数(constructor) ...

  2. javascript中继承(一)-----原型链继承的个人理解

    [寒暄]好久没有更新博客了,说来话长,因为我下定决心要从一个后台程序员转为Front End,其间走过了一段漫长而艰辛的时光,今天跟大家分享下自己对javascript中原型链继承的理解. 总的说来, ...

  3. 深入理解Javascript中构造函数和原型对象的区别(转存)

    Object是构造函数,而Object.prototype是构造函数的原型对象.构造函数自身的属性和方法无法被共享,而原型对象的属性和方法可以被所有实例对象所共享. 首先,我们知道,构造函数是生成对象 ...

  4. Javascript中闭包的作用域链

    作用域定义了在当前上下文中能够被访问到的成员,在Javascript中分为全局作用域和函数作用域,通过函数嵌套可以实现嵌套作用域. 闭包一般发生在嵌套作用域中.闭包是JavaScript最强大的特性之 ...

  5. JavaScript中作用域和作用域链的简单理解(变量提升)

    通过阅读<JS高级程序设计>这本书,对js中的作用域和作用域链知识有了初步的了解和认识,准备成笔记供大家参考,笔记中字数比较多,但个人认为叙述的挺详细的,所以希望读者耐心看.再者,本人了解 ...

  6. javascript中的prototype(原型)认识

    prototype实现了对象与对象的继承,在JS中变量,函数,几乎一切都是对象,而对象又有_ptoro_属性,这个属性就是通常说的原型,是用来指向这个对象的prototype对象,prototype对 ...

  7. JavaScript中的显示原型和隐形原型(理解原型链)

    显式原型:prototype 隐式原型:__proto__ 1.显式原型和隐式原型是什么? 在js中万物皆对象,方法(Function)是对象,方法的原型(Function.prototype)是对象 ...

  8. JavaScript中作用域和作用域链解析

    学习js,肯定要学习作用域,js作用域和其他的主流语言的作用域还存在很大的区别. 一.js没有块级作用域. js没有块级作用域,就像这样: if(){ : console.log(a) //输出100 ...

  9. 《JavaScript 闯关记》之原型及原型链

    原型链是一种机制,指的是 JavaScript 每个对象都有一个内置的 __proto__ 属性指向创建它的构造函数的 prototype(原型)属性.原型链的作用是为了实现对象的继承,要理解原型链, ...

  10. 深入理解javascript中实现面向对象编程方法

    介绍Javascript中面向对象编程思想之前,需要对以下几个概念有了解: 1. 浅拷贝和深拷贝:程序在运行过程中使用的变量有在栈上的变量和在堆上的变量,在对象或者变量的赋值操作过程中,大多数情况先是 ...

随机推荐

  1. DHCP.md

    DHCP 主配置文件   从 /usr/share/doc/dhcp 复制 dhcpd.conf.sample 到/etc/dhcp下                                  ...

  2. ICPC Central Russia Regional Contest (CRRC 19)题解

    题目连接:https://codeforces.com/gym/102780 寒假第二次训练赛,(某菜依旧是4个小时后咕咕咕),战况还行,个人表现极差(高级演员) A:Green tea 暴力枚举即可 ...

  3. NOIP初赛篇——10计算机网络

    网络的定义 ​ 所谓计算机网络,就是利用通信线路和设备,把分布在不同地理位置上的多台计算机连接起来. ​ 计算机网络是现代通信技术与计算机奇数结合的产物. ​ 网络中计算机与计算机之间的通信依靠协议进 ...

  4. Java 中 Executors.newSingleThreadExecutor() 与Executors.newFixedThreadPool(1)有什么区别

    在研究Executors提供的线程池时自然会想到标题这个问题,既然已经有了newFixedThreadPool,为什么还要存在newSingleThreadExecutor这个方法.难道newFixe ...

  5. 【剑指 Offer】08.二叉树的下一个节点

    题目描述 给定一颗二叉树和其中的一个节点,找出中序遍历序列的下一个节点.树中的节点除了有两个分别指向左右节点的指针,还有一个指向父节点的指针. Java public class Solution08 ...

  6. Centos7安装RabbitMQ详细教程

    MQ引言 什么是MQ MQ:message Queue翻译为消息队列,通过典型的生产者和消费者模型不断向消息队列中生产消息,消费者不断从队列中获取消息.因为消息的生产和消费都是一部的,而且只关心消息的 ...

  7. Os-hackNos-特权文件提权

    一 信息收集 netdiscover -i eth0 -r 10.10.10.0/24 扫描ip nmap -sP 192.168.43.0/24 扫描开放的端口 使用"-sP"选 ...

  8. 分布式系统:xxl-job改造spring-cloud

    目录 改造原因 主要改造思路 调度中心 调度中心 执行器侧 总结 修改后的源码仓库地址:GitHub. : 改造原因 原有的xxl-job使用自己实现的http协议进行注册以及调度等,与目前框架中本身 ...

  9. 1.8V转3V,1,8V转3.3V电源芯片的规格书参数

    1.8V电平如何稳压稳定输出3V或者3.3V,就需要用到1.8V转3V,1,8V转3.3V电源芯片,就PW5100(低功耗,外围简单),PW5200A是可调输出电压,可以输出电压根据外围电阻来设置命令 ...

  10. nfs samba文件共享服务

    (注意:实验之前强关闭selinux和防火墙) 一丶nfs ① 1.服务端 启动服务 systemctl start nfs.service   配置文件 vim /etc/exports share ...