簡介

簡介

JavaScript Garden 是一個不斷更新的文件,最主要是要去了解一些 Javascript 比較古怪的部份。 給一些意見來防止遇到一些常見的錯誤和一些難以發現的問題,以及性能問題和不好的習慣。 初學者也可以藉此去了解 Javascript 這項語言的特性。

JavaScript Garden 並 不是 要教導你 Javascript 的語言。 如果要能夠理解這篇文章的內容,你需要事先學習 JavaScript 的基礎知識。 在 Mozilla 開發者網路中有一系列非常棒的學習guide

作者

這個使用手冊是來自於 Stack Overflow 的使用者, Ivo Wetzel (寫作) 和 Zhang Yi Jiang (設計)。

目前為 Tim Ruffles 維護此專案。

貢獻者

繁體中文翻譯

存放空間

JavaScript Garden 目前存放於 GitHub,但是 Cramer Development 讓我們有另一個存放位置在 JavaScriptGarden.info

許可

JavaScript Garden 是在 MIT license 許可協議下發佈,並存在於 GitHub,如果你有發現錯誤或是打字上的錯誤 新增一個任務 或者發一個請求。 你也可以在 StackOverflow 的 JavaScript room 上面找到我們。

物件

物件的使用和屬性

每個變數可以表現像 JavaScript 物件,除了 nullundefined

false.toString(); // 'false'
[1, 2, 3].toString(); // '1,2,3'

function Foo(){}
Foo.bar = 1;
Foo.bar; // 1

一個常見的誤解就是字面值(literal)不是物件。這是因為 JavaScript 編譯器的一個錯誤,它試圖把 點操作符 解析為浮點數的字面值的一部分。

2.toString(); // 出錯: SyntaxError

有很多變通方法可以讓數字的字面值看起來像物件。

2..toString(); // 第二個點號可以正常解析
2 .toString(); // 注意點號前面的空格
(2).toString(); // 2 先被計算

物件做為數據類型

JavaScript 的物件可以作為 Hashmaps使用,主要用來保存命名的鍵與值的對應關係。

使用物件的字面語法 - {} - 可以創建一個簡單的物件。 這個新創建的物件從 Object.prototype 繼承,下面,沒有任何 自定義屬性

var foo = {}; // 一個空的物件

// 一個新的物件,有值為 12 的自定義屬性 'test'
var bar = {test: 12}; 

訪問屬性

有兩種訪問物件的屬性,點操作或是中括號操作。

var foo = {name: 'kitten'}
foo.name; // kitten
foo['name']; // kitten

var get = 'name';
foo[get]; // kitten

foo.1234; // SyntaxError
foo['1234']; // works

兩種語法是相等的,唯一的差別是,使用中括號允許你動態的設定屬性,使用點操作不允許屬性為變數,否則會造成語法錯誤

刪除屬性

唯一刪除屬性的方式就是用 delete 操作符。設置屬性為 undefined 或是 null 只有刪除的屬性和值的關聯,沒有真的刪掉屬性

var obj = {
    bar: 1,
    foo: 2,
    baz: 3
};
obj.bar = undefined;
obj.foo = null;
delete obj.baz;

for(var i in obj) {
    if (obj.hasOwnProperty(i)) {
        console.log(i, '' + obj[i]);
    }
}

上面的輸出結果有 bar undefinedfoo null 只有 baz 真正被刪除而已,所以從輸出結果中消失。

屬姓名的語法

var test = {
    'case': 'I am a keyword, so I must be notated as a string',
    delete: 'I am a keyword, so me too' // raises SyntaxError
};

物件的屬性名可以使用字符串或是普通的宣告。但是由於 JavaScript 編譯器有個另外一個錯誤設計。 上面的兩種方式在 ECMAScript 5之前都會拋出 SyntaxError 的錯誤。

這個錯誤的原因是 delete 是 JavaScript 語言的一個 關鍵字 因此為了在更低的版本能執行最好用 string literal

Prototype

JavaScript 不包含原本繼承的模型。然而它使用的是原型模型。

然而常常有人提及 JavaScript 的缺點,就是基於原本繼承模型比類繼承更強大。 現實傳統的類繼承模型是很簡單。但是在 JavaScript 中實現元繼承則要困難很多。

由於 JavaScript 是唯一一個被廣泛使用的基於原型繼承的語言,所以我們必須要花時間來理解這兩者的不同。

第一個不同之處在於 JavaScript 使用 原型鏈 的繼承方式。

function Foo() {
    this.value = 42;
}
Foo.prototype = {
    method: function() {}
};

function Bar() {}

// 設置 Bar 的 prototype 屬性為 Foo 的實例對象
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';

// 修正 Bar.prototype.constructor 為 Bar 本身
Bar.prototype.constructor = Bar;

var test = new Bar(); // 開啟一個新的實例

// 原型鏈
test [instance of Bar]
    Bar.prototype [instance of Foo]
        { foo: 'Hello World' }
        Foo.prototype
            { method: ... }
            Object.prototype
                { toString: ... /* etc. */ }

上面的例子中,物件 test 會繼承來自 Bar.prototypeFoo.prototype。因此它可以進入來自 Foo 原型的方法 method。 同時它也可以訪問 那個 定義在原型上的 Foo 實例屬性 value

要注意的是 new Bar() 沒有 創立一個新的 Foo 實例,它重複利用的原本的 prototype。因此, Bar 的實例會分享到 相同value 屬性。

屬性查詢

當查詢一個物件的屬性時,JavaScript 會 向上 查詢,直到查到指定名稱的屬性為止。

如果他查到原型鏈的頂部 - 也就是 Object.prototype - 但是仍然沒有指定的屬定,就會返回 undefined

原型屬性

當原型屬性用來建造原型鏈,它還是有可能去把 任意 類型的值給它

function Foo() {}
Foo.prototype = 1; // 無效

分派物件,在上面的例子中,將會動態的創建原型鏈。

效能

如果看在屬性在原型鏈的上端,對於查詢都會有不利的影響。特別的,試圖獲取一個不存在的屬性將會找遍所有原型鏈。

並且,當使用 迴圈找尋所有物件的屬性時,原型鏈上的 所有 屬性都會被訪問。

擴展 Native Prototype

一個經常發生的錯誤,那就是擴展 Object.prototype 或者是其他內建類型的原型物件。

這種技術叫做 monkey patching 並且會破壞 封裝。雖然被廣泛的應用到一些 Javascript 的架構,像是 Prototype , 但仍然沒有好的理由新增一個 非標準 的功能去搞亂內建型別

擴展內置類型的 唯一 理由是為了和新的 JavaScript 保持一致,比如說 Array.forEach

總結

在寫複雜的程式碼的時候,要 充分理解 所有程式繼承的屬性還有原型鏈。 還要堤防原型鏈過長帶來的性能問題,並知道如何通過縮短原型鏈來提高性能。 絕對 不要使用 native prototype 除非是為了和新的 JavaScript 引擎作兼容。

hasOwnProperty

為了判斷一個物件是否包含 自定義 屬性而 不是 原形上的屬性,我們需要使用繼承 Object.prototypehasOwnProperty 方法。

hasOwnProperty 是 JavaScript 中唯一一個處理屬性但是 找原型鏈的函式。

// 修改 Object.prototype
Object.prototype.bar = 1;
var foo = {goo: undefined};

foo.bar; // 1
'bar' in foo; // true

foo.hasOwnProperty('bar'); // false
foo.hasOwnProperty('goo'); // true

只有 hasOwnProperty 給予正確的結果,這對進入物件的屬性很有效果,沒有 其他方法可以用來排除原型上的屬性,而不是定義在物件 自己 上的屬性。

hasOwnProperty 作為屬性

JavaScript 不會 保護 hasOwnProperty被占用,因此如果碰到存在這個屬性,就需要使用 外部hasOwnProperty 來獲取正確的結果。

var foo = {
    hasOwnProperty: function() {
        return false;
    },
    bar: 'Here be dragons'
};

foo.hasOwnProperty('bar'); // 永遠返回 false

// 使用其他對象的 hasOwnProperty,並將其上下設置為 foo
({}).hasOwnProperty.call(foo, 'bar'); // true

// 也可以透過原生 Object prototype 的 hasOwnProperty 函數來達成目的
Object.prototype.hasOwnProperty.call(foo, 'bar'); // true

結論

當檢查一個物件是否存在的時候, hasOwnProperty唯一 可用的方法。 同時在使用 for in loop 建議使用 hasOwnProperty 避免 原型所帶來的干擾。

for in 迴圈

就像其他的 in 操作符一樣, for in 循環也進入所有在物件中的屬性

// 修改 Object.prototype
Object.prototype.bar = 1;

var foo = {moo: 2};
for(var i in foo) {
    console.log(i); // 輸出兩個屬性:bar 和 moo
}

由於不可能改變 for in 本身的行為,因為有必要過濾出那些不希望在迴圈出現的屬性,這可以用 Object.prototype 原型上的 hasOwnProperty 的函數來完成。

hasOwnProperty 來過濾

// foo 變數是上面範例中的
for(var i in foo) {
    if (foo.hasOwnProperty(i)) {
        console.log(i);
    }
}

這個版本的程式碼是唯一正確的寫法。由於我們使用了 hasOwnProperty,這次 輸出 moo。 如果不只用這個程式碼在原型物件中(比如 Object.prototype)被擴展可能會出錯。

一個廣泛的模組 Prototype就礦展了圓型的 JavaScript 物件。 因此,但這模組包含在頁面中時,不使用 hasOwnProperty 過濾的 for in 尋難免會出問題。

總結

推薦 總是 使用 hasOwnProperty。不要對程式碼的環境做任何假設,不要假設原生的對象是否被擴張。

函式

函式的宣告和表達方式

函式在 JavaScript 是第一等物件。這表示他們可以把函式當做值一樣傳遞。 一個常見的用法是用 匿名函式 當做一個回傳去呼叫另一個函式,這是一種非同步函式

函式的宣告

function foo() {}

上面的函式在被執行之前會被 解析(hoisted),因此它可以在 任意 的地方都是 有宣告的 ,就算是在比這個函式還早呼叫。

foo(); // 可以執行,因為 foo 已經在運行前就被建立
function foo() {}

function 的表達式

var foo = function() {};

這個例子把一個 匿名 函式賦值給變數 foo

foo; // 'undefined'
foo(); // 錯誤: TypeError
var foo = function() {};

由於 var 已經宣告變數 foo 在所有的程式碼執行之前。 所以 foo已經在程式運行前就已經被定義過了。 但是因為賦值只會在運行時去職情,所以在程式碼執行前,foo 的值還沒被宣告所以為 undefined

命名函式的賦值表達式

另一個特殊狀況就勢將一個命名函式賦值給一個變數。

var foo = function bar() {
    bar(); // 可以運行
}
bar(); // 錯誤:ReferenceError

bar 不可以在外部的區域被執行,因為它只有在 foo 的函式內才可以去執行。 然而在 bar 內部還是可以看見。這是由於 JavaScript的 命名處理所致。 函式名在函式內 可以去使用。

this 的工作原理

JavaScript 有移到完全部屬於其他語言處理 this 的處理機制。 在 種物同的情況下, this 指向的個不相同

全域變數

this;

如果再全域範圍內使用 this,會指向 全域 的物件

呼叫一個函式

foo();

這裡 this 也會指向 全域 對象。

方法調用

test.foo(); 

這個例子中, this 指向 test 物件。

呼叫一個建構函式

new foo(); 

如果函式傾向用 new 關鍵詞使用,我們稱這個函式為 建構函式。 在函式內部, this 指向 新物件的創立

顯示的設置 this

function foo(a, b, c) {}

var bar = {};
foo.apply(bar, [1, 2, 3]); // Array 會被擴展,如下所示
foo.call(bar, 1, 2, 3); // 傳遞參數 a = 1, b = 2, c = 3

當使用 function.prototype 上的 call 或只 apply 方法時,函式內的 this 將會被 顯示設置 為函式調用的第一個參數。

As a result, in the above example the method case does not apply, and this inside of foo will be set to bar.

常見誤解

While most of these cases make sense, the first can be considered another mis-design of the language because it never has any practical use.

Foo.method = function() {
    function test() {
        // this is set to the global object
    }
    test();
}

A common misconception is that this inside of test refers to Foo; while in fact, it does not.

In order to gain access to Foo from within test, it is necessary to create a local variable inside of method that refers to Foo.

Foo.method = function() {
    var that = this;
    function test() {
        // Use that instead of this here
    }
    test();
}

that is just a normal variable name, but it is commonly used for the reference to an outer this. In combination with closures, it can also be used to pass this values around.

Assigning Methods

Another thing that does not work in JavaScript is function aliasing, which is assigning a method to a variable.

var test = someObject.methodTest;
test();

Due to the first case, test now acts like a plain function call; therefore, this inside it will no longer refer to someObject.

While the late binding of this might seem like a bad idea at first, in fact, it is what makes prototypal inheritance work.

function Foo() {}
Foo.prototype.method = function() {};

function Bar() {}
Bar.prototype = Foo.prototype;

new Bar().method();

When method gets called on an instance of Bar, this will now refer to that very instance.

Closures 和 References

JavaScript 有一個很重要的特徵就是 closures 因為有 Closures,所以作用域 永遠 能夠去訪問作用區間外面的變數。 函數區間 是JavaScript 中唯一擁有自生作用域的結構,因此 Closures 的創立需要依賴函數

模仿私有變數

function Counter(start) {
    var count = start;
    return {
        increment: function() {
            count++;
        },

        get: function() {
            return count;
        }
    }
}

var foo = Counter(4);
foo.increment();
foo.get(); // 5

這裡,Counter 返回兩個 Closures,函數 increment 還有 get。這兩個函數都維持著對外部作用域 Counter 的引用,因此總可以訪問作用域的變數 count

為什麼不可以在外部訪問私有變數

因為 Javascript 不可以 對作用域進行引用或賦值。因此外部的地方沒有辦法訪問 count 變數。 唯一的途徑就是經過那兩個 Closures

var foo = new Counter(4);
foo.hack = function() {
    count = 1337;
};

在上面的例子中 count 不會 改變到 Counter 裡面的 count 的值。因為 foo.hack 沒有在 那個 作用域內被宣告。它只有會覆蓋或者建立在一個 全域 的變數 count

在循環內的 Closures

一個常見的錯誤就是在 Closures 中使用迴圈,假設我們要使用每次迴圈中所使用的進入變數

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

在上面的例子中它 不會 輸出數字從 09,但只會出現數字 10 十次。 在 console.log 被呼叫的時候,這個 匿名 函數中保持一個 參考 到 i ,此時 for迴圈已經結束, i 的值被修改成了 10。 為了要達到想要的結果,需要在每次創造 副本 來儲存 i 的變數。

避免引用錯誤

為了要有達到正確的效果,最好是把它包在一個 匿名函數.

for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);
        }, 1000);
    })(i);
}

匿名外部的函數被呼叫,並把 i 作為它第一個參數,此時函數內 e 變數就擁有了一個 i 的拷貝。 當傳遞給 setTimeout 這個匿名函數執行時,它就擁有了對 e 的引用,而這個值 不會 被循環改變。 另外有一個方法也可以完成這樣的工作,那就是在匿名函數中返回一個函數,這和上面的程式碼有同樣的效果。

for(var i = 0; i < 10; i++) {
    setTimeout((function(e) {
        return function() {
            console.log(e);
        }
    })(i), 1000)
}

另外也可以透過 .bind 完成此工作,它可以將 this 及參數傳入函數內,行為就如同上面程式碼一樣。

for(var i = 0; i < 10; i++) {
    setTimeout(console.log.bind(console, i), 1000);
}

arguments 物件

所有函數在 JavaScript 中都可以有個特別的參數 arguments。 這個變數掌握了一列傳入函數中的參數

arguments 物件 不是 一個 Array,雖然都有很多 Array 的語法 - 就像是 length 屬性 - 但是它沒有繼承來自 Array.prototype 事實上它繼承 object

由於這些原因,這 不可能 用 Array 的一些功能像是 pushpop或是 slicearguments。 但是像 for 迴圈這些迴圈都是可以用的,如果真的需要使用一些標準的 Array 功能可以先把它轉成真的 Array 再去使用。

轉為 Array

下面的程式可以回傳一個新的 Array 包含所有的元素在 Arguments的物件中

Array.prototype.slice.call(arguments);

這種轉化方式比較 ,不建議使用這種作法如果再追求效率的程式中。

傳遞參數

下面是建議用這種方式去傳參數到另一個函數

function foo() {
    bar.apply(null, arguments);
}
function bar(a, b, c) {
    // 在這裡做一些事情
}

另一個技巧是用 callapply 放在一起來創造一個更快的解綁定包裝器

function Foo() {}

Foo.prototype.method = function(a, b, c) {
    console.log(this, a, b, c);
};

// Create an unbound version of "method" 
// 輸入的參數: this, arg1, arg2...argN
Foo.method = function() {

    // 結果: Foo.prototype.method.call(this, arg1, arg2... argN)
    Function.call.apply(Foo.prototype.method, arguments);
};

自動更新

Arguments 物件創造的 gettersetter 的函數方法,可以被視為原本函數的變數。

因此,改變了一個變數會跟著改變它的值而且也間接的改變稻香對應的 arguments 的物件,反之亦然。

function foo(a, b, c) {
    arguments[0] = 2;
    a; // 2

    b = 4;
    arguments[1]; // 4

    var d = c;
    d = 9;
    c; // 3
}
foo(1, 2, 3);

性能

arguments 總是會被宣告,但除了兩個情況,一個是在一個函式中或是在其中一個參入。而不論他是否有被使用。

getterssetter 會永遠被創造。然而,他們對任何性能都沒有影響,除非對它的屬性有多次的訪問

然而會有一種情況來降低 JavaScript 引擎的效能。就是使用 arguments.callee

function foo() {
    arguments.callee; // 做一些在這個函數物件
    arguments.callee.caller; // 然後呼叫這個函數物件
}

function bigLoop() {
    for(var i = 0; i < 100000; i++) {
        foo(); // 通常會在內聯
    }
}

在上面的程式中, foo 不再是一個單存的互聯函數 因為它需要知道他自己和它的調用者。 這不僅減低了它的性能,而且還破壞的封裝

強烈建議不要使用 arguments.callee 或是其他它的屬性

建構函式

JavaScript 中的建構函式和其他語言中的建構函式是不同的。 用 new 的關鍵字方式調用的函式都被認為是建構函式。 在建構函式內部 - 被呼叫的函式 - this 指向一個新建立的 objectprototype 這是一個新的物件一個被指向函式的 prototype 的建構函式。

如果被使用的函式沒有明顯的呼叫 return 的表達式,它會回傳一個隱性的 this 的新物件。

function Foo() {
    this.bla = 1;
}

Foo.prototype.test = function() {
    console.log(this.bla);
};

var test = new Foo();

在上面的例子中 Foo 建立一個建構函式,並設立一個 prototype 來創建一個新的物件叫 Foo.prototype。 這個情況下它顯示的 return 一個表達式,但他 返回一個 Object

function Bar() {
    return 2;
}
new Bar(); // 返回一個新物件

function Test() {
    this.value = 2;

    return {
        foo: 1
    };
}
new Test(); // 回傳物件

如果 new 的關鍵字被忽略,函式就 不會 回傳一個新的物件。

function Foo() {
    this.bla = 1; // 獲取一個全域的參數
}
Foo(); // undefined

雖然上面有些情況也能正常運行,但是由於 JavaScript 中 this 的工作原理,這裡的 this 指向 全域對象

工廠模式

為了不使用 new 關鍵字,建構函式必須顯性的返回一個值。

function Bar() {
    var value = 1;
    return {
        method: function() {
            return value;
        }
    }
}
Bar.prototype = {
    foo: function() {}
};

new Bar();
Bar();

上面兩個呼叫 Bar 的方法回傳的值都一樣,一個新創建的擁有 method 屬性被返回,這裡創建了一個 Closure.

還有注意, new Bar()不會 改變返回物件的原型。 因為建構函式的原型會指向剛剛創立的新物件,而在這裡的 Bar 沒有把這個新物件返回。 在上面的例子中,使用或者不使用 new 關鍵字沒有什麼功能性的區別

通過工廠模式創建的新對象

常聽到建議 不要 使用 new,因為如果忘記如何使用它會造成錯誤。 為了創建一個新的物件,我們可以用工廠方法,來創造一個新的物件在那個方法中。

function Foo() {
    var obj = {};
    obj.value = 'blub';

    var private = 2;
    obj.someMethod = function(value) {
        this.value = value;
    }

    obj.getPrivate = function() {
        return private;
    }
    return obj;
}

雖然上面的方式比起 new 的調用方式更不容易出錯,並且可以充分的使用 私有變數所帶來的便利,但是還是有一些不好的地方

  1. 會占用更多的記憶體,因為創建的物件 沒有 辦法放在在同一個原型上。
  2. 為了要用繼承的方式,工廠方法需要複製所有的屬性或是把一個物件作為新的物件的原型。
  3. 放棄原型鏈僅僅是因為防止遺漏 new 所帶來的問題,這與語言本身的思想鄉違背。

結語

雖然遺漏 new 關鍵字可能會導致問題,但這並 不是 放棄只用原型的藉口。 最終使用哪種方式取決於應用程式的需求,選擇一種程式語言風格並堅持下去才是最重要的。

作用域和命名空間

儘管 JavaScript 支持一個大括號創建的程式碼,但並不支持塊級作用域。 而僅僅支援 函式作用域

function test() { // 一個作用域
    for(var i = 0; i < 10; i++) { // 不是一個作用域
        // 算數
    }
    console.log(i); // 10
}

JavaScript 中沒有寫示的命名空間定義,這代表著它所有定義的東西都是 全域共享 在同一個命名空間下。

每次引用一個變數,JavaScript 會向上找整個作用域直到找到這個變數為止。 如果在全域中無法找到那個變數,它會拋出 ReferenceError 錯誤碼。

全域變數的壞處

// script A
foo = '42';

// script B
var foo = '42'

上面兩個腳本 不會 有同樣的效果。腳本 A 在 全域 空間定義了變數 foo,腳本 B 定義了 foo 在目前的區間內。

再次強調,上面的效果是 完全不同,不使用 var 會導致隱性的全域變數。

// 全域作用區
var foo = 42;
function test() {
    // 局部作用區
    foo = 21;
}
test();
foo; // 21

在函數 test 中部使用 var 會覆蓋到原本在外面的 foo。 雖然看起來不是什麼大問題,但是當程式有幾千行的時候沒有使用 var 會照成難以追蹤的臭蟲。

// 全域作用域
var items = [/* some list */];
for(var i = 0; i < 10; i++) {
    subLoop();
}

function subLoop() {
    // subLoop 的作用域
    for(i = 0; i < 10; i++) { // 缺少了 var
        // 做一些事情
    }
}

在外面的迴圈在呼叫第一次 subLoop 之後就會停止,因為 subLoop 全域變數中的 i 被覆蓋了。 在第二次使用 for 迴圈的時候,使用 var 就可以避免這種錯誤。 在宣告變數的時候 絕對不要 忘記 var,除非就是 希望他的效果 是取改變外部的作用域。

局部變數

在 javascript 中能用兩種方式來宣告局部變數。 函式 參數和透過 var 來宣告變數。

// 全域變數
var foo = 1;
var bar = 2;
var i = 2;

function test(i) {
    // 函式 test 內部的局部作用域
    i = 5;

    var foo = 3;
    bar = 4;
}
test(10);

fooi 是它的局部變數在 test 函式中,但是在 bar 的賦值會覆蓋全區域的作用域內的同名變數。

變數宣告

JavaScript 會 提昇 變數宣告, 這代表著 varfunction 的圈告都會被提升到當前作用域的頂端。

bar();
var bar = function() {};
var someValue = 42;

test();
function test(data) {
    if (false) {
        goo = 1;

    } else {
        var goo = 2;
    }
    for(var i = 0; i < 100; i++) {
        var e = data[i];
    }
}

在上面的程式碼會被轉化在執行之前。 JavaScript 會把 var,和 function 宣告,放到最頂端最接近的作用區間

// var 被移到這裡
var bar, someValue; //  值等於 'undefined'

// function 的宣告也被搬上來
function test(data) {
    var goo, i, e; // 沒有作用域的也被搬至頂端
    if (false) {
        goo = 1;

    } else {
        goo = 2;
    }
    for(i = 0; i < 100; i++) {
        e = data[i];
    }
}

bar(); // 出錯:TypeError , bar 還是 'undefined'
someValue = 42; // 賦值語句不會被提昇規則影響
bar = function() {};

test();

沒有作用域區間不只會把 var 放到迴圈之外,還會使得 if 表達式更難看懂。

在一般的程式中,雖然 if 表達式中看起來修改了 全域變數 goo,但實際上在提昇規則被運用後,卻是在修改 局部變數

如果沒有提昇規則的話,可能會出現像下面的看起來會出現 ReferenceError 的錯誤。

// 檢查 SomeImportantThing 是否已經被初始化
if (!SomeImportantThing) {
    var SomeImportantThing = {};
}

但是它沒有錯誤,因為 var 的表達式會被提升到 全域作用域 的頂端。

var SomeImportantThing;

// 有些程式,可能會初始化。
SomeImportantThing here, or not

// 檢查是否已經被初始化。
if (!SomeImportantThing) {
    SomeImportantThing = {};
}

名稱解析順序

JavaScript 中所有的作用區,包括 全域作用域,都有一個特殊的名字 this, 在它們裡面被定義,指向當前的物件

函式作用域也有一個名稱叫做 arguments, 定義它們,其中包括傳到函式內的參數。

例如,它們開始試著進入到 foo 的作用域裡面, JavaScript 會依照下面的順序去查詢:

  1. 當作用域內是否有 var foo 的定義。
  2. 函式形式參數是否有使用 foo 名稱定義。
  3. 函式自身是剖叫做 foo
  4. 回溯到上一個層級然後再從第一個開始往下去查。

命名空間

只有一個全域作用域會導致常見的錯誤是命名衝突。在 JavaScript 中可以透過 匿名包裝器 來解決。

(function() {
    // 自己本身的匿名空間

    window.foo = function() {
        // 對外公開的函式
    };

})(); // 馬上執行這個匿名函式

匿名函式被認為是 表達式因此為了要可以調用,它們會先被執行。

( // 小括號內的先被執行
function() {}
) // 回傳函數對象
() // 調用上面的執行結果

還有其他方式也可以像上面一樣調用函式的方式達到

!function(){}()
+function(){}()
(function(){}());
// and so on...

結語

建議最好是都用 匿名包裝器 來封裝你的程式碼在自己的命名區間內。這不僅是要防止命名衝突也可以使得程序更有模組化。

另外,全域變數是個 不好的 習慣,因為它會帶來錯誤和更難去維護。

陣列

Array 迴圈和屬性

雖然在 Javascript 中 Array 都是 Objects,但是沒有好的理由要使用他 在 for in 的迴圈中。事實上有很多原因要避免使用 for in 在 Array 之中

因為 for in 迴圈會列舉所有在原型 Array 上的屬性因為他會使用hasOwnProperty, 這會使得 Array 比原本的 for 迴圈慢上二十幾倍

迴圈

為了要達到最好的性能所以最好使用 for 迴圈來讀取一個 Array 裡面的數值。

var list = [1, 2, 3, 4, 5, ...... 100000000];
for(var i = 0, l = list.length; i < l; i++) {
    console.log(list[i]);
}

在上面的例子中利用 l = list.length 來處理 Array 的長度問題。

雖然 length 屬性是屬於 Array 中其中一個屬性,但是他還使有一定的性能消耗在每次循環的訪問。 近期 Javascript 使用 may 來解決在這上面的效率問題,但是在現在的引擎上還不一定有支援。

實際上,不使用暫存 Array 長度的方式比使用暫存的版本還要慢很多。

length 的屬性

length 屬性中的 getter 直接回傳在 Array 之中的程度,而 setter 可以用來 刪除 Array。

var foo = [1, 2, 3, 4, 5, 6];
foo.length = 3;
foo; // [1, 2, 3]

foo.length = 6;
foo.push(4);
foo; // [1, 2, 3, undefined, undefined, undefined, 4]

在上面的例子可以看到,如果給的長度比較小他就會去刪除 Array 中的數值。如果比較大的話,他就會自己增加一些 undefined 的數值進去

結語

為了達到更好的效率,建議使用 for 迴圈還有暫存 length 的屬性。 而 for in 迴圈則是會讓程式中有更多的錯誤和性能問題。

Array 的建構函式

Array 的建構函式在處理參數上一直有模糊的地帶,所以建議使用 array的字面語法來使用 - [] - 來新增一個的Array

[1, 2, 3]; // 結果: [1, 2, 3]
new Array(1, 2, 3); // 結果: [1, 2, 3]

[3]; // 結果: [3]
new Array(3); // 結果: []
new Array('3') // 結果: ['3']

在上面的範例 new Array(3) 當只有一個參數傳入到 Array 的建構函數 且那個參數事宜個數字,建構函數會回傳空值 但是 Array 長度的屬性會變成跟那個參數一樣(以此範例來看他回傳的長度為 3) 注意 只有他長度的屬性會被設定,整個 Array裡面的數值都不會初始化

var arr = new Array(3);
arr[1]; // undefined
1 in arr; // false, 數值沒有被設定進去

被設定用來當做 Array 的長度只有少數情況使用 先設定 Array 的長度可以用一下的範例來避免使用 for loop 的麻煩

new Array(count + 1).join(stringToRepeat);

結語

Array 的建構函式需要避免,建議使用字面語法。因為他們比較簡短、也更增加閱讀性

類型

相等與比較

JavaScript 有兩個不同的方式來比較兩個物件是否相等。

等於操作符

等於操作符是由兩個等號組成: ==

JavaScript 是一個 弱類型 語言。這代表它會為了比較兩個值而做 強制類型轉換

""           ==   "0"           // false
0            ==   ""            // true
0            ==   "0"           // true
false        ==   "false"       // false
false        ==   "0"           // true
false        ==   undefined     // false
false        ==   null          // false
null         ==   undefined     // true
" \t\r\n"    ==   0             // true

上面的表格可以看出來這些結果強制轉換類型,這也代表說用 == 是一個不好的習慣,因為它會很難追蹤問題由於它複雜的規則。

此外,也有效率上面的問題在強制轉換類型。 例如說一個字串會被轉成數字來和別的數字做比較。

嚴格等於操作符

不像普通的等於操作符 === 不會做強制類型轉換。

""           ===   "0"           // false
0            ===   ""            // false
0            ===   "0"           // false
false        ===   "false"       // false
false        ===   "0"           // false
false        ===   undefined     // false
false        ===   null          // false
null         ===   undefined     // false
" \t\r\n"    ===   0             // false

上面的結果比較清楚,也有利於程式碼的分析。如果這兩個操作數的類型不一樣都就不會相等,有助於它性能的提昇。

比較物件

雖然 ===== 都是等於操作符,但其中有一個操作數為物件時,它的行為就會不同。

{} === {};                   // false
new String('foo') === 'foo'; // false
new Number(10) === 10;       // false
var foo = {};
foo === foo;                 // true

在這裡等於操作符比較 不是 值的相等,而是否是 相同 的身分。 有點像 Python 的 is 和 C 中的指標。

結論

強烈建議使用 嚴格等於 如果要轉換類型,應該要在 explicitly的時候轉換,而不是在語言本身用複雜的轉換規則。

typeof 操作符

typeof 操作符 (和 instanceof) 可能是最大的設計錯誤在 JavaScript,因為它幾乎不可能從它們那裡得到想要的結果。

雖然 instanceof 還是有一些限制上的使用, typeof 只有一個實際上的運傭情形,但是 不是 用在檢查物件的類型。

JavaScript 類型表格

Value               Class      Type
-------------------------------------
"foo"               String     string
new String("foo")   String     object
1.2                 Number     number
new Number(1.2)     Number     object
true                Boolean    boolean
new Boolean(true)   Boolean    object
new Date()          Date       object
new Error()         Error      object
[1,2,3]             Array      object
new Array(1, 2, 3)  Array      object
new Function("")    Function   function
/abc/g              RegExp     object (function in Nitro/V8)
new RegExp("meow")  RegExp     object (function in Nitro/V8)
{}                  Object     object
new Object()        Object     object

上面的表格中, Type 這一系列表示 typeof 的操作符的運算結果。可以看到,這個值的大多數情況下都返回物件。

Class 表示物件內部的屬性 [[Class]] 的值。

為了獲取對象的 [[Class]],我們可以使用定義在 Object.prototype 上的方法 toString

物件的類定義

JavaScript 標準文檔只給出了一種獲取 [[Class]] 值的方法,那就是使用 Object.prototype.toString

function is(type, obj) {
    var clas = Object.prototype.toString.call(obj).slice(8, -1);
    return obj !== undefined && obj !== null && clas === type;
}

is('String', 'test'); // true
is('String', new String('test')); // true

上面的例子中,**Object.prototype.toStringthis的值來來調用被設置需要獲取 [[Class]] 值的物件。

測試未定義變數

typeof foo !== 'undefined'

上面的例子確認 foo 是否真的被宣告。如果沒有定義會導致 ReferenceError 這是 typeof 唯一有用的地方

結語

為了去檢查一個物件,強烈建議去使用 Object.prototype.toString 因為這是唯一可以依賴的方式。 正如上面所看到的 typeof 的亦先返回值在標準文檔中未定義,因此不同的引擎可能不同。

除非為了檢測一個變數是否定義,我們應該避免是用 typeof 操作符。

instanceof 操作符

instanceof 操作符用來比較兩個建構函數的操作數。只有在比較字定義的物件時才有意義。這和 typeof operator一樣用處不大。

比較定意義物件

function Foo() {}
function Bar() {}
Bar.prototype = new Foo();

new Bar() instanceof Bar; // true
new Bar() instanceof Foo; // true

// This just sets Bar.prototype to the function object Foo,
// but not to an actual instance of Foo
Bar.prototype = Foo;
new Bar() instanceof Foo; // false

instanceof 比較內置類型

new String('foo') instanceof String; // true
new String('foo') instanceof Object; // true

'foo' instanceof String; // false
'foo' instanceof Object; // false

有一點需要注意的, instanceof 不能用來物件來自上下文不同的屬性(例如:瀏覽器中不同的文檔結構),因為它的建構函數不一樣。

In Conclusion

instanceof 操作符應該 用來比較同一個 JavaScript 上下文定意義的物件。 正如 typeof操作符一樣,任何其他用法都要避免。

類型轉換

JavaScript 是一個 弱類型 的程式語言,所以在 任何 情況下都可以 強制類型轉換

// 這些都是真
new Number(10) == 10; // Number.toString() is converted
                      // back to a number

10 == '10';           // Strings gets converted to Number
10 == '+10 ';         // More string madness
10 == '010';          // And more 
isNaN(null) == false; // null converts to 0
                      // which of course is not NaN

// 下面都假
10 == 010;
10 == '-10';

為了去避免上驗的事件發生,我們會用 嚴格等於操作符 這是強烈建議。 因為它可以避免很多常見的問題,但 JavaScript 的弱類型系同仍然會導致一些其他問題。

內置類型的建構函式

內置類型(比如 NumberString)在被調用時,使用或不使用 new 的結果完全不同。

new Number(10) === 10;     // False, Object and Number
Number(10) === 10;         // True, Number and Number
new Number(10) + 0 === 10; // True, due to implicit conversion

使用內置類型 Number 作為建構函式會建造一個新的 Number 物件,而在不使用 new 關鍵字的 Number 函式更像是一個數字轉換器。

另外,在比較中引入物件的字面值會導致更加複雜的強制類型轉換。

最好的方式是比較值的 顯示 的轉換成最有可能的三種形態

轉換成字符串

'' + 10 === '10'; // true

將一個值加上空字符串可以輕鬆轉為字符串類型。

轉換成一個數字

+'10' === 10; // true

使用 一元 的加號操作符,可以把字符串轉為數字。

轉換成一個 Bool

通過使用 操作符兩字,可以把一個值轉換為 Bool。

!!'foo';   // true
!!'';      // false
!!'0';     // true
!!'1';     // true
!!'-1'     // true
!!{};      // true
!!true;    // true

核心

為什麼不要使用 eval

因為 eval 函數會在 Javascript 的區域性的區間執行那段程式碼。

var foo = 1;
function test() {
    var foo = 2;
    eval('foo = 3');
    return foo;
}
test(); // 3
foo; // 1

但是, eval 只接受直接的呼叫而且那個函數只能叫做 eval,才能在一個區段中執行。

var foo = 1;
function test() {
    var foo = 2;
    var bar = eval;
    bar('foo = 3');
    return foo;
}
test(); // 2
foo; // 3

所有的 eval 都應該去比免試用。有 99.9% 的使用情況都可以 不必 使用到而達到同等效果。

偽裝的 eval

定時函數 setTimeoutsetInterval 都可以接受一個字串當做他們第一個參數。這些字串 永遠 都會在全域範圍內執行,因此在這種情況下 eval 沒有被直接的使用。

安全上的顧慮

eval 同樣有安全上的問題,因為所有的程式碼都可以被直接執行。 而他不應去執行一串未知的字串或是來自不幸任的來源。

結語

eval 應該永遠不要去只用它,任何的程式在被他執行後都有性能和安全上的考慮。如果有情況需要去使用他,他都不應該列為第一順位的解決方法。

應該有更好的方法能夠去使用,但是最好都不要去使用 eval

undefinednull

JavaScript 中有兩個表示空值的方式, nullundefinedundefined式比較常用的一種。

undefined 的值

undefined 是一個值為 undefined 的類型。

語言中也定義了一個全域變數,它的值為 undefined,這個變數的被稱作 undefined 。 這個變數 不是 一個常數,也不是一個關鍵字。這表示它的值可以被輕易的覆蓋。

這裡有一些例子會回傳 undefined 的值:

  • 進入尚未修改的全域變數 undefined
  • 進入一個宣告但 尚未 初始化的變數。
  • return 表示式中沒有返回任何內容。
  • 呼叫不存在的屬性。
  • 函式參數沒有被傳遞數值。
  • 任何被被設定為 undefined 的變數。
  • 任何表達式中形式為 void(expression)

處理 undefined 值的改變

由於全域變數 undefined 只有保存 undefined 類型實際值的一個副本,指定了一個新的值並 不會 改變 undefined類型裡面的值。

為了避免去改變 undefined 的值,常用的技巧就是加上一個新的變數到 匿名包裝器。在使用的時候,這個參數不會接受任何的值。

var undefined = 123;
(function(something, foo, undefined) {
    // undefined 在區域區間內得到了 `undefined` 的值

})('Hello World', 42);

另外一個可以得到同樣的效果就是在內部宣告一個變數

var undefined = 123;
(function(something, foo) {
    var undefined;
    ...

})('Hello World', 42);

唯一的不同就是在下者會多 4 個多 bytes 用來壓縮檔案,而且函數內也沒有其他需要使用 var

使用 null

JavaScript 中所使用的 undefined 類似別的語言中的 null , 但實際上在 JavaScript 中的 null 算是另外一個類型。

它在 JavaScript 有些可以使用的地方 (例如說宣告一個原型的終結,例如 Foo.prototype = null )。 但是在大部分的時候可以用 undefined,來取代。

自動插入分號

雖然 JavaScript 有 C 語言的語法,但是他不強制一定要加上分號。 所以分號可以被忽略。

Javascript 並 不是 一個不需要分號的語言。實際上,它需要分號來讓程式碼更容易被理解。因此 Javascript 的編譯器中遇到了缺少分號的情形,它會自動的在程式碼中插入分號。

var foo = function() {
} // 編輯錯誤,因沒分號
test()

這時候編譯器在編輯的時候,會自動的加上分號,然後重新編輯。

var foo = function() {
}; // 沒有錯誤,編輯繼續
test()

自動的加入分號是被認為 最大 的設計缺陷之一,因為它能改變程式碼的行為。

工作原理

下面的程式碼中沒有使用任何的分號,所以編譯器需要去決定在哪些地方加入分號。

(function(window, undefined) {
    function test(options) {
        log('testing!')

        (options.list || []).forEach(function(i) {

        })

        options.value.test(
            'long string to pass here',
            'and another long string to pass'
        )

        return
        {
            foo: function() {}
        }
    }
    window.test = test

})(window)

(function(window) {
    window.someLibrary = {}

})(window)

下面的程式碼是編譯器 猜測 的結果。

(function(window, undefined) {
    function test(options) {

        // 沒有加入分號,兩行被合併為一行
        log('testing!')(options.list || []).forEach(function(i) {

        }); // <- 插入分號

        options.value.test(
            'long string to pass here',
            'and another long string to pass'
        ); // <- 插入分號

        return; // <- 插入分號,改變了 return 的表達行為
        { // 作為另一個程式碼的處理

            // 被當做一個獨立的函數來看
            foo: function() {} 
        }; // <- 插入分號
    }
    window.test = test; // <- 插入分號

// 兩行又被合併
})(window)(function(window) {
    window.someLibrary = {}; // <- 插入分號

})(window); //<- 插入分號

編譯器在上面的程式碼中改變了原本程式碼的行為。在一些情況下,會做出 錯誤的行為

前置括號

在這種前置括號的情況下,編譯器 不會 自動的插入分號。

log('testing!')
(options.list || []).forEach(function(i) {})

上面的程式碼被編譯器轉為只有一行程式

log('testing!')(options.list || []).forEach(function(i) {})

以上的範例中 log很大 的可能 不是 回傳一個函數。然而這個情況下會出現 TypeError 的錯誤或是會出現 undefined is not a function .

結語

建議永遠 不要 忽略分號。同樣的也建議大括號應在他對應的表達式在同一行。在 if... else...的表達式中也是如此,不應省略大括號。 這個習慣可以不僅僅是讓你的程式更一致,也可以避免編譯器因為改變程式而出錯。

delete 控制符

簡單來說,那是 不可能 去刪除一個全域變數,函式和其他東西在 JavaScript 中有一個 DontDelete 的屬性

全域和函式

當一個變數或是一個函式在一個全域範圍被定義或是在一個 funciton scope ,這些屬性可能是動態的物件或是全域的物件。這些特性有一系列的屬性。其中一個就是 DontDelete。 在這些變數和函式的宣告都會有一個屬性叫 DontDelete,這會使得它無法被刪除。

// 全域變數
var a = 1; // DontDelete 屬性被建立
delete a; // false
a; // 1

// normal function:
function f() {} // DontDelete 屬性被建立
delete f; // false
typeof f; // "function"

// reassigning doesn't help:
f = 1;
delete f; // false
f; // 1

明確的屬性

明確的屬性可以被簡單的刪除。

// explicitly set property:
var obj = {x: 1};
obj.y = 2;
delete obj.x; // true
delete obj.y; // true
obj.x; // undefined
obj.y; // undefined

在上面的例子中, obj.xobj.y 可以被刪除是因為他們沒有 DontDelete 的屬性。 所以下面的例子也可以這樣用。

// 可以運作,除了 IE:
var GLOBAL_OBJECT = this;
GLOBAL_OBJECT.a = 1;
a === GLOBAL_OBJECT.a; // true - just a global var
delete GLOBAL_OBJECT.a; // true
GLOBAL_OBJECT.a; // undefined

這裡我們想要去刪除 athis 這裡指向一個全域的物件,和我們明確了地定義 a 是它的屬性,所以可以刪除它。

IE 有些臭蟲,所以上面的程式碼無法使用(至少 6~8)

函式的參數和內建

函式的普通參數,arguments object 還有一些內建的屬性都有 DontDelete 的建立

// function 參數和屬性
(function (x) {

  delete arguments; // false
  typeof arguments; // "object"

  delete x; // false
  x; // 1

  function f(){}
  delete f.length; // false
  typeof f.length; // "number"

})(1);

接受物件

控制符可以接受無法預測的物件。由於一些特別的情況,會允許它能夠 delete

結語

delete 控制符通常都有難以預料的行為,所以我們只可以安全的刪除顯著的屬性在普通的物件上。

其他

setTimeoutsetInterval

由於 Javascript 具有非同步的特性,因此可以用 setTimeoutsetInterval 來執行一個函式。

function foo() {}
var id = setTimeout(foo, 1000); // returns a Number > 0

setTimeout 被呼叫,它會回傳一個 ID 標準並且 大約 1000 毫秒後在在去呼叫 foo 函式。 foo 函式只會被執行 一次

基於 JavaScript 引擎的計時策略,以及基本的單線程運行的方式,所以其他的程式碼可以被阻塞。 因此 沒法確保函式會在 setTimeout 指定的時可被調用。

第一個參數被函式呼叫的會在 全域物件 被呼叫,這代表 this在這個函式會指向全域物件。

function Foo() {
    this.value = 42;
    this.method = function() {
        // 指向全域
        console.log(this.value); // 會跑出 undefined
    };
    setTimeout(this.method, 500);
}
new Foo();

setInterval 的堆調用

setTimeout 只會在函式上跑一次而已, setInterval - 則會在每隔 X 毫秒執行函式一次。但不鼓勵這種寫法。

當回傳函式的執行被阻塞時, setInterval 仍然會發佈更多的回傳函式。在很小的定時間隔情況像會使得回傳函式被堆疊起來。

function foo(){
    // 執行 1 秒
}
setInterval(foo, 100);

上面的程式中, foo 會執行一次然後被阻塞了一分鐘

foo 被阻塞的時候 setInterval 還是會組織將對回傳函式的調用。因此當第一次 foo 函式調用結束時,已經有 10 次函式的調用在等待執行。

處理可能被阻塞的調用

最簡單的解決方法,也是最容易控制的解決方法,就是在函式中使用 setTimeout

function foo(){
    // something that blocks for 1 second
    setTimeout(foo, 100);
}
foo();

這樣不只封裝了 setTimeout,也防止了堆疊的呼叫,還有給它更多的控制。 foo 可以去決定要不要繼續執行。

手動清理 Timeouts

清除 timeouts 所產生的 ID 標準傳遞給 clearTimeoutclearInterval 函式來清除定時, 至於使用哪個函式取決於調用的時候使用的是 setTimeout 還是 setInterval

var id = setTimeout(foo, 1000);
clearTimeout(id);

清除所有 Timeouts

由於沒有一個內建的方法可以一次清空所有的 timeouts 和 intervals,所以只有用暴力法來達到這樣的需求。

// clear "all" timeouts
for(var i = 1; i < 1000; i++) {
    clearTimeout(i);
}

可能還有一些定時器不會在上面的代碼中被清除,因此我們可以事先保存所有的定時器 ID,然後一把清除。

// clear "all" timeouts
var biggestTimeoutId = window.setTimeout(function(){}, 1),
i;
for(i = 1; i <= biggestTimeoutId; i++) {
    clearTimeout(i);
}

隱藏使用 eval

setTimeout and setInterval 也可以使用字串當作他們的第一個參數. 不過這個特性 絕對 不要使用, 因為在內部他將利用 eval 來實作。

function foo() {
    // will get called
}

function bar() {
    function foo() {
        // never gets called
    }
    setTimeout('foo()', 1000);
}
bar();

在這個範例中,由於 eval 沒有被直接呼叫,在 setTimeout 中被傳入的字串將會在 全域 範圍中被執行,因此,他將不會使用在 bar 區域的 foo

我們進一步建議 不要 用字串當作參數傳到會被 timeout 呼叫的函式中。

function foo(a, b, c) {}

// NEVER use this
setTimeout('foo(1, 2, 3)', 1000)

// Instead use an anonymous function
setTimeout(function() {
    foo(1, 2, 3);
}, 1000)

結論

絕對 不要使用字串當作 setTimeoutsetInterval 參數。當參數要被當成呼叫的函式時,這絕對是 不好 的程式碼,相反的,利用 匿名函式 來完成這樣的行為。

此外,應該避免使用 setInterval,因為他將不會被 Javascript 給中斷。