早幾年學習前端,你們都很是熱衷於研究jQuery源碼。我還記得當初從jQuery源碼中學到一星半點應用技巧的時候常會有一種發自心裏的驚歎,「原來JavaScript竟然能夠這樣用!」javascript
雖然隨着前端的發展,另外幾種前端框架的崛起,jQuery慢慢變得再也不是必須。所以你們對於jQuery的熱情低了不少。可是許多從jQuery中學到的技巧用在實際開發中仍然很是好用。簡單的瞭解它也有助於咱們更加深刻的理解JavaScript。css
這篇文章的主要目的就是跟你們分享一下,jquery對象是如何封裝的。算是對於你們進一步學習jQuery源碼的一個拋磚引玉。前端
使用jQuery對象時,咱們這樣寫:vue
1 // 聲明一個jQuery對象 2 $('.target') 3 4 // 獲取元素的css屬性 5 $('.target').css('width') 6 7 // 獲取元素的位置信息 8 $('.target').offset()
在使用之初可能會有許多疑問,好比$是怎麼回事?爲何不用new就能夠直接聲明一個對象等等。後來瞭解以後,才知道原來這正是jQuery對象建立的巧妙之處。java
先直接用代碼展現出來,再用圖跟你們解釋是怎麼回事。react
(function(ROOT) { // 構造函數 var jQuery = function(selector) { // 在jQuery中直接返回new過的實例,這裏的init是jQuery的真正構造函數 return new jQuery.fn.init(selector) } jQuery.fn = jQuery.prototype = { constructor: jQuery, version: '1.0.0', init: function(selector) { // 在jquery中這裏有一個複雜的判斷,可是這裏我作了簡化 var elem, selector; elem = document.querySelector(selector); this[0] = elem; // 在jquery中返回一個由全部原型屬性方法組成的數組,咱們這裏簡化,直接返回this便可 // return jQuery.makeArray(selector, this); return this; }, // 在原型上添加一堆方法 toArray: function() {}, get: function() {}, each: function() {}, ready: function() {}, first: function() {}, slice: function() {} // ... ... } jQuery.fn.init.prototype = jQuery.fn; // 實現jQuery的兩種擴展方式 jQuery.extend = jQuery.fn.extend = function(options) { // 在jquery源碼中會根據參數不一樣進行不少判斷,咱們這裏就直接走一種方式,因此就不用判斷了 var target = this; var copy; for(name in options) { copy = options[name]; target[name] = copy; } return target; } // jQuery中利用上面實現的擴展機制,添加了許多方法,其中 // 直接添加在構造函數上,被稱爲工具方法 jQuery.extend({ isFunction: function() {}, type: function() {}, parseHTML: function() {}, parseJSON: function() {}, ajax: function() {} // ... }) // 添加到原型上 jQuery.fn.extend({ queue: function() {}, promise: function() {}, attr: function() {}, prop: function() {}, addClass: function() {}, removeClass: function() {}, val: function() {}, css: function() {} // ... }) // $符號的由來,實際上它就是jQuery,一個簡化的寫法,在這裏咱們還能夠替換成其餘可用字符 ROOT.jQuery = ROOT.$ = jQuery; })(window);
在上面的代碼中,我封裝了一個簡化版的jQuery對象。它向你們簡單展現了jQuery的總體框架狀況。若是瞭解了總體框架,那麼你們去讀jQuery源碼,就會變得很是輕鬆。jquery
咱們在代碼中能夠看到,jQuery自身對於原型的處理使用了一些巧妙的語法,好比jQuery.fn = jQuery.prototype
,jQuery.fn.init.prototype = jQuery.fn;
等,這幾句正式jQuery對象的關鍵所在,下面我用圖給你們展現一下這中間的邏輯是怎麼回事。ajax
對象封裝分析segmentfault
在上面的實現中,代碼首先在jQuery構造函數中聲明瞭一個fn屬性,並將其指向了原型jQuery.prototype
。並在原型中添加了init方法。設計模式
jQuery.fn = jQuery.prototype = { init: {} }
以後又將init的原型,指向了jQuery.prototype。
jQuery.fn.init.prototype = jQuery.fn;
而在構造函數jQuery中,返回了init的實例對象。
var jQuery = function(selector) { // 在jQuery中直接返回new過的實例,這裏的init是jQuery的真正構造函數 return new jQuery.fn.init(selector) }
最後對外暴露入口時,將字符$
與jQuery
對等起來。
ROOT.jQuery = ROOT.$ = jQuery;
所以當咱們直接使用$('#test')
建立一個對象時,其實是建立了一個init的實例,這裏的正真構造函數是原型中的init方法。
注意:許多對jQuery內部實現不太瞭解的盆友,在使用jQuery時經常會毫無節制使用$()
,好比對於同一個元素的不一樣操做:
var width = parseInt($('#test').css('width')); if(width > 20) { $('#test').css('backgroundColor', 'red'); }
經過咱們上面的一系列分析,咱們知道每當咱們執行$()
時,就會從新生成一個init的實例對象,所以當咱們這樣沒有節制的使用jQuery時是很是不正確的,雖然看上去方便了一些,可是對於內存的消耗是很是大的。正確的作法是既然是同一個對象,那麼就用一個變量保存起來後續使用便可。
var $test = $('#test'); var width = parseInt($test.css('width')); if(width > 20) { $test.css('backgroundColor', 'red'); }
擴展方法分析
在上面的代碼實現中,我還簡單實現了兩個擴展方法。
jQuery.extend = jQuery.fn.extend = function(options) { // 在jquery源碼中會根據參數不一樣進行不少判斷,咱們這裏就直接走一種方式,因此就不用判斷了 var target = this; var copy; for(name in options) { copy = options[name]; target[name] = copy; } return target; }
要理解它的實現,咱們首先要明確的知道this的指向。若是你搞不清楚,能夠回頭去看看咱們以前關於this指向的講解。傳入的參數options對象爲一個key: value
模式的對象,我經過for in
遍歷options,將key做爲jQuery的新屬性,value做爲該新屬性所對應的新方法,分別添加到jQuery方法和jQuery.fn中。
也就是說,當咱們經過jQuery.extend
擴展jQuery時,方法被添加到了jQuery構造函數中,而當咱們經過jQuery.fn.extend
擴展jQuery時,方法被添加到了jQuery原型中。
上面的例子中,我也簡單展現了在jQuery內部,許多方法的實現都是經過這兩個擴展方法來完成的。
當咱們經過上面的知識瞭解了jQuery的大致框架以後,那麼咱們對於jQuery的學習就能夠具體到諸如css/val/attr等方法是如何實現這樣的程度,那麼源碼學習起來就會輕鬆不少,也會節約更多的時間。也給一些對於源碼敬而遠之的朋友提供了一個學習的可能。
有一個朋友留言給我,說她被靜態方法,工具方法和實例方法這幾個概念困擾了好久,到底他們有什麼區別?
其實在上一篇文章中,關於封裝一個對象,我跟你們分享了一個很是很是乾貨,可是卻只有少數幾個讀者老爺get到的知識,那就是在封裝對象時,屬性和方法能夠具體放置的三個位置,而且對於這三個位置的不一樣作了一個詳細的解讀。
而在實現jQuery擴展方法的想法中,一部分方法須要擴展到jQuery構造函數中,一部分方法須要擴展到原型中,當咱們通讀jQuery源碼,還發現有一些方法放在了模塊做用域中,至於爲何會有這樣的區別,建議你們回過頭去讀讀前一篇文章。
而放在構造函數中的方法,由於咱們在使用時,不須要聲明一個實例對象就能夠直接使用,所以這樣的方法經常被叫作工具方法,或者所謂的靜態方法。工具方法在使用時由於不用建立新的實例,所以相對而言效率會高不少,可是並不節省內存。
而工具方法的特性也和工具一詞很是貼近,他們與實例的自身屬性毫無關聯,僅僅只是實現一些通用的功能,咱們能夠經過$.each
與$('div').each
這2個方法來體會工具方法與實例方法的不一樣之處。
在實際開發中,咱們運用得很是多的一個工具庫就是lodash.js
,你們若是時間充裕必定要去學習一下他的使用。
$.ajax() $.isFunction() $.each() ... ...
而放在原型中的方法,在使用時必須建立了一個新的實例對象才能訪問,所以這樣的方法叫作實例方法。也正是因爲必須建立了一個實例以後才能訪問,因此他的使用成本會比工具方法高不少。可是節省了內存。
$('#test').css() $('#test').attr() $('div').each()
這樣,那位同窗的疑問就很簡單的被搞定了。咱們在學習的時候,必定不要過度去糾結一些概念,而要明白具體怎麼回事兒,那麼學習這件事情就不會在一些奇奇怪怪的地方卡住了。
因此經過$.extend
擴展的方法,其實就是對工具方法的擴展,而經過$.fn.extend
擴展的方法,就是對於實例方法的擴展。那麼咱們在使用的時候就知道如何準確的去使用本身擴展的方法了。
jQuery插件的實現
我在初級階段的時候,以爲要本身編寫一個jQuery插件是一件高大上的事情,望塵莫及。可是經過對於上面的理解,我就知道擴展jQuery插件實際上是一件咱們本身也能夠完成的事情。
在前面我跟你們分享了jQuery如何實現,以及他們的方法如何擴展,而且前一篇文章分享了拖拽對象的具體實現。因此建議你們暫時不要閱讀下去,本身動手嘗試將拖拽擴展成爲jQuery的一個實例方法,那麼這就是一個jQuery插件了。
具體也沒有什麼可多說的了,你們看了代碼就能夠明白一切。
// Drag對象簡化代碼,完整源碼可在上一篇文章中查看 (function() { // 構造 function Drag(selector) {} // 原型 Drag.prototype = { constructor: Drag, init: function() { // 初始時須要作些什麼事情 this.setDrag(); }, // 稍做改造,僅用於獲取當前元素的屬性,相似於getName getStyle: function(property) {}, // 用來獲取當前元素的位置信息,注意與以前的不一樣之處 getPosition: function() {}, // 用來設置當前元素的位置 setPostion: function(pos) {}, // 該方法用來綁定事件 setDrag: function() {} } // 一種對外暴露的方式 window.Drag = Drag; })(); // 經過擴展方法將拖拽擴展爲jQuery的一個實例方法 (function ($) { $.fn.extend({ becomeDrag: function () { new Drag(this[0]); return this; // 注意:爲了保證jQuery全部的方法都可以鏈式訪問,每個方法的最後都須要返回this,即返回jQuery實例 } }) })(jQuery);
後續文章內容一個大概預想
去年年底的時候就有了想要將JavaScript基礎知識總結一下的這樣一個想法,但是JavaScript基礎知識確實並不是所有是層層遞進的關係,有不少碎片化的東西,因此以前一直沒有找到一個合適的整理方法。
直到在segmentfault中我在給題主建議如何快速學習一門諸如react/vue這樣的流行框架時,找到了一個好一點的思路,因而就有了這樣一系列文章,雖然它並不全面,不少知識沒有涉及到,可是其實我是圍繞最終經過模塊化來構建本身代碼這樣一個思路來總結的,所以這系列文章可以解決你們最核心的問題。
也正由於如此,這系列的文章的終點將會是在ES6環境下掌握react的使用。雖然前面我多多少少都涉及到了模塊的一些概念,可是還差一個實踐。所以最終我會以ES6的模塊跟你們分享如何使用。
那麼後續的文章應該會涉及的內容,就大概包括:
這系列文章,算是對於你們學習方向的一個具體的,切實可行的一個指引,而非簡單的經過雞湯的方式告訴你應該如何學習。因此,想要學習這些知識的朋友,趕忙來簡書關注我吧!!!!