从本篇开始会陪你们一块儿从零开始走一遍 jQuery 的奇妙旅途,在整个系列的实践中,咱们会把 jQuery 的主要功能模块都了解和实现一遍。css
这会是一段很长的历程,但也会颇有意思 —— 做为前端领域的经典之做,jQuery 里有着太多奇思妙想,若是可以深刻理解它,对于咱们稳固js基础、提高前端大法技能来讲大有裨益。前端
另外,本系列的相关代码都可以从 个人github 上获取到。jquery
1. 免 new 实现git
咱们在使用不少插件的时候,都须要使用 new XXX() 的写法来实例化一个引用:github
var list = new Slip(document.getElementById('slip'), { //options });
jQuery 一样做为一个面向对象的工具库,在咱们建立一个实例时却无需使用 new 语法,节省了一些代码量:api
var $div = $('div'); //不须要以下写法: //var $div = new $('div');
这种便捷的形式依赖了工厂模式,其实现很是简单,把 new 封装在库内便可,让每次调用 jQuery() 时自行在内部进行一次实例化:ide
(function() { var _jQuery = window.jQuery, _$ = window.$; var version = "0.0.1", jQuery = function(selector) { console.log(document.querySelector(selector)) }; jQuery.prototype = { jquery: version, constructor: jQuery }; window.$ = window.jQuery = function(selector) { return new jQuery(selector); //notice here~ }; })();
留意这里咱们走的 IIFE 形式,让 jQuery 代码库造成本身的做用域,避免污染外部变量。模块化
因而乎以上就是咱写的第一个 JQ 雏形,简单跑一下:函数
<div></div> <script> var $div = $('div'); //<div></div> console.log($div.jquery); //0.0.1 </script>
别忘了后续咱们还但愿能经过 $.extend / $.fn.extend 来扩展 JQ 的静态方法和原型方法,咱们把出口方法抽出来增长这个 extend 的API:工具
function Factory(selector){ //抽出构造函数 return new jQuery(selector); } Factory.fn = jQuery.prototype; Factory.extend = Factory.fn.extend = function(){ console.log(this) }; window.$ = window.jQuery = Factory;
这样咱们也能直接经过 $.fn.jquery 来获取当前 JQ 版本号了。
若是但愿能够经过 $.prototype 直接访问 jQuery 的原型对象,再修改下这句代码便可:
Factory.prototype = Factory.fn = jQuery.prototype;
2. 写法优化
事实上咱们不太喜欢再写多一个冗余的 Factory 构造函数来做为 window.jQuery 的引用,也不喜欢(在模块内部)使用 Factory.extend() 来扩展 JQ,它听起来和 JQ 没有半毛钱关系。
若是能够,直接把 jQuery 方法做为接口输出,且在模块内部能以 jQuery.extend() 的形式来调用扩展接口,这样的形式更佳。
也就是说咱们但愿代码应该是这样写的:
jQuery.extend = jQuery.fn.extend = function(){ console.log(this) }; window.jQuery = window.$ = jQuery;
“直接把 jQuery 方法做为接口输出”意味着咱们要把工厂模式挪入 jQuery 方法中,显然咱们不能这样改:
var version = "0.0.1", jQuery = function (selector) { return new jQuery(selector); };
这样死循环了,调用栈会直接爆掉~
因而咱们能够抽出一个 init 方法来作初始化处理(好比简单地注入检索到的元素到JQ对象中),把 jQuery 方法中的内容更改成 return new init(selector) 就好了。
保证两个前提:
1. this 指向 jQuery 上下文 2. 其原型指向 jQuery 的原型
第一点很好理解,方便咱们直接在 init 方法中经过对 this 的操做来处理 JQ 实例上下文,如:
//注入元素到 JQ 实例对象中 this[0] = elem; this.length = 1;
针对这点,咱们不妨把 init 做为 jQuery.prototype 的属性方法来实现:
var version = "0.0.1", jQuery = function(selector) { return new jQuery.fn.init() //修改点1 }; //方便咱们使用 jQuery.fn 来引用 jQuery 原型对象 jQuery.fn = jQuery.prototype = { jquery: version, constructor: jQuery }; //修改点2 —— init 做为原型方法,确保 this 指向正确 jQuery.fn.init = function( selector ) { if ( !selector ) { return; } else { var elem = document.querySelector( selector ); if ( elem ) { this[0] = elem; this.length = 1; } } }; jQuery.extend = jQuery.fn.extend = function(){ console.log(this) }; window.$ = window.jQuery = jQuery;
然而这时候存在一个问题 —— JQ实例对象没法访问原型属性/方法:
var $div = $('div'); console.log($div.jquery); //undefined
缘由很简单——咱们还未实现上述说起的第二个前提——“init 原型指向 jQuery 的原型”
在 js 中,实例的内部原型(__proto__)老是指向其构造函数的原型(prototype),而通过咱们这番修改,JQ实例的构造函数已经变成了 jQuery.fn.init ,而其原型并不是指向 jQuery 的原型,这致使 JQ 实例没法顺其原型链爬取到 jQuery.prototype。
要实现这个条件,只须要作小小改动——把 jQuery.fn.init 的原型指向 jQuery 的原型(jQuery.prototype / jQuery.fn)便可:
var init = jQuery.fn.init = function( selector ) { if ( !selector ) { return; } else { var elem = document.querySelector( selector ); if ( elem ) { this[0] = elem; this.length = 1; } } }; init.prototype = jQuery.fn; //修改点
这里贴下完整代码:
var version = "0.0.1", jQuery = function(selector) { return new jQuery.fn.init(selector) }; jQuery.fn = jQuery.prototype = { jquery: version, constructor: jQuery }; var init = jQuery.fn.init = function( selector, context, root ) { if ( !selector ) { return; } else { var elem = document.querySelector( selector ); if ( elem ) { this[0] = elem; this.length = 1; } } }; init.prototype = jQuery.fn; jQuery.extend = jQuery.fn.extend = function(){ console.log(this) }; window.$ = window.jQuery = jQuery;
3. 链式写法实现
JQ 里一个很大的亮点是,它支持链式写法,调用起来很是方便:
$('div').removeClass('hide').css('width', '100px')
其实现其实很是简单 —— 确保每一个调用的方法尾部均返回自身便可,这里咱们新增两个实例方法作示例:
jQuery.fn = jQuery.prototype = { jquery: version, constructor: jQuery, setBackground: function(){ this[0].style.background = 'yellow'; return this //返回自身引用 }, setColor: function(){ this[0].style.color = 'blue'; return this //返回自身引用 } }; var init = jQuery.fn.init = function( selector ) { if ( !selector ) { return this; } else { var elem = document.querySelector( selector ); if ( elem ) { this[0] = elem; this.length = 1; } return this; } };
链式调用:
<div>hello world</div> <script> var $div = $('div'); $div.setBackground().setColor(); </script>
效果以下,杠杠的:
4. 冲突处理
存在某些状况,用户可能并不想拿 window.$ 甚至 window.jQuery 来引用 JQ 接口,或者已经有其它库使用了 window.$ 这个变量,若是咱们粗暴地改变其引用确定是不合理的。
so 咱们来实现 JQ 中冲突处理的静态接口 jQuery.noConflict,这意味着在代码段开始时,就得先保存下当前 window.$ 和 window.jQuery 两个变量:
(function(){ var _jQuery = window.jQuery, _$ = window.$; //var version = "0.0.1"...... })()
而后是实现 noConflict 方法,退耕还林,把保存的变量吐回去便可:
(function(){ var _jQuery = window.jQuery, _$ = window.$; //var version = "0.0.1"...... jQuery.noConflict = function( deep ) { //确保window.$没有再次被改写 if ( window.$ === jQuery ) { window.$ = _$; } //确保window.jQuery没有再次被改写 if ( deep && window.jQuery === jQuery ) { window.jQuery = _jQuery; } return jQuery; //返回 jQuery 接口引用 }; window.jQuery = window.$ = jQuery; })();
deep 参数类型为 Boolean,若为真,表示要求连window.jQuery 变量都须要吐回去。
留意在尾部咱们返回了 jQuery 的接口引用,这意味着咱们能够以
var $$$ = jQuery.noConflict()
的形式来把它赋予新的变量。
接着在外部运行以下代码:
<head> <meta charset="UTF-8"> <title>DIY A JQ</title> <script> $ = 'old $'; jQuery = 'old JQ' </script> <script src="jQuery.js"></script> </head> <body> <div>hello world</div> <script> var $div = $('div'); $div.setBackground().setColor(); var $$$ = $.noConflict(true); console.log($); console.log(jQuery); console.log($$$); </script>
输出以下:
第一篇就写到这里,相关的代码能够从 个人github 上下载到。
下次咱们会试着实现模块化的写法,并与时俱进,改用 ES6解构赋值语法 + Rollup 来进行打包以减小可能存在的冗余代码段。
共勉~