JavaScript 語法陷阱


沒有一門編程語言是完美的,JavaScript 也不例外,它語法陷阱重重,防不勝防:

  • 加號
  • "with"
  • 分號自動插入
  • 聲明提升
  • "eval"
  • 多行字符串
  • 變量泄漏
  • "arguments.callee"
  • ...

了解和熟悉這些陷阱,並在開發時注意規避它們,可以給我們省去很多麻煩事。

加號

作為二元運算符時,+ 既是數學運算的加法,也是字符串的拼接。另外,它還可以作為一元符號,表示正數。

看看下面的代碼:

// 1
console.log(   1 + 2   ); // 3
console.log( "3" + "4" ); // "34"
// 2
console.log(   1 + "3" ); // "13"
console.log( "3" + 1   ); // "31"
// 3
console.log( 1 + null      );
console.log( 1 + undefined );
console.log( 1 + NaN       );
// 4
console.log( "3" + null      );
console.log( "3" + undefined );
console.log( "3" + NaN       );
// 5
console.log( 1 + {} );
console.log( 1 + [] );
// 6
console.log( "3" + {} );
console.log( "3" + [] );

也許你可以准確的說出第1組代碼的結果,甚至第2組也能答上,但剩下的幾組你能毫不猶豫地給出答案嗎?

在 JavaScript 中,是如何決定一段代碼中的 + 是數學運算還是字符串拼接呢?答案請看下面這段邏輯:

a + b:
    pa = ToPrimitive(a)
    pb = ToPrimitive(b)
    if (pa is string || pb is string)
        return concat(ToString(pa), ToString(pb))
    else
        return add(ToNumber(pa), ToNumber(pb))

 

  1. 收集 + 兩端的操作數的原始值。
  2. 如果其中之一是字符串,則進行字符串拼接。
  3. 否則,執行數學加法。

需要注意的是,JavaScript 的原始值類型包括 number, string, boolean, undefined, 而 null 也是一種特殊的原始值。另一方面,對於非原始值類型(即復合類型,也即 object )的變量,其原始值被認為是字符串。

按這個邏輯,之前的測試結果就容易理解了。當然,像上面那樣使用加號是不被推薦的,為了避免混淆,利用上面的加號邏輯,我們通常可以這樣使用加號:

// 確保數字相加
a = +b + (+c);
// 確保變量 d 為字符串
d = "" + d;

 

"with"

使用 with 語句,可以將一個語句塊的上下文綁定為一個指定對象。

with (document) {
    write("foo");
    getElemntById("bar").innerHTML = "foobar";
    alert("Hello world!");
}

// 等同於以下代碼
document.write("foo");
document.getElemntById("bar").innerHTML = "foobar";
window.alert("Hello world!");

但是咱們不推薦使用 with ,事實上,ECMAScript 5 中引入的嚴格模式也禁止使用 with :

  1. JavaScript 解釋器引擎將難以對代碼執行優化。解釋器引擎的執行優化是建立在“明確的知道這個變量在運行時所指向的引用”的基礎上的。而在 with 語句塊中的變量或函數,在解釋階段無法判斷其是屬於 with 的上下文,還是其所在作用域,只有等到代碼運行時才能確定。
  2. 代碼可閱讀性差。

分號自動插入

在語句結束時,你不必手動輸入分號,換行即可。

function foo() {
    var bar = "value"
    return bar
}

// `{}` 包圍的語句塊的最后一個語句的分號也可省略
function bar() { return "foo" }

開發者們每寫一行代碼,就可以少敲打一次鍵盤,這看起來很人性化。但過於依賴分號自動插入,會帶來一些潛在問題。

function foo() {
    return
    {
        bar: 1
    }
}

function bar() {
    var a, b, c, d, e
    a = b + c
    (d + e).toString()
}

看看上面的代碼,foo() 將返回什么? bar() 又將怎么運行?

事實上,前者將返回 undefined ,而后者的后兩行代碼將被理解為 a = b + c(d + e).toString()

JavaScript 的分號自動插入的規則並不那么清晰可辨,老實地多敲幾次鍵盤,可以避免那些讓你摸不着頭緒的bug在某一天突然出現。

聲明提升

看看下面這段代碼,我們將得到什么結果?

var foo = 1;
function bar() {
    // 這個條件成立嗎?
    if (! foo) {
        var foo = 10;
    }
    alert(foo);
}
bar();

那么這段代碼呢?

var a = 1;
function b() {
    a = 10;
    return;
    function a() {}
}
b();
alert(a);

第1個例子,也許你會覺得是 "1" ,因為 ! 1 為假,if 里的代碼不會執行。而第2個例子,可能你認為應該是 "10" 。

事實上,結果相反,我們將分別得到 "10" 和 "1" 。

在 JavaScript 中,變量、函數的聲明會被提升到當前函數主體的頂部,而不管這個聲明語句是否出現在了不可到達的地方。

上面的兩段其實等同於:

var foo = 1;
function bar() {
    var foo;
    if (! foo) {
        foo = 10;
    }
    alert(foo);
}
bar();

var a = 1;
function b() {
    function a() {}
    a = 10;
    return;
}
b();
alert(a);

需要注意的是,只有變量或函數的聲明被提升了,而賦值語句並沒有。

"eval"

eval 是 JavaScript 的動態特性之一,在運行時, eval 可以將給定的字符串當作代碼語句執行:

<script>
var func = <?php echo json_encode($user_send['func']); ?>;
eval(func + "()");
function sayHello() {}
function sayGoodbye() {}
</script>

在代碼中用一組字符串與變量拼出另一串代碼來運行,這看起來吊爆了。

但請在使用 eval 之前考慮下它將帶來的潛在問題:

  1. 使用了 eval 的代碼可閱讀性很差,你讀到這樣的代碼時很難判斷它究竟要做啥,即使那是你自己幾天前寫的。
  2. JavaScript 解釋器引擎難以對代碼執行優化。
  3. 如果 eval 中的字符串包含用戶輸入的數據,這會給攻擊者有機可乘。
  4. 如果你是有經驗的開發者,大多數情況下你可以使用更高效的函數嵌套(閉包)等來解決問題;如果你沒有足夠的經驗,那更不要使用 eval ,如果你不想你或你的用戶遭受攻擊。

多行字符串

JavaScript 中不能直接書寫多行的字符串,需要在行尾輸入一個反斜杠 \

假設我們的項目中有一段這樣的代碼:

var multiStr = "this is a multi-line string, \
and this is the second line. \
yes, the string ends here";

然后做了一些維護和更新:

var multiStr = "this is a multi-line string, \
and this is the second line. \ 
now i want to insert a line right here, \
yes, the string ends here";

憑肉眼似乎沒看出毛病,但運行時卻得到了一個語法錯誤,這之前你可能已經注意到語法高亮已經失效了。幾經周折,你終於注意到了第2行行尾的那個不起眼的空格。。

變量泄漏

JavaScript 的全局作用域給了我們很多便利,有時我們無需使用 var 來聲明變量。

很多 JavaScript 的入門開發者,喜歡利用這個“便利”。但事實上它是一個陷阱。它很可能讓我們的一些敲打錯誤被隱藏和掩蓋。

function foo() {
    var type = "first";
    if (something) {
        // 這里假設我們手一抖,把type打成了typo
        typo = "second";
    }
    return type;
}

這段代碼可以讓項目長久地穩定運行,但隨后的某天,我們吃驚地發現,所有的 type 都是 "first" !在找到並修復這個手誤之前,我們以此得到的數據或結論可能都要被廢棄。

直接使用沒有聲明的變量,將自動創建一個全局變量,濫用會導致全局變量污染,或者讓類似上面這樣的手誤逍遙法外。合理的聲明變量,並利用作用域鏈與閉包,是 JavaScript 解決很多問題的思路。

"arguments.callee"

寫一個遞歸函數,我們通常這樣:

function factorial(n) {
    return n <= 1 ? 1 : factorial(n - 1) * n;
}
[1, 2, 3, 4, 5].map(factorial);

有時我們不想污染命名空間,需要遞歸調用一個匿名函數,怎么辦?

[1, 2, 3, 4, 5].map(function(n) {
    return n <= 1 ? 1 : /* what goes here? */ (n - 1) * n;
});

還好我們有 arguments.callee

[1, 2, 3, 4, 5].map(function(n) {
    return n <= 1 ? 1 : arguments.callee(n - 1) * n;
});

但是同樣不推薦使用 arguments.callee :

  1. 訪問 arguments.callee 的開銷是昂貴的。
  2. 使用它將導致 JavaScript 解釋器難以執行優化。
  3. 從 ECMAScript 3 開始,已經支持命名函數表達式

命名函數表達式:

[1, 2, 3, 4, 5].map(function factorial(n) {
    return n <= 1 ? 1 : factorial(n - 1) * n;
});

注意,這里的函數 factorial() 並不是函數聲明,而是命名函數表達式,factorial 所處的作用域是其函數本身的作用域(與參數 n 屬同一個作用域),而不是當前的全局作用域。但是,在 IE8 及以下瀏覽器中,情況則不同,它將屬於全局作用域。

避開陷阱

JavaScript 有這么多的語法陷阱,如何規避,並保證我們的代碼質量呢?后面再談。

 

這篇文章也發表在我的個人網站上:http://wangshenwei.com/article/javascript-syntax-trap


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM