欢迎来个人专栏查看系列文章。javascript
决定你走多远的是基础,jQuery 源码分析,向长者膜拜! css
我虽然接触 jQuery 好久了,但也只是局限于表面使用的层次,碰到一些问题,找到 jQuery 的解决办法,而后使用。显然,这种作法的弊端就是,不管你怎么学,都只能是个小白。html
当我创建这个项目的时候,就表示,我要改变这一切了,作一些人想作,憧憬去作,但从没踏入第一步的事情,学习 jQuery 源码。前端
到目前为止,jQuery 的贡献者团队共 256 名成员,6000 多条 commits,可想而知,jQuery 是一个多么庞大的项目。jQuery 官方的版本目前是 v3.1.1,已经衍生出 jQueryUI、jQueryMobile 等多个项目。vue
虽然我在前端爬摸打滚一年多,自认基础不是很好,在没有外界帮助的状况下,直接阅读项目源码太难了,因此在边参考遍实践的过程当中写下来这个项目。java
首先,先推荐一个 jQuery 的源码查询网站,这个网站给初学者很是大的帮助,不只能查找不一样版本的 jQuery 源码,还能索引函数,功能简直吊炸天。react
另外,推荐两个分析 jQuery 的博客:jquery
jQuery源码分析系列git
这两个博客给我了很大的帮助,谢谢。
另外还有下面的网址,让我在如何使用 jQuery 上驾轻就熟:
首先,jQuery 是一个开发框架,它的火爆程度已经没法用言语来形容,当你随便打开一个网站,一半以上直接使用了 jQuery。或许,早几年,一个前端工程师,只要会写 jQuery,就能够无忧工做。虽然说最近 react、vue 很火,但 jQuery 中许多精彩的方法和逻辑值得每个前端人员学习。
和其众多的框架同样,总要把接口放到外面来调用,内部每每是一个闭包,避免环境变量的污染。
先来看看 jQuery 使用上的几大特色:
$('#id') 函数方式直接生成 jQuery 对象
$('#id').css().html().hide() 链式调用
关于链式调用,我想有点基础都很容易实现,函数结尾 return this 便可,主要来介绍一下无 new 实现建立对象。
下面是一个普通的函数,很显然,会陷入死循环:
var jQuery = function(){ return new jQuery(); } jQuery.prototype = { ... }
这个死循环来的太忽然,jQuery() 会建立一个 new jQuery,new jQuery 又会建立一个 new jQuery...
jQuery 用一个 init 函数来代替直接 new 函数名的方式,还要考虑到 jQuery 中分离做用域:
var jQuery = function(){ return new jQuery.prototype.init(); } jQuery.prototype = { constructor: jQuery, init: function(){ this.jquery = 1.0; return this; }, jquery: 2.0, each: function(){ console.log('each'); return this; } } jQuery().jquery //1.0 jQuery.prototype.jquery //2.0 jQuery().each() // error
上面看似运行正常,可是问题出在 jQuery().each() // error
,访问不到 each 函数。实际上,new jQuery.prototype.init()
返回到是谁的实例?是 init 这个函数的实例,因此 init 函数中的 this 就没了意义。
那么,若是:
var jq = jQuery(); jq.__proto__ === jQuery.prototype; jq.each === jQuery.prototype.each;
若是能够实现上面的 proto 的指向问题,原型函数调用问题就解决了,但实际上:
var jq = jQuery(); jq.__proto__ === jQuery.prototype.init.prototype; //true
实际上,jq 的 proto 是指向 init 函数的原型,因此,咱们能够把 jQuery.prototype.init.prototype = jQuery.prototype
,这个时候,函数调用就瓜熟蒂落了,并且使用的都是引用,指向的都是同一个 prototype 对象,也不须要担忧循环问题。实际上,jQuery 就是这么干的。
var jQuery = function(){ return new jQuery.prototype.init(); } jQuery.prototype = { constructor: jQuery, init: function(){ this.jquery = 1.0; return this; }, jquery: 2.0, each: function(){ console.log('each'); return this; } } jQuery.prototype.init.prototype = jQuery.prototype; jQuery().each() //'each'
在说内部图以前,先说下 jQuery.fn
,它其实是 prototype 的一个引用,指向 jQuery.prototype 的,
var jQuery = function(){ return new jQuery.prototype.init(); } jQuery.fn = jQuery.prototype = { ... }
那么为何要用 fn 指向 prototype?我本人查阅了一些资料,貌似仍是下面的回答比较中肯:简介。你不以为 fn 比 prototype 好写多了吗。
借用网上的一张图:
从这张图中能够看出,window 对象上有两个公共的接口,分别是 $ 和 jQuery:
window.jQuery = window.$ = jQuery;
jQuery.extend
方法是一个对象拷贝的方法,包括深拷贝,后面会详细讲解源码,暂时先放一边。
下面的关系可能会有些乱,可是仔细看了前面的介绍,应该能看懂。fn 就是 prototype,因此 jQuery 的 fn 和 prototype 属性指向 fn 对象,而 init 函数自己就是 jQuery.prototype 中的方法,且 init 函数的 prototype 原型指向 fn。
链式调用的好处,就是写出来的代码很是简洁,并且代码返回的都是同一个对象,提升代码效率。
前面已经说了,在没有返回值的原型函数后面添加 return this:
var jQuery = function(){ return new jQuery.fn.init(); } jQuery.fn = jQuery.prototype = { constructor: jQuery, init: function(){ this.jquery = 3.0; return this; }, each: function(){ console.log('each'); return this; } } jQuery.fn.init.prototype = jQuery.fn; jQuery().each().each(); // 'each' // 'each'
jQuery 中一个重要的函数即是 extend,既能够对自己 jQuery 的属性和方法进行扩张,又能够对原型的属性和方法进行扩展。
先来讲下 extend 函数的功能,大概有两种,若是参数只有一个 object,即表示将这个对象扩展到 jQuery 的命名空间中,也就是所谓的 jQuery 的扩展。若是函数接收了多个 object,则表示一种属性拷贝,将后面多个对象的属性全拷贝到第一个对象上,这其中,还包括深拷贝,即非引用拷贝,第一个参数若是是 true 则表示深拷贝。
jQuery.extend(target);// jQuery 的扩展 jQuery.extend(target, obj1, obj2,..);//浅拷贝 jQuery.extend(true, target, obj1, obj2,..);//深拷贝
如下是 jQuery 3 以后的 extend 函数源码,本身作了注释:
jQuery.extend = jQuery.fn.extend = function () { var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {}, i = 1, length = arguments.length, deep = false; // 判断是否为深拷贝 if (typeof target === "boolean") { deep = target; // 参数后移 target = arguments[i] || {}; i++; } // 处理 target 是字符串或奇怪的状况,isFunction(target) 能够判断 target 是否为函数 if (typeof target !== "object" && !jQuery.isFunction(target)) { target = {}; } // 判断是否 jQuery 的扩展 if (i === length) { target = this; // this 作一个标记,能够指向 jQuery,也能够指向 jQuery.fn i--; } for (; i < length; i++) { // null/undefined 判断 if ((options = arguments[i]) != null) { // 这里已经统一了,不管前面函数的参数怎样,如今的任务就是 target 是目标对象,options 是被拷贝对象 for (name in options) { src = target[name]; copy = options[name]; // 防止死循环,跳过自身状况 if (target === copy) { continue; } // 深拷贝,且被拷贝对象是 object 或 array // 这是深拷贝的重点 if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))) { // 说明被拷贝对象是数组 if (copyIsArray) { copyIsArray = false; clone = src && Array.isArray(src) ? src : []; // 被拷贝对象是 object } else { clone = src && jQuery.isPlainObject(src) ? src : {}; } // 递归拷贝子属性 target[name] = jQuery.extend(deep, clone, copy); // 常规变量,直接 = } else if (copy !== undefined) { target[name] = copy; } } } } // Return the modified object return target; }
extend 函数符合 jQuery 中的参数处理规范,算是比较标准的一个。jQuery 对于参数的处理颇有一套,老是喜欢错位来使得每个位置上的变量和它们的名字同样,各司其职。好比 target 是目标对象,若是第一个参数是 boolean 型的,就对 deep 赋值 target,并把 target 向后移一位;若是参数对象只有一个,即对 jQuery 的扩展,就令 target 赋值 this,当前指针 i 减一。
这种方法逻辑虽然很复杂,可是带来一个很是大的优点:后面的处理逻辑只须要一个就能够。target 就是咱们要拷贝的目标,options 就是要拷贝的对象,逻辑又显得很是的清晰。
extend 函数还须要主要一点,jQuery.extend = jQuery.fn.extend
,不只 jQuery 对象又这个函数,连原型也有,那么如何区分对象是扩展到哪里了呢,又是如何实现的?
其实这一切都要借助与 javascript 中 this 的动态性,target = this
,代码就放在那里,谁去执行,this 就会指向谁,就会在它的属性上扩展。
再看 extend 源码,里面有一些函数,只是看名字知道了它是干什么的,我专门挑出来,找到它们的源码。
jQuery.isFunction = function (obj) { return jQuery.type(obj) === "function"; }
这也太简单了些。这里又要引出 jQuery 里一个重要的函数 jQuery.type
,这个函数用于类型判断。
首先,为何传统的 typeof 不用?由于很差用(此处应有一个哭脸):
// Numbers typeof 37 === 'number'; typeof 3.14 === 'number'; typeof(42) === 'number'; typeof Math.LN2 === 'number'; typeof Infinity === 'number'; typeof NaN === 'number'; // Despite being "Not-A-Number" typeof Number(1) === 'number'; // but never use this form! // Strings typeof "" === 'string'; typeof "bla" === 'string'; typeof (typeof 1) === 'string'; // typeof always returns a string typeof String("abc") === 'string'; // but never use this form! // Booleans typeof true === 'boolean'; typeof false === 'boolean'; typeof Boolean(true) === 'boolean'; // but never use this form! // Symbols typeof Symbol() === 'symbol' typeof Symbol('foo') === 'symbol' typeof Symbol.iterator === 'symbol' // Undefined typeof undefined === 'undefined'; typeof declaredButUndefinedVariable === 'undefined'; typeof undeclaredVariable === 'undefined'; // Objects typeof {a:1} === 'object'; // use Array.isArray or Object.prototype.toString.call // to differentiate regular objects from arrays typeof [1, 2, 4] === 'object'; typeof new Date() === 'object'; // The following is confusing. Don't use! typeof new Boolean(true) === 'object'; typeof new Number(1) === 'object'; typeof new String("abc") === 'object'; // Functions typeof function(){} === 'function'; typeof class C {} === 'function'; typeof Math.sin === 'function'; // This stands since the beginning of JavaScript typeof null === 'object';
能够看得出来,对于一些 new 对象,好比 new Number(1)
,也会返回 object。具体请参考typeof MDN。
网上有两种解决方法(有效性未经考证,请相信 jQuery 的方法),一种是用 constructor.name
Object.prototype.constructor MDN,一种是用 Object.prototype.toString.call()
Object.prototype.toString(),最终 jQuery 选择了后者。
var n1 = 1; n1.constructor.name;//"Number" var n2 = new Number(1); n2.constructor.name;//"Number" var toString = Object.prototype.toString; toString.call(n1);//"[object Number]" toString.call(n2);//"[object Number]"
以上属于科普,原理很少阐述,接下来继续看源码 jQuery.type
:
// 这个对象是用来将 toString 函数返回的字符串转成 var class2type = { "[object Boolean]": "boolean", "[object Number]": "number", "[object String]": "string", "[object Function]": "function", "[object Array]": "array", "[object Date]": "date", "[object RegExp]": "regexp", "[object Object]": "object", "[object Error]": "error", "[object Symbol]": "symbol" } var toString = Object.prototype.toString; jQuery.type = function (obj) { if (obj == null) { return obj + ""; } return typeof obj === "object" || typeof obj === "function" ? class2type[toString.call(obj)] || "object" : typeof obj; }
由于 jQuery 用的是 toString 方法,因此须要有一个 class2type 的对象用来转换。
这个函数用来判断对象是不是一个纯粹的对象,:
var getProto = Object.getPrototypeOf;//获取父对象 var hasOwn = class2type.hasOwnProperty; var fnToString = hasOwn.toString; var ObjectFunctionString = fnToString.call( Object ); jQuery.isPlainObject = function (obj) { var proto, Ctor; // 排除 underfined、null 和非 object 状况 if (!obj || toString.call(obj) !== "[object Object]") { return false; } proto = getProto(obj); // Objects with no prototype (e.g., `Object.create( null )`) are plain if (!proto) { return true; } // Objects with prototype are plain iff they were constructed by a global Object function Ctor = hasOwn.call(proto, "constructor") && proto.constructor; return typeof Ctor === "function" && fnToString.call(Ctor) === ObjectFunctionString; }
看一下效果:
jQuery.isPlainObject({});// true jQuery.isPlainObject({ a: 1 });// true jQuery.isPlainObject(new Object());// true jQuery.isPlainObject([]);// false jQuery.isPlainObject(new String('a'));// false jQuery.isPlainObject(function(){});// false
除了这几个函数以外,还有个 Array.isArray()
,这个真的不用介绍了吧。
总结仍是多说一点的好,如今已经基本理清 jQuery 内部的状况了?no,还差一点,看下面的代码:
(function(window) { // jQuery 变量,用闭包避免环境污染 var jQuery = (function() { var jQuery = function(selector, context) { return new jQuery.fn.init(selector, context, rootjQuery); }; // 一些变量声明 jQuery.fn = jQuery.prototype = { constructor: jQuery, init: function(selector, context, rootjQuery) { // 下章会重点讨论 } // 原型方法 }; jQuery.fn.init.prototype = jQuery.fn; jQuery.extend = jQuery.fn.extend = function() {};//已介绍 jQuery.extend({ // 一堆静态属性和方法 // 用 extend 绑定,而不是直接在 jQuery 上写 }); return jQuery; })(); // 工具方法 Utilities // 回调函数列表 Callbacks Object // 异步队列 Defferred Object // 浏览器功能测试 Support // 数据缓存 Data // 队列 Queue // 属性操做 Attributes // 事件系统 Events // 选择器 Sizzle // DOM遍历 Traversing // 样式操做 CSS(计算样式、内联样式) // 异步请求 Ajax // 动画 Effects // 坐标 Offset、尺寸 Dimensions window.jQuery = window.$ = jQuery; })(window);
能够看出 jQuery 很巧妙的总体布局思路,对于属性方法和原型方法等区分,防止变量污染等,都作的很是好。阅读框架源码只是开头,有趣的还在后面。
jQuery 2.0.3 源码分析core - 总体架构
《jQuery源码解析》读书笔记(第二章:构造jQuery对象)
jQuery.isPlainObject() 函数详解
本文在 github 上的源码地址,欢迎来 star。
欢迎来个人博客交流。