深刻理解JavaScript,這一篇就夠了

前言
  JavaScript 是我接觸到的第二門編程語言,第一門是 C 語言。而後纔是 C++、Java 還有其它一些什麼。因此我對 JavaScript 是很是有感情的,畢竟使用它有十多年了。早就想寫一篇關於 JavaScript 方面的東西,可是在博客園中,寫 JavaScript 的文章是最多的,從入門的學習筆記到高手的心得體會包羅萬象,無論我怎麼寫,都不免落入俗套,因此遲遲沒有動筆。另一個緣由,也是由於在 Ubuntu 環境中一直沒有找到很好的 JavaScript 開發工具,這種困境直到 Node.js 和 Visual Studio Code 的出現才徹底解除。html

  十多年前,對 JavaScript 的介紹都是說他是基於對象的編程語言,而從沒有哪本書會說 JavaScript 是一門面向對象的編程語言。基於對象很好理解,畢竟在 JavaScript 中一切都是對象,咱們隨時可使用點號操做符來調用某個對象的方法。可是十多年前,咱們編寫 JavaScript 程序時,都是像 C 語言那樣使用函數來組織咱們的程序的,只有在論壇的某個角落中,有少數的高手會偶爾提到你能夠經過修改某個對象的prototype來讓你的函數達到更高層次的複用,直到 Flash 的 ActionScript 出現時,纔有人系統介紹基於原型的繼承。十餘年後的如今,使用 JavaScript 的原型鏈和閉包來模擬經典的面向對象程序設計已是廣爲流傳的方案,因此,說 JavaScript 是一門面向對象的編程語言也絲絕不爲過。前端

  我喜歡 JavaScript,是由於它很是具備表現力,你能夠在其中發揮你的想象力來組織各類難以想象的程序寫法。也許 JavaScript 語言並不完美,它有不少缺陷和陷阱,而正是這些頗有特點的語言特性,讓 JavaScript 的世界出現了不少奇技淫巧。java

回到頂部
對象和原型鏈
  JavaScript 是一門基於對象的編程語言,在 JavaScript 中一切都是對象,包括函數,也是被當成第一等的對象對待,這正是 JavaScript 極其富有表現力的緣由。在 JavaScript 中,建立一個對象能夠這麼寫:node

var someThing = new Object();
  這和在其它面向對象的語言中使用某個類的構造函數建立一個對象是如出一轍的。可是在 JavaScript 中,這不是最推薦的寫法,使用對象字面量來定義一個對象更簡潔,以下:程序員

var anotherThing = {};
  這兩個語句其本質是同樣的,都是生成一個空對象。對象字面量也能夠用來寫數組以及更加複雜的對象,這樣:shell

var weekDays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
  這樣:編程

var person = {後端

name : "youxia",
age : 30,
gender : "male",
sayHello : function(){ return "Hello, my name is " + this.name; }

}
  甚至這樣數組和對象互相嵌套:數組

var workers = [{name : "somebody", speciality : "Java"}, {name : "another", speciality : ["HTML", "CSS", "JavaScript"]}];
  須要注意的是,對象字面量中的分隔符都是逗號而不是分號,並且即便 JavaScript 對象字面量的寫法和 JSON 的格式類似度很高,可是它們仍是有本質的區別的。瀏覽器

  在咱們搗鼓 JavaScript 的過程當中,工具是很是重要的。我這裏介紹的第一個工具就是 Chromium 瀏覽器中自帶的 JavaScript 控制檯。在 Ubuntu 中安裝 Chromium 瀏覽器只須要一個命令就能夠搞定,以下:

sudo apt-get install chromium
  啓動 Chromium 瀏覽器後,只須要按 F12 就能夠調出 JavaScript 控制檯。固然,在菜單中找出來也能夠。下面,讓我把上面的示例代碼輸入到 JavaScript 控制檯中,一是能夠看看咱們寫的代碼是否有語法錯誤,二是能夠看看 JavaScript 對象的真面目。以下圖:

  對於博客園中廣大的前端攻城獅來說,Chromium 的 JavaScript 控制檯已是一個爛大街的工具了,在控制檯中寫console.log("Hello, World!");就像是在 C 語言中寫printf("Hello, World!");同樣成爲了入門標配。在控制檯中輸入 JavaScript 語句後,一按 Enter 該行代碼就當即執行,若是要輸入多行代碼怎麼辦呢?一個辦法就是按 Shift+Enter 進行換行,另一個辦法就是在別的編輯器中寫好而後複製粘貼。其實在 Chromium 的 JavaScript 控制檯中還有一些不那麼普遍流傳的小技巧,好比使用console.dir()函數輸出 JavaScript 對象的內部結構,以下圖:

  從圖中,能夠很容易看出每個對象的屬性、方法和原型鏈。

  和其它的面向對象編程語言不一樣, JavaScript 不是基於類的代碼複用體系,它選擇了一種很奇特的基於原型的代碼複用機制。通俗點說,若是你想建立不少對象,而這些對象有某些相同的屬性和行爲,你爲每個對象編寫單獨的代碼確定是不合算的。在其它的面向對象編程語言中,你能夠先設計一個類,而後再以這個類爲模板來建立對象。我這裏稱這種方式爲經典的面向對象體系。而在 JavaScript 中,解決這個問題的方式是把一個對象做爲另一個對象的原型,擁有相同原型的對象天然擁有了相同的屬性和行爲。對象擁有原型,原型又有原型的原型,最終構成一個原型鏈。當訪問一個對象的屬性或方法的時候,先在對象自己中查找,若是找不到,則到原型中查找,若是仍是找不到,則進一步在原型的原型中查找,一直到原型鏈的最末端。在現代 JavaScript 模式中,硬是用函數、閉包和原型鏈模擬了經典的面向對象體系。

  原型這個概念自己並不複雜,複雜的是 JavaScript 中的隱式原型和函數對象。什麼是隱式原型,就是說在 JavaScript 中無論你以什麼方式建立一個對象,它都會自動給你生成一個原型對象,咱們的對象中,有一個隱藏的__proto__屬性,它指向這個自動生成的原型對象;而且在 JavaScript 中無論你以什麼方式建立一個對象,它最終都是從構造函數生成的,以對象字面量構造的對象也有構造函數,它們分別是Object()和Array(),每個構造函數都有一個自動生成的prototype屬性,它也指向那個自動生成的原型對象。並且在 JavaScript 中一切都是對象,構造函數也不例外,因此構造函數既有prototype屬性,又有__proto__屬性。再並且,自動生成的原型對象也是對象,因此它也應該有本身的原型對象。你看,提及來都這麼拗口,理解就更加不容易了,更況且 JavaScript 中還內置了Object()、Array()、String()、Number()、Boolean()、Function()這一系列的構造函數。看來不畫個圖是真的理不順了。下面咱們來抽絲剝繭。

  先考察空對象someThing,哪怕它是以對象字面量的方式建立的,它也是從構造函數Object()構造出來的。這時,JavaScript 會自動建立一個原型對象,咱們稱這個原型對象爲Object.prototype,構造函數Object()的prototype屬性指向這個對象,對象someThing的__proto__屬性也指向這個對象。也就是說,構造函數Object()的prototype屬性和對象someThing的__proto__屬性指向的是同一個原型對象。並且,這個原型對象中有一個constructor屬性,它又指回了構造函數Object(),這樣造成了一個環形的鏈接。以下圖:

  要注意的是,這個圖中所顯示的關係是對象剛建立出來的時候的狀況,這些屬性的指向都是能夠隨意修改的,改了就不是這個樣子了。下面在 JavaScript 控制檯中驗證一下上圖中的關係:

  請注意,構造函數Object()的prototype屬性和__proto__屬性是不一樣的,只有函數對象才同時具備這兩個屬性,普通對象只有__proto__屬性,並且這個__proto__屬性是隱藏屬性,不是每一個瀏覽器都容許訪問的,好比 IE 瀏覽器。下面,咱們來看看 IE 瀏覽器的開發者工具:

  這是一個反面教材,它既不支持console.dir()來查看對象,也不容許訪問__proto__內部屬性。因此,在後面我講到繼承時,須要使用特殊的技巧來避免在咱們的代碼中使用__proto__內部屬性。上面的例子和示意圖中,都只說構造函數Object()的prototype屬性指向原型對象,沒有說構造函數Object()的__proto__屬性指向哪裏,那麼它究竟指向哪裏呢?這裏先留一點懸念。

  下一步,咱們本身建立一個構造函數,而後使用這個構造函數建立一個對象,看看它們之間原型的關係,代碼是這樣的:

function Person(name, age, gender){

this.name = name;
this.age = age;
this.gender = gender;

}
Person.prototype.sayHello = function(){ return "Hello, my name is " + this.name; };
var somebody = new Person("youxia", 30, "male");
  輸入到 Chromium 的 JavaScript 控制檯中,而後使用console.dir()分別查看構造函數Person()和對象somebody,以下兩圖:

  用圖片來表示它們之間的關係,應該是這樣的:

  我使用藍色表示構造函數,黃色表示對象,若是是 JavaScript 自帶的構造函數和 prototype 對象,則顏色深一些。從上圖中能夠看出,構造函數Person()有一個prototype屬性和一個__proto__屬性,__proto__屬性的指向依然留懸念,prototype屬性指向Person.prototype對象,這是系統在咱們定義構造函數Person()的時候,自動建立的一個和構造函數Person()相關聯的原型對象,請注意,這個原型對象是和構造函數Person()相關聯的原型對象,而不是構造函數Person()的原型對象。當咱們使用構造函數Person()建立對象somebody時,somebody的原型就是這個系統自動建立的原型對象Person.prototype,就是說對象somebody的__proto__屬性指向原型對象Person.prototype。而這個原型對象中有一個constructor屬性,又指回構造函數Person(),造成一個環。這和空對象和構造函數Object()是同樣的。並且原型對象Person.prototype的__proto__屬性指向Object.prototype。若是在這個圖中把空對象和構造函數Object()加進去的話,看起來是這樣的:

  有點複雜了,是嗎?不過這還不算最複雜的,想一想看,若是把JavaScript 內置的Object()、Array()、String()、Number()、Boolean()、Function()這一系列的構造函數以及與它們相關聯的原型對象都加進去,會是什麼狀況?每個構造函數都有一個和它相關聯的原型對象,Object()有Object.prototype,Array()有Array.prototype,依此類推。其中最特殊的是Function()和Function.prototype,由於全部的函數和構造函數都是對象,因此全部的函數和構造函數都有構造函數,而這個構造函數就是Function()。也就是說,全部的函數和構造函數都是由Function()生成,包括Function()自己。因此,全部的構造函數的__proto__屬性都應該指向Function.prototype,前面留的懸念終於有答案了。若是隻考慮構造函數Person()、Object()和Function()及其關聯的原型對象,在不解決懸念的狀況下,圖形是這樣的:

  能夠看到,每個構造函數和它關聯的原型對象構成一個環,並且每個構造函數的__proto__屬性無所指。經過前面的分析咱們知道,每個函數和構造函數的__proto__屬性應該都指向Function.prototype。我用紅線標出這個關係,結果應該以下圖:

  若是咱們畫出前面提到過的全部構造函數、對象、原型對象的全家福,會是個什麼樣子呢?請看下圖:

  暈菜了沒?歡迎指出錯誤。把圖一畫,就發現其實 JavaScript 中的原型鏈沒有那麼複雜,有幾個內置構造函數就有幾個配套的原型對象而已。我這裏只畫了六個內置構造函數和一個自定義構造函數,還有幾個內置構造函數沒有畫,好比Date()、Math()、Error()、RegExp(),可是這不影響咱們理解。寫到這裏,是否是應該介紹一下我使用的畫圖工具了?

回到頂部
我使用的畫圖工具Graphviz
  在個人 Linux 系列中,有一篇介紹畫圖工具的文章,不過我此次使用的工具是另闢蹊徑的 Graphviz,聽說這是一個由貝爾實驗室的幾個牛人開發和使用的畫流程圖的工具,它使用一種腳本語言定義圖形元素,而後自動進行佈局和生成圖片。首先,在 Ubuntu 中安裝 Graphiz 很是簡單,一個命令的事兒:

sudo apt-get install graphviz
  而後,建立一個文本文件,我這裏把它命名爲sample.gv,其內容以下:

digraph GraphvizDemo{

Alone_Node;

Node1 -> Node2 -> Node3;

}
  這是一個最簡單的圖形定義文件了,在 Graphviz 中圖形僅僅由三個元素組成,它們分別是:一、Graph,表明整個圖形,上面源代碼中的digraph GraphvizDemo{}就定義了一個 Graph,咱們還能夠定義 SubGraph,表明子圖形,能夠用 SubGraph 將圖形中的元素分組;二、Node,表明圖形中的一個節點,能夠看到 Node 的定義很是簡單,上面源碼中的Alone_Node;就是定義了一個節點;三、Edge,表明鏈接 Node 的邊,上面源碼中的Node1 -> Node2 -> Node3;就是定義了三個節點和兩條邊,能夠先定義節點再定義邊,也能夠直接在定義邊的同時定義節點。而後,調用 Graphviz 中的dot命令,就能夠生成圖形了:

dot -Tpng sample.gv > sample.png
  生成的圖形以下:

  上面的圖形中都是用的默認屬性,因此看起來效果不咋地。咱們能夠爲其中的元素定義屬性,包括定義節點的形狀、邊的形狀、節點之間的距離、字體的大小和顏色等等。好比下面是一個稍微複雜點的例子:

digraph GraphvizDemo{

nodesep=0.5;
ranksep=0.5;
    
node [shape="record",style="filled",color="black",fillcolor="#f4a582",fontname="consolas",fontsize=15];
edge [style="solid",color="#053061"];
    
root  [label="<l>left|<r>right"];
left  [label="<l>left|<r>right"];
right [label="<l>left|<r>right"];
leaf1 [label="<l>left|<r>right"];
leaf2 [label="<l>left|<r>right"];
leaf3 [label="<l>left|<r>right"];
leaf4 [label="<l>left|<r>right"];

root:l:s -> left:n;
root:r:s -> right:n;
left:l:s -> leaf1:n;
left:r:s -> leaf2:n;
right:l:s -> leaf3:n;
right:r:s -> leaf4:n;

}
  在這個例子中,咱們使用了nodesep=0.5;和ranksep=0.5設置了 Graph 的全局屬性,使用了node [shape=...];和[edge [style=...];這樣的語句設置了 Node 和 Edge 的全局屬性,而且在每個 Node 和 Edge 後面分別設置了它們本身的屬性。在這些屬性中,比較特別的是 Node 的shape屬性,我將它設置爲record,這樣就能夠很方便地利用 Node 的label屬性來繪製出相似表格的效果了。同時,在定義 Edge 的時候還能夠指定箭頭的起始點。

  執行dot命令,能夠獲得這樣的圖形:

  是否是漂亮了不少?雖然以上工做使用任何文本編輯器均可以完成,可是爲了提升工做效率,我固然要祭出個人神器 Eclipse 了。在 Eclipse 中能夠定義外部工具,因此我寫一個 shell 腳本,將它定義爲一個外部工具,這樣,每次編寫完圖形定義文件,點一下鼠標,就能夠自動生成圖片了。使用 Eclipse 還能夠解決預覽的問題,只須要編寫一個 html 頁面,該頁面中只包含生成的圖片,就能夠利用 Eclipse 自帶的 Web 瀏覽器預覽圖片了。這樣,每次改動圖形定義文件後,只須要點一下鼠標生成圖片,再點一下鼠標刷新瀏覽器就能夠實時預覽圖片了。雖然不是所見即所得,可是工做效率已經很高了。請看動畫:

  Graphviz 中能夠設置的屬性不少,具體內容能夠查看 Graphviz官網 上的文檔。

回到頂部
做用域鏈、上下文環境和閉包
  關於變量的做用域這個問題應該不用多講,凡是接觸編程的童鞋,無不都要從這個基礎的概念開始。變量做用域的通用規則其實很簡單,無非三條:1.內層的代碼能夠訪問外層代碼定義的變量,外層代碼不能訪問內層代碼定義的變量;2.變量要先定義後使用;3.退出代碼的做用域時,變量會被銷燬。以 C 語言代碼爲例:

int a0 = 0;
{

int a1 = 1;
printf("%d\n", a0);  //能夠訪問外層變量,打印 0
printf("%d\n", a2);  //錯誤,變量 a2 還沒定義呢
int a2 = 2;         //變量要先定義後使用

}
/ 並且,退出做用域後,變量 a1 和 a2 會被自動銷燬 /
printf("%dn", a1); //錯誤,外層代碼不能訪問內層變量
  可是在 JavaScript 中,以上三條規則都有可能會被打破。從如今開始,咱們就要開始踩坑了,在 JavaScript 語言滿滿的陷阱中,關於變量這一塊的最多。首先第一個坑, JavaScript 中沒有塊做用域,只有函數做用域。也就是說,要在 JavaScript 中實現以上相似 C 語言的效果,咱們的代碼應該這樣寫:

var a0 = 0;
function someFunc(){

var a1 = 1;
console.log(a1); //能夠訪問外層變量,打印 0
console.log(a2); //你覺得會出現錯誤,由於變量沒有定義,可是你錯了,這裏不會發生錯誤,而是打印 undefined
var a2 = 2;

}
someFunc();
/ someFunc()執行完以後,變量 a1 和 a2 會被自動銷燬 /
console.log(a1); //錯誤,外層代碼不能訪問內層變量
  把這段代碼複製到控制檯中驗證一下,我就不截圖了,畢竟我這是一篇超長的熊文,圖片太多會被罵的,你們本身驗證就能夠了。注意,定義函數後須要調用它,函數內的代碼纔會執行,爲了方便,我之後把它寫成定義完後當即調用的自執行格式。這裏碰到的第二個坑就是變量提高,在 JavaScript 中,你本覺得沒有定義變量 a2 就使用會出現錯誤,哪知道定義在後面的var a2 = 2;被提高到代碼塊的前面了,結果就輸出 undefined。把上面的例子稍微改一改,就能夠看到經典的變量提高的坑,以下:

var a0 = 0;
(function (){

var a1 = 1;
console.log(a0); //本覺得會訪問外層變量a0,打印 0,哪知道定義在後面的 var a0 = 1; 被提高了,因此打印 undefined
var a0 = 1;

})(); //爲了省事,寫成匿名函數自執行格式
console.log(a1); //錯誤,外層代碼不能訪問內層變量
  本覺得這裏會訪問外層變量a0,打印 0,哪知道定義在後面的 var a0 = 1; 被提高了,因此打印 undefined。爲何是 undefined 而不是 1 呢?那是由於變量提高只是提高了變量的定義,沒有提高變量的賦值。不只變量定義會被提高,函數定義也會被提高,這也是一個經典的坑。以下代碼:

if(true){ //由於條件恆爲true,因此確定會執行這個分支

function someFunc(){
    console.log("true");
}

}else{

function someFunc(){
    console.log("false");
}

}
someFunc(); //本覺得會輸出 true,結果卻輸出 false,就是由於定義在 else 分支中的函數被提高了,覆蓋了定義在 true 分支中的函數
  固然,以上 Bug 只會在部分瀏覽器中出現,在 Chromium 和 FireFox 中仍是能正確輸出 true 的。爲了不函數定義的提高形成的問題,在這種狀況下,咱們可使用函數表達式而不是函數定義,代碼以下:

if(true){ //由於條件恆爲true,因此確定會執行這個分支

var someFunc = function(){
    console.log("true");
}

}else{

var someFunc = function(){
    console.log("false");
}

}
someFunc();
  關於函數定義和函數表達式的區別,我這裏就不深刻討論了。

  內層代碼能夠訪問外層變量,因此內層代碼在訪問一個變量的時候,會從內層到外層逐層搜索該變量,這就是變量做用域鏈,理解這一點有時有助於咱們優化 JavaScript 代碼的執行速度,對變量的搜索的路徑越短,代碼執行就越快。另外,除了全局變量外,定義在函數內部的變量只有在函數執行的時候後,這個變量纔會被建立,這就是執行上下文,裝逼說法叫 context,每個函數執行的時候就會建立一個 context。前面提過,在 C 語言中,一個代碼塊退出的時候,這個代碼塊的 context 和裏面的變量也會被銷燬,可是在 JavaScript 函數執行結束後,函數的 context 和裏面的變量會被銷燬嗎?那可不必定哦。若是一個函數中定義的變量被捕獲,那麼這個函數的 context 和裏面的變量就會保留,好比閉包。這個不叫坑,叫語言特性。

  在博客園中,有不少人寫閉包,可是都寫得無比複雜,定義也不是很準確。其實閉包就是定義在內層的函數捕獲了定義在外層函數中的變量,並把內層函數傳遞到外層函數的做用域以外執行,則外層函數的 context 不能銷燬,就造成了閉包。把內層函數傳遞到外層函數的做用域以外有不少方法,最多見的是使用return,其它的方法還有把內層函數賦值給全局對象的屬性,或者設置爲某個控件的事件處理程序,甚至使用setTimeout和setInterval均可以。

  其實閉包並非 JavaScript 語言特有的概念,只要是把函數當成頭等對象的語言都有。C 語言和早期的 C++ 和 Java 沒有,想一想看,咱們根本就沒辦法在上述語言中定義函數內部的函數。不過自從 C++ 和 Java 引入了 lambda 表達式以後,就有了閉包的概念了。

  下面,咱們來探索 JavaScript 中的函數執行上下文和閉包。爲了印象深入,我這裏定義了一個嵌套四層的函數,函數first()返回定義在first()內的second(),second()返回定義在second()內的third(),third()再返回一個匿名函數,代碼以下:

var a0 = 0;
var b0 = "Global context";

function first(){

var a1 = 1;
var b1 = "first() context";

function second(){
    var a2 = 2;
    var b2 = "second() context";

    function third(){
        var a3 = 3;
        var b3 = "third() context";

        return function(){
            var a4 = 4;
            var b4 = "what's matter, can I see it?";
            console.log([ a1, a2, a3, a4 ]);
            console.log([ b1, b2, b3, b4]);
        }
    }
    return third;
}
return second;

}
  而後,調用var what = first()()();返回最內層的匿名函數,使用console.dir(what);來查看這個匿名函數,以下圖:

  從圖中能夠看到,返回的最內層函數被命名爲function anonymous(),其中有一個<function scope>屬性,將它展開,能夠看到因爲function anonymous()對外層變量a一、a二、a三、b一、b二、b3的捕獲而產生了三個 Closure,也就是閉包,而function anonymous()不只能夠訪問這三個閉包中的變量,還能夠訪問 Global 中的變量。

  下面問題來了,爲何咱們看不到咱們定義的變量a4和b4呢?由於a4和b4只有在function anonymous()被執行後纔會產生。咱們這裏只是返回了function anonymous(),尚未執行它呢。其實就算執行它咱們也看不到變量a4和b4所在的 context,由於函數的執行老是一閃而過,若是沒有造成閉包,函數一執行完該 context 就銷燬了。除非咱們能讓該函數執行到快完的時候定住。有什麼辦法呢?你是否是想到了調試器?只要咱們在這個函數中設置一個 breakpoint,是否是就能夠看到它的 context 了呢?

  Chromium 固然是自帶調試功能的。不過要想在 Chromium 中調試代碼就得把以上 JavaScript 代碼加到 HTML 頁面中。我懶得這麼作。這裏,我就要祭出 Node.js 和 Visual Studio Code 了。在 Ubuntu 中安裝 Node.js 很是方便,只須要使用以下命令:

sudo apt-get install nodejs
sudo apt-get install nodejs-legacy
  爲何要安裝nodejs-legacy呢?那是由於nodejs中的命令是nodejs,而nodejs-legacy中的命令是node,同時安裝這兩個包能夠兼容不一樣的命令調用方式,其實它們本質是同樣的。而編輯器技術哪家強?自從有了 Visual Studio Code 天然就不考慮其它的了。不過 Visual Studio Code 須要本身去它的 官網 下載。

  把上面的代碼寫成一個.js文件,而後在編輯器中每一個函數的返回點設置斷點,直接使用 Node.js 的調試功能,就能夠查看全部的函數執行時的 context 了,以下動圖:

  把斷點設置在每個函數的最後一條語句,按 F5 開始調試,每次暫停均可以看到這個函數執行時產生的 context,在這個 context 中,能夠看到該函數中定義的變量和函數,也就是其中顯示的Local範圍的變量,以及該函數能夠訪問的外層變量,也就是其中顯示的Closure和Global範圍的變量。使用調試功能,咱們終於能夠看到a4和b4了,同時還能夠發現,在每個函數的 context 中,都有一個特殊的變量this,下一節,咱們來討論函數和this,函數、原型、閉包和this是使用 JavaScript 模擬經典的基於類的面向對象編程的基本要素。不過在進入下一節以前,我還要來展現一下 Eclipse。

  Eclipse 的最新版本 neon 終於改進了,在前一個版本中,它只支持 ECMAScript 3,並且其網頁預覽仍是使用的 Webkit-1.0,在今年發佈的這個新版本中,終於支持 ECMAScript 5了,Webkit 也用到了最新版。還加入了對 Node.js 的支持。不過 Eclipse 中關於 JavaScript 的智能提示彷佛仍是不好勁。Eclipse 的更新速度實在是太慢了。不過用 Eclipse 配合 Node.js 調試 JavaScript 也還不錯,下面直接上圖:

  還有 Eclipse 的死對頭,IntelliJ IDEA 和 WebStorm 調試 JavaScript 也是不錯的,我就很少說了。

  關於內層函數怎麼捕獲變量的問題,在編程語言界還有一個經典的爭議,那就是關於詞法做用域和動態做用域的爭議。所謂詞法做用域,就是在函數定義時的環境中去尋找外層變量,而動態做用域,就是在函數運行時的環境中去尋找外層變量。大多數如今程序設計語言都是採用詞法做用域規則,而只有爲數很少的幾種語言採用動態做用域規則,包括APL、Snobol和Lisp的某些方言,還有 C 語言中的宏定義。很顯然, JavaScript 採用的是詞法做用域,變量的做用域鏈是在函數定義的時候就決定了的。而對於動態做用域的例子,咱們能夠看看以下的用 LISP 語言定義的一個函數:

(let ((y 7))
(defun scope-test (x) (list x y)))
  這個函數調用時,若是是採用動態做用域的語言中,如 emacs lisp,它不是在定義它的環境中去尋找自由變量y,也就是說y的值不是7,而是在它運行的環境中向前回溯,尋找變量y的值,因此這樣的代碼:

(let ((y 5))
(scope-test 3))
  在 emacs lisp 的運行結果爲(3 5),而在採用詞法做用域規則的編程語言中,如 common lisp,它會在定義函數的環境中尋找自由變量y的值,因此這段代碼的運行結果爲(3 7)。

  另外,還有一個關於閉包和循環的一個經典的坑,當閉包遇到循環的時候,以下代碼:

(function(){

var i;
for(i = 1; i <= 10; i++){
    setTimeout(function(){console.log(i);}, 500); //本覺得會輸出數字 1-10,結果輸出了 10 次 11 
}

})();
  在上面代碼中,我爲了簡潔,都使用了匿名函數。之因此會出現這樣意想不到的結果,就是由於定義在內層的匿名函數都捕獲了外層函數中的變量i,因此當它們運行的時候,都是輸出的這個i的最終的值,那就是11。若是要想獲得預期的輸出 1-10 這樣的結果,就應該在定義內層函數的時候讓它接受一個參數,而後把i當作參數傳遞給它。代碼改爲這樣就行:

(function(){

var i;
for(i = 1; i <= 10; i++){
    setTimeout((function(a){console.log(a);})(i), 500);
}

})();
  所有寫成匿名函數自調用格式簡潔是簡潔了很多,可是可讀性就差了許多。網上的關於這個坑的描述所用的示例代碼每每是將內層函數設置爲某個按鈕的onClick事件處理程序,而我不想在個人示範中和 BOM、DOM 產生太多的耦合,因此我選擇了setTimeout()。若是不信,能夠本身在 Chromium 的 JavaScript 控制檯中驗證效果。

回到頂部
函數和this
  從前面的調試過程當中咱們能夠看出,每個函數執行的 context 中都有一個特殊的變量this。對this你們都不會陌生,不少面向對象的編程語言中都有,可是在 JavaScript 中,this會稍有不一樣,它的取值會隨着函數的調用方式不一樣而變化。JavaScript 中函數的調用方式多種多樣,總結起來主要有四種:

作爲構造函數調用,好比前面的new Person();、new Object();;
作爲對象的方法調用,好比前面的somebody.sayHello();;
作爲普通函數調用,這是用得最多的,好比前面的first();、what();;
經過apply、call、bind方式調用,這種調用方式我後面會舉例。
  在第一種調用方式中,this的取值就是該構造函數即將建立的對象。在第二種方式中,this的取值就是該方法所在的對象。這兩種調用方式和經典的面向對象編程語言沒有什麼不一樣,很是容易理解。第三種方式,作爲普通函數調用,這時,函數中的this永遠都指向全局對象,無論函數的定義嵌套得有多深,切記切記。而第四中調用方法最特別,它能夠改變函數中this的取值,所以,這種方式調用最靈活,妙用最多,這個須要幾個例子才能說明。先回顧一下我前面定義的Person()構造函數以及somebody對象:

function Person(name, age, gender){

this.name = name;
this.age = age;
this.gender = gender;

}
Person.prototype.sayHello = function(){ console.log("Hello, my name is " + this.name); };
var somebody = new Person("youxia", 30, "male");
  若是咱們調用:

somebody.sayHello(); //sayHello()中的this指向somebody,因此輸出"Hello, my name is youxia"
  那麼這個sayHello();方法中的this指向somebody對象,因此輸出結果很符合預期。可是,若是該函數不是經過對象的方法調用,結果就會大不相同。好比這樣:

var sayHi = somebody.sayHello;
sayHi(); //作爲普通函數調用,該函數中的this指向全局變量因此輸出"Hello, my name is "
  在上面的例子中,由於全局變量中沒有name屬性,因此輸出的結果中就沒有名字了。

  而後,我爲了偷懶,不想定義一個構造函數,只使用對象字面量定義了一個對象worker,表明一個具備Java技術的程序員,以下:

var worker = {name:"javaer", speciality:"Java"};
  這個對象沒有sayHello()方法,可是咱們能夠這樣借用somebody的sayHello()方法:

somebody.sayHello.call(worker); //輸出"Hello, my name is javaer"
  全部的函數均可以經過.call()、.apply()、.bind()的形式調用,由於這三個方法是定義在Function.prototype中的,而全部的函數的原型鏈中都有Function.prototype。這三個函數都會把調用函數的this設置爲這幾個方法的第一個參數。所不一樣者,.call()是接受任意多個參數,而.apply()只接受兩個參數,其第二個參數必須是一個數組,而.bind()返回另一個函數,這個函數的this綁定到.bind()的參數所指定的對象。

  能夠看到,若是某個對象具備和其它對象相同的屬性,好比這裏的name屬性,就經過.call()的方式借用別的對象的方法。因爲.apply()接受的第二個參數是一個數組,因此,若是有某個函數自己只接受不定數量的參數,而要操做的確是一個數組的時候,就能夠用.apply()來在它們之間適配。最多見的例子就是Math.max()方法,該方法接受的是不定數量的參數,假如咱們手頭只有一個數組,好比這樣:

var numbers = [3, 2, 5, 1, 7, 9, 8, 2];
  而咱們又要找出數組中的最大值的話,能夠這樣調用:

Math.max.apply(null, numbers);
  把第一個參數設置爲null,則Math.max()中的this就會自動指向全局對象。不過在這個例子中,this的值不重要。這裏只是改變了Math.max()方法接受參數的形式。

  在 JavaScript 中常用.call()調用來借用內置對象的方法,最多見的是借用Object.prototype.toString()方法。雖然咱們全部的對象都是從Object繼承,全部的對象都有從Object繼承的toString()方法,可是,這些方法能夠隨時被重寫。好比在咱們前面定義的Person類中,咱們能夠重寫它的toString()方法,以下:

Person.prototype.toString = function(){

return 'Person {name: "' + this.name + '", age: ' + this.age + ', gender: "' + this.gender + '"}';

}
  這時,調用somebody的toString()方法,會獲得這樣的輸出:

somebody.toString();
//輸出 "Person {name: "youxia", age: 30, gender: "male"}"
  可是若是借用Object.prototype.toString()方法,則會獲得另一種輸出:

Object.prototype.toString.call(somebody);
//輸出 "[object Object]"
  因此這種技術常被各類庫用來判斷對象的類型。以下:

Object.prototype.toString.call(somebody);
//輸出 "[object Object]"
Object.prototype.toString.call(Person);
//輸出 "[object Function]"
Object.prototype.toString.call("Hello, World!");
//輸出 "[object String]"
Object.prototype.toString.call(["one", "two", "three"]);
//輸出 "[object Array]"
Object.prototype.toString.call(3.14);
//輸出 "[object Number]"
  從上面能夠看出,使用.call()借用別的對象中的方法,不會受到本對象中重寫的同名方法的影響。因此,也能夠在子類中使用此技巧調用父類中的方法,後面我講面向對象和繼承的時候會用到這個技巧。

  下面又要開始踩坑了,這個坑是關於this的。上面提到過,凡是做爲普通函數調用的函數,其 context 中的this都是指向全局對象的。因此,若是咱們在某個對象的構造函數或方法中定義了內部函數,本覺得使用this能夠訪問這個新構造的對象,結果會事與願違。以下代碼:

function Worker(name, speciality){

this.name = name;
this.speciality = speciality;
this.doWork = function(){
    function work(){console.log(this.name + " is working with " + this.speciality);}
    work();
}

}
var worker = new Worker("youxia", "Java");
worker.doWork();
  本覺得會輸出"youxia is working with Java",可是因爲其中定義的work()是一個普通函數,因此其中的this指向全局對象,而全局對象的name和speciality屬性是沒有定義的,因此會輸出"is working with undefined"。若是要解決這個問題,能夠在構造函數中先臨時保存this的值,在網絡中,你們通常喜歡用that這個詞。更改後的代碼以下:

function Worker(name, speciality){

this.name = name;
this.speciality = speciality;
var that = this;
this.doWork = function(){
    function work(){console.log(that.name + " is working with " + that.speciality);}
    work();
}

}
var worker = new Worker("youxia", "Java");
worker.doWork();
  這回輸出就徹底正確了。同時,這裏也提示出一個小技巧,那就是當咱們位於一個閉包中時,若是想訪問全局對象,只須要定義一個普通函數,而後訪問這個普通函數的this便可。

回到頂部
用JavaScript模擬經典的面向對象編程
  經典的面向對象編程語言好比 C++、C#、Java 等都是基於類的,它們都有一套成熟的體系,包括對象的構造、類的繼承、對象的多態、對象屬性的訪問控制等。在 JavaScript 中,多態這個問題能夠不用考慮,由於 JavaScript 語言自己就是動態的,因此不存在類型不符合就編譯不經過這樣的問題。在 JavaScript 中主要考慮的問題就是對象的構造和繼承的問題。

  對象的構造是須要首先考慮的問題,其目標就是要得到一個合理的對象內存佈局。在 JavaScript 中沒有類的概念,可是有構造函數和this就足夠了,因此咱們能夠這樣簡單地建立對象:

function Person(name, age, gender){

this.name = name;
this.age = age;
this.gender = gender;
this.sayHello = function(){ console.log("Hello, my name is " + this.name); };

}
var somebody = new Person("somebody", 30, "male");
var another = new Person("another", 20, "female");
  這和經典的面向對象編程語言在形式上是很像的,經典的面向對象的編程語言是在類裏面定義屬性和方法,而這裏是在構造函數中定義屬性和方法。然而,仔細分析的話,其在內存佈局上還有不合理的地方,經典的面向對象編程語言中每一個對象的屬性是單獨的,可是方法在內存中只有一個拷貝,而上述 JavaScript 代碼每構建一個對象,都會爲每一個對象定義一個方法,若是對象數量很大的話,就會浪費不少內存。

  根據全部對象共享方法的原則,以及 JavaScript 的語言特點,咱們應該把方法放到其原型中,因此代碼應更改以下:

function Person(name, age, gender){

this.name = name;
this.age = age;
this.gender = gender;

}
Person.prototype.sayHello = function(){ console.log("Hello, my name is " + this.name); };
var somebody = new Person("youxia", 30, "male");
var another = new Person("another", 20, "female");
  同時,若是爲了防止別人在調用構造函數的時候忘記使用new而踩入this的陷阱的話,該代碼還能夠繼續這樣完善:

function Person(name, age, gender){

if(!(this instanceof Person)){
    return new Person(name, age, gender);
}
this.name = name;
this.age = age;
this.gender = gender;

}
Person.prototype.sayHello = function(){ console.log("Hello, my name is " + this.name); };
var somebody = new Person("youxia", 30, "male");
var another = new Person("another", 20, "female");
  下面再來看繼承。假設咱們每一個人都有一個工做者身份,咱們會使用咱們掌握的某項技能進行工做,這裏用 Worker 表明工做者,而 Worker 從 Person 繼承。咱們先來寫 Worker,因爲 JavaScript 是一個基於原型的語言,因此理論上講,要讓 Worker 繼承自 Person,只須要把 Person 類的一個對象加入到 Worker 的原型鏈中便可,以下:

function Worker(name, age, gender, speciality){

this.name = name;
this.age = age;
this.gender = gender;
this.speciality = speciality;

}
Worker.prototype = new Person(name, age, gender);
Worker.prototype.doWork = function(){console.log(this.name + " is working with " + this.speciality);}
  很顯然,這也不是很合理的,在這裏須要構建一個 Person 類的對象(這裏暫且這麼稱呼吧,雖然 JavaScript 中沒有類),而構建 Person 類的對象時又要傳遞參數,這些參數哪裏來呢?很顯然編碼不是很方便。同時,既然在 Person 類的對象中構造了name、age、gender等屬性,再在 Worker 類的對象中構建一次就重複了。並且修改了Worker.prototype後,constructor屬性也變了,還要一條語句改回來。若是從經典的面向對象編程語言的角度來考慮,咱們須要繼承的僅僅只是 Person 類中的方法而已。若是從 JavaScript 語言的角度分析,咱們只須要把Person.prototype加入到 Worker 類的對象的原型鏈中便可。代碼是這樣:

Worker.prototype.__proto__ = Person.prototype;
  咱們還可使用前面提到的.call()來借用 Person 類的構造函數讓代碼更簡潔。完整的繼承代碼以下:

function Worker(name, age, gender, speciality){

Person.call(this, name, age, gender);
this.speciality = speciality;

}
Worker.prototype.__proto__ = Person.prototype;
Worker.prototype.doWork = function(){console.log(this.name + " is working with " + this.speciality);}
  這樣使用它:

var worker = new Worker("youxia", 30, "male", ["JavaScript","HTML","CSS"]);
worker.sayHello(); //從Person類繼承的
worker.doWork(); //Worker類中本身定義的
  這是目前最接近經典面嚮對象語言的 JavaScript 模擬了。不過還有一個小小的問題,在 JavaScript 中,__proto__是一個隱藏屬性,不是全部的 JavaScript 平臺都支持的,好比前面展現的 IE 瀏覽器這個反面教材。這時,仍是要把Worker.prototype設置爲一個 Person 類的對象,可是,構建一個 Person 類的對象是個浪費,因此咱們能夠藉助一個空構造函數來完成這個事情:

function EmptyFunc(){}
EmptyFunc.prototype = Person.prototype;
Worker.prototype = new EmptyFunc();
  固然,別忘了把constructor改回來:

Worker.prototype.constructor = Worker;
  這幾條語句有點多,因此能夠寫一個輔助函數來解決這個問題:

function inherit(Sub, Super){

function F(){}
F.prototype = Super.prototype;
Sub.prototype = new F();
Sub.constructor = Sub;

}
  完整代碼以下:

function inherit(Sub, Super){

function F(){}
F.prototype = Super.prototype;
Sub.prototype = new F();
Sub.constructor = Sub;

}

function Person(name, age, gender){

if(!(this instanceof Person)){
    return new Person(name, age, gender);
}
this.name = name;
this.age = age;
this.gender = gender;

}
Person.prototype.sayHello = function(){ console.log("Hello, my name is " + this.name); };
var somebody = new Person("youxia", 30, "male");
var another = new Person("another", 20, "female");

function Worker(name, age, gender, speciality){

if(!(this instanceof Person)){
    return new Worker(name, age, gender, speciality);
}
Person.call(this, name, age, gender);
this.speciality = speciality;

}
inherit(Worker, Person);
Worker.prototype.doWork = function(){console.log(this.name + " is working with " + this.speciality);}

var worker = new Worker("youxia", 30, "male", ["JavaScript","HTML","CSS"]);
worker.sayHello(); //從Person類繼承的
worker.doWork(); //Worker類中本身定義的
  上面的代碼運行結果以下圖:

回到頂部
JavaScript的模塊化寫法
  更進一步,還要解決一個問題,那就是怎麼把這麼大一坨代碼封裝起來,專業的說法,那叫模塊化寫法。

  JavaScript 的一個缺陷就是它沒有模塊化的機制,像前文中我所寫的全部構造函數都是直接暴露在全局做用域中的,這很不科學,一是污染了全局做用域,二是容易和別人寫的代碼發生衝突。當代碼量增大的時候,確定要考慮將咱們本身的代碼組織成一個模塊。怎麼辦呢?很顯然,在 JavaScript 中只能用自執行函數和閉包來模擬。好比這樣:

(function(){

function inherit(Sub, Super){
    ...
}

function Person(name, age, gender){
    ...
}
Person.prototype.sayHello = function(){ console.log("Hello, my name is " + this.name); };

function Worker(name, age, gender, speciality){
    ...
}
inherit(Worker, Person);
Worker.prototype.doWork = function(){console.log(this.name + " is working with " + this.speciality);}

window.myModule = {
    inherit : inherit,
    Person : Person,
    Worker : Worker
};

})();
  代碼比較長,我省略了一部分。最關鍵的代碼實際上是最後的幾句,咱們經過window對象的myModule屬性來暴露咱們想暴露的函數和構造函數。而後,咱們能夠在 HTML 頁面中這樣使用:

<!DOCTYPE html>
<html >

<head>
    <meta charset="utf-8">
    <script src="./myModule.js"></script>
</head>
<body>
    <script>
        var worker = new myModule.Worker("youxia", 30, "male", ["Java","C++"]);
        worker.doWork();
    </script>
</body>

</html>
  下面是我在 FireFox 瀏覽器的開發者工具中的截圖,在調試器中能夠看到源文件,添加一個斷點,就能夠在右側看到定義在myModule中的函數和構造函數了。Chromium 很牛,FireFox 也不錯。

  固然,這只是一個最簡單的前端模塊。我是直接把模塊的 JavaScript 文件路徑寫死在 HTML 中的。我在 HTML 中直接引用我本身的模塊是沒有問題的。可是在使用別人寫的模塊的時候就不必定有這麼簡單了,由於別人的模塊中可能會引用更多另外的模塊,而這些互相引用的模塊咱們不可能所有都寫死在 HTML 中,咱們更加不可能控制這些模塊的加載順序。所以,須要有統一的模塊規範來解決模塊加載和依賴的問題。目前,在瀏覽器中經常使用的規範有 AMD 規範和 CMD 規範。

  AMD 規範是指異步模塊定義,它是 RequireJS 在推廣過程當中對模塊定義的規範化產出。在 AMD 中,全部的模塊將被異步加載,模塊加載不影響後面語句運行。全部依賴某些模塊的語句均放置在回調函數中。AMD規範定義了一個全局 define 的函數:

define( id?, dependencies?, factory );
  第一個參數 id 爲字符串類型,表示了模塊標識,爲可選參數。第二個參數,dependencies 是一個數組,表示當前模塊依賴的模塊。第三個參數,factory,就是用來定義咱們本身模塊的工廠方法, factory 接受的參數和 dependencies 徹底一致。加入咱們本身的模塊依賴於模塊module一、module二、module3的話,咱們的模塊定義就應該這樣寫:

define("myModule", ["module1", "module2", "module3"], function(module1, module2, module3){

// 這裏定義咱們本身的模塊的功能,可使用 module一、module二、module3中提供的功能
var myModule = {
    a: function(){...},
    b: function(){...}
}
return myModule; //必須return個什麼才能被別人使用

}
  若是要使用 myModule 就應該這麼寫:

define("", ["myModule"], function(myModule){

myModule.a();
   myModule.b();

}
  CMD 規範是指通用模塊定義,它是是 SeaJS 在推廣過程當中對模塊定義的規範化產出。和 AMD 比較相似的是,它也定義了一個全局 define 函數,並且其形式也很相似,都是

define( id?, dependencies?, factory );
  所不一樣者,是它的 factory 接受的參數必須是 require、exports 和 moudle,在 factory 內部,可使用 require 引用依賴的模塊,可使用 exports 導出本身的功能,這和 Node.js 自帶的 CommonJS 規範是比較類似的,其用法以下:

define( 'module', ['module1', 'module2'], function( require, exports, module ){

var a = require("./a");
a.doSomething();
exports.do = function(){...};

} );
  AMD 和 CMD 的區別就是:1.對於依賴的模塊,AMD 是提早執行,CMD 是延遲執行。2.CMD 推崇依賴就近,AMD 推崇依賴前置。示例代碼是這樣:

// CMD
define(function(require, exports, module) {

var a = require('./a')
a.doSomething()
...
var b = require('./b')   //依賴能夠就近書寫
b.doSomething()
...

})

// AMD 默認推薦的是
define(['./a', './b'], function(a, b) { //依賴必須一開始就寫好

a.doSomething()
...
b.doSomething()
...

})
  固然,這裏面都有一個約定俗成的規則,那就是每個模塊都是一個同名的.js文件,咱們在寫模塊名的時候,能夠省略這個文件的擴展名。以上規範都是定義在前端的瀏覽器中的,而在後端的 Node.js 中就簡單多了。Node.js 採用的是 CommonJS 模塊規範,每個文件就是一個模塊,也不須要定義 define 什麼的,也不須要定義自執行函數。在這個文件中,能夠直接使用 exports 和 module。

  有時,咱們須要讓咱們編寫的模塊在先後端都能使用,這個要求不過度哦,好比咱們想在 Node.js 中對模塊進行單元測試,而後再發布到瀏覽器執行。利用以前提到的每種模塊定義規範的特色,咱們能夠寫出先後端通用的模塊,代碼片斷以下:

var hasDefine = (typeof define !== "undefined");
var hasModule = (typeof module !== "undefined" && typeof module.exports !== "undefined");
if(hasDefine){ //運行在符合 AMD 或 CMD 規範的環境中

define("myModule",function(){
    return {
        inherit : inherit,
        Person : Person,
        Worker : Worker
    };
});

}else if(hasModule){ //運行在Node.js中

module.exports = {
    inherit : inherit,
    Person : Person,
    Worker : Worker
};

}else{ //不然直接加入到全局對象中

window.myModule = {
    inherit : inherit,
    Person : Person,
    Worker : Worker
};

}
  下面測試一下咱們寫的模塊是否能在先後端通用。先在 Node.js 中測試,寫一個main.js,其內容以下:

var myModule = require("./myModule");
var worker = new myModule.Worker("youxia", 30, "male", ["HTML","CSS","JavaScript"]);
worker.doWork();
  運行結果以下圖:

  若是直接寫死在 HTML 中呢?運行結果以下圖:

  最後,咱們看看使用 RequireJS 的狀況。先到 RequireJS 的官網下載require.js文件,而後編寫一個main_in_amd.js文件,內容以下:

requirejs(["./myModule"], function(myModule){

var worker = new myModule.Worker("youxia", 30, "male", ["HTML","CSS","JavaScript"]);
worker.doWork();

});
  再而後,寫一個 HTML 文件,這樣引用require.js和main_in_amd.js文件:

<!DOCTYPE html>
<html >

<head>
    <meta charset="utf-8">
    <script src="./require.js" defer async="true" data-main="./main_in_amd.js"></script>
</head>
<body>
</body>

</html>
  最後的運行結果以下圖:

  從圖中能夠看出,咱們的模塊確確實實是先後端能夠通用的。該模塊的完整代碼以下:

(function(){

function inherit(Sub, Super){
    function F(){}
    F.prototype = Super.prototype;
    Sub.prototype = new F();
    Sub.constructor = Sub;
}

function Person(name, age, gender){
    if(!(this instanceof Person)){
        return new Person(name, age, gender);
    }
    this.name = name;
    this.age = age;
    this.gender = gender;
}
Person.prototype.sayHello = function(){ console.log("Hello, my name is " + this.name); };

function Worker(name, age, gender, speciality){
    if(!(this instanceof Person)){
        return new Worker(name, age, gender, speciality);
    }
    Person.call(this, name, age, gender);
    this.speciality = speciality;
}
inherit(Worker, Person);
Worker.prototype.doWork = function(){
    console.log(this.name + " is working with " + this.speciality);
}

var hasDefine = (typeof define !== "undefined");
var hasModule = (typeof module !== "undefined" && typeof module.exports !== "undefined");
if(hasDefine){ //運行在符合 AMD 或 CMD 規範的環境中
    define("myModule", function(){
        return {
            inherit : inherit,
            Person : Person,
            Worker : Worker
        };
    });
}else if(hasModule){ //運行在Node.js中
    module.exports = {
        inherit : inherit,
        Person : Person,
        Worker : Worker
    };
}else{  //不然直接加入到全局對象中
    window.myModule = {
        inherit : inherit,
        Person : Person,
        Worker : Worker
    };
}

})();
回到頂部
關於函數的更多探討
  前面探討了 JavaScript 模擬經典面向對象的寫法,而實際應用中,也有很多場景會使用 JavaScript 模擬函數式編程語言的寫法,好比偏函數和柯里化什麼的。JavaScript 之父自己是一個 Scheme(一種 LISP 方言) 高手,因此他在建立 JavaScript 之初就從函數式編程語言中吸取了不少東西,將函數當成一等公民對待就是其證實。在 JavaScript 中,函數是一等公民,能夠把函數當參數傳遞給另外的函數,也能夠從函數中返回函數,因此在 JavaScript 中使用高階函數是一個很簡單很常見的事情。

  在前面的示例的截圖中能夠看到,每個函數中都包含幾個特殊的變量。前面已經介紹了其中一個特殊變量this,另外還有一個特殊變量arguments,它表明了傳遞給函數的全部參數,它是一個類數組的對象(說明它不是數組,可是能夠利用前面介紹的.call()借用數組的方法)。何時會用到arguments呢?就是當函數的形參個數和實參個數不同的時候,可使用arguments變量訪問傳遞給函數的全部參數。這使得定義可變數量參數的函數成爲可能。以下示例,隨便定義一個函數,沒有定義形參,可是調用時能夠指定任意的實參:

function someFunc(){

console.log("arguments count: " + arguments.length);
for(var i = 0; i < arguments.length; i++){
    console.log(arguments[i]);
}

}
  無論用多少個參數調用該函數,均可以輸出參數的個數和全部的參數,以下:

someFunc(1, 2, 3);
//輸出如下內容
// arguments count: 3
// 1
// 2
// 3

someFunc("one", "two", "three", "four");
//輸出如下內容
// arguments count: 4
// one
// two
// three
// four
  在 JavaScript 中,常使用一種叫偏函數的技巧來簡化某些須要太多參數的函數的使用。有時有些函數須要接受不少參數,可是其中一些參數是常常重複的,可是調用時又必須輸入這些參數,比較麻煩。這時,就能夠建立這些函數的偏函數版本,把那些常常重複的參數預先設定爲固定的值,調用這些偏函數時,只須要傳遞少許參數就好了。舉例說明,能夠利用前面提到的判斷對象類型的方法定義一個isType()函數,以下:

function isType(obj, type){

if(Object.prototype.toString.call(obj) === "[object " + type + "]"){
    return true;
}else{
    return false;
}

}
  能夠這樣調用該函數:

var msg = "Hello, World!";
var obj = {};
var arr = [];
isType(msg, "String");
isType(obj, "Object");
isType(arr, "Array");
  每次調用都須要輸入兩個參數,第二個參數經過字符串來指定須要判斷的類型,若是須要判斷類型的對象特別多,這樣調用就特別麻煩,並且容易出錯。因此能夠建立這個函數的偏函數版本,好比isFunction()、isString()、isArray()、isObject()等,這些函數只須要接受一個參數就能夠了。怎麼建立呢?先要寫一個返回偏函數的函數:

function partial(fn, type){ //接受須要偏函數化的原函數和須要預先設置的參數做爲參數

return function(obj){      //返回偏函數
    return fn(obj, type);
}

}
  而後利用這個函數返回isType()的各個偏函數版本,以下:

var isFunction = partial(isType, "Function");
var isString = partial(isType, "String");
var isArray = partial(isType, "Array");
var isObject = partial(isType, "Object");
  而後這樣調用這些函數:

isString(msg);
isObject(obj);
isArray(arr);
isFunction(Person);
  是否是簡單多了,也不容易出錯。可是上面只是把原本須要兩個參數的函數節約了一個參數,收益並非很大。其實,可使用偏函數的理論建立一個將任意函數進行偏函數化的函數partialAny(),以下:

function partialAny(fn){ //接受原函數

var originalArgs = Array.prototype.slice.call(arguments, 1); //得到原始參數,其參數個數和原函數須要的個數相同,其中有佔位符
return function(){      //返回偏函數
    var partialArgs = Array.prototype.slice.call(arguments); //得到偏函數的參數,其個數應該和佔位符的個數相同
    var newArgs = [];
    for(var i=0; i < originalArgs.length; i++){
        if(originalArgs[i] === "_"){ //若是碰到佔位符,則用偏函數的參數填補
            newArgs[i] = partialArgs.shift();
        }else{
            newArgs[i] = originalArgs[i];
        }
    }
    // 若是有任何多餘的參數,則添加到尾部
    return fn.apply(this, newArgs.concat(partialArgs));
}

}
  先用它了建立一個前面的isType()函數的偏函數試一試:

var isString = partialAny(isType, "_", "String");
isString("abc"); // 返回 true
  我這裏選擇了下劃線作爲佔位符,你們能夠根據本身的狀況酌情選擇。下面來個更復雜的例子,好比常常須要建立 RGB 顏色的函數:

function makeColor(r, g, b){

return "#" + r + g + b;

}
  該函數通常狀況下須要三個參數,可是能夠經過偏函數的方式讓 R、G、B 的任何一個份量固定,好比這樣:

var redMax = partialAny(makeColor, "ff", "_", "_");
var blueMax = partialAny(makeColor, "_", "_", "ff");
var greenMax = partialAny(makeColor, "_", "ff", "_");
var magentaMax = partialAny(makeColor, 'ff', "_", 'ff');
  而後這樣調用它們:

redMax("33", "44"); // 輸出"#ff3344"
blueMax("55", "66"); // 輸出"#5566ff"
greenMax("77", "88"); // 輸出"#77ff88"
magentaMax("99"); // 輸出"#ff99ff"
  函數也是對象,因此能夠重寫函數對象的toString()和valueOf()方法來達到意想不到的效果。舉例說明,假如咱們想建立一個add()函數,它既能夠這樣調用:

add(1,2);
又能夠這樣調用:

add(1,2)(3);
add(1)(2)(3)(4);
  能夠看出,這很相似於函數式編程語言中的柯里化(currying)。仔細觀察能夠發現,要實現以上效果,add()必須返回一個函數,這樣才能繼續後面的調用,以下:

function add(){

var result = 0;
for(var i=0; i < arguments.length; i++){  //先計算第一層調用
    result += arguments[i];
}
function temp(){
    for(var i=0; i < arguments.length; i++){  //再計算後續的調用
        result += arguments[i];
    }
    return temp;  //返回函數,因此能夠無限調用下去
}
return temp;      //返回函數,因此能夠無限調用下去

}
  這樣很方便就解決了函數連續調用的問題,可是又引出了新問題:該函數永遠返回的是函數,那怎樣才能獲得求和的值呢?這時就該toString()和valueOf()上場了,咱們只須要重寫函數temp()的toString()和valueOf()方法,就能夠在函數調用結束後,得到該表達式的值,以下:

temp.toString = temp.valueOf = function(){ return result; };
  完整代碼以下:

function add(){

var result = 0;
for(var i=0; i < arguments.length; i++){  //先計算第一層調用
    result += arguments[i];
}
function temp(){
    for(var i=0; i < arguments.length; i++){  //再計算後續的調用
        result += arguments[i];
    }
    return temp;  //返回函數,因此能夠無限調用下去
}
temp.toString = temp.valueOf = function(){ return result; };  //這樣能夠得到函數的值
return temp;      //返回函數,因此能夠無限調用下去

}
  而後,就能夠這樣隨意調用add()了:

add(1,2); //獲得3
add(1,2)(3); //獲得6
add(1,2)(3,4)()(5); //獲得15
add(1,2)(3)(4)(5)()(6,7,8,9); //獲得45
  固然,這並非最嚴格的柯里化,柯里化只是函數式編程語言中的一個特性,嚴格的柯里化每次只接受一個參數,直到總共接受了指定數量的參數後函數才執行。使用柯里化的好處是參數複用和延遲執行。因此,從本質上講,前面提到的偏函數更接近函數式編程語言中柯里化的功能。上面的例子雖然形式上相似柯里化,可是比柯里化更靈活,能夠接受無限多個參數和無限次調用,然而,這並無什麼卵用,炫耀技巧而已。

回到頂部
總結
  好吧,就寫這麼多吧,這篇文章已經夠長了。可是仍然不可能覆蓋 JavaScript 的方方面面。在我這篇文章中,主要關注的是 JavaScript 語言自己,而沒有涉及瀏覽器中的 BOM、DOM 操做,也沒有涉及 Node.js 的 API。

  這篇文章中的內容都是我根據本身的理解寫成的,我沒有《JavaScript 權威指南》和《JavaScript 高級程序設計》那麼全面和囉嗦,我建立對象和實現繼承的方式也許和網絡上那些流行的作法不同,可是,Whatever,這就是個人理解,歡迎你們不服來辯。若是你能堅持讀到這裏,請不要吝嗇點個贊。謝謝!

相關文章
相關標籤/搜索