JavaScript中的陷阱大集合
本文主要介紹怪異的Javascript,毋庸置疑,它絕對有怪異的一面。當軟件開發(fā)者開始使用世界上使用最廣泛的語言編寫代碼時,他們會在這個過 程中發(fā)現(xiàn)很多有趣的“特性”。即便是老練的Javascript開發(fā)者也可以在本文找到一些有趣的新陷阱,請留意這些陷阱,當然也可以盡情享受由這些陷阱 帶來的“樂趣”!
函數(shù)和操作符
雙等號
==操作符比較時會進行類型的強制轉(zhuǎn)換,這意味著它可以比較兩個不同類型的對象,在執(zhí)行比較之前它將會嘗試把這兩個對象轉(zhuǎn)換成同一個類型,舉一個例子:
- "1" == 1 //true
 
然而,這樣往往會誤導(dǎo)我們,而且我們也不需要這樣子來比較。在上面的例子中,我們完全可以先將字符串轉(zhuǎn)換成數(shù)字型,然后利用對類型敏感的三重等號(===)來進行比較,如:
- Number("1") === 1; //true
 
或者,更好的是,確保你放在首位的操作數(shù)的類型是正確的。
由于雙等號具有強制類型轉(zhuǎn)換的行為,所以它會打破一般的傳遞性規(guī)則,這點有點嚇人,請看下面的列子:
- "" == 0 //true - 空字符串會被強制轉(zhuǎn)換為數(shù)字0.
 - 0 == "0" //true - 數(shù)字0會被強制轉(zhuǎn)換成字符串"0"
 - "" == "0" //false - 兩操作數(shù)都是字符串所以不執(zhí)行強制轉(zhuǎn)換
 
如果使用三重等號,上面的三個比較都將返回false。
parseInt不把10作為數(shù)字基數(shù)
如果你忽略parseInt的第二個參數(shù),那么數(shù)字的基數(shù)將由下面的規(guī)則所決定:
◆ 默認基數(shù)為10,即按10進制解析
◆ 如果數(shù)字以0x開頭,那么基數(shù)為16,即按16進制解析
◆ 如果數(shù)字以0開頭,那么基數(shù)為8,即按8進制解析
一個常見的錯誤是我們讓用戶輸入以0開頭的數(shù)字,這時候它就按8進制的方式去解析了,于是我們就看到了如下的效果:
- parseInt("8"); //8
 - parseInt("08"); //0
 
因此,我們很多時候都會指定parseInt的第二個參數(shù),如下所示:
- parseInt("8", 10); //8
 - parseInt("08", 10); //8
 
ECMAScript5方面的說明:ECMAScript已不再支持8進制的解析假設(shè),另外,如果忽略parseInt的第二個參數(shù)將會引起JSLint的警告。
字符串替換
字符串替換函數(shù)僅僅會替換第一個匹配項,并不能替換你所期望的全部匹配項。如下代碼:
- "bob".replace("b", "x"); // "xob"
 - "bob".replace(/b/, "x"); // "xob" (使用了正則表達式)
 
如果要替換所有的匹配項,我們可以使用正則表達式,并為他它添加全局修飾符,如下代碼:
- "bob".replace(/b/g, "x"); // "xox"
 - "bob".replace(new RegExp("b", "g"), "x"); // "xox" (alternate explicit RegExp)
 
全局修飾符確保了替換函數(shù)找到第一個匹配項后不會停止對下一個匹配項的替換。
“+"操作符會執(zhí)行相加操作和字符串連接操作
php作為另一種弱類型語言,可以使用”.“操作符對字符串進行連接。Javascript卻不是這樣的 - 所以當操作數(shù)是字符串的時候”a+b“通常是執(zhí)行連接操作。如果你想執(zhí)行數(shù)字相加那你就要引起注意了,因為輸入的內(nèi)容可能是字符串類型的,所以你在執(zhí)行相 加操作前需要先將其轉(zhuǎn)換成數(shù)字類型,代碼如下:
- 1 + document.getElementById("inputElem").value; // 連接操作
 - 1 + Number(document.getElementById("inputElem").value); // 相加操作
 
需要注意的是,相減操作會嘗試將操作數(shù)轉(zhuǎn)換成數(shù)字類型,代碼如下:
- "3" - "1"; // 2
 
盡管有時候你想用減法將字符串從另一個字符串中減掉,但這時候往往會產(chǎn)生一些邏輯錯誤。
很多時候我們用數(shù)字和空串相加來實現(xiàn)數(shù)字轉(zhuǎn)換成字符串的操作,代碼如下:
- 3 + ""; // "3"
 
但是這樣做并不好,所以我們可以用String(3)來取代上面的方法。
typeof
typeof這會返回一個javascript基本類型的實例的類型。Array實際上不是基本類型,所以typeof Array對象將返回Object,代碼如下:
- typeof {} === "object" //true
 - typeof "" === "string" //true
 - typeof [] === "array"; //false
 
當你對自己的對象的實例使用這個操作符時將會得到相同的結(jié)果(typeof = "object")。
另外說明一點,”typeof null“將返回”object“,這個有點詭異。
instanceof
instanceof返回指定對象是否是由某個類構(gòu)造的實例,這個對我們檢查指定對象是否是自定義類型之一很有幫助,但是,如果你是用文本語法創(chuàng)建的內(nèi)置類型那可能會得出錯誤的結(jié)果,代碼如下:
- "hello" instanceof String; //false
 - new String("hello") instanceof String; //true
 
由于Array實際上不是內(nèi)置類型(只是偽裝成內(nèi)置類型 - 因此對它使用typeof不能得到預(yù)期的結(jié)果),但是使用instanceof就能得到預(yù)期效果了,代碼如下所示:
- ["item1", "item2"] instanceof Array; //true
 - new Array("item1", "item2") instanceof Array; //true
 
唉,不爽!總的來說,如果你想測試Boolean, String, Number, 或者Function的類型,你可以使用typeof,對于其他的任何類型,你可以使用instanceof測試。
哦,還有一點,在一個function中,有一個預(yù)定義變量叫“arguments”,它以一個array的形式傳遞給function。然而,它并不是真正的array,它只是一個類似array的對象,帶有長度屬性并且屬性值從0-length。非常奇怪...你可以用下面的小伎倆將它轉(zhuǎn)換成真正的數(shù)組:
- var args = Array.prototype.slice.call(arguments, 0);
 
這個對由getElementsByTagName返回的NodeList對象也是一樣的 - 它們都可以用以上的代碼轉(zhuǎn)換成合適的數(shù)組。
eval
eval 可以將字符串以javascript代碼的形式來解析執(zhí)行,但是一般來說我們不建議這么做。因為eval非常慢 - 當javascript被加載到瀏覽器中時,它會被編譯成本地代碼;然而執(zhí)行的過程中每次遇到eval表達式,編譯引擎都將重新啟動執(zhí)行編譯,這樣做的代 價太大了。而且這樣做也丑陋無比,有很多eval被濫用的例子。另外,在eval中的代碼會在當前范圍內(nèi)執(zhí)行,因此它可以修改局部變量,以及在你的范圍內(nèi) 添加一些讓你意想不到的東西。
JSON 轉(zhuǎn)換是我們經(jīng)常要做的;通常我們使用“var obj = eval(jsonText);”來進行轉(zhuǎn)換。然而現(xiàn)在幾乎所有的瀏覽器都支持本地JSON對象,你可以使用“var obj = JSON.parse(jsonText);”來替代前面的代碼。相反你也可以用“JSON.stringify”將JSON對象轉(zhuǎn)換成字符串。更妙的 是,你可以使用“jQuery.parseJSON”來完成上述的工作。
setTimeout和setInterval函數(shù)的第一個參數(shù)可以用字符串作為函數(shù)體來解析執(zhí)行,當然,我們也不建議這樣做,我們可以用實際的函數(shù)來替代。
最后,F(xiàn)unction的構(gòu)造函數(shù)和eval非常像,唯一不同的是,F(xiàn)unction構(gòu)造函數(shù)是在全局范圍內(nèi)執(zhí)行的。
with
with表達式將為你提供訪問對象屬性的速記方式,但我們是否應(yīng)該使用它,仍然存在矛盾的觀點。Douglas Crockford不太喜歡它。John Resig在他的書中有找了很多with的巧妙用法,但是他也承認這將會影響性能并且會產(chǎn)生一點混亂。來看看我們分離出來的with代碼塊,他不能準確地告訴我們現(xiàn)在正在執(zhí)行什么,代碼如下所示:
- with (obj) {
 - bob = "mmm";
 - eric = 123;
 - }
 
我是否剛剛修改了一個叫bob的局部變量?或者我是否設(shè)置了obj.bob?如果obj.bob已經(jīng)被定義,那么它將會被重置為“mmm”。否則,如果有 另一個bob在這個范圍中,那么他將會被改變。否則,全局變量bob會被設(shè)置。最后,下面的寫法可以非常明確地表達你的意思:
- obj.bob = "mmm";
 - obj.eric = 123;
 
ECMAScript5說明:ES5嚴格的來說已經(jīng)不支持with表達式。
#p#
類型和構(gòu)造函數(shù)
使用“new”關(guān)鍵字構(gòu)造內(nèi)置類型
Javascript中有Object, Array, Boolean, Number, String, 和Function這些類型,他們各自都有各自的文字語法,所以就不需要顯式構(gòu)造函數(shù)了。
| 顯式構(gòu)造(不建議) | 文字語法(推薦) | 
| var a = new Object(); a.greet = "hello";  | 
            var a = { greet: "hello" }; | 
| var b = new Boolean(true); | var b = true; | 
| var c = new Array("one", "two"); | var c = ["one", "two"]; | 
| var d = new String("hello"); | var d = "hello" | 
| var e = new Function("greeting", "alert(greeting);"); | var e = function(greeting) { alert(greeting); }; | 
然而,如果你使用new關(guān)鍵字來構(gòu)造上面其中的一種類型,你實際上將會得到一個類型為Object并且繼承自你要構(gòu)造的類型的原型的對象(Function類型除外)。所以盡管你用new關(guān)鍵字構(gòu)造了一個Number類型,它也將是一個Object類型,如下代碼:
- typeof new Number(123); // "object"
 - typeof Number(123); // "number"
 - typeof 123; // "number"
 
上面的第三項是文本語法,為了避免沖突,我們應(yīng)該使用這種方法來構(gòu)造上面的這些類型。
使用“new”關(guān)鍵字來構(gòu)造任何東西
如果你自寫構(gòu)造函數(shù)并且忘記了new關(guān)鍵字,那么悲劇就發(fā)生了:
- var Car = function(colour) {
 - this.colour = colour;
 - };
 - var aCar = new Car("blue");
 - console.log(aCar.colour); // "blue"
 - var bCar = Car("blue");
 - console.log(bCar.colour); // error
 - console.log(window.colour); //"blue"
 
使用new關(guān)鍵字調(diào)用函數(shù)會創(chuàng)建一個新的對象,然后調(diào)用新對象上下文中的函數(shù),最后再返回該對象。相反的,如果不使用new關(guān)鍵在調(diào)用函數(shù),那它將會變成一個全局對象。
偶然忘記使用new關(guān)鍵字意味著很多可選擇的對象構(gòu)造模式已經(jīng)出現(xiàn)可以完全刪除使用這個關(guān)鍵字的需求的情況,盡管這超出了本文的范圍,但我還是建議你去進一步閱讀。
沒有Integer類型
數(shù)值計算是相對緩慢的,因為沒有Integer類型。只有Number類型 - Number是IEEE標準中雙精度浮點運算(64位)類型。這就意味著Number會引起下面的精度舍入錯誤:
- 0.1 + 0.2 === 0.3 //false
 
因為integers和floats沒有區(qū)別,不像C#和JAVA下面代碼是true:
- 0.0 === 0; //true
 
最后是一個關(guān)于Number的疑問,我們該如何實現(xiàn)下面的問題:
- a === b; //true
 - 1/a === 1/b; //false
 
答案是按照Number的規(guī)范是允許出現(xiàn)+0和-0的,+0等于-0,但是正無窮大不等于負無窮大,代碼如下:
- var a = 0 * 1; // 這個結(jié)果為0
 - var b = 0 * -1; // 這個結(jié)果為-0 (你也可以直接"b=-0",但是你為何要這樣做?)
 - a === b; //true: 0等于-0
 - 1/a === 1/b; //false: 正無窮大不等于負無窮大
 
作用域
沒有塊作用域
因為你可能已經(jīng)注意到上一個觀點,javascript中沒有塊作用域的概念,只有函數(shù)作用域??梢栽囋囅旅娴拇a:
- for(var i=0; i<10; i++) {
 - console.log(i);
 - }
 - var i;
 - console.log(i); // 10
 
當i被定義在for循環(huán)中,退出循環(huán)后它人被保留在這個作用域內(nèi),所以最后調(diào)用console.log輸出了10。這里有一個JSLint警告來讓你避免這個問題:強制將所有的變量定義在函數(shù)的開頭。 我們有可能通過寫一個立即執(zhí)行的function來創(chuàng)建一個作用域:
- (function (){
 - for(var i=0; i<10; i++) {
 - console.log(i);
 - }
 - }());
 - var i;
 - console.log(i); // undefined
 
當你在內(nèi)部函數(shù)之前聲明一個變量,然后在函數(shù)里重聲明這個變量,那將會出現(xiàn)一個奇怪的問題,示例代碼如下:
- var x = 3;
 - (function (){
 - console.log(x + 2); // 5
 - x = 0; //No var declaration
 - }());
 
但是,如果你在內(nèi)部函數(shù)中重新聲明x變量,會出現(xiàn)一個奇怪的問題:
- var x = 3;
 - (function (){
 - console.log(x + 2); //NaN - x is not defined
 - var x = 0; //var declaration
 - }());
 
這是因為在函數(shù)中x變量被重新定義了,這說明了翻譯程序?qū)ar表達式移動到了函數(shù)頂部了,最終就變成這樣執(zhí)行了:
- var x = 3;
 - (function (){
 - var x;
 - console.log(x + 2); //NaN - x is not defined
 - x = 0;
 - }());
 
這個實在是太有意義了!
全局變量
Javascript 有一個全局作用域,在為你的代碼創(chuàng)建命名空間時一定要小心謹慎。全局變量會給你的應(yīng)用增加一些性能問題,因為當你訪問它們時,運行時不得不通過每一個作用 域來建立知道找到它們?yōu)橹?。他們會因你的有意或者無意而被訪問或者修改,這將導(dǎo)致另外一個更加嚴重的問題 - 跨站點腳本攻擊。如果一個不懷好意的家伙在你的頁面上找出了如何執(zhí)行那些代碼的方法,那么他們就可以通過修改全局變量非常容易地擾亂你的應(yīng)用。缺乏經(jīng)驗的 開發(fā)者在無意中會不斷的將變量添加到全局作用域中,通過本文,將會告訴大家這樣會發(fā)生什么意外的事情。
我曾經(jīng)看到過下面的代碼,它將嘗試聲明兩個值相等的局部變量:
- var a = b = 3;
 
這樣非常正確的得到了a=3和b=3,但是a在局部作用域中而b在全局作用域中,”b=3“將會被先執(zhí)行,全局操作的結(jié)果,3,再被分配給局部變量a。
下面的代碼聲明了兩個值為3的變量,這樣能達到預(yù)期的效果:
- var a = 3,
 - b = a;
 
“this”和內(nèi)部函數(shù)
“this“關(guān)鍵字通常指當前正在執(zhí)行的函數(shù)所在的對象,然而,如果函數(shù)并沒有在對象上被調(diào)用,比如在內(nèi)部函數(shù)中,”this“就被設(shè)置為全局對象(window),如下代碼:
- var obj = {
 - doSomething: function () {
 - var a = "bob";
 - console.log(this); // 當前執(zhí)行的對象
 - (function () {
 - console.log(this); // window - "this" is reset
 - console.log(a); // "bob" - still in scope
 - }());
 - }
 - };
 - obj.doSomething();
 
#p#
雜項
數(shù)據(jù)不存在:”null“和”undefined“
有兩種對象狀態(tài)來表明數(shù)據(jù)不存在:null和undefined。這會讓那些從其他編程語言比如C#轉(zhuǎn)過來的程序員變得相當混亂。也許你會期望下面的代碼返回true:
- var a;
 - a === null; //false
 - a === undefined; //true
 
”a“實際上是undefined的(盡管你用雙等號==來與null比較會得出true的結(jié)果,但這只是表面上看起來正確的另一個錯誤)。
如果你想檢查一個變量是否真的存在值,那你不能用雙等號==去判斷,要用下面的方法:
- if(a !== null && a !== undefined) {
 - ...
 - }
 
”哈“,你也許會說,既然null和undefined都是false,那么你可以這樣去做:
- if(a) {
 - ...
 - }
 
當然,0是false,空字符串也是。那么如果這其中一個是a的正確的值的話,你就要用前者了。那種比較短小的比較方式,適合于比較objects, arrays, 和booleans類型。
重定義undefined
非常正確,你可以重定義undefined,因為它不是一個保留字:
- undefined = "surprise!";
 
但是,你要通過給undefined變量分配一個值或者使用”void“操作符來取回值(否則這是相當沒用的)。
- undefined = void 0;
 
這就是為什么jquery腳本庫的第一行要這樣寫了:
- (function ( window, undefined ) {
 - ... // jQuery library!
 - }(window));
 
這個函數(shù)被調(diào)用時是傳入一個參數(shù)的,同時確保了第二個參數(shù)”undefined“實際上是undefined的。
順便說一下,你不能重定義null - 但是你可以重定義NaN,Infinity和帶構(gòu)造函數(shù)的內(nèi)置類型??梢赃@樣嘗試一下:
- Array = function (){ alert("hello!"); }
 - var a = new Array();
 
當然,你可以在任何地方用文字語法聲明Array。
可選的分號
Javascript代碼中分號是可選的,所以初學(xué)者寫代碼就簡單多了。但是很不幸的是如果忽略了分號并不會給任何人帶來方便。結(jié)果是當解釋器遇到錯誤時,必須追溯并嘗試去猜測因為哪些分號漏寫導(dǎo)致的問題。
這里有一個經(jīng)典的例子:
- return
 - {
 - a: "hello"
 - };
 
上面的代碼并不會返回一個對象,而是返回了undefined - 但是也沒有錯誤拋出。其實是因為分號自動加到了return語句后面,其他的代碼都是非常正確的,但是就是什么都不執(zhí)行,這就證明了在 javascript中,左花括號應(yīng)該緊跟這一行而不該換行,這不只是一個編程風(fēng)格的問題。下面的代碼才會正確返回一個屬性為a的對象:
- return {
 - a: "hello"
 - };
 
NaN
NaN的類型是...Number
- typeof NaN === "number" //true
 
另外NaN和任何東西比較都是false:
- NaN === NaN; // false
 
因為NaN之間是不能比較的,唯一判斷一個數(shù)字是否為NaN的方法是調(diào)用isNaN方法。
從另一個方面可以說明,我們也可以用函數(shù)isFinite,當其中一個操作數(shù)為NaN或者InFinity時返回false。
arguments對象
在一個函數(shù)中,我們可以引用arguments對象來遍歷傳入的參數(shù)列表,第一個比較怪異的地方是這個對象并不是Array,而是一個類似 Array的對象(有一個length屬性,其值在0-length-1之間)。為了將其轉(zhuǎn)換成array,我們可以array的splice函數(shù)來創(chuàng)建 其對應(yīng)的array數(shù)組:
- (function(){
 - console.log(arguments instanceof Array); // false
 - var argsArray = Array.prototype.slice.call(arguments);
 - console.log(argsArray instanceof Array); // true
 - }());
 
第二個比較怪異的地方是當一個函數(shù)的簽名中有顯式arguments參數(shù)時,它們是可以被重新分配的并且arguments對象也會被改變。這就表明了arguments對象指向了變量本身。你不能利用arguments對象來給出它們的初始值:
- (function(a){
 - alert(arguments[0]); //1
 - a = 2;
 - alert(arguments[0]); //2
 - }(1));
 
結(jié)束本文!
這樣我就總結(jié)完了這些javascript陷阱。我肯定還會有更多這樣的陷阱,期待大家更多的意見和點評。
PS - 我真的很喜歡Javascript。
原文:http://www.codeproject.com/KB/scripting/javascript-gotchas.aspx
譯文:http://www.cnblogs.com/sxwgf/archive/2011/11/14/javascript-gotchas.html
譯者:王國峰
【編輯推薦】















 
 
 

 
 
 
 