你是js程式設計新手嗎,如果是的話,你可能會感到沮喪。所有的語言都有自己的怪癖(quirks)——但從基於強類型的伺服器端語言轉移過來的開發人員可能會感到困惑。我就曾經這樣,幾年前,當我被推到了全職JavaScript開發者的時候,有很多事情我希望我一開始就知道。在這篇文章中,我將分享一些怪癖,希望我能分享給你一些曾經令我頭痛不已的經驗。這不是一個完整清單——只是一部分——但希望它讓你看清這門語言的強大之處,可能曾經被你認為是障礙的東西。
我們將看下列技巧:
相等
-
點號vs括號
-
#函數上下文
-
函數宣告vs函數表達式
-
#命名vs匿名函數
#立即執行函數表達式
typeof vs Object.prototype.toString
-
##1.) 相等
C#出身的我非常熟悉==比較運算子。值類型(或字串)當有相同值是相等的。引用型別相等需要有相同的引用。 (我們假設你沒有重載==運算符,或實作你自己的等值運算和GetHashCode方法)我很驚訝為什麼JavaScript有兩個等值運算符:==和===。最初我的程式碼大部分都是用的==,所以我並不知道當我執行如下程式碼的時候JavaScript為我做了什麼:
var x = 1;
if(x == "1") {
console.log("YAY! They're equal!");
#}
#這是黑暗魔法嗎?整數1是如何和字串”1”相等的?
-
在JavaScript中,有相等(==)和嚴格相等(===)之說。相等運算子將強制轉換兩邊的運算元為相同型別後執行嚴格相等比較。所以在上面的例子中,字串”1”會被轉換為整數1,這個過程在幕後進行,然後與變數x進行比較。
嚴格相等不進行型別轉換。如果運算元類型不同(如整數和字串),那麼他們不全等(嚴格相等)。
var x = 1;
// 嚴格平等,型別必須相同
if(x === "1") {
console.log("Sadly, I'll never write this to the console");-
#########}#########################
if(x === 1) {
console.log("YES! Strict Equality FTW.")
}
#}
# #你可能正在考慮可能發生強制類型轉換而引起的各種恐怖問題——假設你的引用中發生了這種轉換,可能導致你非常困難找到問題出在哪裡。這並不奇怪,這也是為什麼經驗豐富的JavaScript開發者總是建議使用嚴格相等。
2.) 點號vs 括號 這取決於你來自其他什麼語言,你可能看過或沒看過這種方式(這就是廢話)。
-
// 取得person物件的firstName值
- var name = person.firstName;
// 取得數組的第三個元素
var theOneWeWant = myArray[2]; // remember, 0-based index不要忘了第一個元素的索引是0
然而,你知道它也可以使用括號引用物件的成員嗎?比如說:
var name = person["firstName"];
##為什麼會這樣有用嗎?而你會用點符號的大部分時間,有幾個實例的括號使某些方法可能無法這樣做。例如,我會經常重構大開關語句到一個調度表,所以這樣的事情: 為什麼可以這樣用?你以前可能對使用點比較熟悉,有幾個特例只能用括號表示法。例如,我常常會將switch語句重構為查找表(速度更快),其實就像這樣:
var doSomething = function(doWhat) {
# switch(doWhat) {
case "doThisThing":
// 更多
# break;
case "doThatThing":
// more code...
break;
case "doThisOtherThing":
// more code....
break;-
### // additional cases here, etc.####### ############## default:#################### # ///default beior#
break;
}
}
##}
可以轉換成像下面這樣:
var thingsWeCanDo = {
doThisThing : function() { /* behavior */ },
doThatThing : function() { /* behavior */ },
doThisOtherThing : function() { /* behavior */ },
default : function() { /* behav#ior# *##.
##};
- var doSomething = function(doWhat) {
- var thingToDo = thingsWeCanDo.hasOwnProperty(doWhat) ? doWhat : "default"
###### thingsWeCanDo[thingToDo]();#####################}# #######################使用switch並沒有錯誤(並且在許多情況下,如果被迭代多次並且非常關注性能,switch可能比查找表表現更好)。然而查找表提供了一個很好的方法來組織和擴展程式碼,並且括號允許你的屬性延時求值。 ############3.) 函數上下文#########已經有一些很棒的部落格發表了文章,正確理解了JavaScript中的this上下文(在文章的結尾我會給出一些不錯的鏈接),但它確實應該添加到“我希望我知道”的列表。它真的困難看懂程式碼並且自信的知道在任何位置this的值——你只需要學習一組規則。不幸的是,我早起讀到的許多解釋只是增加了我的困惑。因此我試著簡明扼要的做出解釋。 ############第一-首先考慮全域情況(Global)#########預設情況下,直到某些原因改變了執行上下文,否則this的值都指向全域物件。在瀏覽器中,那將會是window物件(或在node.js中為global)。 ############第二-方法中的this值#########當你有一個對象,其有一個函數成員,衝父對象呼叫這方法,this的值將指向父對象。例如:########################var marty = {################### # firstName: "Marty",###################### lastName: "McFly",################# ##### timeTravel: function(year) {#################### console.log(this.firstName + " " + this.lastName + " is time traveling to " + year);##########
}
#}
##marty.timeTravel(1955);
/ / Marty McFly is time traveling to 1955
你可能已經知道你能引用marty物件的timeTravel方法並且建立一個其他物件的新引用。這其實是JavaScript非常強大的特色——使我們能夠在不同的實例上引用行為(呼叫函數)。
var doc = {
# firstName: "Emmett ",
lastName: "Brown",
}
-
#doc.timeTravel = marty.timeTravel;
-
#所以-如果我們呼叫doc.timeTravel(1885)會發生什麼事?
doc.timeTravel(1885);
/ / Emmett Brown is time traveling to 1885
- #再一次-上演黑暗魔法。嗯,並不是真的。記住,當你呼叫一個方法的時候,this上下文是被呼叫函數父的父物件。
當我們儲存marty.TimeTravel方法的參考然後呼叫我們儲存的參考時發生了什麼?讓我們來看看:
-
var getBackInTime = marty.timeTravel;
-
###getBackInTime(2014);#####################// undefined undefined is time traveling to 2014######### #############為什麼是「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方法當有人點選一個按鈕:
var flux = document.getElementById("flux-capacitor");
flux.addEventListener("click", marty.timeTravel);
在上面的程式碼中,當使用者點擊按鈕是,我們會看到「undefined undefined is time traveling to [object MouseEvent]」。什麼?好——首先,非常明顯的問題是我們沒有給我們的timeTravel方法提供year參數。反而,我們直接訂閱這方法作為事件處理程序,並且MouseEvent參數被當作第一個參數傳遞個事件處理程序。這是很容易修復的,但真正的問題是我們再次見到“undefined undefined”。不要無望-你已經知道為什麼會發生這種情況(即使你還沒意識到)。讓我們修改我們的timeTravel函數,輸出this,從而幫助我們搞清事實:
#marty.timeTravel = function(year) {
console.log(this.firstName + " " + this.lastName + " is time traveling to " + year);
# console.log(this);
}; #
現在-當我們點擊這按鈕,我們將類似下面的輸出在你的瀏覽器控制台:
當方法被呼叫時,第二個console.log輸出出this上下文-它其實是我們訂閱事件的按鈕元素。你感到吃驚嗎?就像之前——當我們將marty.timeTravel賦值給getBackInTime變數時——對marty.timeTravel的引用被保存到事件處理程序,並被調用,但容器物件不再是marty物件。在這種情況下,它將在按鈕實例的點擊事件中非同步呼叫。
所以-有可能將this設定為我們想要的結果嗎?絕對可以!在這個例子裡,解決方法非常簡單。不在事件處理程序中直接訂閱marty.timeTravel,而是使用匿名函數作為事件處理程序,並在匿名函數中呼叫marty.timeTravel。這也能修復year參數遺失的問題。
flux.addEventListener("click", function(e) {
# marty.timeTravel(someYearValue);
});
#點選按鈕將會在控制台輸出類似下面的資訊:
成功了!但為什麼這樣可以?思考我們是如何呼叫timeTravel方法的。在我們按鈕點擊的第一個例子中,我們在事件處理程序中訂閱方法本身的引用,所以它沒有從父物件marty上呼叫。在第二個例子中,透過this為按鈕元素的匿名函數,並且當我們調用marty.timeTravel時,我們從其父物件marty上調用,所以this為marty。
第四-建構子中的this值 當你用建構子建立物件實例時,函數內部的this值就是新建立的對象。例如:
var TimeTraveler = function(fName, lName) {
-
# this.firstName = fName;
# this.lastName = lName;
# // Constructor functions return the
-
// newly created object for us unless ## // newly created object for us unless
## // we specifically return something else
#};
#var marty = new TimeTraveler("Marty", "McFly");
console.log(marty.firstName + " " + marty.lastName);
-
// Marty McFly ###
Call,Apply和BindCall 你可能開始疑惑,上面的例子中,沒有語言層級的特性允許我們在運行時指定調用函數的this值嗎?你是對的。存在於函數原型上的call和apply方法允許我們呼叫函數並傳遞this值。
call方法的第一個參數是this,後面是被呼叫函數的參數序列:
someFn.call(this, arg1, arg2, arg3);
apply的第一個參數也是this,後面是其餘參數組成的陣列:
someFn.apply(this, [arg1, arg2, arg3]);
#我們的doc和marty實例他們自己能時間旅行,但einstein(愛因斯坦)需要他們的幫助才能完成時間旅行。所以讓我們為我們的doc實例加入一個方法,以至於doc可以幫助einstein完成時間旅行。
doc.timeTravelFor = function(instance, year) {
-
this.timeTravel.call(instance, year);
// 如果你使用apply使用下面的語法
// this.timeTravel.apply(instance, [year]);
};
現在它可以傳送Einstein 了:
##var einstein = {
# firstName: "Einstein",
# lastName: "(the dog)"
};
doc.timeTravelFor(einstein, 1985);
- ##// Einstein (the dog) is time traveling to 1985
-
我知道這個例子有些牽強,但它足以讓你看到應用函數到其他物件的強大之處。 ###這種方法還有我們沒有發現的另一個用途。讓我們為我們的marty實例新增一個goHome方法,作為this.timeTravel(1985)的捷徑。 ########################marty.goHome = function() {################## ### this.timeTravel(1985);#####################}#######
然而,我們知道如果我們訂閱marty.goHome作為按鈕的點擊事件處理程序,this的值將是按鈕——並且不幸的是按鈕沒有timeTravel方法。我們能用上面的方法解決──用個一匿名函數作為事件處理程序,並在其內部呼叫上述方法──但我們有另一個選擇──bind函數:
flux.addEventListener("click", marty.goHome.bind(marty));
bind函數實際上會傳回一個新函數,新函數的this值會根據你提供的參數設定。如果你需要支援低版本瀏覽器(例如:ie9以下版本),你可能需要bind函數的shim(或者,如果你使用jQuery你可以用$.proxy代替,underscore和lodash都提供_ .bind方法)。
記住重要一點,如果你直接使用原型上的bind方法,它將建立一個實例方法,這將繞過原型方法的優點。這不是錯誤,做到心裡清楚就好了。我寫了關於這個問題得更多信息在這裡。
4.) 函數表達式vs函數宣告 #函數宣告不需要var關鍵字。事實上,如Angus Croll所說:「把他們想像成變數聲明的兄弟有助於理解」。例如:
function timeTravel(year) {
console.log(this.firstName + " " + this.lastName + " is time traveling to " + year);
}上面例子裡的函數名字timeTravel不僅在它所宣告的在作用域可見,同時在函數本身內部也是可見的(這對遞歸函數呼叫非常有用)。函數聲明,本質上說其實就是命名函數。換句話說,上面函數的名稱屬性是timeTravel。
函數表達式定義一個函數並指派給一個變數。典型應用如下:
var someFn = function() {
console.log("I like to express myself...");
}; 也可以对函数表达式命名——然而,不像函数声明,命名函数表达式的名字仅在它自身函数体内可访问:
var someFn = function iHazName() {
console.log("I like to express myself...");
if(needsMoreExpressing) {
iHazName(); // 函数的名字在这里可以访问
}
};
// 你可以在这里调用someFn(),但不能调用iHazName()
someFn();
#討論函數表達式和函數宣告不能不提「hoisting (提升)」-函數和變數宣告被編譯器移到作用域的頂端。在這裡我們無法詳細解釋hoisting,但你可以讀Ben Cherry和Angus Croll兩個人的偉大解釋。
5.) 命名vs匿名函數 基於我們剛才的討論,你可能一進猜到「匿名」函數其實就是一個沒有名字的函數。大多數JavaScript開發者能快速辨識出瞎買年第一個參數為匿名函數:
#
someElement.addEventListener("click", function(e) {
// I' m anonymous!
});
## 然而,同樣的我們的marty.timeTravvel方法也是一個匿名函數:
-
#var marty = {
firstName: "Marty",
lastName: "McFly",
# timeTravel: function(year) {
- ## console.log(this.firstName + " " + this.lastName + " is time traveling to " + year);
- }
- #}
#因為函數宣告必須有一個唯一的名字,只有函數表達式可以沒有名字。 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吧。
|