jQuery 是一个很是优秀且经典的库。怎么形容它的优秀呢?即便近两年流行了如 Vue 、 React 等众多热门的库,但对于封装方法、思想而言,这些库都未曾超越jQuery。所以,对于前端工程师而言,阅读 jQuery 源码是一条提高自个人必经之路。那么接下来,就让咱们一块儿走进 jQuery 内幕的世界。javascript
首先,咱们从 jQuery 源码的 github 上下载并使用 vscode 打开 jQuery 源码。html
打开 jQuery 目录,能够很明显的看见 package.json 和 gruntfile.js 两个文件,熟悉 grunt 的小伙伴,看见 gruntfile.js 就很清楚,该目录代码使用的是 grunt 做为其构建工具。前端
打开src文件夹,文件夹里面就是 jQuery 的源码目录,咱们能够从目录清晰的看见jQuery的各个模块:java
接下来,咱们打开src文件夹中的jquery.js,便可看到 jQuery 的代码加载:
从图片中,咱们能够看见,采用的是AMD方式定义。咱们甚至能够直接从该文件看出 jQuery 有哪些功能,可供咱们使用。jquery
首先,咱们能够从jquery官网,使用grunt编译一下 jQuery 源码或下载编译事后、未压缩版本的 jQuery 。若使用grunt编译,咱们能够从dist/jquery.js中,看到以下代码:git
(function(global, factory){ ... })(typeof window !== "undefined" ? window : this, function( window, noGlobal(){...});
咱们对其,进行一番简化:github
(function(global,factory){ ... })(window,funciton(){});
这样,就很是一目了然了,这是经典的当即执行函数(IIFE):json
(function(){ ... })()
Q:采用当即执行函数,这样作,有什么好处呢?
A:经过定义一个匿名函数,建立了一个新的函数做用域,至关于建立了一个“私有”的命名空间,该命名空间的变量和方法,不会破坏污染全局的命名空间。此时如果想访问全局对象,将全局对象以参数形式传进去便可。此外,新做用域内对象想访问传入的全局对象时,就不须要一步一步的往上找,可提升效率。segmentfault
咱们看以下一段代码:前端工程师
var s = new $('.test'); var p = $('.test'); console.log(s); console.log(p);
咱们引入一下jQuery,并处理一下这段代码,能够看到效果以下:
使人惊讶的是,new出来的和直接调用的,竟然是如出一辙的。
这是为何呢?
这就涉及到了jQuery的经典的init操做:
咱们打开jQuery目录下的src/core.js文件,咱们能够看见一段很是经典的代码:
从上面这张图,咱们能够了解到:
[注]咱们也能够从src/core/init.js中,看init是如何具体实现初始化的。
为了方便讲解,咱们对其进行一些简化:
//1 jQuery = function( selector, context ) { return new jQuery.fn.init( selector, context ); } //2 jQuery.fn = jQuery.prototype = { init:function( selector, context ){ ... } } //3 init = jQuery.fn.init = function( selector, context, root ){ ... } init.prototype = jQuery.fn;
饶了一大圈,就至关于 jQuery = new JQuery();
Q:那么,为啥要绕那么远呢?
A:为了获得jQuery原型链上的方法。
[特别标注]若是你看了五遍,依旧看不懂这个过程,亦或对Q/A没有看懂,你可能对js建立对象中的构造函数模式、原型模式、组合模式等的理解还不够深入,你能够戳我这篇博文学习一下,亦可翻阅《javascript 高级程序设计》第六章-面向对象的程序设计中的建立对象部份内容。
接下来,咱们看一段官方给的jQuery链式对象的示例:
//html <div class="grandparent"> <div class="parent"> <div class="child"> <div class="subchild"></div> </div> </div> <div class="surrogateParent1"></div> <div class="surrogateParent2"></div> </div>
//js //return [div.surrogateParent1] $("div,parent").nextAll().first(); //return [div.surrogateParent2] $("div.parent").nextAll().last();
$("div,parent").nextAll().first()这是咱们使用jQuery时,常用的调用方法,链式调用。那它是如何作到的呢?咱们先看一眼这个代码:
var test = { a:function(){ console.log('a'); }, b:function(){ console.log('b'); }, c:function(){ console.log('c'); } } test.a().b().c();
结果如何呢?
答案很明显,b()和c()是没法访问的。jQuery是如何实现它的呢?很简单,返回它自己便可。如:
var test = { a:function(){ console.log('a'); return this; }, b:function(){ console.log('b'); return this; }, c:function(){ console.log('c'); } } test.a().b().c(); //a //b //c
$('.test','td') $(['.test','#id']) $(function(){...})
$()就是一个函数,参数不一样,就涉及到了函数的重载。参数个数不等,用传统js实现起来很是困难。那么jQuery到底是如何实现的呢?
咱们经过两段代码,领悟它的实现方式:
(1)首先咱们看一个普通的例子:
function addMethod( object, name, func ) { var old = object[name]; object[name] = function(){ if(func.length === arguments.length){ return func.apply(this,arguments); }else{ return old.apply(this,arguments); } } } var people = { name:["a","b","c"] } var find0 = function(){ return this.name; } addMethod(people,'find',find0); console.log(people.find());//["a", "b", "c"]
调用people.find,将find()方法加到了people中,调用people下的find()方法后,返回的是people.name,即:["a", "b", "c"]。
(2)咱们加上一些代码,造成重载,再来看看这个例子:
添加一个addMethod(people,'find',find1):
function addMethod( object, name, func ) { var old = object[name]; object[name] = function(){ if(func.length === arguments.length){ return func.apply(this,arguments); }else{ return old.apply(this,arguments); } } } var people = { name:["a","b","c"] } var find0 = function(){ return this.name; } //新增 var find1 = function(name){ var arr = this.name; for(var i = 0;i <= arr.length;i++ ){ if(arr[i]=name){ return arr[i]; } } } addMethod(people,'find',find0); //新增 addMethod(people,'find',find1); console.log(people.find());//["a", "b", "c"] console.log(people.find("a"));//a
在第一次执行addMethod方法是,这个过程是:
一、object -> people,name -> find,func -> find0; 二、old -> people[find],为undefined 三、people[find],关联的是find0
在第二次执行addMethod方法是,这个过程是:
一、object -> people,name -> find,func -> find1; 二、old 为 object[name],即上一次执行object[name]=function(){..}时的函数,这个函数关联的是find0。 三、people[find],关联的是find1
两次调用后,此时,若调用people.find("a")的话,过程以下:
一、两次addMethod()后,形式参数为1个参数,调用people.find("a"),实际参数为1个参数 二、形参长度与实参长度相等,调用return func.apply(this,arguments),即find1 三、运行find1,打印出“a”
你看到这,是否也和博主同样,以为这是无所必要的呢?接下来,就是令你兴奋的时刻:
若调用people.find()的话,这个过程会以下:
一、两次addMethod()后,形式参数为1个参数,调用people.find(),实际参数为0个参数 二、形参长度与实参长度不相等,先调用return old.apply(this,arguments),咱们在第二次调用addMethod中阐述了,它关联的是find0,于是,此时的程序,会再次调用第一次addMethod中无参数的function(){...},即find0 三、此时的形式参数为0个,实际参数为0个 三、运行find0,打印出["a", "b", "c"]