雖然現在基本不怎么使用 jQuery
了,但 jQuery
流行 10多年
的 JS庫
,還是有必要學習它的源碼的。也可以學著打造屬于自己的 js
類庫,求職面試時可以增色不少。
本文章學習的是 v3.4.1
版本。unpkg.com
源碼地址:https://unpkg.com/jquery@3.4.1/dist/jquery.js
jQuery
github
倉庫
自執行匿名函數
(function(global, factory){
})(typeof window !== "underfined" ? window: this, function(window, noGlobal){
});
外界訪問不到里面的變量和函數,里面可以訪問到外界的變量,但里面定義了自己的變量,則不會訪問外界的變量。匿名函數將代碼包裹在里面,防止與其他代碼沖突和污染全局環境。關于自執行函數不是很了解的讀者可以參看這篇文章。[譯] JavaScript:立即執行函數表達式(IIFE)
瀏覽器環境下,最后把 $
和 jQuery
函數掛載到 window
上,所以在外界就可以訪問到 $
和 jQuery
了。
if ( !noGlobal ) {window.jQuery = window.$ = jQuery;
}
// 其中`noGlobal`參數只有在這里用到。
支持多種環境下使用 比如 commonjs、amd規范
commonjs 規范支持
commonjs
實現 主要代表 nodejs
// global是全局變量,factory 是函數
( function( global, factory ) {// 使用嚴格模式"use strict";// Commonjs 或者 CommonJS-like 環境if ( typeof module === "object" && typeof module.exports === "object" ) {// 如果存在global.document 則返回factory(global, true);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 );}
// Pass this if window is not defined yet
// 第一個參數判斷window,存在返回window,不存在返回this
} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {});
amd 規范 主要代表 requirejs
if ( typeof define === "function" && define.amd ) {define( "jquery", [], function() {return jQuery;} );
}
cmd 規范 主要代表 seajs
很遺憾, jQuery
源碼里沒有暴露對 seajs
的支持。但網上也有一些方案。這里就不具體提了。畢竟現在基本不用 seajs
了。
無 new 構造
實際上也是可以 new
的,因為 jQuery
是函數。而且和不用 new
效果是一樣的。new顯示返回對象,所以和直接調用 jQuery
函數作用效果是一樣的。如果對 new
操作符具體做了什么不明白。可以參看我之前寫的文章。
面試官問:能否模擬實現JS的new操作符
源碼:
varversion = "3.4.1",// Define a local copy of jQueryjQuery = function( selector, context ) {// 返回new之后的對象return new jQuery.fn.init( selector, context );};
jQuery.fn = jQuery.prototype = {// jQuery當前版本jquery: version,// 修正構造器為jQueryconstructor: jQuery,length: 0,
};
init = jQuery.fn.init = function( selector, context, root ) {// ...if ( !selector ) {return this;}// ...
};
init.prototype = jQuery.fn;
jQuery.fn === jQuery.prototype; // true
init = jQuery.fn.init;
init.prototype = jQuery.fn;
// 也就是
jQuery.fn.init.prototype === jQuery.fn; // true
jQuery.fn.init.prototype === jQuery.prototype; // true
關于這個筆者畫了一張 jQuery
原型關系圖,所謂一圖勝千言。
jquery-v3.4.1 原型關系圖
<sciprt src="https://unpkg.com/jquery@3.4.1/dist/jquery.js">
</script>
console.log({jQuery});
// 在谷歌瀏覽器控制臺,可以看到jQuery函數下掛載了很多靜態屬性和方法,在jQuery.fn 上也掛著很多屬性和方法。
Vue
源碼中,也跟 jQuery
類似,執行的是 Vue.prototype._init
方法。
function Vue (options) {if (!(this instanceof Vue)) {warn('Vue is a constructor and should be called with the `new` keyword');}this._init(options);
}
initMixin(Vue);
function initMixin (Vue) {Vue.prototype._init = function (options) {};
};
核心函數之一 extend
用法:
jQuery.extend( target [, object1 ] [, objectN ] ) Returns: Object
jQuery.extend( [deep ], target, object1 [, objectN ] )
jQuery.extend APIjQuery.fn.extend API
看幾個例子:(例子可以我放到在線編輯代碼的jQuery.extend例子codepen了,可以直接運行)。
// 1. jQuery.extend( target)
var result1 = $.extend({job: '前端開發工程師',
});
console.log(result1, 'result1', result1.job); // $函數 加了一個屬性 job // 前端開發工程師
// 2. jQuery.extend( target, object1)
var result2 = $.extend({name: '若川',
},
{job: '前端開發工程師',
});
console.log(result2, 'result2'); // { name: '若川', job: '前端開發工程師' }
// deep 深拷貝
// 3. jQuery.extend( [deep ], target, object1 [, objectN ] )
var result3 = $.extend(true, {name: '若川',other: {mac: 0,ubuntu: 1,windows: 1,},
}, {job: '前端開發工程師',other: {mac: 1,linux: 1,windows: 0,}
});
console.log(result3, 'result3');
// deep true
// {
// "name": "若川",
// "other": {
// "mac": 1,
// "ubuntu": 1,
// "windows": 0,
// "linux": 1
// },
// "job": "前端開發工程師"
// }
// deep false
// {
// "name": "若川",
// "other": {
// "mac": 1,
// "linux": 1,
// "windows": 0
// },
// "job": "前端開發工程師"
// }
結論:extend
函數既可以實現給 jQuery
函數可以實現淺拷貝、也可以實現深拷貝。可以給jQuery上添加靜態方法和屬性,也可以像 jQuery.fn
(也就是 jQuery.prototype
)上添加屬性和方法,這個功能歸功于 this
, jQuery.extend
調用時 this
指向是 jQuery
, jQuery.fn.extend
調用時 this
指向則是 jQuery.fn
。
淺拷貝實現
知道這些,其實實現淺拷貝還是比較容易的:
// 淺拷貝實現
jQuery.extend = function(){// options 是擴展的對象object1,object2...var options,// object對象上的鍵name,// copy object對象上的值,也就是是需要拷貝的值copy,// 擴展目標對象,可能不是對象,所以或空對象target = arguments[0] || {},// 定義i為1i = 1,// 定義實參個數lengthlength = arguments.length;// 只有一個參數時if(i === length){target = this;i--;}for(; i < length; i++){// 不是underfined 也不是nullif((options = arguments[i]) != null){for(name in options){copy = options[name];// 防止死循環,continue 跳出當前此次循環if ( name === "__proto__" || target === copy ) {continue;}if ( copy !== undefined ) {target[ name ] = copy;}}}}// 最后返回目標對象return target;
}
深拷貝則主要是在以下這段代碼做判斷。可能是數組和對象引用類型的值,做判斷。
if ( copy !== undefined ) {target[ name ] = copy;
}
為了方便讀者調試,代碼同樣放在jQuery.extend淺拷貝代碼實現codepen,可在線運行。
深拷貝實現
$.extend = function(){// options 是擴展的對象object1,object2...var options,// object對象上的鍵name,// copy object對象上的值,也就是是需要拷貝的值copy,// 深拷貝新增的四個變量 deep、src、copyIsArray、clonedeep = false,// 源目標,需要往上面賦值的src,// 需要拷貝的值的類型是函數copyIsArray,//clone,// 擴展目標對象,可能不是對象,所以或空對象target = arguments[0] || {},// 定義i為1i = 1,// 定義實參個數lengthlength = arguments.length;// 處理深拷貝情況if ( typeof target === "boolean" ) {deep = target;// Skip the boolean and the target// target目標對象開始后移target = arguments[ i ] || {};i++;}// Handle case when target is a string or something (possible in deep copy)// target不等于對象,且target不是函數的情況下,強制將其賦值為空對象。if ( typeof target !== "object" && !isFunction( target ) ) {target = {};}// 只有一個參數時if(i === length){target = this;i--;}for(; i < length; i++){// 不是underfined 也不是nullif((options = arguments[i]) != null){for(name in options){copy = options[name];// 防止死循環,continue 跳出當前此次循環if ( name === "__proto__" || target === copy ) {continue;}// Recurse if we're merging plain objects or arrays// 這里deep為true,并且需要拷貝的值有值,并且是純粹的對象// 或者需拷貝的值是數組if ( deep && copy && ( jQuery.isPlainObject( copy ) ||( copyIsArray = Array.isArray( copy ) ) ) ) {// 源目標,需要往上面賦值的src = target[ name ];// Ensure proper type for the source value// 拷貝的值,并且src不是數組,clone對象改為空數組。if ( copyIsArray && !Array.isArray( src ) ) {clone = [];// 拷貝的值不是數組,對象不是純粹的對象。} else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {// clone 賦值為空對象clone = {};} else {// 否則 clone = srcclone = src;}// 把下一次循環時,copyIsArray 需要重新賦值為falsecopyIsArray = false;// Never move original objects, clone them// 遞歸調用自己target[ name ] = jQuery.extend( deep, clone, copy );// Don't bring in undefined values}else if ( copy !== undefined ) {target[ name ] = copy;}}}}// 最后返回目標對象return target;
};
為了方便讀者調試,這段代碼同樣放在jQuery.extend深拷貝代碼實現codepen,可在線運行。
深拷貝衍生的函數 isFunction
判斷參數是否是函數。
var isFunction = function isFunction( obj ) {// Support: Chrome <=57, Firefox <=52// In some browsers, typeof returns "function" for HTML <object> elements// (i.e., `typeof document.createElement( "object" ) === "function"`).// We don't want to classify *any* DOM node as a function.return typeof obj === "function" && typeof obj.nodeType !== "number";
};
深拷貝衍生的函數 jQuery.isPlainObject
jQuery.isPlainObject(obj)
測試對象是否是純粹的對象(通過 "{}" 或者 "new Object" 創建的)。
jQuery.isPlainObject({}) // true
jQuery.isPlainObject("test") // false
var getProto = Object.getPrototypeOf;
var class2type = {};
var toString = class2type.toString;
var hasOwn = class2type.hasOwnProperty;
var fnToString = hasOwn.toString;
var ObjectFunctionString = fnToString.call( Object );
jQuery.extend( {isPlainObject: function( obj ) {var proto, Ctor;// Detect obvious negatives// Use toString instead of jQuery.type to catch host objects// !obj 為true或者 不為[object Object]// 直接返回falseif ( !obj || toString.call( obj ) !== "[object Object]" ) {return false;}proto = getProto( obj );// Objects with no prototype (e.g., `Object.create( null )`) are plain// 原型不存在 比如 Object.create(null) 直接返回 true;if ( !proto ) {return true;}// Objects with prototype are plain iff they were constructed by a global Object functionCtor = hasOwn.call( proto, "constructor" ) && proto.constructor;// 構造器是函數,并且 fnToString.call( Ctor ) === fnToString.call( Object );return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;},
});
extend
函數,也可以自己刪掉寫一寫,算是 jQuery
中一個比較核心的函數了。而且用途廣泛,可以內部使用也可以,外部使用擴展 插件等。
鏈式調用
jQuery
能夠鏈式調用是因為一些函數執行結束后 returnthis
。比如 jQuery
源碼中的 addClass
、 removeClass
、 toggleClass
。
jQuery.fn.extend({addClass: function(){// ...return this;},removeClass: function(){// ...return this;},toggleClass: function(){// ...return this;},
});
jQuery.noConflict
?很多?js
庫都會有的防沖突函數
jQuery.noConflict API
用法:
<script>var $ = '我是其他的$,jQuery不要覆蓋我';
</script>
<script src="./jquery-3.4.1.js">
</script>
<script>$.noConflict();console.log($); // 我是其他的$,jQuery不要覆蓋我
</script>
jQuery.noConflict 源碼
var// Map over jQuery in case of overwrite_jQuery = window.jQuery,// Map over the $ in case of overwrite_$ = window.$;
jQuery.noConflict = function( deep ) {// 如果已經存在$ === jQuery;// 把已存在的_$賦值給window.$;if ( window.$ === jQuery ) {window.$ = _$;}// 如果deep為 true, 并且已經存在jQuery === jQuery;// 把已存在的_jQuery賦值給window.jQuery;if ( deep && window.jQuery === jQuery ) {window.jQuery = _jQuery;}// 最后返回jQueryreturn jQuery;
};
總結
全文主要通過淺析了 jQuery
整體結構,自執行匿名函數、無 new
構造、支持多種規范(如commonjs、amd規范)、核心函數之 extend
、鏈式調用、 jQuery.noConflict
等方面。
重新梳理下文中學習的源碼結構。
// 源碼結構
( function( global, factory )"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 );}
} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {var version = "3.4.1",// Define a local copy of jQueryjQuery = function( selector, context ) {return new jQuery.fn.init( selector, context );};jQuery.fn = jQuery.prototype = {jquery: version,constructor: jQuery,length: 0,// ...};jQuery.extend = jQuery.fn.extend = function() {};jQuery.extend( {// ...isPlainObject: function( obj ) {},// ...});init = jQuery.fn.init = function( selector, context, root ) {};init.prototype = jQuery.fn;if ( typeof define === "function" && define.amd ) {define( "jquery", [], function() {return jQuery;} );}jQuery.noConflict = function( deep ) {};if ( !noGlobal ) {window.jQuery = window.$ = jQuery;}return jQuery;
});
可以學習到 jQuery
巧妙的設計和架構,為自己所用,打造屬于自己的 js
類庫。相關代碼和資源放置在github blog中,需要的讀者可以自取。
下一篇文章可能是學習 underscorejs
的源碼整體架構。
讀者發現有不妥或可改善之處,歡迎評論指出。另外覺得寫得不錯,可以點贊、評論、轉發,也是對筆者的一種支持。
關于
作者:常以若川為名混跡于江湖。前端路上 | PPT愛好者 | 所知甚少,唯善學。
個人博客 http://lxchuan12.cn
https://github.com/lxchuan12/blog,相關源碼和資源都放在這里,求個 star
^_^~
微信交流群,加我微信lxchuan12
,注明來源,拉您進前端視野交流群
下圖是公眾號二維碼:若川視野,一個可能比較有趣的前端開發類公眾號
往期文章
工作一年后,我有些感悟(寫于2017年)
高考七年后、工作三年后的感悟
點擊閱讀原文,或許閱讀體驗更佳