若是你是一個JavaScript新手或僅僅最近纔在你的開發工做中接觸它,你可能感到沮喪。全部的語言都有本身的怪癖(quirks)——但從基於強類型的服務器端語言轉移過來的開發人員可能會感到困惑。我就曾經這樣,幾年前,當我被推到了全職JavaScript開發者的時候,有不少事情我但願我一開始就知道。在這篇文章中,我將分享一些怪癖,但願我能分享給你一些曾經令我頭痛不已的經驗。這不是一個完整列表——僅僅是一部分——但但願它讓你看清這門語言的強大之處,可能曾經被你認爲是障礙的東西。javascript
C#出身的我很是熟悉==比較運算符。值類型(或字符串)當有相同值是是相等的。引用類型相等須要有相同的引用。(咱們假設你沒有重載==運算符,或實現你本身的等值運算和GetHashCode方法)我很驚訝爲何JavaScript有兩個等值運算符:==和===。最初個人大部分代碼都是用的==,因此我並不知道當我運行以下代碼的時候JavaScript爲我作了什麼:html
var x = 1; if(x == "1") { console.log("YAY! They're equal!"); }
這是黑暗魔法嗎?整數1是如何和字符串"1"相等的?java
在JavaScript中,有相等(==)和嚴格相等(===)之說。相等運算符將強制轉換兩邊的操做數爲相同類型後執行嚴格相等比較。因此在上面的例子中,字符串"1"會被轉換爲整數1,這個過程在幕後進行,而後與變量x進行比較。node
嚴格相等不進行類型轉換。若是操做數類型不一樣(如整數和字符串),那麼他們不全等(嚴格相等)。git
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開發者老是建議使用嚴格相等。github
這取決於你來自其餘什麼語言,你可能見過或沒見過這種方式(這就是廢話)。web
// 獲取person對象的firstName值 var name = person.firstName; // 獲取數組的第三個元素 var theOneWeWant = myArray[2]; // remember, 0-based index不要忘了第一個元素的索引是0
然而,你知道它也可使用括號引用對象的成員嗎?好比說:正則表達式
var name = person["firstName"];
爲何會這樣有用嗎?而你會用點符號的大部分時間,有幾個實例的括號使某些方法可能沒法這樣作。例如,我會常常重構大開關語句到一個調度表,因此這樣的事情:express
爲何能夠這樣用?你之前可能對使用點更熟悉,有幾個特例只能用括號表示法。例如,我常常會將switch語句重構爲查找表(速度更快),其實就像這樣:後端
var doSomething = function(doWhat) { switch(doWhat) { case "doThisThing": // more code... break; case "doThatThing": // more code... break; case "doThisOtherThing": // more code.... break; // additional cases here, etc. default: // default behavior break; } }
能夠轉化爲像下面這樣:
var thingsWeCanDo = { doThisThing : function() { /* behavior */ }, doThatThing : function() { /* behavior */ }, doThisOtherThing : function() { /* behavior */ }, default : function() { /* behavior */ } }; var doSomething = function(doWhat) { var thingToDo = thingsWeCanDo.hasOwnProperty(doWhat) ? doWhat : "default" thingsWeCanDo[thingToDo](); }
使用switch並無錯誤(而且在許多狀況下,若是被迭代屢次而且很是關注性能,switch可能比查找表表現更好)。然而查找表提供了一個很好的方法來組織和擴展代碼,而且括號容許你的屬性延時求值。
已經有一些偉大的博客發表了文章,正確理解了JavaScript中的this上下文(在文章的結尾我會給出一些不錯的連接),但它確實應該加到「我但願我知道」的列表。它真的困難看懂代碼而且自信的知道在任何位置this的值——你僅須要學習一組規則。不幸的是,我早起讀到的許多解釋只是增長了個人困惑。所以我試圖簡明扼要的作出解釋。
默認狀況下,直到某些緣由改變了執行上下文,不然this的值都指向全局對象。在瀏覽器中,那將會是window對象(或在node.js中爲global)。
當你有一個對象,其有一個函數成員,衝父對象調用這方法,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的主要用武之地之一了:訂閱事件處理。
因此,讓咱們僞裝咱們想調用咱們的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值就是新建立的對象。例如:
var TimeTraveler = function(fName, lName) { this.firstName = fName; this.lastName = lName; // Constructor functions return the // 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
你可能開始疑惑,上面的例子中,沒有語言級別的特性容許咱們在運行時指定調用函數的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方法,它將建立一個實例方法,這將繞過原型方法的優勢。這不是錯誤,作到內心清楚就好了。我寫了關於這個問題得更多信息在這裏。
函數聲明不須要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兩我的的偉大解釋。
基於咱們剛纔的討論,你可能一進猜到「匿名」函數其實就是一個沒有名字的函數。大多數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); } }
由於函數聲明必須有一個惟一的名字,只有函數表達式能夠沒有名字。
由於咱們正在談論函數表達式,有一個東西我但願我早知道:當即執行函數表達式(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中模塊模式的啓蒙。我將在結尾處包括一些我瀏覽的模塊模式的連接。
最終,可能發如今某些狀況下,你須要檢查傳遞給函數參數的類型,或其餘相似的東西。typeof運算符會是顯而易見的選擇,可是,這並非萬能的。例如,當咱們對一個對象,數組,字符串或正則表達式,調用typeof運算符時會發生什麼?
還好——至少咱們能夠將字符串和對象,數組,正則表達式區分開,對嗎?幸運的是,咱們能夠獲得更準確的類型信息,咱們有其餘不一樣的方法。咱們將使用Object.prototype.toString方法,而且應用咱們前面提到的call方法:
爲何咱們要使用Object.prototype上的toString方法?由於第三方庫或你本身的代碼可能重寫實例的toString方法。經過Object.prototype,咱們能夠強制實現實例原來的toString行爲。
若是你知道typeof將會返回什麼那麼你不須要進行多餘的檢查(例如,你僅須要知道是或不是一個字符串),此時用typeof很是好。然而,若是你須要區分數組和對象,正則表達式和對象,等等,那麼使用Object.prototype.toString吧。
我已經從其餘JavaScript開發者的看法中收益頗多,因此請看看下面的這些連接,並給這些人一些鼓勵,他們給予了咱們諄諄教誨。
英文:http://developer.telerik.com/featured/seven-javascript-quirks-i-wish-id-known-about/
CSS家園 188275051,Web開發者(先後端)的天堂,歡迎有興趣的同窗加入
GitHub家園 225932282,Git/GitHub愛好者的天堂,歡迎有興趣的同窗加入