1. 程式人生 > >[原生js] 前端開發必須掌握的七個JavaScript技巧

[原生js] 前端開發必須掌握的七個JavaScript技巧

如果你是一個JavaScript新手或僅僅最近才在你的開發工作中接觸它,你可能感到沮喪。所有的語言都有自己的怪癖(quirks)——但從基於強型別的伺服器端語言轉移過來的開發人員可能會感到困惑。我就曾經這樣,幾年前,當我被推到了全職JavaScript開發者的時候,有很多事情我希望我一開始就知道。在這篇文章中,我將分享一些怪癖,希望我能分享給你一些曾經令我頭痛不已的經驗。這不是一個完整列表——僅僅是一部分——但希望它讓你看清這門語言的強大之處,可能曾經被你認為是障礙的東西。

我們將看下列技巧:
  • 相等
  • 點號vs括號
  • 函式上下文
  • 函式宣告vs函式表示式
  • 命名vs匿名函式
  • 立即執行函式表示式
  • typeof vs Object.prototype.toString


1.) 相等
C#出身的我非常熟悉==比較運算子。值型別(或字串)當有相同值是是相等的。引用型別相等需要有相同的引用。(我們假設你沒有過載==運算子,或實現你自己的等值運算和GetHashCode方法)我很驚訝為什麼JavaScript有兩個等值運算子:==和===。最初我的大部分程式碼都是用的==,所以我並不知道當我執行如下程式碼的時候JavaScript為我做了什麼:
  1. var x = 1;
  2. if(x == "1") {
  3.     console.log("YAY! They're equal!");
  4. }
複製程式碼

這是黑暗魔法嗎?整數1是如何和字串”1”相等的?

在JavaScript中,有相等(==)和嚴格相等(===)之說。相等運算子將強制轉換兩邊的運算元為相同型別後執行嚴格相等比較。所以在上面的例子中,字串”1”會被轉換為整數1,這個過程在幕後進行,然後與變數x進行比較。

嚴格相等不進行型別轉換。如果運算元型別不同(如整數和字串),那麼他們不全等(嚴格相等)。
  1. var x = 1;
  2. // 嚴格平等,型別必須相同
  3. if(x === "1") {
  4.     console.log("Sadly, I'll never write this to the console");
  5. }
  6. if(x === 1) {
  7.     console.log("YES! Strict Equality FTW.")
  8. }
複製程式碼

你可能正在考慮可能發生強制型別轉換而引起的各種恐怖問題——假設你的引用中發生了這種轉換,可能導致你非常困難找到問題出在哪裡。這並不奇怪,這也是為什麼經驗豐富的JavaScript開發者總是建議使用嚴格相等。

2.) 點號 vs 括號
這取決於你來自其他什麼語言,你可能見過或沒見過這種方式(這就是廢話)。
  1. // 獲取person物件的firstName值
  2. var name = person.firstName;
  3. // 獲取陣列的第三個元素
  4. var theOneWeWant = myArray[2]; // remember, 0-based index不要忘了第一個元素的索引是0
複製程式碼

然而,你知道它也可以使用括號引用物件的成員嗎?比如說:
  1. var name = person["firstName"];
複製程式碼

為什麼會這樣有用嗎?而你會用點符號的大部分時間,有幾個例項的括號使某些方法可能無法這樣做。例如,我會經常重構大開關語句到一個排程表,所以這樣的事情:

為什麼可以這樣用?你以前可能對使用點更熟悉,有幾個特例只能用括號表示法。例如,我經常會將switch語句重構為查詢表(速度更快),其實就像這樣:
  1. var doSomething = function(doWhat) {
  2.     switch(doWhat) {
  3.         case "doThisThing":
  4.             // more code...
  5.         break;
  6.         case "doThatThing":
  7.             // more code...
  8.         break;
  9.         case "doThisOtherThing":
  10.             // more code....
  11.         break;
  12.         // additional cases here, etc.
  13.         default:
  14.             // default behavior
  15.         break;
  16.     }
  17. }
複製程式碼

可以轉化為像下面這樣:
  1. var thingsWeCanDo = {
  2.     doThisThing      : function() { /* behavior */ },
  3.     doThatThing      : function() { /* behavior */ },
  4.     doThisOtherThing : function() { /* behavior */ },
  5.     default          : function() { /* behavior */ }
  6. };
  7. var doSomething = function(doWhat) {
  8.     var thingToDo = thingsWeCanDo.hasOwnProperty(doWhat) ? doWhat : "default"
  9.     thingsWeCanDo[thingToDo]();
  10. }
複製程式碼

使用switch並沒有錯誤(並且在許多情況下,如果被迭代多次並且非常關注效能,switch可能比查詢表表現更好)。然而查詢表提供了一個很好的方法來組織和擴充套件程式碼,並且括號允許你的屬性延時求值。

3.) 函式上下文
已經有一些偉大的部落格發表了文章,正確理解了JavaScript中的this上下文(在文章的結尾我會給出一些不錯的連結),但它確實應該加到“我希望我知道”的列表。它真的困難看懂程式碼並且自信的知道在任何位置this的值——你僅需要學習一組規則。不幸的是,我早起讀到的許多解釋只是增加了我的困惑。因此我試圖簡明扼要的做出解釋。

第一——首先考慮全域性情況(Global)
預設情況下,直到某些原因改變了執行上下文,否則this的值都指向全域性物件。在瀏覽器中,那將會是window物件(或在node.js中為global)。

第二——方法中的this值
當你有一個物件,其有一個函式成員,衝父物件呼叫這方法,this的值將指向父物件。例如:
  1. var marty = {
  2.     firstName: "Marty",
  3.     lastName: "McFly",
  4.     timeTravel: function(year) {
  5.         console.log(this.firstName + " " + this.lastName + " is time traveling to " + year);
  6.     }
  7. }
  8. marty.timeTravel(1955);
  9. // Marty McFly is time traveling to 1955
複製程式碼

你可能已經知道你能引用marty物件的timeTravel方法並且建立一個其他物件的新引用。這實際上是JavaScript非常強大的特色——使我們能夠在不同的例項上引用行為(呼叫函式)。
  1. var doc = {
  2.     firstName: "Emmett",
  3.     lastName: "Brown",
  4. }
  5. doc.timeTravel = marty.timeTravel;
複製程式碼

所以——如果我們呼叫doc.timeTravel(1885)將會發生什麼?
  1. doc.timeTravel(1885);
  2. // Emmett Brown is time traveling to 1885
複製程式碼

再次——上演黑暗魔法。嗯,並不是真的。記住,當你呼叫一個方法的時候,this上下文是被呼叫函式父的父物件。

當我們儲存marty.TimeTravel方法的引用然後呼叫我們儲存的引用時發生了什麼?讓我們看看:
  1. var getBackInTime = marty.timeTravel;
  2. getBackInTime(2014);
  3. // undefined undefined is time traveling to 2014
  4. 為什麼是“undefined undefined”?!而不是“Matry McFly”?
複製程式碼

讓我們問一個關鍵的問題:當我們呼叫我們的getBackInTime函式時父物件/容器物件是什麼?當getBackIntTime函式存在於window中時,我們呼叫它作為一個函式,而不是一個物件的方法。當我們像這樣呼叫一個函式——沒有容器物件——this上下文將是全域性物件。David Shariff有一個偉大的描述關於這:

無論何時呼叫一個函式,我們必須立刻檢視括號的左邊。如果在括號的左邊存在一個引用,那麼被傳遞個呼叫函式的this值確定為引用所屬的物件,否則是全絕物件。

由於getBackInTime的this上下文是window——沒有firstName和lastName屬性——這解釋了為什麼我們看見“undefined undefined”。

因此我們知道直接呼叫一個函式——沒有容器物件——this上下文的結果是全域性物件。然而我也說我早就知道我們的getBackInTime函式存在於window上。我是如何知道的?好的,不像上面我包裹getBackInTime在不同的上下文(我們探討立即執行函式表示式的時候),我宣告的任何變數都被新增的window。來自Chrome控制檯的驗證:



是時候討論下this的主要用武之地之一了:訂閱事件處理。

第三(僅僅是#2的擴充套件)——非同步呼叫方法中的this值
所以,讓我們假裝我們想呼叫我們的marty.timeTravel方法當有人點選一個按鈕時:
  1. var flux = document.getElementById("flux-capacitor");
  2. flux.addEventListener("click", marty.timeTravel);
複製程式碼

在上面的程式碼中,當用戶點選按鈕是,我們會看見“undefined undefined is time traveling to [object MouseEvent]”。什麼?好——首先,非常明顯的問題是我們沒有給我們的timeTravel方法提供year引數。反而,我們直接訂閱這方法作為事件處理程式,並且MouseEvent引數被作為第一個引數傳遞個事件處理程式。這是很容易修復的,但真正的問題是我們再次見到“undefined undefined”。不要無望——你已經知道為什麼會發生這種情況(即使你還沒意識到)。讓我們修改我們的timeTravel函式,輸出this,從而幫助我們搞清事實:
  1. marty.timeTravel = function(year) {
  2.     console.log(this.firstName + " " + this.lastName + " is time traveling to " + year);
  3.     console.log(this);
  4. };
複製程式碼

現在——當我們點選這按鈕,我們將類似下面的輸出 在你的瀏覽器控制檯:



當方法被呼叫時,第二個console.log輸出出this上下文——它實際上是我們訂閱事件的按鈕元素。你感到吃驚嗎?就像之前——當我們將marty.timeTravel賦值給getBackInTime變數時——對marty.timeTravel的引用被儲存到事件處理程式,並被呼叫,但容器物件不再是marty物件。在這種情況下,它將在按鈕例項的點選事件中非同步呼叫。

所以——有可能將this設定為我們想要的結果嗎?絕對可以!在這個例子裡,解決方法非常簡單。不在事件處理程式中直接訂閱marty.timeTravel,而是使用匿名函式作為事件處理程式,並在匿名函式中呼叫marty.timeTravel。這也能修復year引數丟失的問題。
  1. flux.addEventListener("click", function(e) {
  2.     marty.timeTravel(someYearValue); 
  3. });
複製程式碼

點選按鈕將會在控制檯輸出類似下面的資訊:



成功了!但為什麼這樣可以?思考我們是如何呼叫timeTravel方法的。在我們按鈕點選的第一個例子中,我們在事件處理程式中訂閱方法自身的引用,所以它沒有從父物件marty上呼叫。在第二個例子中,通過this為按鈕元素的匿名函式,並且當我們呼叫marty.timeTravel時,我們從其父物件marty上呼叫,所以this為marty。

第四——建構函式中的this值
當你用建構函式建立物件例項時,函式內部的this值就是新建立的物件。例如:
  1. var TimeTraveler = function(fName, lName) {
  2.     this.firstName = fName;
  3.     this.lastName = lName;
  4.     // Constructor functions return the
  5.     // newly created object for us unless
  6.     // we specifically return something else
  7. };
  8. var marty = new TimeTraveler("Marty", "McFly");
  9. console.log(marty.firstName + " " + marty.lastName);
  10. // Marty McFly
複製程式碼

Call,Apply和BindCall
你可能開始疑惑,上面的例子中,沒有語言級別的特性允許我們在執行時指定呼叫函式的this值嗎?你是對的。存在於函式原型上的call和apply方法允許我們呼叫函式並傳遞this值。

call方法的第一個引數是this,後面是被呼叫函式的引數序列:
  1. someFn.call(this, arg1, arg2, arg3);
複製程式碼

apply的第一個引數也是this,後面是其餘引數組成的陣列:
  1. someFn.apply(this, [arg1, arg2, arg3]);
複製程式碼

我們的doc和marty例項他們自己能時間旅行,但einstein(愛因斯坦)需要他們的幫助才能完成時間旅行。所以讓我們給我們的doc例項新增一個方法,以至於doc能幫助einstein完成時間旅行。
  1. doc.timeTravelFor = function(instance, year) {
  2.     this.timeTravel.call(instance, year);
  3.     // 如果你使用apply使用下面的語法
  4.     // this.timeTravel.apply(instance, [year]);
  5. };
複製程式碼

現在它可以傳送Einstein 了:
  1. var einstein = {
  2.     firstName: "Einstein", 
  3.     lastName: "(the dog)"
  4. };
  5. doc.timeTravelFor(einstein, 1985);
  6. // Einstein (the dog) is time traveling to 1985
複製程式碼

我知道這個例子有些牽強,但它足以讓你看到應用函式到其他物件的強大之處。

這種方法還有我們沒有發現的另一種用處。讓我們給我們的marty例項新增一個goHome方法,作為this.timeTravel(1985)的快捷方式。
  1. marty.goHome = function() {
  2.     this.timeTravel(1985);
  3. }
複製程式碼

然而,我們知道如果我們訂閱marty.goHome作為按鈕的點選事件處理程式,this的值將是按鈕——並且不幸的是按鈕沒有timeTravel方法。我們能用上面的方法解決——用個一匿名函式作為事件處理程式,並在其內部呼叫上述方法——但我們有另一個選擇——bind函式:
  1. flux.addEventListener("click", marty.goHome.bind(marty));
複製程式碼

bind函式實際上會返回一個新函式,新函式的this值根據你提供的引數設定。如果你需要支援低版本瀏覽器(例如:ie9以下版本),你可能需要bind函式的shim(或者,如果你使用jQuery你可以用$.proxy代替,underscore和lodash都提供_.bind方法)。

記住重要一點,如果你直接使用原型上的bind方法,它將建立一個例項方法,這將繞過原型方法的優點。這不是錯誤,做到心裡清楚就行了。我寫了關於這個問題得更多資訊在這裡

4.) 函式表示式vs函式宣告
函式宣告不需要var關鍵字。事實上,如Angus Croll所說:“把他們想象成變數宣告的兄弟有助於理解”。例如:
  1. function timeTravel(year) {
  2.     console.log(this.firstName + " " + this.lastName + " is time traveling to " + year);
  3. } 上面例子裡的函式名字timeTravel不僅在它宣告的在作用域可見,同時在函式本身內部也是可見的(這對遞迴函式呼叫非常有用)。函式宣告,本質上說其實就是命名函式。換句話說,上面函式的名稱屬性是timeTravel。
複製程式碼

函式表示式定義一個函式並指派給一個變數。典型應用如下:
  1. var someFn = function() {
  2.     console.log("I like to express myself...");
  3. }; 也可以對函式表示式命名——然而,不像函式宣告,命名函式表示式的名字僅在它自身函式體內可訪問:
  4. var someFn = function iHazName() {
  5.     console.log("I like to express myself...");
  6.     if(needsMoreExpressing) {
  7.         iHazName(); // 函式的名字在這裡可以訪問
  8.     }
  9. };
  10. // 你可以在這裡呼叫someFn(),但不能呼叫iHazName()
  11. someFn();
複製程式碼

討論函式表示式和函式宣告不能不提“hoisting(提升)”——函式和變數宣告被編譯器移到作用域的頂部。在這裡我們無法詳細解釋hoisting,但你可以讀Ben Cherry和Angus Croll兩個人的偉大解釋。

5.) 命名vs匿名函式
基於我們剛才的討論,你可能一進猜到“匿名”函式其實就是一個沒有名字的函式。大多數JavaScript開發者能迅速識別瞎買年第一個引數為匿名函式:
  1. someElement.addEventListener("click", function(e) {
  2.     // I'm anonymous!
  3. });
複製程式碼

然而,同樣的我們的marty.timeTravvel方法也是一個匿名函式:
  1. var marty = {
  2.     firstName: "Marty",
  3.     lastName: "McFly",
  4.     timeTravel: function(year) {
  5.         console.log(this.firstName + " " + this.lastName + " is time traveling to " + year);
  6.     }
  7. }
複製程式碼

因為函式宣告必須有一個唯一的名字,只有函式表示式可以沒有名字。

6.) 立即執行函式表示式
因為我們正在談論函式表示式,有一個東西我希望我早知道:立即執行函式表示式(IIFE)。有很多關於IIFE的好文章(我將在文章結尾出列出),但用一句話來形容,函式表示式不是通過將函式表示式賦值給一個標量,稍後再執行,而是理解執行。可以在瀏覽器控制檯看這一過程。

首先——讓我們先敲入一個函式表示式——但不給它指派變數——看看會發什麼:



語法錯誤——這被認為是函式宣告,缺少函式名字。然而,為了使其變為表示式,我們僅需將其包裹在括號內:



讓其變為表示式後控制檯返回給我們一個匿名函式(記住,我們沒有為其指派值,但表示式會有返回值)。所以——我們知道“函式表示式”是“立即呼叫函式表示式”的一部分。為了等到“立即執行”的特性,我們通過在表示式後面新增另一個括號來呼叫返回的表示式(就像我們呼叫其他函式一樣):



“但是等一下,Jim!(指作者)我想我以前見過這種呼叫方式”。 事實上你可能見過——這是合法的語法(眾所周知的是Douglas Crockford的首選語法)



這兩種方法都起作用,但是我強烈建議你讀一讀這裡

OK,非常棒——現在我們已經知道了IIFE是什麼——以及為什麼要用它?

它幫助我們控制作用域——任何JavaScript教程中非常重要的部分!前面我們看到的許多例項都建立在全域性作用域。這意味著window(假設環境是瀏覽器)物件將有很多屬性。如果我們全部按照這種方式寫我們的JavaScript程式碼,我們會迅速在全域性作用域積累一噸(誇張)變數宣告,window程式碼會被汙染。即使在最好的情況下,在全域性變數暴漏許多細節是糟糕的建議,但當變數的名字和已經存在的window屬性名字相同時會發生什麼呢?window屬性會被重寫!

例如,如果你最喜歡的“Amelia Earhart”網站在全域性作用域聲明瞭一個navigator變數,下面是設定之前和之後的結果:



哎呀!

顯而易見——全域性變數被汙染是糟糕的。JavaScript使用函式作用域(而不是塊作用域,如果你來自C#或Java,這點非常重要!),所以保持我們的程式碼和全域性作用域分離的辦法是建立一個新作用域,我們可以使用IIFE來實現,因為它的內容在它自己的函式作用域內。在下面的例子中,我將在控制檯向你顯示window.navigator的值,然後我常見一個IIFE(立即執行函式表示式)去包裹Amelia Earhart的行為和資料。IIFE結束後返回一個作為我們的“程式名稱空間”的物件。我在IIFE內宣告的navigator變數將不會重寫window.navigator的值。



作為額外好處,我們上面建立的IIFE是JavaScript中模組模式的啟蒙。我將在結尾處包括一些我瀏覽的模組模式的連結。

7.) ‘typeof’操作符和’Object.prototype.toString’
最終,可能發現在某些情況下,你需要檢查傳遞給函式引數的型別,或其他類似的東西。typeof運算子會是顯而易見的選擇,但是,這並不是萬能的。例如,當我們對一個物件,陣列,字串或正則表示式,呼叫typeof運算子時會發生什麼?



還好——至少我們可以將字串和物件,陣列,正則表示式區分開,對嗎?幸運的是,我們可以得到更準確的型別資訊,我們有其他不同的方法。我們將使用Object.prototype.toString方法,並且應用我們前面提到的call方法:



為什麼我們要使用Object.prototype上的toString方法?因為第三方庫或你自己的程式碼可能重寫例項的toString方法。通過Object.prototype,我們可以強制實現例項原來的toString行為。

如果你知道typeof將會返回什麼那麼你不需要進行多餘的檢查(例如,你僅需要知道是或不是一個字串),此時用typeof非常好。然而,如果你需要區分陣列和物件,正則表示式和物件,等等,那麼使用Object.prototype.toString吧。

接下來呢
我已經從其他JavaScript開發者的見解中收益頗多,所以請看看下面的這些連結,並給這些人一些鼓勵,他們給予了我們諄諄教誨。

Axel Rauschmayer’s 非常棒的文章 在JavaScript中什麼時候使用==是正確的? (提示:從不)
Fixing the typeof Operator by Angus Croll
Airbnb Github Issue comment that’s the single best explanation on IIFE parens placement
Function Declarations vs. Function Expressions – by Angus Croll
Getting Into Context Binds by yours truly
Immediately-Invoked Function Expression (IIFE) by Ben Alman
Learning JavaScript Design Patterns by Addy Osmani
Understanding the “this” keyword in JavaScript by Nicholas Bergson-Shilcock
MDN – Function.prototype.bind
MDN – Function.prototype.apply
MDN – Function.prototype.call
Named function expressions demystified by Juriy “kangax” Zaytsev
Basic JavaScript for the impatient programmer by Axel Rauschmayer
JavaScript Scoping and Hoisting by Ben Cherry
JavaScript’s ‘this’ Keyword by David Shariff
What is the Execution Context & Stack in JavaScript? by David Shariff


英文:http://developer.telerik.com/fea ... ish-id-known-about/