最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美。
其结构明晰,高内聚、低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷、渐进增强)优雅的处理能力以及 Ajax 等方面周到而强大的定制功能无不令人惊叹。
另外,阅读源码让我接触到了大量底层的知识。对原生JS 、框架设计、代码优化有了全新的认识,接下来将会写一系列关于 jQuery 解析的文章。
我在 github 上关于 jQuery 源码的全文注解,感兴趣的可以围观一下。jQuery v1.10.2 源码注解 。
系列第一篇:【深入浅出jQuery】源码浅析--整体架构
本篇是系列第二篇,标题起得有点大,希望内容对得起这个标题,这篇文章主要总结一下在 jQuery 中一些十分讨巧的 coding 方式,将会由浅及深,可能会有一些基础,但是我希望全面一点,对看文章的人都有所帮助,源码我还一直在阅读,也会不断的更新本文。
即便你不想去阅读源码,看看下面的总结,我想对提高编程能力,转换思维方式都大有裨益,废话少说,进入正题。
短路表达式 与 多重短路表达式
短路表达式这个应该人所皆知了。在 jQuery 中,大量的使用了短路表达式与多重短路表达式。
短路表达式:作为"&&"和"||"操作符的操作数表达式,这些表达式在进行求值时,只要最终的结果已经可以确定是真或假,求值过程便告终止,这称之为短路求值。这是这两个操作符的一个重要属性。
// ||短路表达式 var foo = a || b; // 相当于 if(a){ foo = a; }else{ foo = b; } // &&短路表达式 var bar = a && b; // 相当于 if(a){ bar = b; }else{ bar = a; }
当然,上面两个例子是短路表达式最简单是情况,多数情况下,jQuery 是这样使用它们的:
// 选自 jQuery 源码中的 Sizzle 部分 function siblingCheck(a, b) { var cur = b && a, diff = cur && a.nodeType === 1 && b.nodeType === 1 && (~b.sourceIndex || MAX_NEGATIVE) - (~a.sourceIndex || MAX_NEGATIVE); // other code ... }
嗯,可以看到,diff 的值经历了多重短路表达式配合一些全等判断才得出,这种代码很优雅,但是可读性下降了很多,使用的时候权衡一下,多重短路表达式和简单短路表达式其实一样,只需要先把后面的当成一个整体,依次推进,得出最终值。
var a = 1, b = 0, c = 3; var foo = a && b && c, // 0 ,相当于 a && (b && c) bar = a || b || c; // 1
这里需要提出一些值得注意的点:
1、在 Javascript 的逻辑运算中,0、""、null、false、undefined、NaN 都会判定为 false ,而其他都为 true ;
2、因为 Javascript 的内置弱类型域 (weak-typing domain),所以对严格的输入验证这一点不太在意,即便使用 && 或者 || 运算符的运算数不是布尔值,仍然可以将它看作布尔运算。虽然如此,还是建议如下:
if(foo){ ... } //不够严谨 if(!!foo){ ... } //更为严谨,!!可将其他类型的值转换为boolean类型
注重细节,JavaScript 既不弱也不低等,我们只是需要更努力一点工作以使我们的代码变得真正健壮。
预定义常用方法的入口
在 jQuery 的头几十行,有这么一段有趣的代码:
(function(window, undefined) { var // 定义了一个对象变量,一个字符串变量,一个数组变量 class2type = {}, core_version = "1.10.2", core_deletedIds = [], // 保存了对象、字符串、数组的一些常用方法 concat push 等等... 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; })(window);
不得不说,jQuery 在细节上做的真的很好,这里首先定义了一个对象变量、一个字符串变量、数组变量,要注意这 3 个变量本身在下文是有自己的用途的(可以看到,jQuery 作者惜字如金,真的是去压榨每一个变量的作用,使其作用最大化);其次,借用这三个变量,再定义些常用的核心方法,从上往下是数组的 concat、push 、slice 、indexOf 方法,对象的 toString 、hasOwnProperty 方法以及字符串的 trim 方法,core_xxxx 这几个变量事先存储好了这些常用方法的入口,如果下文行文当中需要调用这些方法,将会:
jQuery.fn = jQuery.prototype = { // ... // 将 jQuery 对象转换成数组类型 toArray: function() { // 调用数组的 slice 方法,使用预先定义好了的 core_slice ,节省查找内存地址时间,提高效率 // 相当于 return Array.prototype.slice.call(this) return core_slice.call(this); } }
可以看到,当需要使用这些预先定义好的方法,只需要借助 call 或者 apply(戳我详解)进行调用。
那么 jQuery 为什么要这样做呢,我觉得:
1、以数组对象的 concat 方法为例,如果不预先定义好 core_concat = core_deletedIds.concat 而是调用实例 arr 的方法 concat 时,首先需要辨别当前实例 arr 的类型是 Array,在内存空间中寻找 Array 的 concat 内存入口,把当前对象 arr 的指针和其他参数压入栈,跳转到 concat 地址开始执行,而当保存了 concat 方法的入口 core_concat 时,完全就可以省去前面两个步骤,从而提升一些性能;
2、另外一点,借助 call 或者 apply 的方式调用,让一些类数组可以直接调用数组的方法。就如上面是示例,jQuery 对象是类数组类型,可以直接调用数组的 slice 方法转换为数组类型。又譬如,将参数 arguments 转换为数组类型:
function test(a,b,c){ // 将参数 arguments 转换为数组 // 使之可以调用数组成员方法 var arr = Array.prototype.slice.call(arguments); ... }
钩子机制(hook)
在 jQuery 2.0.0 之前的版本,对兼容性做了大量的处理,正是这样才让广大开发人员能够忽略不同浏览器的不同特性的专注于业务本身的逻辑。而其中,钩子机制在浏览器兼容方面起了十分巨大的作用。
钩子是编程惯用的一种手法,用来解决一种或多种特殊情况的处理。
简单来说,钩子就是适配器原理,或者说是表驱动原理,我们预先定义了一些钩子,在正常的代码逻辑中使用钩子去适配一些特殊的属性,样式或事件,这样可以让我们少写很多 else if 语句。
如果还是很难懂,看一个简单的例子,举例说明 hook 到底如何使用:
现在考公务员,要么靠实力,要么靠关系,但领导肯定也不会弄的那么明显,一般都是暗箱操作,这个场景用钩子实现再合理不过了。