【前言】最近项目忙的脚不沾地,刚刚结束,准备整理一下以前写的一些学习笔记和技术文章。本文原是很久之前看jq源码时写的片段,隔了很久再看都忘得差不多了。简单整理出来,做个记录。
作为一名前端工程师,jQuery是我们熟的不能再熟的工具之一,其强大的功能和近乎完美的兼容封装使其成为前端领域必备的技能。用了很长时间的jq,却一直没有去探索学习过jQuery源码,似乎不算是一名合格的前端er。最近趁着空闲研究了一下,jQuery源码可谓是深邃如海,奥妙无穷,平时工作中偶尔需要封装工具,也不必像它这样面面俱到,但是简单学习其封装思路还是很有意义的。后面还要抽时间仔细学习每一块的源码。这里简单说一说我对jq框架封装的理解,并仿照着封装css的两个方法。
首先,jQuery的本质是一个封装了众多方法的库
这个库的框架是一个闭包
其最外层的框架或者说其骨架如下:
(function(window, undefined) {
var jQuery = function () {}
window.jQuery = window.$ = jQuery;//暴露全局
})(window)复制代码
可以很明显的看出其框架结构,在一个闭包(沙箱)中,所有的函数、方法都放在沙箱内部,有一个名为jQuery的函数,这是核心函数,所有的成员都围绕它运转。
为什么要传入window
和undefined
,原因很简单,暴露局部变量为全局,自不必说,还有一方面是为了精简代码,减少变量检索时间,window作为形参直接在函数作用域内被检索到,无需每次再向上查找全局,极大地节省了性能提高了效率。
至于undefined
,这玩意是为了处理IE8以下的一个小问题,在这些老式浏览器中,undefined是可以作为变量名并被重新赋值的,但是新式浏览器已经不支持这种做法。这里传入undefined就是为了防止undefined被重新赋值。
此时,我们可以在外部直接new一个jQuery的实例对象:
console.log(new jQuery())//空对象,只有一个__proto__属性复制代码
但是显然,这个对象和我们实际使用的jq对象相去甚远,甚至看不到相似之处,别着急,一步步来。
我们的目标是实现类似jq的方式,在外部可拿到jq对象如$('div'),这个jq对象简化一下,大致是如下结构:
['div','div','div',length:3]复制代码
这就意味着,我们必须传入一个元素选择器,在构造函数内接收,通过一些方法,返回出来这么一个对象。
因此,我们先做一个并不严谨的假设,假设jQuery是一个构造函数,通过它来创建对象。
在沙箱内部的jQuery‘构造函数’内,我们传入一个选择器,然后就可以通过构造函数new出对象来:
var jQuery = function (selector) {
var ele = document.querySelectorAll(selector);
Array.prototype.push.apply(this, ele);
}复制代码
docuemnt.querySelectorAll
获取的是一个‘伪数组’或者说集合,得到的dom对象都存放在ele上面,但是我们需要在jq对象上拿到这些对象,所以我们必须手动的将这些对象添加到实例上。借用数组的push
方法,不仅能够方便快捷的达到目的,而且数组的length属性可以自动更新。这是一个小技巧。
这里的this指向,毫无疑问就是jQuery的实例对象,因此,我们只需要在jQuery的原型上添加方法,就可以使外部的实例对象访问到这些方法,这已经非常接近jq的思路了。
在此基础上,我们简单给原型上添加两个方法css和html:
jQuery.prototype.css=function(){
console.log('hello css')
},
jQuery.prototype.html=function(){ console.log('hello html')
},
...复制代码
通过给原型添加方法,jQuery实例对象可以直接访问使用这些方法,但是这么做似乎太麻烦了些,如果有几十上百个方法,每次都这么添加,代码太过冗余。
因此,使用原型替换的思想,改变原型指向到某个对象上,给这个对象添加方法。可以节省很多代码。
jQuery.fn = jQuery.prototype = {
constructor: jQuery, // 手动添加了丢失的constructor属性
css: function() {
console.log("css is ok again");
},
html: function() {
console.log("html is ok again");
}
}复制代码
其中,为了书写方便,我们将jQuery的原型jQuery.prototype赋值给jQuery‘构造函数’的一个属性 jQuery.fn ,后者可以完全代替前者。
至此,我们在外面new 一个jQuery对象就可以拿到一个接近原版的jq对象了,也可以访问原型链上的方法和属性。但是似乎还有哪里不对,new对象这个操作似乎应该在内部完成?没错,我们继续完善它。
在进一步完善我们的小jQuery之前,我们要打个岔,回忆一下工厂函数是怎么回事。
关于工厂函数:
作用:创建实例对象,然后把实例对象给返回出去。
function Person(name, age){
this.name = name;
this.age = age;
}
//上面是构造函数,我们创建对象的做法:
var xm = new Person("xm", 20)
console.log(xm);//创建了xm对象
var xh = new Person("xh", 21);
console.log(xh);//创建了xh对象复制代码
将这个过程封装一下:
function $(name, age){//$就是工厂函数
return new Person(name, age);
}// 省去外部的new操作,还能得到实例对象
var xm = $("xm", 20);
console.log(xm);
var xh = $("xh", 22);
console.log(xh);//得到两个实例对象复制代码
可见,封装好的工厂函数可以通过直接调用,批量创建对象出来。我们回到jQuery的话题来
按照这个思路,我们将jQuery函数也封装一下,使之变成工厂函数:
var jQuery = function (selector) {
return new jQuery(selector);
}复制代码
完成了吗?似乎完成了?但是又好像有哪里不对,怎么看着这么眼熟,这不是隔壁的递归函数吗,自己调自己,把自己玩死了。so。。?难道jQuery不能当做工厂函数吗?那么问题来了,他不做谁能做呢?或者,他不是构造函数?听着有点乱,但还真被我们蒙对了!
实际上,在jQuery中,真正的构造函数,并不是jQuery函数!我们先前的假设要改一改了。
真正的构造函数,另有其人,不兜圈子了,直接上结论:jQuery函数的真正作用是“工厂函数”,正牌儿构造函数是jQuery.fn.init
这个jQuery.fn.init是什么鬼?怎么就把正主jQuery赶下位上台了呢?
直接上结论:这个jQuery.fn.init其实是jQuery函数的原型上的一个方法,它是真正的构造函数,通过它创建对象。
那么我们前面的代码要改改了。该挪窝的挪窝,该上位的上位:
var jQuery = function (selector) {
return new jQuery.fn.init(selector);
}复制代码
init函数去它该去的地方:
jQuery.fn = jQuery.prototype = {
constructor: jQuery, // 手动添加了constructor属性
init: function(selector) {
var ele = document.querySelectorAll(selector); // this??? ==> init的实例对象
Array.prototype.push.apply(this, ele);
}
}复制代码
经过这么一改,this的指向发生了变化,原本存放在jQuery原型上的dom对象们,现在变成了init的儿子,this指向了init。但是我们的方法都是存放在jQuery原型上的,难道还要手动搬回来?算了算了,太麻烦,还好有原型链这个好东西。手绘了一张草图,将就看一下:
init的实例对象想要使用jQuery的方法,丝毫不难,只需要改变原型链指向即可,将自己的原型指向由原本指向init.prototype改为指向jQuery.prototype即可,结果:
代码层面即一句话:
jQuery.fn.init.prototype = jQuery.fn;复制代码
至此,我们的jQuery架构基本搭建完毕。此时,在沙箱外面,不需要手动new了,直接调用$(),例如$('div'),已经得到了和原版jQuery一样的对象。
剩下的就是添砖加瓦,封装一些方法了,我们以css方法为例,做个简单的封装。
完整代码如下:
(function(window, undefined) {
var jQuery = function(selector) { //jQuery是工厂函数
return new jQuery.fn.init(selector); //传入选择器,实例化对象
} //参数selector会传入init
jQuery.fn = jQuery.prototype = {//工厂函数的原型
constructor: jQuery,
init: function(selector) { //由jQuery一路传来的形参
var ele = document.querySelectorAll(selector); //实现获取对象。
// this ==> init的实例对象
// 把获取到的元素添加到init的实例对象上
Array.prototype.push.apply(this, ele);
},
css: function(name, value) {// 通过判断参数的个数就能确定css方法要实现什么功能
if (arguments.length === 2) {
// 设置单个样式
// 是把获取到的所有元素都设置上这个样式
// this ==>$("p"); 伪数组,有length属性,是需要把伪数组中每一项都设置上样式
for (var i = 0; i < this.length; i++) {
this[i].style[name] = value;
}
}else if(arguments.length === 1){
//说明是个对象 设置多个样式 || 获取样式
if (typeof name === "object") {
// 设置多个样式 需要给获取到的所有元素都设置上多个样式
for(var i = 0; i < this.length; i++){
//this[i] ==> 每一个元素
// 循环的是对象,是设置的样式和样式值
for(var k in name){
this[i].style[k] = name[k];
}
}
}else if(typeof name === "string"){
// 获取样式 注意点: 获取第一个元素对应的值
// this ==>$("p") this[0] ==> 获取到的元素中的第一个元素
// style 操作的是行内样式
//window.getComputedStyle(元素, null); 获取在元素上其效果的样式
// 返回值: 是一个对象
return window.getComputedStyle(this[0], null)[name];
}
}
return this;// 目的:实现链式编程
}
}
// 修改init的原型对象 目的是为了让init的实例对象可以访问jq上的方法
jQuery.fn.init.prototype = jQuery.fn;
window.jQuery = window.$ = jQuery;
})(window)复制代码
css方法的封装略显繁琐,但css这玩意不一直都这样么,在js中处理css,吃力又难受。但也没啥好办法,还好这部分没什么难度。
但是有两点仍然是需要我们注意并且必须做到的,就是关于jQuery的两个主要特点:
隐式迭代和链式编程。
前者使jq所设置的所有样式对所有获取到的对象都起作用,后者则要求在方法的封装结尾,必须返回该对象,以供连续调用实现链式编程。如果封装方法没做到这两点,那么封装出来的也就跟jq没啥关系了,这是需要格外注意的。
【结语】本文是很久以前学习时写的片段整理而成,限于个人水平,对jq封装的思想可能理解的不够深入,可能有许多地方说的不够严谨或者似是而非,还请看官大佬们不吝指出。感谢。