jQuery源码分析之$.isWindow、$.isArraryLike以及$.type

今天来和大家说说$.isWindow、$.isArraryLike以及$.type这三个函数在jquery中是怎么实现的,上一篇文章中有提到过$.type这个函数,而且这个函数在日常开发中还蛮好用的,所以今天就来说说。其实这三个函数的实现都比较简单,并不复杂。我也想借着解读jquery中源码的过程,将javascript中的一些注意的小点提出来,下面我们一个个看。

$.isWindow

先说$.isWindow(obj)这个函数吧。这个函数接受一个参数,来判断传入的对象是否是一个窗口。看这个函数在jquery中的源码部分:


$.isWindow(obj)这个函数就一句话,哈哈,估计这个函数是jquery中实现最简单的方法之一了吧...不过代码虽然只有一句,但是这一句code中却包含着很多js需要注意的点,下面我们来一个一个的罗列出来。看看就这一句代码我们可以罗列出几点。

A && B 和 A || B两种模式究竟返回的是什么

源码可以看出主体结构就是 A && B,在日常开发中A && B 和 A || B这两种结构算是经常遇到的,但是很多人往往忽略了微小但是却很重要的一点,那就是在js中 逻辑“与”和逻辑“或”返回的不一定是一个布尔值,而是A、B表达式中的一个。下面我们用一个小demo来说明这个问题。

 var a = 20;
    var b = 'abc';
    var c = null;

    var r1 = a || b;  //返回 r1=20
    var r2 = a && b;  //返回 r2='abc'
    var r3 = c || b;  //返回 r3='abc'
    var r4 = c && b;  //返回 r4=null

    console.log(r1,r2,r3,r4)

看输出结果如下:


可以从这个demo中看出 A && B和A || B的结构中返回的是A/B表达式中的一个,具体规则如下:

  • A&&B中,A 为true则返回B的表达式结果,反之则返回A的表达式结果。
  • A || B中,A为true则返回A的表达式结果,反之则返回B的表达式结果。

这与在java或者C中的行为略有不同。所以我们可以看到源码中的return语句中,如果前半部分 obj != null返回true的话则整个表达式返回值是后半部分 obj.window = window的运行结果,反之则返回obj !=null的返回结果也就是false了。


严格相等(===)和宽松相等(==)的常见误区

'=='和 '==='这种比较在js中出现频率也是非常之高,大部分开发人员对'=='和'==='的理解是这样子的:'=='判断两边的值是否相等,而'==='不仅判断两边的值是否相等,还需要判断两边的值类型是否相等。这样的理解看上去好像'==='做的事情要多一点,不仅判断了两边的值也需要判断两边的值类型是否也一样,这也是很多大神的帖子和博客上会常常说道'=='会比'==='的运行速度要快些的原因。可是事实总是爱和我们开些小玩笑,关于'=='和'==='的真正理解应该是:在'=='比较中允许两边的值发生类型转换,而'==='的比较则不允许两边发生类型转换。哈哈,这样子看的话,应该是'=='做的事情要多一点,因为在‘==’比较的过程中他允许两边的值发生类型转换。而'==='则压根不会做这个事情,所以应该是'==='要比'=='的运行速度更加的快一点。虽然这两者之间的差距非常小,仅仅只是百万分之一秒的差别而已,然而作为一个严谨的前端开发者而言,我们还是需要弄清楚比较好,这也是我们平时常见的误区之一。我们在选择使用'=='还是‘===’的时候,只需要考虑在进行相等比较的时候是否允许两边进行强制类型转化,如果允许则用'=='反之则选择'==='就好了。

这儿还需要提出一点就是不管是'=='还是'==='他们在进行比较的时候都会检查两边的类型,不同的是发现两边的数据类型不同的时候怎么做,‘==’就会选择强制类型转换之后在进行比较,而‘===’则直接返回false了。(!=和!==也是同理)

强制类型转换

如上第二点所述,在obj != null中使用的是 宽松相等的判断模式,那如上所述在比较的时候允许两边发生强制类型装换,其实关于强制类型转换是一个巨大的知识模块,如果详细展开描述的话,其篇幅足以再写另一篇文章了,所以在这儿就简单的说一点,不然就会跑题了,打算后面有时间会详细写一篇关于强制类型转换的文章。

关于强制类型转换特别是隐式类型转换其中的机制比较隐蔽和晦涩,很多大神的帖子博客包括一些书籍比如 javascript语言精粹这本书的作者就不建议我们使用隐式类型转换,因为这样会使得我们的代码比较晦涩。可是他自己本人在这本书中的很多小demo都使用了隐式类型转换...~~~~(>_<)~~~~因为的确大部分的前端开发对这一块都没有完全理清楚,有时候会自己掉进这个坑里面。比如举几个例子:


我们会发现 [ ] == [ ]返回false, 而[ ] == ![ ]居然返回的是true 可是{ } == !{} 返回的又是false。[2] == 2返回的居然是true。很多人会觉得这些判断是‘不可思议’的,其实在这背后有一套完整的符合ECMAScript规范的机制。

在这儿我们只需要知道 obj != null的表达式中只要我们传入的值不是null 和undefined的话期返回的值就是true。

注意在'=='判断中 undefined == null的返回值是true所以undefined != null返回的是false。


关于window的特点

在前端浏览器环境下我们都知道全局对象window,所有的属性和方法都在这个window下面,但是有一点很奇特的是,window中还存在一个属性window这个属性其实就是自己。这就会有window === window.window 的特点,其实只要你乐意,可以一直这么window.window.windwo...的写下去,返回都永远都是true。如下图:


所以正是利用了window的这个特点来实现$.isWindow()的。

$.isArrayLike

下面我们来看看$.isArrayLike(obj),这个函数主要是用来检测传入的参数对象是否是一个类数组对象,所谓类数组对象就像他的名字一样,首先它是一个对象,但是又可以像数组一样的用索引去取值,最常见的类数组对象就是DOM List 和函数中传入的参数arguments,这个函数的源码如下:


这部分的函数也并不复杂,下面我们来一句一句的分析,首先是第一句,!!obj也是强制类型转换,将穿入的参数对象obj转换为一个布尔值,在这儿其实就是只要传入的值不是0,'',null,undefined的话返回的都是true。然后运用in运算符来检测传入的参数对象中是否包含length这个属性,其实一个类数组对象的最根本特征就是含有length属性。如果有的话就把传入的参数对象中的length属性取出并赋值给length变量。

接下来的一句type = jQuery.type(obj)就是得出这个传入值的数据类型是什么,有关$.type()我们下文中会详述。

有个if判断语句,根据传入的参数对象的数据类型来判断传入的是否是function即是否是一个函数,或者用上文中我们分析过的$.isWindow()这个函数来判断是否是当前的窗口。如果传入进来的是一个函数或者是当前的窗口对象的话,就会直接返回false了,为什么呢,因为刚刚我们也有说道类数组对象中的一个根本特点就是具有length属性,但是window.length和functionName.length返回的分别是一个窗口中包含的iframe的数量和一个函数中期望传入的参数的数量(注意并非是形参的数量,因为在es6中可以给形参设置默认值会影响到functionName.length的值。)所以我们要将这两种情况去掉。

到了最后一句return了,看上去有点长,其实好像也并不复杂。根据这个结构我们可以看出 主题机构是属于A || B || C而C中又包含了 A && B && C的结构,但是不管几层 && || 结构,这种结构的分析方法我们上文也已近详细的介绍过了,按照我们介绍过的套路来分析的话可以看出会在三中情况下返回 true。下面我们一一罗列一下:

  • 传入的本身就是一个数组。
  • 传入的参数对象中有length属性且其值为0
  • 传入的参数对象中有length属性在他的值大于0的情况下,前一个数值在obj中可以遍历到。

第一点第二点非常简单,我们看下第三点,举个小例子给大家看下就很好理解了


我们可以看大这个demo中,$divs就是一个类数组对象,其中包含着length属性,他的length值为4那我们在$divs中用in来查询3看看是否能查询到,其实这个3就是这个类数组对象用来取值的索引。可以看到$divs[3]返回的就是索引3对应的值。

$.type

    $.type(obj)这个函数在我们日常开发中用到的频率应该说是很多的,这是jquery中用来判断传入参数对象究竟是属于js中的那种数据类型。如果用原生的js的话,我们还得区分是基础类型还是引用类型然后用type of 判断基础类型,用instance of(A instance of B其实就是判断A的原型链上是否有B有则返回true反之则返回false)去判断引用类型的数据,而基础类型中的null 返回的又是object这是一个历史悠久的bug了,如果是引用类型的话还得先看看是不是一个数组,如果树数组的话用Array.isArray()去判断,因为如果跨iframe的话用instanceof 去判断还会出错。天呐‼️,说道这儿要是让我用原生的js去判断一个数据类型的话简直太痛苦了,就这么一点小小的判断数据类型就需要考虑这么多的情况,于是jquery就自己封装了这个$.type()方法,只要调用了这个方法就能准确的返回传入的参数对象的数据类型是什么,听上去就很酷。在讲解$.type()这个函数的时候我们需要知道一点额外的东西。就是内置属性[[class]]

内置属性[[class]]

在js中有很多的内置属性比如[[class]],还有函数运行时候的上下文[[scope]],这些内置属性都有自己独特的作用,但是一般而言我们开发人员是无法通过js代码像访问对象属性的方式直接去访问内置属性。在这儿我们就先说说关于[[class]]的这个内置属性。

在js中的素有数据类型:Boolean Number String Function Array Date RegExp Object Error Symbol  这些数据类型都有内置属性[[class]]而且各不相同。说道这儿思路也就出来了,既然每个数据类型都有[[class]]这个内置属性而各不相同,那么我们就可以根据[[class]]的属性值不同来区分到底是哪种数据类型了。事实上说道这儿$.type()函数的实现思路就说完了,其实$.type()就是运用[[class]]的属性值不同来区分各个变量的,方便且准确。似乎还有一个问题,如同我刚刚所说一般无法通过代码直接访问到[[class]]这个内置属性,那我们该怎么获取呢。?答案就是通过Object.prototype.toString.call()这种方式来实现获取传入参数对象的内置[[class]]属性。

下面我们看这段代码就了解了:

var dataType = [];

    dataType.push(Object.prototype.toString.call(null))
    dataType.push(Object.prototype.toString.call(undefined))
    dataType.push(Object.prototype.toString.call(true))
    dataType.push(Object.prototype.toString.call(1))
    dataType.push(Object.prototype.toString.call('1'))
    dataType.push(Object.prototype.toString.call(Symbol('symbol')))
    dataType.push(Object.prototype.toString.call(new Object()))
    dataType.push(Object.prototype.toString.call(new Array()))
    dataType.push(Object.prototype.toString.call(function () {}))
    dataType.push(Object.prototype.toString.call(new Date()))
    dataType.push(Object.prototype.toString.call(new Error()))
    dataType.push(Object.prototype.toString.call(new RegExp()))

然后在控制台看看dataType最后是什么样子的:



我们可以很清楚的看到12种数据类型中的内置属性[[class]]都不一样,他们的组成结构都是[object '自己的数据类型']前面的object我们可以看成在js中一切都是对象。(虽然我觉得这有点奇怪,但是先暂且这么认为吧,有利于我们理解)前面是object是所有数据类型都是object这个大类中的某一个“分支”,后面是自己这种数据类型所在object这个大的“范畴”之中自己所属于那一个“分支”。

源码解读

其实说道这儿$.type的函数中的实现思路就已近说完了,那我们下面具体来看看在jquery中$.type函数究竟是怎么实现的呢。?

首先在jquery的头部声明中有一个变量叫做class2type:


他就是吧所有内置属性[[class]]的值都作为class2type这个对象中的key而对应的每一个value其实就是[[class]]部分的后面的“分支部分”。

具体实现代码如下:


这样就能吧所有数据类型的[[class]]放在class2type这个对象中了。

然后我们再看看$.type(obj)部分的源码:


我们知道了实现思路在看这部分的源码就很简单了,首先是判断 obj == null,如果是的话就直接返回对应的字符串形式,但是这儿需要注意,一个是传入的如果是undefined的话在 ‘==’中上文也说过 undefined == null是返回true的。然后就是运用了我们上文也说过的隐式强制类型转换将 null 和undefined转化为 "null"和"undefined"作为返回结果。这些也都是jquery中运用到的一些"奇技淫巧",但是如果对基本知识掌握透彻的话,可以明显感觉到这样写可以使得代码非常简洁。

然后我们可以看到最后一个return 看上去好像有点长,其实也很简单,我们先看清它的结构,整体结构应该是 A?B:C的模式。其中A的部分就是typeof obj === "object" || typeof obj === "function",B的部分是class2type[toString.call(obj)] || "object"而C的部分就是typeof obj。那我们分别来看下A、B、C部分各自是做了什么事情。

其中A部分就是判断这个传入的参数对象是否是一个引用类型的值,看下图:


我们在上文中也罗列除了js中一共是12中数据类型,其中六中是基本数据类型或者说是原始数据类型,另外6中就是引用类型的数据类型,那原始数据类型中除了null,undefined之外的其余四中(Boolean Number String Symbol)都可以使用type去判断到底是哪一种。而引用类型的数据类型对其使用typeof运算符的话其中函数数据类型会返回"function",其余的5中都会返回"object"所以如果对传入的参数进行typeof的运算的话如果运算结果是"object"或者是"function"的话就证明是引用类型的数据,那到底是哪一种引用类型的数据呢?那就是用到了class2type中事先登记好的我们所说的用内置属性[[class]]来区分了。如果判断是基础数据类型的话,那就直接运用typeof就可以得到是那种基础数据类型了。因为在此之前我们已近对null 和undefined进行处理过了。所以可以很安全的对其余的几种基础数据类型用typeof进行检测。

说道这儿其实已经说完99%的部分了,还剩下一点点就在如果是引用类型的话,也就是 源码的这部分的后半部分有个 || "object"

class2type[ toString.call( obj ) ] || "object"

那什么时候回用到 "object"呢,那就是我们在开发中如果是自己创建了数据类型的话,那么就会统一返回"object"。比如看下面的这个小demo:



这例子中我们是使用自己建立的一个函数foo 作为构造函数来创建的一个新的对象,这个我们自己所创建的新数据类型自然不在js中12个数据类型之列了,那么就会统一返回"object"

好了,说道这儿有关$.isWindow(obj)、$.isArrayLike(obj)、$.type(obj)源码部分都写完了。如果大家有什么不同的看法,欢迎一起交流学习。


猜你喜欢

转载自blog.csdn.net/weixin_38080573/article/details/79497943