前端基础进阶(十三):详细图解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.prototypejQuery.fn.init.prototype = jQuery.fn;等,这几句正是jQuery对象的关键所在。看图分析。

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阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容