文章來源:小青年原創
發佈時間:2016-07-03
關鍵詞:JavaScript,原型鏈,jQuery類庫
轉載需標註本文原始地址: http://zhaomenghuan.github.io...javascript
寫這篇文章的目的很簡單,就是想把以前一些不太清晰的概念梳理一下,網上這類教程不少,可是本文儘量還原問題本質,注意知識點之間的聯繫。相信看過我前面的博客的朋友必定知道我寫文章的風格了,儘量詳盡,並且不是隻是爲了解決某一個小問題而寫,方便你們知識點更體系,一篇內容其實至關於一章節的內容,容量有點大,我也不是一天完成的,通常是一週時間左右,因此你們閱讀的話可能也須要一些時間纔能有所收穫。做爲進階教程,本文將簡要講述JavaScript面向對象編程的內容,可是不會介紹什麼是接口,什麼是對象,什麼是對象屬性,什麼是對象方法,可是相信你看完了後天然理所固然的理解了這些基本概念。css
在開始學習以前咱們首先用一個工具,就是瀏覽器自帶的開發者工具控制檯。我這裏用hbuider直接打開這個工具,在【web瀏覽器】預覽工具欄右鍵單擊會彈出一個框框,在這個框框中選擇【Console】,而後在命令行輸入js代碼咱們就能夠看到執行結果,這裏咱們先輸入一個window
,而後會發現有json
結構的內容。html
咱們若是不用控制檯,直接用document.write(window);
,頁面上寫出:[object Window]
,很顯然內容不及這裏直觀,因此咱們後面的不少內容會在控制檯演示。熟練使用這個工具會給咱們的開發帶來不少好處,我想不少新手吐槽JavaScript不易調試,其實不少時候是他們不會調試。java
不少博客在文章一開始講一大堆理論,爲了不落入俗套,咱們這裏先作幾個有趣的實驗,咱們在控制檯繼續輸入window.top
,window.window
甚至window.window.window
,...,咱們會發現獲得的結果竟然如出一轍。node
> window === window.window // true > window === window.top // true > window.top === window.window // true > window.window === window.window.window // true
是否是有點暈了,這是什麼鬼。。。不要急,咱們接着看,咱們來個更暈的,哈哈!git
> window // Window {top: Window, window: Window, location: Location, chrome: Object, document: document…} > Window // function Window() { [native code] } > window.Window // function Window() { [native code] } > Window.window // undefined
這究竟是什麼鬼?window
和Window
究竟是什麼關係? es6
咱們下面來接着看看他們各自的類型:github
> typeof Window // "function" > typeof window // "object" > window instanceof Window // true
查閱資料發現:web
window 對象表示一個包含DOM文檔的窗口,其 document 屬性指向窗口中載入的 DOM文檔 。window 對象實現了 Window 接口,此接口繼承自 AbstractView 接口。一些額外的全局函數、命名空間、對象、接口和構造函數與 window 沒有典型的關聯,但倒是有效的。這個接口從 EventTarget 接口繼承屬性,經過 WindowTimers 、WindowBase64 和 WindowEventHandlers 實現屬性。chrome
這麼說相信新手應該沒啥感受,最好仍是舉個例子說說,好比咱們去吃飯要點菜,Window說白了一個菜單,window是端上桌子的那道菜,至於這道菜色香味以及製做方法和Window無關,只和window有關。
Window規定了對象的類型,因此咱們不難理解window instanceof Window
的值爲啥是true
,Window
是function
。那麼這裏咱們就清楚了window
是Window
的具體方法實現,而Window
對象沒有Window
屬性,因此Window.window
爲undefined
,因此咱們須要關注的是Window
的屬性方法。
咱們在前面能夠看出來window
對象自身有top
和window
屬性,類型爲 Window
,而且值是window
自己;另外有個Window
屬性,值是Window
對象,天然至此前面的內容也解釋清楚了。
window.document指向document對象的引用,document對象是Document 接口接口的具體實現。Document 接口表明在瀏覽器及服務器中加載任意 web 頁面,也做爲 web 頁面內容(DOM tree, 包含如 <body> 和 <table< 等 element)的入口。其它爲文檔(document)提供了全局性的函數,例如獲取頁面的URL、在文檔中建立新的 element 的函數。Document 接口描述了支持全部文檔類型的屬性和方法。根據文檔的類型(如 HTML、XML、SVG等等 ),對文檔對象操做的API也會不同:HTML 文檔(text/html 類型的內容)實現了 HTMLDocument 接口,而SVG 文檔實現了 SVGDocument 接口。
雖然這段話看起來,可是實際上意思很簡單:
若是咱們要想獲取一個document的內容,咱們可使用document對象下的方法屬性和方法去獲取,好比獲取標題:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>hello world</title> </head> <body> <script type="text/javascript"> // 方法1 alert(document.title); // 方法2 // alert(window.document.title); </script> </body> </html>
咱們這裏的document爲啥不加window也能夠彈出結果呢,由於window爲頂層對象,這裏能夠忽略不寫,好比alert()
方法實際上是window.alert()
下的方法,咱們這裏不寫window,同樣能夠獲得結果。另外,咱們這裏只是獲取了title,至於其餘的內容,那就要學習document的屬性和方法。
__proto__
屬性(原型指針) 和 prototype屬性(原型對象)說到這兩個屬性,咱們真的很糾結,這二者到底有什麼聯繫和區別呢?咱們先看下面的例子:
> window.prototype === window.__proto__ // false > Window.prototype === window.__proto__ // true > window.constructor === Window // true > Window.__proto__.__proto__.__proto__.__proto__ // null
臥槽,這是什麼鬼?prototype
和__proto__
到底分別各自指什麼,Window鏈式調用__proto__
怎麼最後會變成null? 彷佛說到這裏謎團愈來愈多了,咱們這裏就要跳出window對象舉個簡單例子說說,否則你們真的是暈的。
function person(name) { this.name = name; this.getName = function() { alert(this.name) } } var zhangsan = new person('zhangsan'); var lisi = new person('lisi'); console.log(zhangsan.name) console.log(lisi.name) zhangsan.getName(); lisi.getName(); 結果: "zhangsan" "lisi"
注:可使用關鍵字 this調用類中的屬性, this是對當前對象的引用。
這樣一個例子咱們彷佛看到了面向對象中繼承的特性,在其餘面嚮對象語言中,這裏的person函數被設計爲「類」,可是在JavaScript中這裏設計得有點畸形的感受,爲啥這麼說呢,由於這裏的person是一個構造函數(constructor),用new實例化的也不是其餘面嚮對象語言中的類,而是構造函數,這種設計致使一個問題是啥呢?沒法共享屬性和方法,每個實例對象,都有本身的屬性和方法的副本!!!
好比:每個實例對象都有getName(),都是從父親構造器中繼承獲得,這樣就產生多個副本,可是咱們但願這個方法是公用的,避免多個副本的資源浪費,咱們但願可以把公用的屬性方法提取出來,而後實例化的對象也能夠引用,可是不會直接拷貝一份做爲副本。這個時候構造函數(constructor)顯得有點力不從心了,JavaScript的設計者引入了一個重要的屬性prototype,這個屬性包含一個對象(一般稱爲「prototype對象")。咱們把這個例子改爲用prototype寫試試:
function person(name) { this.name = name; } person.prototype.getName = function() { alert(this.name) } var zhangsan = new person('zhangsan'); var lisi = new person('lisi');
這樣咱們多個實例化對象能夠公用同一個方法,換句話說全部的實例對象共享同一個prototype對象,一般稱爲原型。一層層的繼承實現了鏈條式的"原型鏈"(prototype chain),JavaScript所以經過這個原型鏈實現繼承。至於爲啥最開始怎麼設計,都是爲了開發者簡單,可是也所以給你們的感受是特別,並且特別難理解,可是事實上其實並無那麼神奇!!!
prototype屬性很特殊,它還有一個隱式的constructor,指向了構造函數自己。
> person.prototype.constructor === person // true > zhangsan.constructor === person // true > zhangsan.constructor === person.prototype.constructor // true
說了這個多,咱們一直沒有解釋__proto__
屬性,咱們上面講了能夠經過構造函數的prototype
屬性實現繼承共用公用的屬性方法,可是咱們沒有說明實例化對象如何訪問到它所繼承的對象的原型對象,這裏的__proto__
屬性就是這個做用。咱們再回過頭去看以前的問題:
由於window
是經過實例化Window
獲得,天然咱們訪問Window
原型對象有兩種方法:1.直接經過Window的prototype屬性;2.經過實例化子對象的__proto__
訪問父對象的原型對象。這兩種方法實現的結果如出一轍。
Window.prototype === window.__proto__ // true
另外在JavaScript中有一個很特別的地方:萬物皆對象,萬物皆爲空。
怎麼理解呢,在JavaScript中的一切都源於對象,並且最頂層的對象是null對象,這會讓人很費解的。因此當咱們經過__proto__
不斷的尋找最頂層的原型對象時會發現爲null。
基於原型的編程不是面向對象編程中體現的風格,且行爲重用(在基於類的語言中也稱爲繼承)是經過裝飾它做爲原型的現有對象的過程實現的。這種模式也被稱爲弱類化,原型化,或基於實例的編程。
最後有幾點須要說明的是:
每一個構造函數都有一個原型對象(prototype),原型對象都包含一個指向構造函數的指針(constructor),而實例都包含一個指向原型對象的內部指針(__proto__
)。
除了使用__proto__
方式訪問對象的原型,還能夠經過Object.getPrototypeOf
方法來獲取對象的原型,以及經過Object.setPrototypeOf
方法來重寫對象的原型。__proto__
屬性只有瀏覽器才須要部署,其餘環境能夠沒有這個屬性,並且先後的兩根下劃線,表示它本質是一個內部屬性,不該該對使用者暴露。
instanceof和Object.isPrototypeOf()能夠判斷兩個對象是不是繼承關係。如上面那個例子:
// instanceof 運算符返回一個布爾值,表示一個對象是否由某個構造函數建立。 > zhangsan instanceof person => true // Object.isPrototypeOf()只要某個對象處在原型鏈上,都返回true。 > person.prototype.isPrototypeOf(zhangsan) => true
這裏推薦你們看看下面幾篇文章:
文章寫到原本是準備從新開篇的,剛剛上面在window下將原型鏈繼承不知道會不會有點誤導一些朋友,由於最開始是準備以window對象入手將面向對象的內容整理一下,發現寫着寫着有點零散了,由於window對象有不少其餘內容值得將,可是篇幅和本文主題影響,只能先停下後面再開篇補充,講了原型鏈繼承的理論知識,咱們天然要實際動手作點項目才能說明問題。
若是咱們去查看一些js庫的寫法,咱們會發現常常有這樣一種結構:
(function(w,undefined) { //... })(window);
在理解爲何要這樣寫以前咱們首先要明白什麼JavaScript的做用域,什麼是匿名函數,什麼是閉包?
在es6以前,JavaScript是遵循函數做用域,不支持塊級做用域。
var i=0; if(i<2){ var i = 2; } alert(i); // 2
在es6中支持使用let聲明瞭一個塊級域的本地變量,而且能夠同時初始化該變量。
var i=0; if(i<2){ let i = 2; } alert(i); // 0
函數內部能夠直接讀取函數全局變量。函數內的變量若是是使用var 申明,則是局部變量,做用域範圍爲函數體內部,不可讀取;可是須要注意的是未通過var申明,就變成了全局變量,在函數外部也能夠調用。
// 局部變量類型: var i=0; var fn = function () { if(i<2){ var i = 2; } } fn(); alert(i); // 0 // 全局變量類型 var i=0; var fn = function () { if(i<2){ i = 2; } } fn(); alert(i); // 2
變量提高:一個變量或函數能夠在它被引用以後聲明。
【變量】 foo = 2 var foo; // 被隱式地解釋爲: var foo; foo = 2; 【函數】 hoisted(); // logs "foo" function hoisted() { console.log("foo"); }
匿名函數是這樣的:
function(arg1,arg2){ // code }
可是一般咱們會把匿名函數寫成自執行的匿名函數:
(function(arg1,arg2){ // code })(a1,a2); 等價於: var fn = function(arg1,arg2){ // code } fn(a1,a2);
其實這裏就是實參與形參的關係,arg1,arg2在函數體內做爲形參被引用,a1,a2做爲實參在調用的時候傳入到函數體中被調用,至於變量內部存儲原理這裏不作深刻探究,畢竟學過編程的人應該都清楚。
咱們如今回過頭來看看本小節開頭說的那個例子,爲啥要那樣寫呢?
(function(w,undefined) { //... })(window);
爲何要傳入 window?
經過傳入 window變量,使得 window由全局變量變爲局部變量,當在咱們封裝的代碼塊中訪問 window時,不須要將做用域鏈回退到頂層做用域,這樣能夠更快的訪問 window;同時將 window做爲參數傳入,能夠在壓縮代碼時進行優化。
爲何要傳入 undefined?
在只執行匿名函數的做用域內,確保 undefined 是真的未定義。由於 undefined 可以被重寫,賦予新的值。
咱們前面說了在函數外能夠調用函數內未通過var聲明的全局變量,可是如何從外部讀取函數局部變量呢?咱們能夠在函數內部再定義一個函數。
var fn = function(){ var name = 'local'; var f = function(){ alert(name); } return f } // 調用 var resurlt = fn(); resurlt(); // or fn()();
閉包主要有兩個做用:
一是能夠讀取函數內部的變量,另外一個就是讓這些變量的值始終保持在內存中。
讀取函數內部變量咱們很好理解,可是至於內部變量的值保存在存儲中這個就有點難理解,咱們看個例子:
var fn = function(){ var i = 0; add = function(){ i++; } var f = function(){ alert(i); } return f } var result = fn(); result(); // 0 add(); result(); // 1
add未加var 聲明是全局變量,若是變量i不在內存中存儲,那麼咱們第一次和第二次調用result()
值都應該爲0。緣由在於咱們將fn()的返回值f()數賦值給一個全局變量,因爲這個全局變量一直處於內存中,f函數一樣也在內存中,f()函數依賴於fn()函數,所以fn()中的局部變量i一直處於內存之中。
若是上面的例子在調用的時候使用fn()()
則不會出現這種狀況。
1)因爲閉包會使得函數中的變量都被保存在內存中,內存消耗很大,因此不能濫用閉包,不然會形成網頁的性能問題,在IE中可能致使內存泄露。解決方法是,在退出函數以前,將不使用的局部變量所有刪除。
2)閉包會在父函數外部,改變父函數內部變量的值。因此,若是你把父函數看成對象(object)使用,把閉包看成它的公用方法(Public Method),把內部變量看成它的私有屬性(private value),這時必定要當心,不要隨便改變父函數內部變量的值。—— 學習Javascript閉包(Closure)
這裏簡要講解了一下閉包的一些做用,主要是爲了幫助咱們理解爲啥一些js庫採用閉包。
首先咱們怎麼實現私有命名空間?
經過定義一個匿名函數,建立了一個"私有"的命名空間,該命名空間的變量和方法,不會破壞全局的命名空間,咱們只暴漏出一個頂層的對象供外部調用便可。
前面咱們講到window對象的時候有個知識點沒有說的是,咱們在頁面定義一個全局變量的時候,這個全局變量最終是會在window對象下,對於調用window對象下的屬性和方法咱們通常無需經過window.
的形式就能夠調用。同理當咱們引用jQuery這種庫的時候,jQuery對象會在引用頁面的window對象下,這是由於jQuery庫最後會將jQuery或者$對象掛在window對象下,這樣就實現了頂層對象的暴漏。
下面咱們實現一個適用於現代瀏覽器的極小DOM操做庫,主要解決移動端,因此咱們這裏取名爲mjs。
(function(w,undefined) { // 構造函數 var mjs = function(selector, context) { return new mjs.fn.init(selector, context); } // 構造函數mjs的原型對象 mjs.fn = mjs.prototype = { constructor: mjs, init: function (selector, context) { //... } } mjs.fn.init.prototype = mjs.fn; // 爲window全局變量添加mjs對象 w.mjs = w.m = mjs; })(window);
這樣咱們就能夠無需new mjs(),直接使用 mjs.* 或者 m.* 鏈式調用相關方法。
下面咱們實現一個最簡的選擇器,這裏我不考慮兼容低級版本瀏覽器,使用querySelectorAll實現。咱們接着上面的完善mjs.prototype.init方法。咱們若是不考慮鏈式調用,咱們最簡單的選擇器甚至能夠長這樣:
var $ = function (selector) { return document.querySelector(selector); } 調用: $(".content")
若是想給選擇器加一個上下文,咱們進一步能夠這樣:
/** * 選擇器 * @param {Object} selector * @param {Object} context */ var $ = function (selector, context) { var context = context || document; var el = context.querySelectorAll(selector); return Array.prototype.slice.call(el); }; // 調用 var divObj = $('.div',$(".content")[0]); console.log(divObj[0].innerHTML)
固然咱們這裏補充完整就是這樣了:
// 構造函數mjs的原型對象 mjs.prototype = { constructor: mjs, init: function (selector, context) { if(!selector) { return mjs; }else if(typeof selector === 'object'){ var selector = [selector]; for (var i = 0; i < selector.length; i++) { this[i] = selector[i]; } this.length = selector.length; return mjs; }else if(typeof selector === 'string'){ var selector = selector.trim(); var context = context || document; var el = context.querySelectorAll(selector); var dom = Array.prototype.slice.call(el); var length = dom.length; for (var i = 0; i < length; i++) { this[i] = dom[i]; } this.context = context; this.selector = selector; this.length = length; return this; } } }
這裏咱們先只完成最簡單的選擇器功能,還有當selector是function類型的咱們沒有進行判斷,這裏不貼出來,你們具體能夠看看源代碼。咱們能夠驗證一下咱們封裝的這個選擇器:
<div class="divBox"> div1 <span>span1</span> </div> console.log(mjs('.divBox')[0].innerHTML) // "div1<span>span1</span>" console.log(mjs('.divBox span')[0].innerHTML) // "span1" var divBox = mjs('.divBox')[0]; console.log(mjs('span',divBox)[0].innerHTML) // "span1"
由於innerHTML是原生DOM操做的方法,咱們mjs對象沒有這個方法,因此咱們這裏是將mjs對象轉成了原生DOM對象,轉換方法:mjs(selector)[0]。
爲了簡單起見咱們繼續封裝,先完成一個html()方法。
... html: function (content) { if (content === undefined && this[0].nodeType === 1) { return this[0].innerHTML.trim(); }else{ var len = this.length; for (var i = 0; i < len; i++) { this[i].innerHTML = content; } return this; } }, text: function (val) { if (!arguments.length) { return this[0].textContent.trim(); } else { for (var i = 0; i < this.length; i++) { this[i].innerText = val; } return this; } } ...
上面的例子咱們能夠這樣調用:
// 直接獲取內容,默認獲取第一個匹配項 console.log(mjs('.divBox').html()) console.log(mjs('.divBox span').html()) console.log(mjs('.divBox span').text()) // 更新內容,默認更新全部匹配項 mjs('.divBox span').html('我是新的內容') mjs('.divBox span').text('我是新的內容') // 支持上下文查找方法 console.log(mjs('span',mjs('.divBox')[0]).html()) // 設置屬性 mjs('.divBox').attr('name','divBox'); // 獲取屬性 console.log(mjs('.divBox').attr('name'))
prepend: function(str) { var len = this.length; for (var i = 0; i < len; i++) { this[i].insertAdjacentHTML('afterbegin', str); } return this; }, append: function (str) { var len = this.length; for (var i = 0; i < len; i++) { this[i].insertAdjacentHTML('beforeend', str); } return this; }, before: function (str) { var len = this.length; for (var i = 0; i < len; i++) { this[i].insertAdjacentHTML('beforebegin', str); } return this; }, after: function (str) { var len = this.length; for (var i = 0; i < len; i++) { this[i].insertAdjacentHTML('afterend', str); } return this; }, remove: function () { var len = this.length; for (var i = 0; i < len; i++) { this[i].parentNode.removeChild(this[i]); } return this; }
調用:
// 添加元素 mjs('.divBox').prepend('<ul><li>prepend</li></ul>') mjs('.divBox').append('<ul><li>append</li></ul>') mjs('.divBox').before('<ul><li>before</li></ul>') mjs('.divBox').after('<ul><li>after</li></ul>') // 刪除元素 mjs('.divBox').remove();
insertAdjacentHTML() 將指定的文本解析爲 HTML 或 XML,而後將結果節點插入到 DOM 樹中的指定位置處。該方法不會從新解析調用該方法的元素,所以不會影響到元素內已存在的元素節點。從而能夠避免額外的解析操做,比直接使用 innerHTML 方法要快。——MDN insertAdjacentHTML
語法:
element.insertAdjacentHTML(position, text);
position 是相對於 element 元素的位置,而且只能是如下的字符串之一:
beforebegin: 在 element 元素的前面。
afterbegin:在 element 元素的第一個子節點前面。
beforeend:在 element 元素的最後一個子節點後面。
afterend:在 element 元素的後面。
... hasClass: function (cls) { return this[0].classList.contains(cls); }, addClass: function (cls) { var len = this.length; for (var i = 0; i < len; i++) { if(!this[i].classList.contains(cls)){ this[i].classList.add(cls); } } return this; }, removeClass: function (cls) { var len = this.length; for (var i = 0; i < len; i++) { if(this[i].classList.contains(cls)){ this[i].classList.remove(cls); } } return this; }, toggleClass: function (cls) { return this[0].classList.toggle(cls); } ...
調用方法:
// hasClass(返回值爲布爾值) console.log(mjs(".divBox").hasClass('divBox')) // addClass mjs(".divBox").addClass('red') // removeClass mjs(".divBox").removeClass('red') // toggleClass mjs(".divBox").toggleClass('red')
css: function (attr,val) { var len = this.length; for(var i = 0;i < len; i++) { if(arguments.length === 1){ var obj = arguments[0]; if(typeof obj === 'string'){ return getComputedStyle(this[i],null)[attr]; }else if(typeof obj === 'object'){ for(var attr in obj){ this[i].style[attr] = obj[attr]; } } } else { if(typeof val === 'function'){ this[i].style[attr] = val(); }else{ this[i].style[attr] = val; } } } return this; }
調用方法:
// 獲取樣式屬性值 console.log(mjs(".divBox").css("color")); // 設置樣式屬性值 // 方法1 mjs(".divBox").css("color","red"); // 方法2 mjs(".divBox").css({ "width":"100px", "color":"white", "background-color":"#98bf21", "font-family":"Arial", "font-size":"20px", "padding":"5px" }); // 方法3 mjs('.divBox').css( 'background-color',function(){ return '#F00' } )
find: function(selector){ return this.init(selector,this[0]) }, first: function(){ return this.init(this[0]) }, last: function(){ return this.init(this[this.length-1]) }, eq: function(index){ return this.init(this[index]) }, parent: function(){ return this.init(this[0].parentNode); }
咱們前面想經過上下文查找內容:
console.log(mjs('span',mjs('.divBox')[0]).html())
咱們能夠經過find方法這樣寫:
console.log(mjs('.divBox').find('span').html()) console.log(mjs('.divBox span').first().html()) console.log(mjs('.divBox span').last().html()) console.log(mjs('.divBox span').eq(1).html()) console.log(mjs('.divBox span').eq(1).parent().html())
關鍵在於mjs對象和原生dom的區別和相互轉換。
至此咱們封裝了一個簡單的類jQuery的工具庫,固然對於一個完整的工具庫,好比jQuery、zepto等,它們功能確定是更爲完善,封裝了更多的方法,在異常處理及性能、可拓展性方法作得更好,因爲本文的重點不是爲了完成一個完整的庫,在此只是拋磚引玉,只是學習一下經常使用的思想,有興趣的朋友能夠繼續完善這個庫。
mjs github地址:https://github.com/zhaomenghu...
MDN javascript
可想造一個屬於你本身的jQuery庫?