我但願本身儘早知道的 7 個 JavaScript 怪癖(轉載oschina)

若是對你來講JavaScript仍是一門全新的語言,或者你是在最近的開發中纔剛剛對它有所瞭解,那麼你可能會有些許挫敗 感。任何編程語言都有它本身的怪癖(quirks)——然而,當你從那些強類型的服務器端語言轉向JavaScript的時候 ,你會感到很是困惑。我就是這樣!當我在幾年前作全職JavaScript開發的時候,我多麼但願關於這門語言的許多事情我能儘早地知道。我但願經過本文中分享的一些怪癖能讓你免於遭受我所經歷過的那些頭疼的日子。本文並不是一個詳盡的列表,只是一些取樣,目的是拋磚引玉,而且讓你明白當你一旦逾越了這些障礙,你會發現JavaScript是多麼強大。javascript

咱們會把焦點放在下面這些怪癖上:php

1.) 相等

由於C#的緣故我習慣於用==運算符來作比較。具備相同值的值類型(以及字符串)是相等 的,反之否則。指向相同引用的引用類型是相等的,反之也否則。(固然這是創建在你沒有重載==運算符或者GetHashCode方法的前提下)當我知道 JavaScript有==和===兩種相等運算符時,令我驚詫不已。我所見過的大多數狀況都是使用==,因此我如法炮製。然而,當我運行下面的代碼時 JavaScript並無給我想固然的結果:html

1 var x = 1;
2  
3 if(x == "1") {
4     console.log("YAY! They're equal!");
5 }

呃……這是什麼黑魔法?整型數1怎麼會和字符串」1」相等?java

在JavaScript裏有相等(equality ==)和恆等(strict equality ===)。相等運算符會先會先把運算符兩邊的運算元強制轉換爲同種類型,而後再進行恆等比較。因此上面例子中的字符串」1」會先被轉換成整數1,而後再和 咱們的變量x進行比較。node

恆等不會進行強制類型轉換。若是運算元是不一樣類型的(就像整型數1和字符串」1」)那麼他們就是不相等的:git

01 var x = 1;
02  
03 // 對於恆等,首先類型必須同樣
04 if(x === "1") {
05     console.log("Sadly, I'll never write this to the console");
06 }
07  
08 if(x === 1) {
09     console.log("YES! Strict Equality FTW.")
10 }

你可能已經開始爲各類不可預知的強制類型轉換擔心了,它們可能會在你的應用中讓真假混亂,致使一些bug,而這些bug你很難從代碼中看出來。這並不奇怪,所以,那些有經驗的JavaScript開發者建議咱們老是使用恆等運算符。github

2.) 點號 vs 方括號

你可能會對JavaScript中用訪問數組元素的方式來訪問一個對象的屬性這種形式感到詫異,固然,這取決於你以前使用的其餘語言:web

1 // getting the "firstName" value from the person object:
2 var name = person.firstName;
3  
4 // getting the 3rd element in an array:
5 var theOneWeWant = myArray[2]; // remember, 0-based index

然而 ,你知道咱們也能用方括號來引用對象的成員嗎?例如:正則表達式

1 var name = person["firstName"];

那這有什麼用呢?可能大部分時間你仍是使用點號,然而有些爲數很少的狀況下,方括號給咱們提供了一些點號方式沒法完成的捷徑。好比,我可能會常常把一些大的switch語句重構成一個調度表(dispatch table),像下面這樣:express

01 var doSomething = function(doWhat) {
02     switch(doWhat) {
03         case "doThisThing":
04             // more code...
05         break;
06         case "doThatThing":
07             // more code...
08         break;
09         case "doThisOtherThing":
10             // more code....
11         break;
12         // additional cases here, etc.
13         default:
14             // default behavior
15         break;
16     }
17 }

它們能被轉換成下面這樣:

01 var thingsWeCanDo = {
02     doThisThing      : function() { /* behavior */ },
03     doThatThing      : function() { /* behavior */ },
04     doThisOtherThing : function() { /* behavior */ },
05     default          function() { /* behavior */ }
06 };
07  
08 var doSomething = function(doWhat) {
09     var thingToDo = thingsWeCanDo.hasOwnProperty(doWhat) ? doWhat : "default"
10     thingsWeCanDo[thingToDo]();
11 }

固然,使用switch自己並無什麼錯(而且,在大多數狀況下,若是你對迭代和性能很在乎的話,switch可能比調度表要好)。然而,調度表提供了一種更好的組織和擴展方式,而且方括號容許你在運行時動態地引用屬性。

3.) 函數上下文

已經有不少不錯的博客裏解釋過JavaScript中的this所表明的上下文(而且, 我在本文末尾也添加了這些博文的連接),然而,我仍是明確地決定把它加到我「但願本身儘早知道的事」的清單裏。在代碼的任意地方明確this所表明的東西 是什麼並不困難——你只須要記住幾條規則。然而,我以前讀過的那些關於這點的解讀只能增添個人困惑,所以,我嘗試用一種簡單的方式來表述。

第一,開始時假設它是全局的

默認狀況下,this引用的是全局對象(global object),直到有緣由讓執行上下文發生了改變。在瀏覽器裏它指向的就是window對象(或者在node.js裏就是global)。

第二,方法內部的this

若是你有個對象中的某個成員是個function,那麼當你從這個對象上調用這個方法的時候this就指向了這個父對象。例如:

01 var marty = {
02     firstName: "Marty",
03     lastName: "McFly",
04     timeTravel: function(year) {
05         console.log(this.firstName + " " + this.lastName + " is time traveling to " + year);
06     }
07 }
08  
09 marty.timeTravel(1955);
10 // Marty McFly is time traveling to 1955

你可能已經知道你能夠經過建立一個新的對象,來引用marty對象上的timeTravel方法。這確實是JavaScript一個很是強大的特性——能讓咱們把函數應用到不止一個目標實例上:

1     var doc = {
2     firstName: "Emmett",
3     lastName: "Brown",
4 }
5  
6 doc.timeTravel = marty.timeTravel;

那麼,咱們調用doc.timeTravel(1885)會發生什麼事呢?

1 doc.timeTravel(1885);
2 // Emmett Brown is time traveling to 1885

呃……再一次被黑魔法深深地刺傷了。其實事實也並不是如此,還記得咱們前面提到過的當你調用一個方法,那麼這個方法中的this將指向調用它的那個父對象。握緊你德羅寧(DeLoreans)跑車的方向盤吧,由於車子變重了。(譯註:做者示例代碼的參考背景是一部叫《回到將來》 的電影,Marty McFly 是電影裏的主角,Emmett Brown 是把DeLoreans跑車改裝成時光旅行機的博士,因此marty對象和doc對象分別指代這兩人。而此時this指向了doc對象,博士比Marty 重,因此……我必定會看一下這部電影。 )

當咱們保存了一個marty.TimeTravel方法的引用而且經過這個引用調用這個方法時到底發生了什麼事呢?咱們來看一下:

1 var getBackInTime = marty.timeTravel;
2 getBackInTime(2014);
3 // undefined undefined is time traveling to 2014

爲何是「undefined undefined」?!爲何不是「Marty McFly」?

讓咱們問一個關鍵的問題:當咱們調用getBackInTime函數時,它的父/擁有者 對象是誰呢?由於getBackInTime函數是存在於window上的,咱們是把它看成函數(function)調用,而不是某個對象的方 (method)。當咱們像上面這樣直接調用一個沒有擁有者對象的函數的時候,this將會指向全局對象。David Shariff對此有個很妙的描述:

不管什麼時候,當一個函數被調用,咱們必須看方括號或者是圓括號左邊緊鄰的位置,若是咱們看到一個引用(reference),那麼傳到function裏面的this值就是指向這個方法所屬於的那個對象,如若否則,那它就是指向全局對象的。

由於getBackInTime的this是指向window的,而window對象裏並無firstName和lastName屬性,這就是解釋了爲何咱們看到的會是「undefined undefined」。

所以,咱們就知道了直接調用一個沒有擁有者對象的函數時結果就是其內部的this將會是 全局對象。可是,我也說過咱們的getBackInTime函數是存在於window上的。我是怎麼知道的呢?除非我把getBackInTime包裹到 另外一個不一樣的做用域中,不然我聲明的任何變量都會附加到window上。下面就是從Chrome的控制檯中獲得的證實:

jsquirgwfwrks_afjq_1

如今是討論關於this諸多重點之一——綁定事件處理函數——的最佳時機。

第三(其實只是第二點的一個擴展),異步調用的方法內部的this

咱們假設在某個button被點擊的時候咱們想調用marty.timeTravel方法:

1 var flux = document.getElementById("flux-capacitor");
2 flux.addEventListener("click", marty.timeTravel);

當咱們點擊button的時候,上面的代碼會輸出「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 };

如今咱們再點擊button的時候,應該就能在瀏覽器控制檯中看到相似下面這樣的輸出:

jsquigwerrks_afjq_2

在方法被調用時第二個console.log輸出了this,它其實是咱們綁定的 button元素。感到奇怪麼?就像以前咱們把marty.timeTravel賦值給一個getBakInTime的變量引用同樣,此時的 marty.timeTravel被保存爲咱們事件處理函數的引用,而且被調用了,可是並非從「擁有者」marty對象那裏調用的。在這種狀況下,它是 被button元素實例中的事件觸發接口調用的。

那麼,有沒有可能讓this是咱們想要的東西呢?固然能夠!這種狀況下,解決方案很是簡 單。咱們能夠用一個匿名函數代替marty.timeTravel來作事件處理函數,而後在這個匿名函數裏調用marty.timeTravel。同時這 樣也讓咱們有機會修復以前丟失year參數的問題。

1 flux.addEventListener("click"function(e) {
2     marty.timeTravel(someYearValue);
3 });

點擊button會看到像下面這樣的輸出:

jsquisgwegerks_afjq_3

成功了!可是爲何成功呢?思考一下咱們是怎麼調用timeTravel方法的。第一次 的時候咱們是把這個方法的自己的引用做爲事件處理函數,所以它並非從父對象marty上調用的。第二次的時候,咱們的匿名函數中的this是指向 button元素的,然而當咱們調用marty.timeTravel時,咱們是從父對象marty上調用的,因此此時這個方法裏的this是 marty。

第四,構造函數裏的this

當你用構造函數建立一個對象的實例時,那麼構造函數裏的this就是你新建的這個實例。例如:

01 var TimeTraveler = function(fName, lName) {
02     this.firstName = fName;
03     this.lastName = lName;
04     // Constructor functions return the
05     // newly created object for us unless
06     // we specifically return something else
07 };
08  
09 var marty = new TimeTraveler("Marty""McFly");
10 console.log(marty.firstName + " " + marty.lastName);
11 // Marty McFly

使用Call,Apply和Bind

從上面給出的例子你可能已經猜到了,經過一些語言級別的特性是容許咱們在調用一個函數的時候指定它在運行時的this的。讓你給猜對了。call和apply方法存在於Function的prototype中,它們容許咱們在調用一個方法的時候傳入一個this的值。

call方法的簽名中先是指定this參數,其後跟着的是方法調用時要用到的參數,這些參數是各自分開的。

1 someFn.call(this, arg1, arg2, arg3);

apply的第一個參數一樣也是this的值,而其後跟着的是調用這個函數時的參數的數組。

1 someFn.apply(this, [arg1, arg2, arg3]);

咱們的doc和margy對象本身能進行時光旅行(譯註:即對象中有 timeTravel方法),然而愛因斯坦(譯註:Einstein,電影中博士的寵物,是一隻狗)須要別人的幫助才能進行時光旅行,因此如今讓咱們給之 前的doc對象(就是以前把marty.timeTravel賦值給doc.timeTravel的那個版本)添加一個方法,這樣doc對象就能幫助 einstein對象進行時光旅行了:

1 doc.timeTravelFor = function(instance, year) {
2     this.timeTravel.call(instance, year);
3     // alternate syntax if you used apply would be
4     // this.timeTravel.apply(instance, [year]);
5 };

如今咱們能夠送愛因斯坦上路了:

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的方法,這個方法是個讓marty回到將來的捷徑,由於它實際上是調用了this.timeTravel(1985):

1 marty.goHome = function() {
2     this.timeTravel(1985);
3 }

咱們已經知道,若是把 marty.goHome 做爲事件處理函數綁定到button的click事件上,那麼this就是這個button。而且,button對象上也並無timeTravel這個 方法。咱們能夠用以前那種匿名函數的辦法來綁定事件處理函數,再在匿名函數裏調用marty對象上的方法。不過,咱們還有另一個辦法,那就是bind函數:

1 flux.addEventListener("click", marty.goHome.bind(marty));

bind函數實際上是返回一個新函數,而這個新函數中的this值正是用bind的參數來指定的。若是你須要支持那些舊的瀏覽器(好比IE9如下的)你就須要用個bind方法的補丁(或者,若是你使用的是jQuery,那麼你能夠用$.proxy;另外underscore和lodash庫中也提供了_.bind)。

有一件事須要注意,若是你在一個原型方法上使用bind,那它會建立一個實例級別的方法,這樣就屏蔽了原型上的同名方法,你應該意識到這並非個錯誤。關於這個問題的更多細節我在這篇文章裏進行了描述。

4.) 函數聲明 vs 函數表達式

在JavaScript主要有兩種定義函數的方法(而ES6會在這裏做介紹):函數聲明和函數表達式。

函數聲明不須要var關鍵字。事實上,正如 Angus Croll 所說:「把他看成變量聲明的兄弟是頗有幫助的」。例如:

1 function timeTravel(year) {
2     console.log(this.firstName + " " this.lastName + " is time traveling to " + year);
3 }

上例中名叫timeTravel的函數不只僅只在其被聲明的做用域內可見,並且對這個函數自身內部也是可見的(這一點對遞歸函數的調用尤其有用)。函數聲明其實就是命名函數,換句話說,上面的函數的name屬性就是timeTravel。

函數表達式是定義一個函數並把它賦值給一個變量。通常狀況下,它們看起來會是這樣:

1 var someFn = function() {
2     console.log("I like to express myself...");
3 };

函數表達式也是能夠被命名的,只不過不像函數聲明那樣,被命名的函數表達式的名字只能在 該函數內部的做用域中訪問(譯註:上例中的代碼,關鍵字function後面直接跟着圓括號,此時你能夠用someFn.name來訪問函數名,可是輸出 將會是空字符串;而下例中的someFn.name會是」iHazName」,可是你卻不能在iHazName這個函數體以外的地方用這個名字來調用此函 數):

1 var someFn = function iHazName() {
2     console.log("I like to express myself...");
3     if(needsMoreExpressing) {
4         iHazName(); // the function's name can be used here
5     }
6 };
7  
8 // you can call someFn() here, but not iHazName()
9 someFn();

函數表達式和函數聲明的討論遠不止這些,除此以外至少還有提高(hoisting)。提高是指函數和變量的聲明被解釋器移動到包含它們的做用域的頂部。雖然咱們在這裏沒有細說提高,可是務必讀一下Ben CherryAngus Croll對它的解讀。

5.) 具名和匿名函數

基於咱們剛剛討論的,你確定猜到所謂的匿名函數就是沒有名字的函數。大多數JavaScript開發者都能很快認出下例中第二個參數是一個匿名函數:

1 someElement.addEventListener("click"function(e) {
2     // I'm anonymous!
3 });

而事實上咱們的marty.timeTravel方法也是匿名的:

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.) 自調用函數表達式

自從咱們開始討論函數表達以來,有件事我就想立馬搞清楚,那就是自調用函數表達式( the Immediately Invoked Function Expression (IIFE))。我會在本文的結尾羅列幾篇對IIFE講解得不錯的文章。但簡而言之,它就是一個沒有賦值給任何變量的函數表達式,它並不等待稍後被調用, 而是在定義的時候就當即執行。下面這些瀏覽器控制檯的截圖能幫助咱們理解:

首先讓咱們輸入一個函數表達式,可是不把它賦值給任何變量,看看會發生什麼

jsqduirks_afjq_4

無效的JavaScript語法——它實際上是一個缺乏名字的函數聲明。想讓它變成一個表達式,咱們只需用一對圓括號把它包裹起來:

jsquirks_afjq_5

當把它變成一個表達式後控制檯當即返回給咱們這個匿名函數(咱們並無把這個函數賦值給 其餘變量,可是,由於它是個表達式,咱們只是獲取到了表達式的值)。然而,這只是實現了「自調用函數表達式」中的「函數表達式」部分。對於「自調用」這部 分,咱們是經過給這個返回的表達式後面加上另一對圓括號來實現的(就像咱們調用任何其餘函數同樣)。

jsquirks_afjq_6

「可是等等!Jim,我記得我之前在哪看到過把後面的那對圓括號放進表達式括號裏面的狀況。」你說得對,這種語法徹底正確(由於Douglas Crockford 更喜歡這種語法,才讓它變得衆所周知):

jsquirks_afjq_7

這兩種語法都是可用的,然而我強烈建議你讀一下對這兩種用法有史以來最好的解釋

OK,咱們如今已經知道什麼是IIFE了,那爲何說它頗有用呢?

它能夠幫助咱們控制做用域,這是JavaScript中很重要的一部分!marty對象 一開始是被建立在一個全局做用域裏。這意味着window對象(假定咱們運行在瀏覽器裏)裏有個marty屬性。若是咱們JavaScript代碼都照這 個寫法,那麼很快全局做用域下就會被大量的變量聲明給填滿,污染了window對象。即便是在最理想的狀況下,這都是很差的作法,由於把不少細節暴露給了 全局做用域,那麼,當你在聲明一個對象時對它命名,而這個名字恰巧又和window對象上已經存在的一個屬性同名,那麼會發生什麼事呢?這個屬性會被覆蓋 掉!好比,你打算建個「阿梅莉亞·埃爾哈特(Amelia Earhart)」的粉絲網站,你在全局做用域下聲明瞭一個叫navigator的變量,那麼咱們來看一下這先後發生了些什麼(譯註:阿梅莉亞·埃爾哈特 是一位傳奇的美國女性飛行員,不幸在1937年,當她嘗試全球首次環球飛行時,在飛越太平洋期間失蹤。當時和她一塊兒在飛機上的導航員 (navigator)就是下面代碼中的這位佛萊得·努南(Fred Noonan)):

jsquirks_afjq_8

呃……

顯然,污染全局做用域是種很差的作法。JavaScript使用的是函數做用域(而不是 塊做用域,若是你是從C#或者Java轉過來的,這點必定要當心!)因此,阻止咱們的代碼污染全局做用域的辦法就是建立一個新做用域,咱們能夠用IIFE 來達到這個目的,由於它裏面的內容只會在它本身的函數做用域裏。下面的例子裏,我要先在控制檯查看一下window.navigator的值,再用一個 IIFE來包裹起具體的行爲和數據,並把他賦值給amelia。這個IIFE返回一個對象做爲咱們的「應用程序做用域」。在這個IIFE裏我聲明瞭一個 navigator變量,它不會覆蓋window.navigator的值。

jsquirks_afjq_9

做爲一點額外的福利,咱們上面建立的IIFE實際上是JavaScript模塊模式(module pattern)的一個開端。在文章結尾有一些相關的連接,以便你能夠繼續探索JavaScript的模塊模式。

7.) typeof運算符和Object.prototype.toString

終有一天你會遇到與此相似的情形,那就是你須要檢測一個函數傳進來的值是什麼類型。typeof運算符彷佛是不二之選,然而,它並非那麼可靠。例如,當咱們對一個對象,一個數組,一個字符串,或者一個正則表達式使用typeof時,會發生什麼呢?

jsquirks_afjq_10

好吧,至少它能把字符串從對象,數組和正則表達式中區分出來。幸好咱們還有其它辦法能從這些檢測的值裏獲得更多準確的信息。咱們可使用Object.prototype.toString函數而且應用上咱們以前掌握的call方法的知識:

jsquirks_afjq_11

爲何咱們要使用Object.prototype上的toString方法呢?由於它可能被第三方的庫或者咱們本身的代碼中的實例方法給重載掉。而經過Object.prototype咱們能夠強制使用原始的toString。

若是你知道typeof會給你返回什麼,而且你也不須要知道除此以外的其餘信息(例如, 你只須要知道某個值是否是字符串),那麼用typeof就再好不過了。然而,若是你想區分數組和對象或者正則表達式和對象等等的,那麼就用 Object.prototype.toString吧。

接下來去哪裏

我從其餘的JavaScript開發者的真知灼見裏受益不淺,所以,請訪問下面的連接而且感謝一下他們吧。

原文出處: Jim Cowart   譯文出處: codingserf

相關文章
相關標籤/搜索