[toc]
當我建立這個項目的時候,就表示,我要改變這一切了,做一些人想做,憧憬去做,但從沒踏入第一步的事情,學習 jQuery 源碼。
到目前為止,jQuery 的貢獻者團隊共 256 名成員,6000 多條 commits,可想而知,jQuery 是一個多么龐大的項目。jQuery 官方的版本目前是 v3.1.1,已經衍生出 jQueryUI、jQueryMobile 等多個項目。
雖然我在前端爬摸打滾一年多,自認基礎不是很好,在沒有外界幫助的情況下,直接閱讀項目源碼太難了,所以在邊參考遍實踐的過程中寫下來這個項目。
首先,先推薦一個 jQuery 的源碼查詢網站,這個網站給初學者非常大的幫助,不僅能查找不同版本的 jQuery 源碼,還能索引函數,功能簡直吊炸天。
## jQuery 總體架構
首先,jQuery 是一個開發框架,它的火爆程度已經無法用言語來形容,當你隨便打開一個網站,一半以上直接使用了 jQuery。或許,早幾年,一個前端工程師,只要會寫 jQuery,就可以無憂工作。雖說最近 react、vue 很火,但 jQuery 中許多精彩的方法和邏輯值得每一個前端人員學習。
和其眾多的框架一樣,總要把接口放到外面來調用,內部往往是一個閉包,避免環境變量的污染。
先來看看 jQuery 使用上的幾大特點:
- $('#id') 函數方式直接生成 jQuery 對象
- $('#id').css().html().hide() 鏈式調用
關于鏈式調用,我想有點基礎都很容易實現,函數結尾 return this 即可,主要來介紹一下無 new 實現創建對象。
## 無 new 函數實現
下面是一個普通的函數,很顯然,會陷入死循環:
```
var jQuery = function(){
return new jQuery();
}
jQuery.prototype = {
...
}
```
這個死循環來的太突然,jQuery() 會創建一個 new jQuery,new jQuery 又會創建一個 new jQuery...
jQuery 用一個 init 函數來代替直接 new 函數名的方式,還要考慮到 jQuery 中分離作用域:
```
var jQuery = function(){
return new jQuery.prototype.init();
}
jQuery.prototype = {
constructor: jQuery,
init: function(){
this.jquery = 1.0;
return this;
},
jquery: 2.0,
each: function(){
console.log('each');
return this;
}
}
jQuery().jquery //1.0
jQuery.prototype.jquery //2.0
jQuery().each() // error
```
上面看似運行正常,但是問題出在 jQuery().each() // error,訪問不到 each 函數。實際上,new jQuery.prototype.init() 返回到是誰的實例?是 init 這個函數的實例,所以 init 函數中的 this 就沒了意義。
那么,如果:
```
var jq = jQuery();
jq.__proto__ === jQuery.prototype;
jq.each === jQuery.prototype.each;
```
如果可以實現上面的 proto 的指向問題,原型函數調用問題就解決了,但實際上:
```
var jq = jQuery();
jq.__proto__ === jQuery.prototype.init.prototype; //true
```
實際上,jq 的 proto 是指向 init 函數的原型,所以,我們可以把 `jQuery.prototype.init.prototype = jQuery.prototype`,這個時候,函數調用就順理成章了,而且使用的都是引用,指向的都是同一個 prototype 對象,也不需要擔心循環問題。實際上,jQuery 就是這么干的。
```
var jQuery = function(){
return new jQuery.prototype.init();
}
jQuery.prototype = {
constructor: jQuery,
init: function(){
this.jquery = 1.0;
return this;
},
jquery: 2.0,
each: function(){
console.log('each');
return this;
}
}
jQuery.prototype.init.prototype = jQuery.prototype;
jQuery().each() //'each'
```
## jQuery 內部結構圖
在說內部圖之前,先說下 jQuery.fn,它實際上是 prototype 的一個引用,指向 jQuery.prototype 的,
```
var jQuery = function(){
return new jQuery.prototype.init();
}
jQuery.fn = jQuery.prototype = {
...
}
```
那么為什么要用 fn 指向 prototype?我本人查閱了一些資料,貌似還是下面的回答比較中肯:簡介。你不覺得 fn 比 prototype 好寫多了嗎。
借用網上的一張圖:

從這張圖中可以看出,window 對象上有兩個公共的接口,分別是 $ 和 jQuery:
```
window.jQuery = window.$ = jQuery;
```
jQuery.extend 方法是一個對象拷貝的方法,包括深拷貝,后面會詳細講解源碼,暫時先放一邊。
下面的關系可能會有些亂,但是仔細看了前面的介紹,應該能看懂。fn 就是 prototype,所以 jQuery 的 fn 和 prototype 屬性指向 fn 對象,而 init 函數本身就是 jQuery.prototype 中的方法,且 init 函數的 prototype 原型指向 fn。
## 鏈式調用
鏈式調用的好處,就是寫出來的代碼非常簡潔,而且代碼返回的都是同一個對象,提高代碼效率。
前面已經說了,在沒有返回值的原型函數后面添加 return this:
```
var jQuery = function(){
return new jQuery.fn.init();
}
jQuery.fn = jQuery.prototype = {
constructor: jQuery,
init: function(){
this.jquery = 3.0;
return this;
},
each: function(){
console.log('each');
return this;
}
}
jQuery.fn.init.prototype = jQuery.fn;
jQuery().each().each();
// 'each'
// 'each'
```
## extend
jQuery 中一個重要的函數便是 extend,既可以對本身 jQuery 的屬性和方法進行擴張,又可以對原型的屬性和方法進行擴展。
先來說下 extend 函數的功能,大概有兩種,如果參數只有一個 object,即表示將這個對象擴展到 jQuery 的命名空間中,也就是所謂的 jQuery 的擴展。如果函數接收了多個 object,則表示一種屬性拷貝,將后面多個對象的屬性全拷貝到第一個對象上,這其中,還包括深拷貝,即非引用拷貝,第一個參數如果是 true 則表示深拷貝。
```
jQuery.extend(target);// jQuery 的擴展
jQuery.extend(target, obj1, obj2,..);//淺拷貝
jQuery.extend(true, target, obj1, obj2,..);//深拷貝
```
以下是 jQuery 3 之后的 extend 函數源碼,自己做了注釋:
```
jQuery.extend = jQuery.fn.extend = function () {
var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false;
// 判斷是否為深拷貝
if (typeof target === "boolean") {
deep = target;
// 參數后移
target = arguments[i] || {};
i++;
}
// 處理 target 是字符串或奇怪的情況,isFunction(target) 可以判斷 target 是否為函數
if (typeof target !== "object" && !jQuery.isFunction(target)) {
target = {};
}
// 判斷是否 jQuery 的擴展
if (i === length) {
target = this; // this 做一個標記,可以指向 jQuery,也可以指向 jQuery.fn
i--;
}
for (; i < length; i++) {
// null/undefined 判斷
if ((options = arguments[i]) != null) {
// 這里已經統一了,無論前面函數的參數怎樣,現在的任務就是 target 是目標對象,options 是被拷貝對象
for (name in options) {
src = target[name];
copy = options[name];
// 防止死循環,跳過自身情況
if (target === copy) {
continue;
}
// 深拷貝,且被拷貝對象是 object 或 array
// 這是深拷貝的重點
if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))) {
// 說明被拷貝對象是數組
if (copyIsArray) {
copyIsArray = false;
clone = src && Array.isArray(src) ? src : [];
// 被拷貝對象是 object
} else {
clone = src && jQuery.isPlainObject(src) ? src : {};
}
// 遞歸拷貝子屬性
target[name] = jQuery.extend(deep, clone, copy);
// 常規變量,直接 =
} else if (copy !== undefined) {
target[name] = copy;
}
}
}
}
// Return the modified object
return target;
}
```
extend 函數符合 jQuery 中的參數處理規范,算是比較標準的一個。jQuery 對于參數的處理很有一套,總是喜歡錯位來使得每一個位置上的變量和它們的名字一樣,各司其職。比如 target 是目標對象,如果第一個參數是 boolean 型的,就對 deep 賦值 target,并把 target 向后移一位;如果參數對象只有一個,即對 jQuery 的擴展,就令 target 賦值 this,當前指針 i 減一。
這種方法邏輯雖然很復雜,但是帶來一個非常大的優勢:后面的處理邏輯只需要一個就可以。target 就是我們要拷貝的目標,options 就是要拷貝的對象,邏輯又顯得非常的清晰。
extend 函數還需要主要一點,jQuery.extend = jQuery.fn.extend,不僅 jQuery 對象又這個函數,連原型也有,那么如何區分對象是擴展到哪里了呢,又是如何實現的?
其實這一切都要借助與 javascript 中 this 的動態性,target = this,代碼就放在那里,誰去執行,this 就會指向誰,就會在它的屬性上擴展。
## 由 extend 衍生的函數
再看 extend 源碼,里面有一些函數,只是看名字知道了它是干什么的,我專門挑出來,找到它們的源碼。
### jQuery.isFunction 源碼
```
jQuery.isFunction = function (obj) {
return jQuery.type(obj) === "function";
}
```
這也太簡單了些。這里又要引出 jQuery 里一個重要的函數 jQuery.type,這個函數用于類型判斷。
首先,為什么傳統的 typeof 不用?因為不好用(此處應有一個哭臉):
```
// Numbers
typeof 37 === 'number';
typeof 3.14 === 'number';
typeof(42) === 'number';
typeof Math.LN2 === 'number';
typeof Infinity === 'number';
typeof NaN === 'number'; // Despite being "Not-A-Number"
typeof Number(1) === 'number'; // but never use this form!
// Strings
typeof "" === 'string';
typeof "bla" === 'string';
typeof (typeof 1) === 'string'; // typeof always returns a string
typeof String("abc") === 'string'; // but never use this form!
// Booleans
typeof true === 'boolean';
typeof false === 'boolean';
typeof Boolean(true) === 'boolean'; // but never use this form!
// Symbols
typeof Symbol() === 'symbol'
typeof Symbol('foo') === 'symbol'
typeof Symbol.iterator === 'symbol'
// Undefined
typeof undefined === 'undefined';
typeof declaredButUndefinedVariable === 'undefined';
typeof undeclaredVariable === 'undefined';
// Objects
typeof {a:1} === 'object';
// use Array.isArray or Object.prototype.toString.call
// to differentiate regular objects from arrays
typeof [1, 2, 4] === 'object';
typeof new Date() === 'object';
// The following is confusing. Don't use!
typeof new Boolean(true) === 'object';
typeof new Number(1) === 'object';
typeof new String("abc") === 'object';
// Functions
typeof function(){} === 'function';
typeof class C {} === 'function';
typeof Math.sin === 'function';
// This stands since the beginning of JavaScript
typeof null === 'object';
```
可以看得出來,對于一些 new 對象,比如 new Number(1),也會返回 object。具體請參考typeof MDN。
網上有兩種解決方法(有效性未經考證,請相信 jQuery 的方法),一種是用 constructor.nameObject.prototype.constructor MDN,一種是用 Object.prototype.toString.call()Object.prototype.toString(),最終 jQuery 選擇了后者。
```
var n1 = 1;
n1.constructor.name;//"Number"
var n2 = new Number(1);
n2.constructor.name;//"Number"
var toString = Object.prototype.toString;
toString.call(n1);//"[object Number]"
toString.call(n2);//"[object Number]"
```
以上屬于科普,原理不多闡述,接下來繼續看源碼 jQuery.type:
```
// 這個對象是用來將 toString 函數返回的字符串轉成
var class2type = {
"[object Boolean]": "boolean",
"[object Number]": "number",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Date]": "date",
"[object RegExp]": "regexp",
"[object Object]": "object",
"[object Error]": "error",
"[object Symbol]": "symbol"
}
var toString = Object.prototype.toString;
jQuery.type = function (obj) {
if (obj == null) {
return obj + "";
}
return
typeof obj === "object" || typeof obj === "function" ?
class2type[toString.call(obj)] || "object" :
typeof obj;
}
```
因為 jQuery 用的是 toString 方法,所以需要有一個 class2type 的對象用來轉換。
### jQuery.isPlainObject
這個函數用來判斷對象是否是一個純粹的對象,:
```
var getProto = Object.getPrototypeOf;//獲取父對象
var hasOwn = class2type.hasOwnProperty;
var fnToString = hasOwn.toString;
var ObjectFunctionString = fnToString.call( Object );
jQuery.isPlainObject = function (obj) {
var proto, Ctor;
// 排除 underfined、null 和非 object 情況
if (!obj || toString.call(obj) !== "[object Object]") {
return false;
}
proto = getProto(obj);
// Objects with no prototype (e.g., `Object.create( null )`) are plain
if (!proto) {
return true;
}
// Objects with prototype are plain iff they were constructed by a global Object function
Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
return typeof Ctor === "function" && fnToString.call(Ctor) === ObjectFunctionString;
}
```
看一下效果:
```
jQuery.isPlainObject({});// true
jQuery.isPlainObject({ a: 1 });// true
jQuery.isPlainObject(new Object());// true
jQuery.isPlainObject([]);// false
jQuery.isPlainObject(new String('a'));// false
jQuery.isPlainObject(function(){});// false
```
除了這幾個函數之外,還有個 Array.isArray(),這個真的不用介紹了吧。
## 總結
總結還是多說一點的好,現在已經基本理清 jQuery 內部的情況了?no,還差一點,看下面的代碼:
```
(function(window) {
// jQuery 變量,用閉包避免環境污染
var jQuery = (function() {
var jQuery = function(selector, context) {
return new jQuery.fn.init(selector, context, rootjQuery);
};
// 一些變量聲明
jQuery.fn = jQuery.prototype = {
constructor: jQuery,
init: function(selector, context, rootjQuery) {
// 下章會重點討論
}
// 原型方法
};
jQuery.fn.init.prototype = jQuery.fn;
jQuery.extend = jQuery.fn.extend = function() {};//已介紹
jQuery.extend({
// 一堆靜態屬性和方法
// 用 extend 綁定,而不是直接在 jQuery 上寫
});
return jQuery;
})();
// 工具方法 Utilities
// 回調函數列表 Callbacks Object
// 異步隊列 Defferred Object
// 瀏覽器功能測試 Support
// 數據緩存 Data
// 隊列 Queue
// 屬性操作 Attributes
// 事件系統 Events
// 選擇器 Sizzle
// DOM遍歷 Traversing
// 樣式操作 CSS(計算樣式、內聯樣式)
// 異步請求 Ajax
// 動畫 Effects
// 坐標 Offset、尺寸 Dimensions
window.jQuery = window.$ = jQuery;
})(window);
```
可以看出 jQuery 很巧妙的整體布局思路,對于屬性方法和原型方法等區分,防止變量污染等,都做的非常好。閱讀框架源碼只是開頭,有趣的還在后面。