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"]