美文网首页JavaScript码无界前端%……%
前端基础进阶(十一):详细图解jQuery对象,以及如何扩展jQ

前端基础进阶(十一):详细图解jQuery对象,以及如何扩展jQ

作者: 这波能反杀 | 来源:发表于2017-03-19 12:11 被阅读7362次
配图与本文无关

早几年学习前端,大家都非常热衷于研究jQuery源码。我还记得当初从jQuery源码中学到一星半点应用技巧的时候常会有一种发自内心的惊叹,“原来JavaScript居然可以这样用!”

虽然随着前端的发展,另外几种前端框架的崛起,jQuery慢慢变得不再是必须。因此大家对于jQuery的热情低了很多。但是许多从jQuery中学到的技巧用在实际开发中仍然非常好用。简单的了解它也有助于我们更加深入的理解JavaScript。

这篇文章的主要目的就是跟大家分享一下,jquery对象是如何封装的。算是对于大家进一步学习jQuery源码的一个抛砖引玉。

使用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自身对于原型的处理使用了一些巧妙的语法,比如jQuery.fn = jQuery.prototypejQuery.fn.init.prototype = jQuery.fn;等,这几句正式jQuery对象的关键所在,下面我用图给大家展示一下这中间的逻辑是怎么回事。

jQuery对象核心图

** 对象封装分析 **

在上面的实现中,代码首先在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);

相关文章

网友评论

  • 181ebcbf1475:谢谢楼主的更新,整个课程都在学习,学会不少知识点呢.
  • 7e3c682da2f0:我看你写的例子是用纯javascript封装,然后再扩展到jquery里面的。那我可以在封装对象的收直接使用jquery里面的方法吗,比如var width=$(this).width();这样用的时候也可以用的。
  • 1db626686eef:收货很多,我是一个后端转前段的
    这波能反杀:@骷髅头_cbcb 那就多给我的文章点几个赞呀:smiley:
  • d1d263397f01:其实作者大神封装的拖拽事件有一点点美中不足,就是拖拽时若两元素重合,在重合元素部分上抬起,那么该元素可能丢失拖拽事件
  • zyg:有个问题 在上一篇文章中说 “ 构造函数中: 属性与方法为当前实例单独拥有,只能被当前实例访问,并且每声明一个实例,其中的方法都会被重新创建一次。”
    但是在本文中又说“而放在构造函数中的方法,因为我们在使用时,不需要声明一个实例对象就可以直接使用,因此这样的方法常常被叫做工具方法,或者所谓的静态方法。工具方法在使用时因为不用创建新的实例,因此相对而言效率会高很多,但是并不节省内存。 ”

    虽然能理解你说的 感觉表述有问题

    合理的处理属性与方法的位置:应该有四种
    1、构造函数里面当做实例属性与方法
    2、构造函数上 与实例无关 当做静态属性与方法
    3、在原型上 对外暴露实例共享的属性与方法
    4、在模块作用域(即闭包中) 私有的实例共享的属性与方法

    不知道对不对?
    zyg:@波同学 好的 谢谢
    这波能反杀:@zyg 嗯,对的
  • 029a45d4d61e:jQuery.f只是单纯jQuery.prototype的简写,如果调用了jQuery和 $ 方法,那么内部返回了new 出来的jQuery原型下的init方法,因为是new出来的,所以this指向了自身,而且init的prototype让jQuery的prototype给赋值,那么返回的this东西里面就有一个__proto__属性,所以return实例的__proto__能够得到init原型对象的所有方法就是jQuery的prototype下的所以方法。我理解的对吗,波同学?
  • 99f5c153836e:波波,jQuery是闭包么。真心求解😓
  • 全凭一口仙气儿活着:希望以后也能分享下关于网络请求,数据交互方面的😁
  • 89baf1829ac2:波老师,今天上午到现在一有空就在看你的文章,解决了我很多地方的遗漏和疑问,由衷感谢!希望波老师坚持创作,技术源于分享!点赞、赞赏通通都给了,加油哦!:fist: :stuck_out_tongue:
  • 好想要挥霍_85d7:一直在看,对js的一些原理性知识有了很大的认识,非常感谢~如果能出点css相关的文章,如BFC,IFC什么的就更好啦。
    好想要挥霍_85d7:@波同学 嗯嗯~会一直关注的
    这波能反杀:@好想要挥霍_85d7 我之后想写一些react的,然后就写css基础
  • 浩风起兮:老哥 啊 最后那里扩展jq为什么是new Drag(this[0])啊,我换成this.selector好使了,这是我弹的 this :[div.block, prevObject: init[1], context: document, selector: ".block"]
    浩风起兮:@波同学 说错了,哪里蹦出来的elem,他是被赋值的,参数是selector,反正周末了,看看我迷在哪了,拜托拜托:stuck_out_tongue_winking_eye::sob:,我改成this.selector确实好用了
    浩风起兮:elem呀,但是函数里面选择到的是整个标签啊,那个判断的选择是selector或document.getbyid,我弹出this,看到this[0]是一个标签,那样子报错了,难道我复制错了?
    这波能反杀:@浩风起兮 你看看drag需要的参数是什么就明白了,有一个简单的判断
  • 浩风起兮:看见图先进来评论下,大红袍好喝吗?码农对着电脑喝茶是不是还能减少点坏处,辐射什么的:joy:
  • qxm_idance:看你这个系列了 学到了很多 ,总结的我这个渣渣都看懂了,很是感谢
  • 3ad024301c8a:👍👍👍太棒了!我也是干前端以后要多像您请教和学习!
  • 29f26f9f36ce:作者的后续文章顺序正式我的学习计划,感动的不行
  • 29f26f9f36ce:看完10,这两天就在学习jQuery源码,正好看到这个,激动
  • 128f587d6ed6:学到了很多!谢谢波哥!
  • 小帅帅kkk:作者棒棒的
  • 全栈路人甲:写得很好,这俩篇更是涨知识了
  • 64d816ca3e5c:写的真好。🐾
  • 春雨棲姬::blush: 这系列一直在追..很有帮助!.非常感谢.
    这里有一个问题想请教一下...实例代码中 jQuery.fn 的意义是什么.?
    自己改写了一下实例代码.把jQuery.fn 替换为 jQuery.prototype 然后令
    jQuery.prototype.init.prototype = jQuery.prototype
    扩展功能也能够做到..
    最后new出来的对象与之前的对象也是相同的..
    所以jQuery.fn的意义是类似$符号,是简化了单词的写法这样嘛?
    春雨棲姬:@波同学 ...噗嗯.:joy:
    这波能反杀:@時津風 可以这样说吧:joy:
  • chenpipy:前两天学的闭包竟然无意中用上了,特来感谢,你的文章每篇都读了好几遍,能理解各百分之80左右,就差实践了:smile: ,现在自学vue中。。
  • ivank:从开始看到这边,所以,作者能自我介绍来一下吗:smirk:
  • 97900e44363d:波大,会出个webpack的文章。感觉webpack通俗易懂的文章挺少。
    128f587d6ed6:@波同学 希望也能讲些nodejs的知识~
    这波能反杀:暂时没有计划,webpack的基础是nodejs中的一些知识,你把nodejs的fs,path等模块什么的搞明白,学起来应该会轻松很多
  • assassian_zj:说真的看你文章对js的了解又深了几分,但我想问下作者一个问题。就是当前前端的任职要求大多数都是要求会前端工程化,即webpack打包,vue/ng/rn3大mvvm框架中熟悉掌握一种。这样的要求对于一个刚进入一年或者半年的前端来说,我有时候感觉到很无力,因为我觉得自己的js基础并不算得上很好,而为了找到更好的工作就不得不去学习这些市面上主流的东西。就拿vue来说,它入门的确友好,但当我学到组件那块的时候又只是停留在了照着官网的例子敲一遍的水平上并没有很太幅度的提高,如果没有真正用vue做一个完整的项目的话。这是我的一些困扰,还请作者指点下
    这波能反杀:把你会的东西学好就行了,工程化不知道自己如何构建是没什么问题的,只要简单的明白一些配置项就行。找工作你要明白自己的现阶段的定位,进去之后又不是要你构建项目,就算给你构建你肯定也搞不好,所以暂时不要去花心思担心一些乱七八糟的事情
  • 04d5a7e549e0:更加期待后续文章啊,毕竟ES6已经很成熟了,希望能加深ES6和ES5的对比,ES6主要解决了ES5那些方面的问题。Promise这个东西希望深讲
  • plus_b17e:点赞
  • chenpipy:更新了,占个位置😀
  • 054622b78336:终于更新了,看了后续文章预告,好期待:heart_eyes:

本文标题:前端基础进阶(十一):详细图解jQuery对象,以及如何扩展jQ

本文链接:https://www.haomeiwen.com/subject/zzmrnttx.html