资讯专栏INFORMATION COLUMN

初步学习 jQuery 核心 API

张巨伟 / 1174人阅读

摘要:进一步了解类数组对象可以看这篇文章对象的构建和分离构造器然后我们回来看看,让我们悲伤的代码。。。然后又通过下面的语句,将两个独立的构造器关联起来了。

背景

不造轮子的程序员不是好程序员,所以我们今天尝试造一下轮子。今天的主角是 jQuery ,虽然现在市面上已被 ReactAngularVue 等挤的容不下它的位置,但是它的简单 API 设计依然优秀,值得学习和体会。

任务

今天造轮子的目标不是实现功能,而是专注在 API 和架构。你需要完成的东西支持以下功能:

1、$(selector) 根据选择器构造一个jQuery 对象

2、jQuery 对象是一个类数组,需要支持以下方法:

var a = $(selector);
a[0]                                   访问元素
a.length                               元素个数
a.each(function(){ console.log(this)}) 迭代操作

3、链式调用

var a = $(selector);
a.addClass("hello").click(function(){...});

4、扩展实例方法

$.fn.tabs = function(){
  console.log(this);
};

之后就可以这样使用

$(selector).tabs();

好,开始我们的任务。

我在 jQuery 的官网下载的开发版(没有压缩)代码,版本 3.2.1我记的上一次用的时候好像才 1.8左右 ?。只有一个 js 文件,打开一看,我的天,一万多行代码。。。

代码有点多,我们先梳理一下结构,找个入口开始看。

jQuery 的整体架构
( 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.jQuerywindow.$ 所以我们在使用 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 对应的 htmlidclass 等不同选择器的处理方式,并返回一个类数组对象。

看到这我们就能实现我们今天任务第一个目标以及第二个目标的 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 );
}

之所以这样写,演变过程是这样的:

1、出于实例化 jQuery 对象性能的考虑 jQuery 采用了原型式的结构构建对象 
(jQuery.prototype)

                                  ⬇

2、jQuery 为了初始化对象实例更方便,采用了无 new 化,初始化对象时,可以不写 new 操作符
(return new jQuery...)

                                  ⬇

3、jQuery 为了避免出现 return jQuery 无限递归自己,这种死循环的问题,采取的手段是把原型上的一个 init 方法作为构造器

                                  ⬇

4、最后,就成了这样了。
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;

这样整个结构就串起来了,不得不佩服作者的设计思路,别具匠心。

上面说的如果没看懂,可以参考这两篇文章:

jQuery 源码解析 - 对象的构建

jQuery 源码解析 - 分离构造器

静态与实例方法共享设计

我们要实现目标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源码解析 系列,感兴趣的小伙伴,可以看一下整个系列。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/85035.html

相关文章

  • 学Java编程需要注意的地方

    摘要:学编程真的不是一件容易的事不管你多喜欢或是多会编程,在学习和解决问题上总会碰到障碍。熟练掌握核心内容,特别是和多线程初步具备面向对象设计和编程的能力掌握基本的优化策略。   学Java编程真的不是一件容易的事,不管你多喜欢或是多会Java编程,在学习和解决问题上总会碰到障碍。工作的时间越久就越能明白这个道理。不过这倒是一个让人进步的机会,因为你要一直不断的学习才能很好的解决你面前的难题...

    leanxi 评论0 收藏0
  • JavaScript入门的5条建议

    摘要:你是否已经初步掌握了和,但完全不知道从何入手如果是,那么这篇文章一定会对你有所帮助,这里总结了条建议,帮助初学者总结学习方法,提高学习效率。这样的结果就是,个小时最多只利用了个小时。 你是否已经初步掌握了html和css,但完全不知道从何入手JavaScript?如果是,那么这篇文章一定会对你有所帮助,这里总结了5条建议,帮助JavaScript初学者总结学习方法,提高学习效率。 一、...

    李昌杰 评论0 收藏0
  • jQuery笔记总结篇

    摘要:希望在做所有事情之前,操作文档。不受层级限制子选择器在给定的父元素下匹配所有子元素。相邻选择器匹配所有紧接在元素后的元素。判断当前对象中的某个元素是否包含指定类名,包含返回,不包含返回下标过滤器精确选出指定下标元素获取第个元素。 原文链接 http://blog.poetries.top/2016... 首先,来了解一下jQuery学习的整体思路 showImg(https://seg...

    NoraXie 评论0 收藏0
  • jQuery(一)-- 初步了解

    摘要:一初步了解介绍由创建于年一月的开源项目,凭借着跨平台的兼容性,简洁的语法,极大的简化了人员遍历文档,操作,处理事件,执行动画,和开发的操作。只建立一个名为的对象。对发生在同一个对象上的一组动作,可以直接连写无需重复获取对象。 jQuery(一)-- 初步了解 jQuery介绍 由John Resig创建于2006年一月的开源项目,jQuery凭借着跨平台的兼容性,简洁的语法,极大的简...

    quietin 评论0 收藏0
  • 简聊初步尝试服务端渲染的一些感想

    摘要:多多少少有些不开心的事觉得精力没有被投入在重点上创业公司遇到问题变成盲人摸象也许正常吧不过最近这段时间因为服务端的策略调整我开始做一些服务端渲染主要的站点是简聊的登录页面整体从切换到了以及做了一些整体项目结构统一的工作或者说一些思考我估计这 多多少少有些不开心的事, 觉得精力没有被投入在重点上创业公司遇到问题变成盲人摸象也许正常吧不过最近这段时间因为服务端的策略调整, 我开始做一些服务...

    Clect 评论0 收藏0

发表评论

0条评论

张巨伟

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<