摘要:不造轮子的程序员不是好程序员,所以我们今天尝试造一下轮子。今天的主角是 jQuery ,虽然现在市面上已被 React,Angular,Vue 等挤的容不下它的位置,但是它的简单 API 设计依然优秀,值得学习和体会。希望阅读本篇文章以后大家有所收获,加深大家对jQuery的理解。
不造轮子的程序员不是好程序员,所以我们今天尝试造一下轮子。今天的主角是 jQuery ,虽然现在市面上已被 React,Angular,Vue 等挤的容不下它的位置,但是它的简单 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左右
代码有点多,我们先梳理一下结构,找个入口开始看。
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.jQuery 和 window.$ 所以我们在使用 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 对应的 html、id 和 class 等不同选择器的处理方式,并返回一个类数组对象。
看到这我们就能实现我们今天任务第一个目标以及第二个目标的 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,从而又可以访问自己的原型了,这样的就节省代码量,提高代码的效率,代码看起来更优雅。
本文由职坐标整理发布,欢迎关注职坐标WEB前端jQuery频道,获取更多jQuery知识!
您输入的评论内容中包含违禁敏感词
我知道了
请输入正确的手机号码
请输入正确的验证码
您今天的短信下发次数太多了,明天再试试吧!
我们会在第一时间安排职业规划师联系您!
您也可以联系我们的职业规划师咨询:
版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
沪公网安备 31011502005948号