前端基礎進階(十三):詳細圖解jQuery對象,以及如何擴展jQuery插件

UI 鑒賞

早幾年學習前端,大家都非常熱衷于研究jQuery源碼。

我至今還記得當初從jQuery源碼中學到一星半點應用技巧的時候常會有一種發自內心的驚嘆,“原來JavaScript居然可以這樣用!”

但是隨著前端的迅猛發展,另外幾種前端框架的崛起,jQuery慢慢變得不再是必須。所有人對jQuery的熱情都降低了許多。

jQuery在前端史上有它非常超然的歷史地位,許多從中學到的技巧在實踐開發中仍然非常好用。簡單的了解它有助于我們更加深入的理解JavaScript。如果你能夠從中看明白jquery是如何一步步被取代的,那么,我想你的收益遠不止學會使用了一個庫那么簡單。

因此,我的態度是,項目中你可以不用,但是我仍然建議你學。

這篇文章的主要目的,是從面向對象的角度,跟大家分享jquery對象是如何封裝的。算是對大家進一步學習jQuery源碼的一個拋磚引玉。

1

使用jQuery對象時,我們這樣寫:

// 聲明一個jQuery對象
$('.target')

// 獲取元素的css屬性
$('.target').css('width')

// 獲取元素的位置信息
$('.target').offset()

在使用之初可能會有許多疑問,比如$是怎么回事?為什么不用new就可以直接聲明一個對象?等等。了解之后,才知道原來這正是jQuery對象創建的巧妙之處。

先直接用代碼展示出來,再用圖跟大家解釋是怎么回事。

;
(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.fn = jQuery.prototype,jQuery.fn.init.prototype = jQuery.fn;等,這幾句正是jQuery對象的關鍵所在??磮D分析。

jQuery對象核心圖

2

對象封裝分析

在上面的實現中,首先在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內部實現不太了解的盆友,常常會毫無節制使用$(),比如對于同一個元素的不同操作:

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');
}

3

擴展方法分析

在上面的代碼實現中,我還簡單實現了兩個擴展方法。

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等方法是如何實現這樣的程度,那么源碼學習起來就會輕松很多,節省更多的時間。也給一些對于源碼敬而遠之的朋友提供一個學習的可能。

4

有一個朋友留言給我,說她被靜態方法,工具方法和實例方法這幾個概念困擾了很久,到底他們有什么區別?

其實在上一篇文章中,關于封裝一個對象,我跟大家分享了一個非常非常干,但是卻只有少數幾個讀者大佬get到的知識,那就是在封裝對象時,屬性和方法可以具體放置的三個位置,并且對于這三個位置的不同做了一個詳細的解讀。

image.png

在實現jQuery擴展方法時,一部分方法需要擴展到構造函數中,一部分方法需要擴展到原型中,當我們通讀jQuery源碼時,還發現有一些方法放在了模塊作用域中,至于為什么會有這樣的區別,建議大家回過頭去讀讀前一篇文章。

這里用一個例子簡單區分一下。

// 模塊內部
const a = 20;

function Person(name, age) {
  this.name = name;
  this.age = age;
  // 構造函數方法,每聲明一個實例,都會重新創建一次,屬于實例獨有
  this.getName = function() {
    return this.name;
  }
}

// 原型方法,僅在原型創建時聲明一次,屬于所有實例共享
Person.prototype.getAge = function() {
  return this.age;
}

// 工具方法,直接掛載在構造函數名上,僅聲明一次,無法直接訪問實例內部屬性與方法
Person.each = function() {}

如上例中,each就是一個工具方法,或者說靜態方法。

工具方法的特性也和工具一詞非常貼近,他們與實例的自身屬性毫無關聯,僅僅只是實現一些通用的功能,我們可以通過$.each$('div').each這2個方法來體會工具方法與實例方法的不同之處。

在實際開發中,我們運用得非常多的一個工具庫就是lodash.js,大家如果時間充裕一定要去學習一下他的使用。

$.ajax()
$.isFunction()
$.each()
... ...

放在原型中的方法,在使用時必須創建了一個新的實例對象才能訪問,因此這樣的方法叫做實例方法。也正是因為這一點,他的使用成本會比工具方法高一些。但是相比構造函數方法,原型方法更節省內存。

$('#test').css()
$('#test').attr()
$('div').each()

這樣,那位同學的疑問就被搞定啦。我們在學習的時候,一定不要過分去糾結一些概念,而要明白這些概念背后的具體場景是怎么回事兒,那么學習這件事情就不會在一些奇奇怪怪的地方卡住了。

所以通過$.extend擴展的方法,其實就是對工具方法的擴展,而通過$.fn.extend擴展的方法,就是對于實例方法的擴展。

5

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);

下一篇:前端基礎進階(十四):深入核心,詳解事件循環機制
上一篇:前端基礎進階(十二):面向對象實戰之封裝拖拽對象
前端基礎進階目錄

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容