w3ctech

jQuery源码剖析(一)——概览&工具方法

前言

其实这剖析jQuery源码系列文章是我自己酝酿很久的想法,当然因为之前的不成熟以及理解不深刻一直不敢提笔写。近期阅读的书籍多,激发我写文章的欲望,于是把这个计划开始实施。

为什么要选择jQuery源码,我想对于一个前端开发人员来说,不言自明。jQuery的出现对于众多前端开发人员来说,是一个无比强大的武器。那为什么jQuery如此流行,深得大家喜欢?我认为秘籍就在于jQuery的API设计。

在jQuery的设计中,最主要的两块就是(个人看法):

  1. 提供工具方法;
  2. DOM元素的操作,包括选取,属性取值等等。 对于一个库来说,第一点是必然要有所体现的,没有基础的工具方法,如何成为库。

对于Javascript来说,最重要的一块就是DOM操作,jQuery在DOM操作这块的贡献是巨大的,以至于现在很多新手完全不会用原生的JS API(例如document.getElementById等)了,jQuery把复杂的DOM选取映射到了简单的CSS选择器,对复杂的DOM操作(不同浏览器的DOM操作接口不一致)封装非常简单的委托API,以达到其核心的目的:The Write Less, Do More

我选择的jQuery版本是1.9.0,一开始肯定是先把jQuery最简单的部分做剖析。

1.外层沙箱以及命名空间$

几乎稍微有点经验前端人员都这么做,为了避免声明了一些全局变量而污染,把代码放在一个“沙箱执行”,然后在暴露出命名空间(可以为API,函数,对象):


(function( window, undefined ) {
     //用一个函数域包起来,就是所谓的沙箱
     //在这里边var定义的变量,属于这个函数域内的局部变量,避免污染全局
     //把当前沙箱需要的外部变量通过函数参数引入进来
     //只要保证参数对内提供的接口的一致性,你还可以随意替换传进来的这个参数
    "use strict";
    window.jQuery = window.$ = jQuery;
})( window );

有人会疑问,为什么要第二个参数undefined 。在这里,jquery中有一个针对压缩的小小策略。

先看以下代码:


(function( window, undefined ) {
  var a = undefined;
  if (a == undefined){blabla...}

  ....
  if (c == undefined) return;
})( window );

经过压缩后,可以变成:


(function(w, u) {
  var a = u;
  if (a == u){blabla...}

  ....
  if (c == u) return;
})(w);

因为这个外层函数只传了一个参数,因此沙箱执行时,u自然会undefined,把9个字母缩成1个字母,可以看出压缩后的代码减少一些字节数。 评论中nodejser对undefined的补充中也给出了另一种解答:

在ECMAScript5之前undefined都是可写的,也就是undefined可以赋值的。jQuery作者这么做的目的还有防止2B程序员对undefined进行赋值后使得代码出现了不可预料的bug。

沙箱中第一句"use strict";是表示使用javascript的严格模式,对于低级的浏览器,这里相当一字符串,所以兼容性是没问题的,详细的话,在阮一峰的文章Javascript 严格模式详解有介绍。

最后jQuery暴露一个全局的命名空间jQuery(为了书写更简单,一个简写就是$,幸好Javascript用$来做变量命名是合法的)

实际上jQuery是一个函数,为什么要这样设计呢,是因为:

  1. 函数也是对象,于是在jQuery这个命名空间上可以绑定工具方法
  2. 函数可以有原型prototype,每当通过dom = $("#id")取得的所谓jQuery对象,本质就是dom = new jQuery('#id'); 如果懂得原型的话,就知道如果在jQuery的原型上绑定方法,像上边那样生成的实例dom可以调用这些方法。

简单来说,就是把jQuery看成是一个类,在原型上绑定方法就相当于成员方法,在jQuery上绑定工具方法,相当于类的静态方法,举例如下:


jQuery.A = function(){};
jQuery.prototype.B = function(){};

相当于:


Class jQuery{
    public static A(){}
    public B(){}
}

面向对象的思想在jQuery是有所体现的,也给我很多思考,面向对象的思想指导了如何设计一个更合理的API,乃至于库。所谓的封装,继承,通通的都是为了前边那个目的,如何设计出更好的API,我认为这才是面向对象的精髓。

2、jQuery核心的工具方法

以下方法是在jQuery的core定义的工具方法(可以去github的jQuery项目),core是整个jQuery最核心的组成部分,所以从这部分先剖析: $.trim() 去除字符串两端的空格。(内部调用7次) $.each() 遍历数组或对象,这个方法在jQuery内部中被使用很多次,有几个不错的用法,之后剖析再举例吧。(内部调用59次) $.inArray() 返回一个值在数组中的索引位置。如果该值不在数组中,则返回-1。(内部调用9次) $.grep() 返回数组中符合某种标准的元素。(内部调用6次) $.merge() 合并两个数组。(内部调用11次) $.map() 将一个数组中的元素转换到另一个数组中。(内部调用12次) $.makeArray() 将对象转化为数组。(内部调用6次) $.globalEval() 在全局作用域下执行一段js脚本。(内部调用2次) $.proxy() 接受一个函数,然后返回一个新函数,并且这个新函数始终保持了特定的上下文(context)语境。(内部调用0次) $.nodeName() 返回DOM节点的节点名字,或者判断DOM节点名是否为某某名字。(内部调用51次) $.extend() 将多个对象,合并到第一个对象。(内部调用42次)

以下均是对类型的判断,本文只是针对$.type做一下讨论,isXXX的方法基本都是调用$.type来实现,不对它们做细节探讨。 $.type() 判断对象的类别(函数对象、日期对象、数组对象、正则对象等等)。这个方法的实现就是用$.each辅助的。(内部调用65次) $.isArray() 判断某个参数是否为数组。(内部调用12次) $.isEmptyObject() 判断某个对象是否为空(不含有任何属性)。(内部调用4次) $.isFunction() 判断某个参数是否为函数。(内部调用32次) $.isPlainObject() 判断某个参数是否为用"{}"或"new Object"建立的对象。(内部调用4次) $.isWindow() 判断是否为window对象。(内部调用6次)

以下三个函数比较简单,没必要在文章剖析。 $.noop() 一个空函数,个人觉得是用来作为一个默认的回调函数,无需每次去定义一个空的function消耗资源。(内部调用2次) $.now() 获取当前时间戳,代码很简单:return (new Date()).getTime();。(内部调用4次) $.error() 报错,对外抛出一个异常,代码很简单:throw new Error(msg);。(内部调用2次)

以下三个是jQuery主要用来在ajax处理返回数据时使用,其中parseJSON这个接口在实际工程中被用得最多,经常用来把一段文本解析成json格式 $.parseHTML() 解析HTML,之后再单独一节写。(内部调用2次) $.parseJSON() 解析JSON,之后再单独一节写。(内部调用2次) $.parseXML() 解析XML,之后再单独一节写。(内部调用1次)

其中我认为是内部辅助函数如下: $.access() 这个函数我更认为是jQuery内部的辅助函数,没必要暴漏出来,在内部用于去一些对象的属性值等,在之后剖析到DOM操作等再细细探讨一下。(内部调用9次) $.camelCase() 转化成骆驼峰命名。(内部调用12次)

开始窥探源码吧!

2.0 在开始之前

在jQuery一开始的定义里边,有这么一小段



  class2type = {},
  core_deletedIds = [],
  core_version = "1.9.0",

  // Save a reference to some core methods
  core_concat = core_deletedIds.concat,
  core_push = core_deletedIds.push,
  core_slice = core_deletedIds.slice,
  core_indexOf = core_deletedIds.indexOf,
  core_toString = class2type.toString,
  core_hasOwn = class2type.hasOwnProperty,
  core_trim = core_version.trim,

  //等同以下代码:
  core_concat = Array.prototype.concat, 
  //文章一开始的介绍有稍微提到prototype
  //core_deletedIds是一个数组实例
  //core_deletedIds.concat方法就相当于调了Array类中的成员方法concat。

需要调用concat时可以通过以下方法调用,关于call跟apply的用法自行理解,:) var arr = []; 方式一:arr.concat(); 方式二:core_concat.call(arr); 方式三:core_concat.apply(arr);

思考下边2个问题:

  1. jQuery为什么要先把这些方法存储起来?
  2. jQuery为什么要采用方式二或者三,而不直接使用方式一的做法? 在不查阅资料的前提下,唯一让我觉得作者这么做的原因是因为效率问题。 以下是我的理解:

    调用实例arr的方法concat时,首先需要辨别当前实例arr的类型是Array,在内存空间中寻找Array的concat内存入口,把当前对象arr的指针和其他参数压入栈,跳转到concat地址开始执行。 当保存了concat方法的入口core_concat时,完全就可以省去前面两个步骤,从而提升一些性能。 nodejser在评论中也给出了另一种答案: var obj = {}; 此时调用obj.concat是非法的,但是如果jQuery采用上边方式二或者三的话,能够解决这个问题。 也即是让类数组也能用到数组的方法(这就是call跟apply带来的另一种用法),尤其在jQuery里边引用一些DOM对象时,也能完美的用这个方法去解决,妙!

2.1 $.trim

jQuery的trim函数是用来去除字符串两端空格(jQuery源码里边使用了7次),这个函数也是使用频率很高的,因为时常要对用户在页面上输入的文本trim一下~

用法:$.trim(" 前尾有空格 ") === "前尾有空格"

jQuery的trim源码如下:


core_version = "1.9.0",
core_trim = core_version.trim,
rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,

trim: core_trim && !core_trim.call("\uFEFF\xA0") ?
  function( text ) {
    return text == null ?
      "" :
      core_trim.call( text );
  } :

  // Otherwise use our own trimming functionality
  function( text ) {
    return text == null ?
      "" :
      ( text + "" ).replace( rtrim, "" );
  }

剖析之:

var core_trim = String.prototype.trim; if (core_trim && !core_trim.call("\uFEFF\xA0")) 相当于: if (String.prototype.trim && "\uFEFF\xA0".trim() !== "") 高级的浏览器已经支持原生的String的trim方法,但是jQuery还为了避免它没法解析全角空白,所以加多了一个判断:"\uFEFF\xA0".trim() !== ""

\uFEFF是utf8的字节序标记,详见:字节顺序标记 "\xA0"是全角空格 如果以上条件成立了,那就直接用原生的trim函数就好了,展开也即是:


$.trim = function( text ) {
    return text == null ?
        "" :
        text.trim();
}

如果上述条件不成立了,那jQuery就自己实现一个trim方法:


$.trim = function( text ) {
    return text == null ?
        "" :
        ( text + "" ).replace( rtrim, "" );
}

当然你还得自己看懂rtrim这个正则表达的意思了。

2.2 $.each

jQuery的each是我使用最频繁的方法,在它内部也是使用很频繁的一个API(一共用了59处),使得遍历一个数组或者对象的代码段变得十分的简洁,API的设计准则应当如此。

先看几个each的例子:


//each接受2个参数, 数组[1,2,3],回调函数
$.each([1,2,3], function(key, value){
  console.log("[" + key + "]=" + value);
  return false;
});
//输出:
[0]=1
[1]=2
[2]=3
[1,2,3]
//可以看到回调函数具有两个参数,key是数组的索引,value是对应的元素

//each接受3个参数, 数组[1,2,3],回调函数,一个额外的参数数组args=[4,5]
$.each([1,2,3], function(arg1, arg2){
  console.log(this + "," + arg1 + "," + arg2);
}, [4, 5]);
//输出:
1,4,5
2,4,5
3,4,5
[1,2,3]
//可以看到回调函数的两个参数就是each的第三个参数args,在函数里边的this就是遍历元素自己

接着我们来看jQuery的each实现。


//其实我觉得jQuery这段each代码写得一点也不好
//代码重复率太高了!我下边对它进行解析并改造
//貌似看注释是为了让代码运行更快,具体没测试。
//源码如下
$.each = function( obj, callback, args ) {
  //obj 是需要遍历的数组或者对象
  //callback是处理数组/对象的每个元素的回调函数,它的返回值实际会中断循环的过程
  //

  var value,
    i = 0,
    length = obj.length,
    isArray = isArraylike( obj );//判断是不是数组

  if ( args ) {
    if ( isArray ) {//数组
      for ( ; i < length; i++ ) {
        value = callback.apply( obj[ i ], args );
        //相当于:
        //args = [arg1, arg2, arg3];
        //callback(args1, args2, args3)。然后callback里边的this指向了obj[i]

        if ( value === false ) {
          //注意到,当callback函数返回值会false的时候,注意是全等!循环结束
          break;
        }
      }
    } else {//非数组
      for ( i in obj ) {//遍历对象的做法
        value = callback.apply( obj[ i ], args );

        if ( value === false ) {
          break;
        }
      }
    }

  // A special, fast, case for the most common use of each
  } else {
    if ( isArray ) {
      for ( ; i < length; i++ ) {
        value = callback.call( obj[ i ], i, obj[ i ] );
        //相当于callback(i, obj[i])。然后callback里边的this指向了obj[i]

        if ( value === false ) {
            break;
          }
        }
      } else {
        for ( i in obj ) {
          value = callback.call( obj[ i ], i, obj[ i ] );

          if ( value === false ) {
            break;
          }
        }
      }
    }

  return obj;
}

each的实现比较简单,但是我不是很明白为什么jQuery的实现需要这么长,如果说jQuery为了效率写成以上那么长的代码的话,那我宁愿牺牲那一点点效率把代码量降低


//我改写的版本
//这里并不是代表我的代码没有bug,for in对于数组来说性能比较低而且存在某些bug
//这里想表达的意思只是我希望用更简洁的代码来减少原先代码的重复性
$.each = function( obj, callback, args ) {
  var value,
    i = 0,
    length = obj.length;

  for ( i in obj ) {
    value =
      args ?
      callback.apply( obj[ i ], args ) :
      callback.call( obj[ i ], i, obj[ i ] );

    if ( value === false ) {
      break;
    }
  }
  return obj;
}

要熟记$.each的用法,因为在之后的剖析中肯定会多次用到each函数。:)

2.3 $.inArray

jQuery的inArray实现比较简单,有一个小小的点在这里讨论一下,代码如下:


core_deletedIds = [],
core_indexOf = core_deletedIds.indexOf,
//相当于 core_indexOf = Array.indexOf;

//elem 规定需检索的值。
//arr 数组
//i 可选的整数参数。规定在数组中开始检索的位置。它的合法取值是 0 到 arr.length - 1。如省略该参数,则将从数组首元素开始检索。
inArray: function( elem, arr, i ) {
  var len;

  if ( arr ) {
    //原生的Array对象支持indexOf方法,直接调用
    if ( core_indexOf ) {
      return core_indexOf.call( arr, elem, i );
    }

    len = arr.length;
    //当i为负数的时候,从数组后边len+i的位置开始索引
    i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;

    for ( ; i < len; i++ ) {
      // Skip accessing in sparse arrays
      //jQuery这里的(i in arr)判断是为了跳过稀疏数组中的元素
      //例如 var arr = []; arr[2] = 1;
      //此时 arr == [undefined, undefined, 1]
      //结果是 => (0 in arr == false) (1 in arr == false) (2 in arr == true)
      //但是不解的是这里
      //测试了一下 $.inArray(undefined, arr, 0)是返回-1的
      //也许你很不解,因为数组中明明第一个元素就是undefined,后边举个例子探讨一下
      if ( i in arr && arr[ i ] === elem ) {
        return i;
      }
    }
  }

  //全部都不符合,返回-1
  return -1;
},

测试了一下(在火狐下): var arr = [undefined, 1]; 输出是: arr == [undefined, 1] 0 in arr == true; arr.indexOf(undefined) == 0; arr[0] == undefined; 但是如果是以下代码: var arr = []; a[1] = 1; 输出是: arr == [undefined, 1] 0 in arr == false; arr.indexOf(undefined) == -1; arr[0] == undefined;

这就说明了在第二种情况中(就是注释中提到的稀疏数组),不能只是通过arr[ i ] === elem来得到元素在数组中的位置 而应该加入多一个条件;i in arr

2.4 $.grep

jQuery的grep是为了传入一个数组elems,通过过滤器callback后,等到过滤后的结果。 其中callback就是过滤器,$.grep是根据其返回值过滤; inv为true表示是callback过滤器返回true的那些被过滤掉。 说起来有点绕,看两个小例子就完全明白了:


$.grep( [0,1,2], function(n,i){
  return n <= 0;
});
//结果是:[0] 

$.grep( [0,1,2], function(n,i){
  return n <= 0;
}, true);
//结果是:[1, 2]

实现比较简单,但是不知道在这里设计的callback接受的参数顺序没跟$.each的callback一致,我认为这里这样设计的原因是往往过滤器只需使用value,这样就可以忽略第二个参数了 代码如下:


grep: function( elems, callback, inv ) {
  var retVal,
    ret = [],
    i = 0,
    length = elems.length;
  inv = !!inv;//转成布尔型

  // Go through the array, only saving the items
  // that pass the validator function
  for ( ; i < length; i++ ) {
    retVal = !!callback( elems[ i ], i );//注意这里的callback参数是先value,后key
    if ( inv !== retVal ) {
      ret.push( elems[ i ] );
    }
  }

  return ret;
}

2.5 $.merge

$.merge([0,1,2], [2,3,4]) == [0, 1, 2, 2, 3, 4] merge的两个参数必须为数组,作用就是修改第一个数组,使得它末尾加上第二个数组。 其实单单从例子或者说明来看,merge函数的两个参数貌似必须为数组,但是看其源码就知道有个小细节,jQuery允许第二个参数不为数组。 $.merge()源码如下:



merge: function( first, second ) {
  //难道first 跟second参数必须为数组?
  var l = second.length,
    i = first.length,
    j = 0;

  //从这个判断来看,second可以是一个对象
  if ( typeof l === "number" ) {//如果second是数组
    for ( ; j < l; j++ ) {
      first[ i++ ] = second[ j ];
    }
  } else {//如果second不是数组
    //假设second = {}; second[0] = 1; second[1] = 2; second[2] = 3;
    //那这里的逻辑也是可以成立的
    while ( second[j] !== undefined ) {
      first[ i++ ] = second[ j++ ];
    }
  }

  first.length = i;

  return first;
}

从上边源码来看,貌似jQuery的merge函数的first参数貌似是要数组类型,其实不然,first={length:3};first[0] = 1; first[1] = 2; first[2] = 3; 同样也是可以达到merge目的。 当然了,这里这样分析的目的是赞一下javascript的灵活性,实际使用merge函数时,我们应当保证两个参数都是数组类型,避免了书写出上述分析那样难以理解意思的代码。

2.6 $.map

从$.map代码上看,还算比较容易理解,把数组每一项经过callback处理后的值依次加入到返回数组中 例如



$.map( [0,1,2], function(n){
  return n + 4;
});
//结果:[4, 5, 6]

$.map源码如下:



core_deletedIds = [],
core_concat = core_deletedIds.concat,
// arg is for internal usage only
map: function( elems, callback, arg ) {
  var value,
    i = 0,
    length = elems.length,
    isArray = isArraylike( elems ),
    ret = [];

  // Go through the array, translating each of the items to their
  if ( isArray ) {
    for ( ; i < length; i++ ) {
      value = callback( elems[ i ], i, arg );

      if ( value != null ) {//如果返回值是null,则不加入结果中
        ret[ ret.length ] = value;
      }
    }

  // Go through every key on the object,
  } else {
    for ( i in elems ) {
      value = callback( elems[ i ], i, arg );

      if ( value != null ) {
        ret[ ret.length ] = value;
      }
    }
  }

  // Flatten any nested arrays
  //这里相当于 var a = [];a.concat(ret)
  return core_concat.apply( [], ret );
},

但是看到map实现代码的最后一句,为什么不直接return ret呢? 一开始很费解这里,但是如果看了以下例子之后,你就焕然大悟了,:)



$.map( [0,1,2], function(n){
  return [ n, n + 1 ];
});
//输出:[0, 1, 1, 2, 2, 3]
//如果是return ret的话,输出将会是:[[0,1], [1,2], [2,3]]

w3cschool里边对此方法的描述有误:.map(callback(index,domElement)) 其实应该是:.map(callback(domElement,index))

2.7 $.makeArray

$.makeArray将类数组对象转换为数组对象,源码简单,注释如下:



makeArray: function( arr, results ) {
  var ret = results || [];//不由得不赞js这个技巧
  //等同于:var ret = (!results) ? [] : results;

  if ( arr != null ) {
    if ( isArraylike( Object(arr) ) ) {
      //如果arr是一个类数组对象,调用merge合到返回值
      jQuery.merge( ret,
        typeof arr === "string" ?
        [ arr ] : arr
      );
    } else {//如果不是数组,则将其放到返回数组末尾
      //等同于ret.push(arr);
      core_push.call( ret, arr );
    }
  }

  return ret;
},

2.8 $.globalEval

看名字就知道,这是一个在eval的变种而已,它的作用域是在window下,当然采用call方法。 源码如下:



// Evaluates a script in a global context
// Workarounds based on findings by Jim Driscoll
// http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context
globalEval: function( data ) {
  if ( data && jQuery.trim( data ) ) {
    // We use execScript on Internet Explorer
    // We use an anonymous function so that context is window
    // rather than jQuery in Firefox
    ( window.execScript || function( data ) {
      window[ "eval" ].call( window, data );
    } )( data );
  }
},

等同于



globalEval: function( data ) {
  if ( data && jQuery.trim( data ) ) {//data不是空字符串
    var fn = window.execScript;
    //在IE跟旧版本的Chrome是支持此方法的
    if (!window.execScript){//新版浏览器没有此api
      fn = function( data ) {
        window[ "eval" ].call( window, data );
        //这里为何不能直接:eval.call( window, data );
        //查了以下,在chrome一些旧版本里eval.call( window, data )无效 
      }
    }
    fn(data);
  }
},

那为什么需要使用globalEval呢?跟直接用eval的差别在于哪? 我认为以下场景需要用到: a.js在某个函数域里边动态加载b.js内容,然后执行。 b.js的代码为:function B(){},它是希望B函数暴漏在全局域的。 此时如果在a.js里边采用eval就会使得B只能成为一个局部域里边的函数,所以就必须借globalEval方法做到。 在jQuery源码里边,对于操作dom里边的script以及ajax拉回来的script都有globalEval的身影。

2.9 $.proxy

proxy这个函数用的较少,我认为使用它会造成一些困惑,所以还不如用一些比较好理解的方式去替代它。 源码如下:



// Bind a function to a context, optionally partially applying any
// arguments.
proxy: function( fn, context ) {
  var tmp, args, proxy;

  if ( typeof context === "string" ) {
    tmp = fn[ context ];
    context = fn;
    fn = tmp;
  }

  // Quick check to determine if target is callable, in the spec
  // this throws a TypeError, but we will just return undefined.
  if ( !jQuery.isFunction( fn ) ) {
    return undefined;
  }

  // Simulated bind
  //由此看出,proxy的参数必须是2个,否则有无法预料的bug
  args = core_slice.call( arguments, 2 );
  proxy = function(/*arguments*/) {
    //需要注意到的是这里的arguments是当前函数的参数列表
    return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) );
  };

  // Set the guid of unique handler to the same of original handler, so it can be removed
  //之后再探讨以下guid是做什么的
  proxy.guid = fn.guid = fn.guid || jQuery.guid++;

  return proxy;
},

乍看之下,会把上述proxy函数里边的arguments跟其外层的arguments搞混。 看个例子:



function a(){
  console.log(arguments);//输出[1,2,3,4,5]
}
var b = $.proxy(a, this, 1, 2, 3);//这里arguments = [a, this, 1, 2, 3];
b(4,5);//这里arguments = [4,5];

如果分析不够准确地化会把上边输出认为是[1,2,3] 其实理解透上边proxy的arguments,就知道 proxy函数里边的args.concat( core_slice.call( arguments )其实就是[1,2,3].concat([4,5])

2.10 $.nodeName

nodeName函数是获取dom节点的节点名字或者判断其名字跟传入参数是否匹配。代码很简单,这里可以看到一个技巧,这是在javascript里边惯用的。(见代码注释) 源码如下:



nodeName: function( elem, name ) {
  //IE下,DOM节点的nodeName是大写的,例如DIV
  //所以统一转成小写再判断
  //这里不return elem.nodeName.toLowerCase();
  //我认为原因是为了保持浏览器自身的对外的规则,避免所有引用nodeName都要做转换的动作
  return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();

  //可以看到这里的&&使用技巧,以上代码等同于:
  //if (elem.nodeName) return elem.nodeName.toLowerCase() === name.toLowerCase();
  //else return elem.nodeName;
  //如此之简洁
},

2.11 $.type

接下来只需要介绍type函数,这里就是$.each的一个好样例。 先了解一下typeof以及toString:



typeof 1 == 'number'
typeof {} == 'object'
typeof [] == 'object'
(1).toString() == "1"
({}).toString() == "[object Object]"
//再针对一些边界的测试,
typeof null == "object"
typeof undefined == "undefined"
(null).toString()//非法
(undefined).toString()//非法

//再看看很诡异的几个:
([]).toString() == ""
(new Error()).toString() == "Error"
//出现以上两个的结果的原因是,Array跟Error类重写了toSting方法
//如果用Object的toString方法的话,就是一下结果
Object.prototype.toString.call([]) == "[object Array]"
Object.prototype.toString.call(new Error()) == "[object Error]"

看完以上的实验,再回过头看一下$.type函数,其作用就是类似typeof这个操作符,简单来说,也即是: $.type = function(obj) { return typeof obj; } 但是对于数组来说,本质它还是算是一个对象,typeof并不能区分出它是Array类型,jQuery为了扩展typeof的表达力,因此扩展了type方法,针对一些特殊的对象(例如null,window,RegExp)也进行精准的类型判断。 它先将常见类型打表(打表的意思就是先存在一个Hash表class2type里边),源码如下:



// Populate the class2type map
jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
  class2type[ "[object " + name + "]" ] = name.toLowerCase();
});

$.type = function( obj ) {
  if ( obj == null ) {//如果是null或者undefined,直接转成String返回
    //注意到这里是==而不是===
    return String( obj );
  }
  //RegExp,Array等都属于Object
  //为了精准判断类型,借由Object.prototype.toString跟class2type表
  //这里为什么要用core_toString而不用obj.toString的原因在刚刚试验中说明了
  return typeof obj === "object" || typeof obj === "function" ?
    class2type[ core_toString.call(obj) ] || "object" :
    typeof obj;
},

isXXX在这里就不详细展开了,源码如下



// See test/unit/core.js for details concerning isFunction.
// Since version 1.3, DOM methods and functions like alert
// aren't supported. They return false on IE (#2968).
isFunction: function( obj ) {
  return jQuery.type(obj) === "function";
},

isArray: Array.isArray || function( obj ) {
  return jQuery.type(obj) === "array";
},

isWindow: function( obj ) {
  return obj != null && obj == obj.window;
},

isNumeric: function( obj ) {
  return !isNaN( parseFloat(obj) ) && isFinite( obj );
},
isPlainObject: function( obj ) {
  // Must be an Object.
  // Because of IE, we also have to check the presence of the constructor property.
  // Make sure that DOM nodes and window objects don't pass through, as well
  if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
    return false;
  }

  try {
    // Not own constructor property must be Object
    if ( obj.constructor &&
      !core_hasOwn.call(obj, "constructor") &&
      !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
      return false;
    }
  } catch ( e ) {
    // IE8,9 Will throw exceptions on certain host objects #9897
    return false;
  }

  // Own properties are enumerated firstly, so to speed up,
  // if last one is own, then all properties are own.

  var key;
  for ( key in obj ) {}

  return key === undefined || core_hasOwn.call( obj, key );
},

isEmptyObject: function( obj ) {
  var name;
  for ( name in obj ) {
    return false;
  }
  return true;
},

2.12 $.extend

extend函数(注意extends是js里边的保留关键字,所以这里命名末尾是没有s的)是使用频率很高的一个函数,通常你写一个插件,会利用extend传进来的参数来覆盖插件原有的配置。 例如:



  var Plugin = function(opt){
    //opt为传入的配置
    this.opt = $.extend({
      'title':'默认标题',
      'content':'默认内容'
    }, opt);
  };

  var p = new Plugin({title:'传入标题'})

$.extend源码如下:



jQuery.extend = jQuery.fn.extend = function() {
  var options, name, src, copy, copyIsArray, clone,
    target = arguments[0] || {},
    i = 1,
    length = arguments.length,
    deep = false;

  // Handle a deep copy situation
  if ( typeof target === "boolean" ) {
    //第一个参数表示是否要深递归,类型是布尔值
    deep = target;
    target = arguments[1] || {};
    // skip the boolean and the target
    i = 2;
  }

  // Handle case when target is a string or something (possible in deep copy)
  if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
    target = {};
  }

  // extend jQuery itself if only one argument is passed
  if ( length === i ) {
    //$("#id").extend(dest)的时候
    target = this;
    --i;
  }

  for ( ; i < length; i++ ) {//可以传入多个复制源
    // Only deal with non-null/undefined values
    if ( (options = arguments[ i ]) != null ) {
      // Extend the base object
      //将每个源的属性全部复制到target上
      for ( name in options ) {
        src = target[ name ];
        copy = options[ name ];

        // Prevent never-ending loop
        if ( target === copy ) {
          //防止有环,例如 extend(true, target, {'target':target});
          continue;
        }

        // Recurse if we're merging plain objects or arrays
        if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
          //如果是深复制
          if ( copyIsArray ) {
            copyIsArray = false;//这句话我认为是多余的。
            //克隆原来target上的原属性
            clone = src && jQuery.isArray(src) ? src : [];

          } else {
            clone = src && jQuery.isPlainObject(src) ? src : {};
          }

          // Never move original objects, clone them
          //递归深复制
          target[ name ] = jQuery.extend( deep, clone, copy );

        // Don't bring in undefined values
        //可以看到undefined的属性对时不会复制到target上的
        } else if ( copy !== undefined ) {
          target[ name ] = copy;
        }
      }
    }
  }

  // Return the modified object
  return target;
};

================================================================================== 至此,绑定在jQuery上一些对外的方法已经分析完毕,代码算比较简单清晰,篇幅略长。 下一篇会把jQuery对ready事件的处理进行讨论。

w3ctech微信

扫码关注w3ctech微信公众号

共收到11条回复

  • 很少看到有写新版本jq的,太好啦!顶一个!

    回复此楼
  • 拜读

    回复此楼
  • 见识了!很不错

    回复此楼
  • 作者可以读一下 关于 基于类的面向对象和基于原型的面向对象方式 的一些比较

    回复此楼
  • jQuery.A = function(){}; jQuery.prototype.B = function(){};

    Class jQuery{ public static A(){} public B(){} }

    是不是写反了

    回复此楼
  • 个人专为IT技术人员搭建的问答平台——欢迎前来拍砖。 [嘻嘻] http://www.dreawer.com/home.html

    目前最火的领域是前端领域,欢迎一起交流!

    回复此楼
  • @newwo 没有写反,你用点心,不解释

    回复此楼
  • @newwo 没有写反,用点心,不解释

    回复此楼
  • 工具是为了给别人用的,性质不一样,即便是多写1000行代码能让效率提高一点点,也是值得的,杯水车薪,他实现写的再多对我们用的人也无所谓,性能第一

    回复此楼
  • 写的好

    回复此楼
  • @super良人 怎么我也觉得是写反了啊:B方法放在原型对象下,所有继承jqeury原型的对象都拥有B方法。应该是public static B(){}才对吧?

    回复此楼