【前言】最近專案忙的腳不沾地,剛剛結束,準備整理一下以前寫的一些學習筆記和技術文章。本文原是很久之前看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封裝的思想可能理解的不夠深入,可能有許多地方說的不夠嚴謹或者似是而非,還請看官大佬們不吝指出。感謝。