jQuery源码学习笔记系列(一)

时间:2022-12-02 20:47:35

js接触比较早,但开始接触时js语法表现的随意性并不能让人一开始
就喜欢,直到后来接触nodejs,才感觉,原来js也是不错的,在深入
学习的过程中,想将学习笔记记录下来,于是有了这个学习笔记,由于
水平有限,所以错误疏漏的地方欢迎大家指出。
其中需要说明的是,内容比较杂,建议有js基础后再阅读,
该系列测试环境为chrome(60.0.3112.90),node(v6.10.2),
参考书籍(《javaScript权威指南》(很好的一本书,翻译也很赞),
《了不起的nodejs》,《jQuery高级编程》)
部分参考博客文章等会在相关地方注明
还有两份源代码,一个是jQuery-3.2.1,一份是Google的v8源码(参考)
基于ES5

第一块代码

( function( global, factory ) {...})(typeof window !== "undefined" ? window : this, function( window, noGlobal ) {...})

其中省略的是代码细节,详细将在后续展示
首先,这是一个匿名函数,js指南中对匿名函数的描述是作为命名空间,命名空间很早就在cpp中出现,用于解决命名冲突的问题,js中匿名
函数有同样的作用,但其有另一个作用,立即执行。针对这两个特性,下面继续。

命名冲突问题

命名冲突在编程中很常见,尤其是面向对象的概念深入人心后,模块化组织程序时从原则上应该是做到高类聚,低耦合,如果能完美做到这一点,那么我认为是没有命名冲突的,但实际中,就很难做到这一点,比如cpp中很常见的using namespace std;std就是一个标准库的命名空间,这个时候是为了简化操作,比如cout就可以认为是std::cout,但在js中,我倒并不是为了简化操作,而是因为js很多实际使用的时候,拥有太多‘不规范’的操作,大量的全局变量,而匿名函数就是为了避免污染全局变量,另外,在js中,全局变量可用可不用var,而局部变量中,应该必须要用var,但是,这个全局的概念可能要稍稍有点不同

var global = 5 ;
function test(){global=8;console.log(global)}
test()//8
global //8

这就是所谓的污染全局
而如果是这样

function test(){var global=8;console.log(global){
test() //8
global //5

而另一端代码示例

function test(){var global=8;function test2(){global=10;console.log(global);test2()}
test() //10
global //5

这段代码似乎说明了
不使用var污染的不是全局变量,而是——
破折号并不是一个惊喜的转折,而是需要先补充一下其它知识
我们先来回顾一下在c语言里面的函数
c语言的函数,可以认为是一段可以编译成机器码的一段代码
如下一段代码

int main(){int a=10; a+=8;return 0}

main也是函数,为了方便直接使用这个
这一段代码可以转成这样的汇编代码(机器码不适合人类阅读)


.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $10, -4(%rbp)
addl $9, -4(%rbp)
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc

不需要探究太多,有一点了解汇编就可以知道其操作了很多底层的东西,比如寄存器之类
而这样转化成某种机器码的语言可以认为是静态语言(java机器码运行在java虚拟机上面)
这样的函数,在某种程度上是写的,并不是说不能改变,而是说其具有一定的约束,反馈到代码中就是无处不在的指针,ok,到此为止,毕竟这是js
而js,所谓的动态语言,在我的理解中,就是为了方便使用这个目的,其牺牲了性能,而得到了简化操作,所以它内置了很多方便的操作,比如,使用变量就是一个简单的var,没有int,char之类,因为从使用的角度来说,变量就是为了存储数据,并不关心是哪样类型的数据,但是对于 函数,表明上看来,和c中函数很像很像,但是,c中函数可以转化为汇编,而汇编之后的机器码是可以在一定程度上可以独立执行,但是,js中函数能类似独立执行吗?
答案当然是不能,js函数是为了方便使用,既方便我们使用,也要有一个使用者,也就是,这个函数是给谁在用,至于为什么强调这,首先现在不能过分去探究为何有有一个我所说的使用者,但是要记住,首先考虑是谁在使用这个test()
上面说了很多废话,下面开始正经内容,也是重头戏,js中的闭包概念

{
var a = 10;
{
var b =9;
{
var c = 8
....
}
}
}

用书中的话说:

和现代大多数编程语言一样,js也采用词法作用域,也就是说,函数执行依赖于变量作用域,而变量作用域是在函数定义时决定的,为了实现这个词法作用,js函数定义时不仅包含函数的代码逻辑,还必须引用当前的作用域链

来了一个关键词,作用域链

作用域链的概念,将作用域视作一个链(或者说一个数据结构中的链表)
其从局部向全局 延伸,用于查找变量,而下面是关键一点,在最顶层的
作用域链中,其由一个全局对象组成,在不包含嵌套的函数体中,作用域
由两个对象,一个是包含函数参数和局部变量的对象,一个是全局对象,
在包含嵌套的函数体中,作用域链上至少有三个对象,也就是会增加
嵌套的函数参数以及局部变量。而定义一个函数时,实际上是保存了
一个作用域链,当调用这个函数时,它创建新的对象来存储它的局部
变量,并将这个对象添加至保存的那个作用域链上,同时创建一个新的
更长的表示函数调用作用域的‘链’,这样对嵌套函数而言,每次调用外部
函数时,内部函数又会重新定义一遍,因为其作用域链改变了

这段话听着不好理解,但是看上面的伪代码示例,很简单的解释就是最里面的括号,可以访问a,b,c,倒数第二层可以访问a,b,但不能访问c,同理最外围的可以访问a,不能访问b,c,如果换成函数,也就是一个嵌套了一个嵌套函数的函数,(类似的嵌套函数定义在c语言这样的静态语言是不能使用的,而js这样的动态语言就可以),函数有一个使用者,而当这个使用者使用这个函数时,也就意味着里面的嵌套函数又定义了一遍,如:

function test(k){var d =k; function test2(){var n=d; console.log(n)};test2()};
test(2)//2
test(5)//5

也就是test(2)和test(5)定义了两个不同的test2(),这就是所谓的内部函数的会被重新定义一便,在test(2)中,调用test函数时给其保存了一个局部变量d=2,而在test(5)中,其另外保存了一个局部变量d=5,这就是所谓的作用域链发生了改变。
现在对闭包和作用域链有了一定的认识,思路开始回收,回到为何污染的global不是全局的,而是其外层的,因为,其从局部向全局 延伸,用于查找变量,,也就是从里向外查找作用域链,理所当然的会优先使用其外层,(也就是通常所说的局部优先全局),所以,使用var可以定义一个新的变量,但不使用var则可能污染一个外部变量,所以应该尽量的使用var,而在循环中使用较多的i,j之类,应该尽量避免在别的地方使用这些,避免麻烦。
到这里,命名冲突问题似乎可以告一段落,但上文仅仅是讲述了命名冲突在js中的一些原因和细节,并么有给出解决方案。
从一个命名冲突扯出了那么多,看着就像托勒密为了解决地心说的不完美之处作出的种种复杂而精巧的补充一样,有种感觉就是说的全是错的废话,但是,学习笔记就该胡思乱想一些。

立即执行

这很好理解,定义了马上执行,因为匿名,没有标识符如果不立即执行应该会永远不能再执行?
这个特性主要表现在用法上面,
来看一个很常见的例子:

function test(){for(var i=0;i<5;i++){setTimeout(function(){console.log(i)},1000)}}
test()//五个5

而如果想要打印12345如何做?
想要打印01234的问题在于for是一个同步函数,而setTimeout是一个异步函数,(或许更准确来说,是setTimeout的第一参数所定义的那个函数是异步执行的,同步和异步的问题在这里先不过分思考,但可以从另一个角度解释一下,同步是阻塞的,也就是要先做完同步的事,才能再去做下一 事,而异步,是非阻塞的,可以在这个事没做完之前去做别的事,所以即使是设置setTimeout的时间间隔为0也是一样的效果) 。
而看setTimeout的第一参数可以猜到,这里定义了一个函数,而在上文提到过,定义一个函数会根据当前的作用域链来定义函数,所以在第一个代码中,每次循环时都会执行一次setTimeout,但setTimeout不会立即做,它要等待for循环做完之后才能执行,而当它执行的时候,又会定义一个一个function,而这个时候,所有的i当然是5了,所以会出现5个5。
而想做到01234,就要考虑一个问题,如何将循环时候的i传递给循环做完后的setTimeout所定义的函数,让我们再来思考作用域链,定义一个函数时,实际上是保存了一个作用域链,当调用这个函数时,它创建新的对象来存储它的局部变量,并将这个对象添加至保存的那个作用域上,同时创建一个新的更长的表示函数调用作用域的‘链’
所以,我们需要一个保存存储i的地方,也就是一个函数,而这里选择匿名函数,因为它可以很简单做到改变作用域链,以保存局部变量,代码如下:

function test(){for(var i=0;i<5;i++){(function(n){setTimeout(function(){console.log(n)},1000)}(i))}}
test()//0,1,2,3,4,5

当setTimeout最后定义其异步函数的时候,应该参考的作用域链必须包括匿名函数的作用域,所以,i被传递给n,n作为局部变量保存着,等待着setTimeout定义其异步函数。
那么,不用异步而用普通函数能做到这一点吗?
当然可以,这里它只是需要一个保存局部变量的作用域链而已,所以以下代码也是可以的:

function test(){for(var i=0;i<5;i++){function test2(n){setTimeout(function(){console.log(n)},1000)};test2(i)}}
test()//01234

一点小提示,将时间设置n*1000,即可做到没隔一秒出现一个数字
在这里匿名函数简洁,所以经常被用到,比较实际的另一个例子是利用jQuery异步生成一系列带不同id的标签时.
关于匿名函数的两个特性暂时停止
下面继续回到jQuery源码上来,先看这个匿名函数的参数

typeof window !== "undefined" ? window : this, function( window, noGlobal ) {·····}

首先出现了一个typeof运算符,再就比较常见的三目云算法 ? : ,typeof用于检测操作数类型,jQeury是用在客户端的js,所以其必须要有一个window对象,而js中检测是否存在通常会用 typeof o ===undefined,这也是js这样动态语言的蛋疼之处,变量对象存在不存在都不知道,然后给window赋值为this,this在客户端中为

this
Window {stop: ƒ, open: ƒ, alert: ƒ, confirm: ƒ, prompt: ƒ, …}

关于this的详细会在后面讨论
然后又一个函数,暂时不看
再看匿名函数内部

 "use strict";

if ( typeof module === "object" && typeof module.exports === "object" ) {
module.exports = global.document ?
factory( global, true ) :
function( w ) {
if ( !w.document ) {
throw new Error( "jQuery requires a window with a document" );
}
return factory( w );
};
} else {
factory( global );
}

值得关注的地方是其说明了为严格模式(详细参考文档)
然后看注释,就有一个说明,如果不是运行在浏览器环境,比如在nodejs中(nodejs用jQuery我还没试过),那么就会将其作用一个模块,并且暴露一部分api,这一段就是在讲jQuery应用时的环境问题
当然这不是重点。

第一块代码罗嗦了很多东西,后面会加快节奏。