不造輪子的程序員不是好程序員,因此咱們今天嘗試造一下輪子。今天的主角是 jQuery
,雖然如今市面上已被 React
,Angular
,Vue
等擠的容不下它的位置,可是它的簡單 API 設計依然優秀,值得學習和體會。html
任務jquery
今天造輪子的目標不是實現功能,而是專一在 API 和架構。你須要完成的東西支持如下功能:git
一、$(selector)
根據選擇器構造一個jQuery
對象程序員
二、jQuery
對象是一個類數組,須要支持如下方法:es6
var a = $(selector); a[0] 訪問元素 a.length 元素個數 a.each(function(){ console.log(this)}) 迭代操做
三、鏈式調用github
var a = $(selector); a.addClass('hello').click(function(){...});
四、擴展實例方法數組
$.fn.tabs = function(){ console.log(this); };
以後就能夠這樣使用瀏覽器
$(selector).tabs();
好,開始咱們的任務。架構
我在 jQuery 的官網下載的開發版(沒有壓縮)代碼,版本 3.2.1
我記的上一次用的時候好像才 1.8
左右 ?。只有一個 js 文件,打開一看,個人天,一萬多行代碼。。。模塊化
代碼有點多,咱們先梳理一下結構,找個入口開始看。
( function( global, factory ) { //省略... } )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { jQuery = function( selector, context ) { return new jQuery.fn.init( selector, context ); //這裏用new,省去了構造函數 jQuery() 前面的運算符new,所以咱們能夠直接寫 jQuery() }; jQuery.fn = jQuery.prototype = { jquery: version, constructor: jQuery, ... }; // 經過覆蓋原型的方式,把 jQuery.prototype 覆蓋到 jQuery.fn.init.prototype 上 jQuery.fn.init.prototype = jQuery.fn; //... jQuery.extend = jQuery.fn.extend = function(){ ....// }; jQuery.extend( { isFunction, type, isWindow, ... }) //jQuery.extend()和jQuery.fn.extend() //用於合併多個對象的屬性到第一個對象,相似於 es6 的 Object.assign(),不過仍是有區別的 if ( !noGlobal ) { window.jQuery = window.$ = jQuery; } return jQuery; }));
jQuery 當即調用表達式簡化版
(function(window, factory) { factory(window) }(this, function() { return function() { //jQuery的調用 } }))
一上來,是個 當即調用表達式。 解決命名空間與變量污染的問題,全局變量是魔鬼, 匿名函數能夠有效的保證在頁面上寫入 JavaScript,而不會形成全局變量的污染,經過小括號,讓其加載的時候當即初始化,這樣就造成了一個單例模式的效果從而只會執行一次。
jQuery 的這個當即調用表達式的具體講解能夠參考這裏。
jQuery = function( selector, context ) { return new jQuery.fn.init( selector, context ); } //... window.jQuery = window.$ = jQuery;
jQuery 賦值給了 window.jQuery
和 window.$
因此咱們在使用 jQuery 的時候 $
和 jQuery
是等價的。
可是 jQuery()
返回了 new jQuery.fn.init()
,爲何這樣寫?一臉懵逼。。。。
悲傷先放一邊,咱們先看一下這個函數 jQuery.fn.init(selector, context)
init = jQuery.fn.init = function( selector, context, root ) { // HANDLE: $(""), $(null), $(undefined), $(false) // Handle HTML strings // HANDLE: $(html) -> $(array) // HANDLE: $(html, props) // HANDLE: $(#id) // HANDLE: $(expr, $(...)) // HANDLE: $(expr, context) // HANDLE: $(DOMElement) // HANDLE: $(function) return jQuery.makeArray( selector, this ); }; init.prototype = jQuery.fn;
這個函數就是對參數 selector
對應的 html
、id
和 class
等不一樣選擇器的處理方式,並返回一個類數組對象。
看到這咱們就能實現咱們今天任務第一個目標以及第二個目標的 1/2
了。? 上代碼!
var jQuery = function(selector) { return new jQuery.fn.init(selector); } init = jQuery.fn.init = function( selector ) { var elem = document.querySelectorAll(selector); this.length = elem.length; this[0] = elem[0]; for (i = 0; i < elem.length; i++) { this[i] = elem[i]; } this.context = document; this.selector = selector; return this; }
這裏有一個 jQuery 的特色 類數組對象結構。
所謂的類數組對象:
擁有一個 length 屬性和若干索引屬性的對象
舉個例子:
var array = ['name', 'age', 'sex']; var arrayLike = { 0: 'name', 1: 'age', 2: 'sex', length: 3 }
jQuery 能像數組同樣操做,經過對象 get 方法或者直接經過下標 0 索引就能轉成 DOM 對象。同時還擁有各類自定義方法,自定義屬性,看 jquery 對象的優雅的訪問方式便可知是如此美妙的對象。
進一步瞭解類數組對象能夠看這篇文章
而後咱們回來看看,讓咱們悲傷
的代碼。。。
jQuery = function( selector, context ) { return new jQuery.fn.init( selector, context ); }
之因此這樣寫,演變過程是這樣的:
一、出於實例化 jQuery 對象性能的考慮 jQuery 採用了原型式的結構構建對象 (jQuery.prototype) ⬇ 二、jQuery 爲了初始化對象實例更方便,採用了無 new 化,初始化對象時,能夠不寫 new 操做符 (return new jQuery...) ⬇ 三、jQuery 爲了不出現 return jQuery 無限遞歸本身,這種死循環的問題,採起的手段是把原型上的一個 init 方法做爲構造器 ⬇ 四、最後,就成了這樣了。 return new jQuery.fn.init()
這樣確實解決了循環遞歸的問題,可是又問題來了,init 是 jQuery 原型上做爲構造器的一個方法,那麼其 this 就不是 jQuery了,因此 this 就徹底引用不到 jQuery 的原型了,因此這裏經過 new 把 init 方法與 jQuery 給分離成2個獨立的構造器。
而後 jQuery 又經過下面的語句,將兩個獨立的構造器關聯起來了。
jQuery.fn = jQuery.prototype; jQuery.fn.init.prototype = jQuery.fn;
這樣整個結構就串起來了,不得不佩服做者的設計思路,別具匠心。
上面說的若是沒看懂,能夠參考這兩篇文章:
咱們要實現目標2中的 each
迭代操做,就要說一下 jQuery 的另外一個特性 靜態與實例方法共享
$(".box").each() //做爲實例方法存在 遍歷一個jQuery對象的,是爲jQuery內部服務的 $.each() //做爲靜態方法存在 能夠迭代任何集合
咱們要寫兩個方法嘛?看看 jQuery 怎麼作的?
jQuery.prototype = { each: function( callback, args ) { return jQuery.each( this, callback, args ); } }
實例方法取於靜態方法,這裏是靜態與實例方法共享設計,靜態方法掛在jQuery構造器上,原型方法通過下面的兩句代碼就掛載到 init 的原型上了,也就是對象的實例方法上了。
jQuery.fn = jQuery.prototype; jQuery.fn.init.prototype = jQuery.fn;
那麼剩下的問題就是怎麼實現靜態方法 jQuery.each
這個靜態方法是在
jQuery.extend({ each: function( obj, callback ) { var length, i = 0; if ( isArrayLike( obj ) ) { length = obj.length; for ( ; i < length; i++ ) { if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { break; } } } else { for ( i in obj ) { if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { break; } } } return obj; } })
咱們實現 each
,代碼以下:
var jQuery = function(selector) { return new jQuery.fn.init(selector); } jQuery.fn = jQuery.prototype = { constructor: jQuery, length:0, get: function( num ) { return this[ num ]; }, each: function( callback ) { return jQuery.each( this, callback ); } } init = jQuery.fn.init = function( selector ) { var elem = document.querySelectorAll(selector); this.length = elem.length; this[0] = elem[0]; for (i = 0; i < elem.length; i++) { this[i] = elem[i]; } this.context = document; this.selector = selector; return this; } init.prototype = jQuery.fn; jQuery.extend = jQuery.fn.extend = function() { var options, copy, target = arguments[0] || {}, i = 1, length = arguments.length; //只有一個參數,就是對jQuery自身的擴展處理 //extend,fn.extend if (i === length) { target = this; //調用的上下文對象jQuery/或者實例 i--; } for (; i < length; i++) { //從i開始取參數,不爲空開始遍歷 if ((options = arguments[i]) != null) { for (name in options) { copy = options[name]; //覆蓋拷貝 target[name] = copy; } } } return target; } jQuery.extend( { each: function( obj, callback ) { var length, i = 0; for ( i in obj ) { if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { break; } } return obj; } });
既然出現 extend
了,咱們就先實現第四個小目標 擴展實例方法 tabs
jQuery 中
jQuery.extend = jQuery.fn.extend = function() { }
雖然指向了同一個函數,可是它們的 this
指向是不一樣。
fn 與 jQuery 實際上是2個不一樣的對象,在以前有講解:jQuery.extend 調用的時候,this是指向 jQuery 對象的( jQuery 是函數,也是對象!),因此這裏擴展在 jQuery 上。而jQuery.fn.extend 調用的時候,this 指向 fn 對象,jQuery.fn 和 jQuery.prototype指向同一對象,擴展 fn 就是擴展 jQuery.prototype 原型對象。這裏增長的是原型方法,也就是對象方法了。因此jQuery的API中提供了以上2個擴展函數。
咱們這樣擴展實例方法便可。
jQuery.fn.extend({ tabs: function() { console.log('擴展實例方法:tabs'); } });
jQuery 抽出了全部可複用的特性,分離出單一模塊,經過組合的用法,無論在設計思路與實現手法上 jQuery 都是很是高明的。由於 jQuery 的設計中最喜歡的作的一件事,就是抽出共同的特性使之
模塊化
,固然也是更貼近S.O.L.I.D
五大原則的單一職責SRP
了,遵照單一職責的好處是可讓咱們很容易地來維護這個對象,好比,當一個對象封裝了不少職責的時候,一旦一個職責須要修改,勢必會影響該對象的其它職責代碼。經過解耦可讓每一個職責更加有彈性地變化。
經過簡單擴展原型方法並經過
return this
的形式來實現跨瀏覽器的鏈式調用。
因此咱們若是須要鏈式的處理,只須要在方法內部返回當前的這個實例對象 this 就能夠了,由於返回當前實例的 this,從而又能夠訪問本身的原型了,這樣的就節省代碼量,提升代碼的效率,代碼看起來更優雅。
詳細解說請點擊 方法鏈式調用的實現
最終的代碼演示
文章的不少內容參考的慕課網的 jQuery源碼解析 系列,感興趣的小夥伴,能夠看一下整個系列。