是否曾對Mootools的魔力感到驚奇?是否有想知道Dojo如何作到那樣的?是否對jQuery感到好奇?在這個教程中,咱們將瞭解它們背後的東西而且動手建立一個超級簡單的你最喜歡的庫。javascript
咱們其乎天天都在使用JavaScript庫。當你剛入門時,利用jQuery是一件很是奇妙的事,主要是由於它的DOM操做。首先,DOM對於入門者來講多是相對困難的事情;其次用它咱們幾乎能夠不用考慮跨瀏覽器兼容的問題。html
在這個教程中,咱們將試着從頭開始實現一個很簡單的庫。是的,它很是有意思,可是在你高興以前讓我申明幾點:java
這不會是全功能的庫。咱們有不少方法要寫,可是它不是jQuery。咱們將會作足工做來讓你感覺到在你建立一個庫時會遇到的各類問題。node
咱們不會徹底解決全部瀏覽器的兼容性問題。咱們寫的代碼能支持IE8+,Firefox 5+,Opera 10+,Chrome和Safari。express
咱們不會覆蓋使用咱們庫的全部可能性。好比咱們的append和prepend方法只在你傳入一個咱們庫的實例時纔有效,它們不支持原生的DOM節點或節點集合。數組
咱們以一些封裝代碼開始,它將會包含咱們整個庫。它就是你常常用到的當即執行函數表達式。瀏覽器
window.dome = (function () { function Dome (els) { } var dome = { get: function (selector) { } }; return dome; }());
如你所見,咱們把咱們的庫叫Dome,由於它主要就是一個針對DOM的庫,是的,它很不完整。app
到此咱們作了兩件事。首先,咱們定義了一個函數,它最終會是實例化咱們庫的構造函數,這些對象將會封裝咱們選擇或建立的元素。dom
接下來咱們建立了dome對象,它是咱們實際的庫對象;你能看到,它在最後被返回。它有一個空的get函數,咱們將用它來從頁面中選擇元素。因此,讓咱們如今來填充它的代碼。函數
dome.get函數傳入一個參數,可是它能夠有好幾種狀況。若是它是一個字符串,咱們假定它是一個CSS選擇器;可是咱們也能夠傳入單個DOM節點或是一個NodeList。
get: function (selector) { var els; if (typeof selector === "string") { els = document.querySelectorAll(selector); } else if (selector.length) { els = selector; } else { els = [selector]; } return new Dome(els); }
咱們使用document.querySelectorAll來簡化元素的查找:固然這有瀏覽器兼容性問題,可是對於咱們的例子來講它是ok的。若是 selector不是字符串,咱們將檢查它的length屬性。若是它存在,咱們就知道它是一個NodeList;不然它是單個元素而後咱們將它放到一個數組中。這就是咱們下面須要將調用Dome的結果傳給一個數組的緣由;你能夠看到咱們返回一個新的Dome對象。因此讓咱們回頭看看Dome函數並填充它。
下面是Dome函數:
function Dome (els) { for(var i = 0; i < els.length; i++ ) { this[i] = els[i]; } this.length = els.length; }
它確實很簡單:咱們只是遍歷咱們選擇的元素並把它們附到帶有數字索引的新對象中。而後咱們添加一個length屬性。
可是這的關鍵是什麼呢?爲何不直接返回元素?咱們將元素封裝到一個對象由於咱們想爲這個對象建立方法;這些方法可讓咱們與這些元素交互。這實際上就是jQuery採用的方法的簡化版本。
因此,咱們返回了Dome對象,讓咱們在它的原型上添加一些方法。我把這些方法直接寫在Dome函數中。
咱們要寫的第一個方法是一個簡單的工具函數。由於咱們的Dome對象能夠封裝多個DOM元素,幾乎每一個方法都須要遍歷每一個元素;因此,這些工具函數會很是便利。
讓咱們以一個map函數開始:
Dome.prototype.map = function (callback) { var results = [], i = 0; for ( ; i < this.length; i++) { results.push(callback.call(this, this[i], i)); } return results; };
固然,map函數傳入單個參數,一個回調函數。咱們遍歷數組中的每一項,收集回調函數返回的全部內容放到results數組中。注意咱們如何調用回調函數:
callback.call(this, this[i], i));
這樣函數就會在咱們的Dome實例的上下文中被調用,它接受兩個參數:當前元素,以及索引號。
咱們也想要一個forEach函數。它確實很是簡單:
Dome.prototype.forEach(callback) { this.map(callback); return this; };
map和forEach間的惟一區別是map須要返回一些東西,所以咱們也能夠只傳入咱們的回調函數給this.map並忽略返回的數組,咱們將返回 this來使得咱們的庫支持鏈式操做。咱們將常用forEach。因此,注意當返回咱們的this.forEach對函數的調用時,咱們事實上是返回了this。例如,下面的方法實際上返回相同的東西:
Dome.prototype.someMethod1 = function (callback) { this.forEach(callback); return this; }; Dome.prototype.someMethod2 = function (callback) { return this.forEach(callback); };
另外:mapOne。很容易看出這個函數是幹什麼的,可是問題是爲何咱們須要它?它須要一些你能夠叫作「庫哲學」的東西來解釋。
若是建立一個庫只是寫代碼,那就不是什麼難的工做了。可是我正在作這個項目,我發現困難的部分是決定一些方法應該如何工做。
很快,咱們將建一個text方法,它返回咱們選擇元素的文本。若是咱們的Dome對象封裝幾個DOM節點(如dome.get("li")),它會返回什麼呢?若是你在jQuery作相似的事情($("li").text()),你將會獲得一個全部元素的文本拼起來的字符串。它有用嗎?我認爲沒用,可是我不知道更好的返回是什麼。
在這個項目中,我將以數組形式返回多個元素的文本,除非數組中只有一個元素,那咱們就返回一個文本字符串,而不是隻有一個元素的數組。我想你最經常使用的是獲取單個元素的文本,因此咱們對這個狀況進行優化。然而,若是你獲取多個元素的文本,咱們也會返回一些你能操做的東西。
因此,mapOne方法只是簡單的運行map,而後要麼返回數組,要麼返回單元素數組中的元素。若是你仍是不肯定這有什麼用,等一會你會發現的!
Dome.prototype.mapOne = function (callback) { var m = this.map(callback); return m.length > 1 ? m : m[0]; };
接下來,讓咱們添加text方法。就像jQuery同樣,咱們能夠給它傳入一個字符串並設置元素的文本,或不傳參數來獲取元素的文本。
Dome.prototype.text = function (text) { if (typeof text !== "undefined") { return this.forEach(function (el) { el.innerText = text; }); } else { return this.mapOne(function (el) { return el.innerText; }); } };
Dome.prototype.text = function (text) { if (typeof text !== "undefined") { return this.forEach(function (el) { el.innerText = text; }); } else { return this.mapOne(function (el) { return el.innerText; }); } };
你可能也想到了,咱們須要檢查text的值來看它是要設置仍是要獲取。注意若是隻是用if(text)會有問題,由於空字符串會被判斷爲false。
若是咱們在設置值,咱們將對元素調用forEach而且設置它們的innerText屬性爲text。若是咱們要獲取,咱們將返回元素的 innerText屬性。注意咱們使用mapOne方法:若是咱們在處理多個元素,它將返回一個數組,不然它將就是一個字符串。
html方法幾乎與text同樣,除了它使用innerHTML屬性而不是innerText。
Dome.prototype.html = function (html) { if (typeof html !== "undefined") { this.forEach(function (el) { el.innerHTML = html; }); return this; } else { return this.mapOne(function (el) { return el.innerHTML; }); } };
就像我說的:幾乎徹底同樣。
再接下來,咱們但願能添加和刪除樣式,所以讓咱們來寫一個addClass和removeClass方法。
咱們的addClass方法將接收一個字符串或是樣式名稱的數組。爲了作到這點,咱們須要檢查參數的類型。若是是數組,咱們將遍歷它並建立一個樣式名的字符串。不然,咱們就簡單的在樣式名前加一個空格,這樣它就不會和元素已有的樣式混在一些。而後咱們遍歷元素而且將新的樣式附加到className屬性後面。
Dome.prototype.addClass = function (classes) { var className = ""; if (typeof classes !== "string") { for (var i = 0; i < classes.length; i++) { className += " " + classes[i]; } } else { className = " " + classes; } return this.forEach(function (el) { el.className += className; }); };
很直接,對嗎?
那如何刪除樣式呢?爲了保持簡單,咱們只容許一次刪除一個樣式。
Dome.prototype.removeClass = function (clazz) { return this.forEach(function (el) { var cs = el.className.split(" "), i; while ( (i = cs.indexOf(clazz)) > -1) { cs = cs.slice(0, i).concat(cs.slice(++i)); } el.className = cs.join(" "); }); };
對每一個元素,咱們將el.className分隔成一個數組。而後,咱們使用一個while循環來剔除咱們傳入的樣式,直到 cs.indexOf(clazz)返回-1。咱們這樣作是爲了處理一樣的樣式在一個元素中出現的不止一次的特殊狀況:咱們必須保證它真的被刪除了。一旦咱們確保刪除每一個樣式的實例,咱們用空格鏈接數組的每一項並把它設置到el.className。
咱們正在處理的最糟糕的瀏覽器是IE8。在咱們的小小的庫中,只有一個IE bug須要咱們處理,很幸運它很簡單。IE8不支持Array的indexOf方法;咱們在removeClass中使用到它,因此讓咱們修復它:
if (typeof Array.prototype.indexOf !== "function") { Array.prototype.indexOf = function (item) { for(var i = 0; i < this.length; i++) { if (this[i] === item) { return i; } } return -1; }; }
它很是簡單,而且這不是一個徹底的實現(不支持第二個參數),可是能達到咱們的目的。
如今,咱們想要一個attr函數。這很容易,由於它與咱們的text或html方法很是相似。像那些方法同樣,咱們可以獲取或設置屬性值:咱們能夠傳入元素名和值來設置,也能夠只傳入屬性名來獲取。
Dome.prototype.attr = function (attr, val) { if (typeof val !== "undefined") { return this.forEach(function(el) { el.setAttribute(attr, val); }); } else { return this.mapOne(function (el) { return el.getAttribute(attr); }); } };
若是val有一個值,咱們將遍歷這些元素而且將選擇的屬性設置爲這個值,使用元素的setAttribute方法。不然,咱們使用mapOne經過getAttribute方法來返回屬性值。
像不少好的庫同樣,咱們應該可以建立新的元素。固然它做爲一個Dome實例的一個方法不是很好,因此讓咱們直接把它掛到dome對象上去。
var dome = { // get method here create: function (tagName, attrs) { } };
你已經看到,咱們使用兩個參數:元素的名字,和屬性值對象。大部分屬性能過attr方法賦值,可是兩種方法能夠作特殊處理。咱們使用addClass 方法操做className屬性,以及text方法操做text屬性。固然,咱們首先須要建立元素和Dome對象。下面是整個操做的代碼:
create: function (tagName, attrs) { var el = new Dome([document.createElement(tagName)]); if (attrs) { if (attrs.className) { el.addClass(attrs.className); delete attrs.className; } if (attrs.text) { el.text(attrs.text); delete attrs.text; } for (var key in attrs) { if (attrs.hasOwnProperty(key)) { el.attr(key, attrs[key]); } } } return el; }
咱們建立元素並將它傳給一個新的Dome對象。而後中咱們處理屬性。注意在操做完它們後咱們必須刪除className和text屬性。這樣能夠避免當咱們在attrs中遍歷剩下的key值時被應用爲屬性。固然咱們最後要返回這個新建的Dome對象。
可是如今只是建立了新的元素,咱們但願把它插入到DOM中對嗎?
下一步,咱們將寫append和prepend方法。這些確實是有點難搞的函數,主要是由於有不少種使用狀況。如下是咱們但願能作到的:
dome1.append(dome2);
dome1.prepend(dome2);
使用狀況以下:咱們可能想要append或prepend
一個新的元素到一個或多個已存在的元素
多個新元素到一個或多個已存在的元素
一個已存在的元素到一個或多個已存在的元素
多個已存在的元素到一個或多個已存在的元素
注意:我使用「新」來表示元素尚未在DOM中;已存在的元素是已經在DOM中有的。
讓咱們一步一步來:
Dome.prototype.append = function (els) { this.forEach(function (parEl, i) { els.forEach(function (childEl) { }); }); };
咱們指望els參數是一個Dome對象。一個完整的DOM庫能夠接受一個節點或nodelist做爲參數,可是咱們暫時不這樣作。咱們必須遍歷咱們每個元素,而且在它裏面,咱們還要遍歷每一個咱們須要append的元素。
若是咱們將els到多個元素,咱們須要克隆它們。然而,咱們不想在他們第一次被附加的時候克隆節點,而時隨後再說。因此咱們這樣:
if (i > 0) { childEl = childEl.cloneNode(true); }
這個i來自外層的forEach循環:它是當前父元素的索引。若是咱們不是附加到第一個父元素,咱們將克隆節點。這樣,真正的節點將會放到第一個父節點中,其它父節點將得到一個拷貝。這樣很好用,由於傳入的Dome對象將只會擁有原始的節點。因此若是咱們只是附加單個元素到單個元素,使用的全部節點都將是各自Dome對象的一部分。
最後,咱們終於能夠附加元素:
parEl.appendChild(childEl);
因此,彙總起來是這樣
Dome.prototype.append = function (els) { return this.forEach(function (parEl, i) { els.forEach(function (childEl) { if (i > 0) { childEl = childEl.cloneNode(true); } parEl.appendChild(childEl); }); }); };
prepend
方法咱們想要prepend方法也知足一樣的狀況,因此這個方法很是相似:
Dome.prototype.prepend = function (els) { return this.forEach(function (parEl, i) { for (var j = els.length -1; j > -1; j--) { childEl = (i > 0) ? els[j].cloneNode(true) : els[j]; parEl.insertBefore(childEl, parEl.firstChild); } }); };
當prepend時所不一樣的是若是你順次prepend一系列元素到另一個元素時,它們是倒序的。由於咱們不能反向forEach,我將使用for循環反向遍歷。一樣,咱們將克隆節點若是它不是咱們第一個要附件到的父節點。
對於咱們最後一個節點處理方法,咱們想要從DOM中刪除節點。其實很簡單:
Dome.prototype.remove = function () { return this.forEach(function (el) { return el.parentNode.removeChild(el); }); };
就是遍歷節點並在每一個元素的parentNode上調用removeChild方法。這裏漂亮的地方在於這個Dome對象還將正常工做;咱們能夠在它上面使用任何方法,包括從新放回到DOM中去。
最後,可是確定不是用得最少的,咱們將寫一些函數處理事件。你能夠知道,IE8使用老式的IE事件,因此咱們須要檢查它。同時,咱們將拋出DOM 0事件,就由於咱們能夠。
簽出方法,而後中咱們將討論它:
Dome.prototype.on = (function () { if (document.addEventListener) { return function (evt, fn) { return this.forEach(function (el) { el.addEventListener(evt, fn, false); }); }; } else if (document.attachEvent) { return function (evt, fn) { return this.forEach(function (el) { el.attachEvent("on" + evt, fn); }); }; } else { return function (evt, fn) { return this.forEach(function (el) { el["on" + evt] = fn; }); }; } }());
在這,咱們使用了一個當即執行函數表達式,在函數裏面咱們作了特徵檢查。若是document.addEventListener存在,咱們將使用它;不然咱們檢查document.attachEvent或者求助於DOM 0事件。注意咱們如何返回最後的函數:它將在結束時被賦給Dome.prototype.on。當作特徵檢測時,很是方便地像這樣賦給合適的函數,而不是每次函數運行時都得檢查一次。
off函數用於卸載事件,它與前面很是相似。
Dome.prototype.off = (function () { if (document.removeEventListener) { return function (evt, fn) { return this.forEach(function (el) { el.removeEventListener(evt, fn, false); }); }; } else if (document.detachEvent) { return function (evt, fn) { return this.forEach(function (el) { el.detachEvent("on" + evt, fn); }); }; } else { return function (evt, fn) { return this.forEach(function (el) { el["on" + evt] = null; }); }; } }());
我但願你能試一試咱們的小小的庫,而且能稍稍擴展一點點。
讓我再申明一下,這個教程的目的不是說建議你老是要寫一個本身的庫。
有專業的團隊在作一個龐大的,穩定的愈來愈好的庫。這裏咱們只是想讓你們看看一個庫內部是什麼樣子的,但願你能在這學到一些東西。
本文轉載自:http://code.tutsplus.com/tutorials/build-your-first-javascript-library--net-26796