js綜合

對象git

對象使用和屬性程序員


JavaScript 中全部變量都是對象,除了兩個例外 null 和 undefined。github


false.toString(); // 'false'編程

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


function Foo(){}瀏覽器

Foo.bar = 1;緩存

Foo.bar; // 1安全

一個常見的誤解是數字的字面值(literal)不是對象。這是由於 JavaScript 解析器的一個錯誤, 它試圖將點操做符解析爲浮點數字面值的一部分。閉包


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

有不少變通方法可讓數字的字面值看起來像對象。


2..toString(); // 第二個點號能夠正常解析

2 .toString(); // 注意點號前面的空格

(2).toString(); // 2先被計算

對象做爲數據類型


JavaScript 的對象能夠做爲哈希表使用,主要用來保存命名的鍵與值的對應關係。


使用對象的字面語法 - {} - 能夠建立一個簡單對象。這個新建立的對象從 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

兩種語法是等價的,可是中括號操做符在下面兩種狀況下依然有效 - 動態設置屬性 - 屬性名不是一個有效的變量名(譯者注:好比屬性名中包含空格,或者屬性名是 JS 的關鍵詞)


譯者注:在 JSLint 語法檢測工具中,點操做符是推薦作法。

刪除屬性


刪除屬性的惟一方法是使用 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 undefined 和 foo null - 只有 baz 被真正的刪除了,因此從輸出結果中消失。


屬性名的語法


var test = {

    'case': 'I am a keyword so I must be notated as a string',

    delete: 'I am a keyword too so me' // 出錯:SyntaxError

};

對象的屬性名可使用字符串或者普通字符聲明。可是因爲 JavaScript 解析器的另外一個錯誤設計, 上面的第二種聲明方式在 ECMAScript 5 以前會拋出 SyntaxError 的錯誤。


這個錯誤的緣由是 delete 是 JavaScript 語言的一個關鍵詞;所以爲了在更低版本的 JavaScript 引擎下也能正常運行, 必須使用字符串字面值聲明方式。


原型


JavaScript 不包含傳統的類繼承模型,而是使用 prototype 原型模型。


雖然這常常被看成是 JavaScript 的缺點被說起,其實基於原型的繼承模型比傳統的類繼承還要強大。 實現傳統的類繼承模型是很簡單,可是實現 JavaScript 中的原型繼承則要困難的多。 (It is for example fairly trivial to build a classic model on top of it, while the other way around is a far more difficult task.)


因爲 JavaScript 是惟一一個被普遍使用的基於原型繼承的語言,因此理解兩種繼承模式的差別是須要必定時間的。


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


注意: 簡單的使用 Bar.prototype = Foo.prototype 將會致使兩個對象共享相同的原型。 所以,改變任意一個對象的原型都會影響到另外一個對象的原型,在大多數狀況下這不是但願的結果。

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() // 建立Bar的一個新實例


// 原型鏈

test [Bar的實例]

    Bar.prototype [Foo的實例] 

        { foo: 'Hello World' }

        Foo.prototype

            {method: ...};

            Object.prototype

                {toString: ... /* etc. */};

上面的例子中,test 對象從 Bar.prototype 和 Foo.prototype 繼承下來;所以, 它能訪問 Foo 的原型方法 method。同時,它也可以訪問那個定義在原型上的 Foo 實例屬性 value。 須要注意的是 new Bar() 不會創造出一個新的 Foo 實例,而是 重複使用它原型上的那個實例;所以,全部的 Bar 實例都會共享相同的 value 屬性。


注意: 不要使用 Bar.prototype = Foo,由於這不會執行 Foo 的原型,而是指向函數 Foo。 所以原型鏈將會回溯到 Function.prototype 而不是 Foo.prototype,所以 method 將不會在 Bar 的原型鏈上。

屬性查找


當查找一個對象的屬性時,JavaScript 會向上遍歷原型鏈,直到找到給定名稱的屬性爲止。


到查找到達原型鏈的頂部 - 也就是 Object.prototype - 可是仍然沒有找到指定的屬性,就會返回 undefined。


原型屬性


當原型屬性用來建立原型鏈時,能夠把任何類型的值賦給它(prototype)。 然而將原子類型賦給 prototype 的操做將會被忽略。


function Foo() {}

Foo.prototype = 1; // 無效

而將對象賦值給 prototype,正如上面的例子所示,將會動態的建立原型鏈。


性能


若是一個屬性在原型鏈的上端,則對於查找時間將帶來不利影響。特別的,試圖獲取一個不存在的屬性將會遍歷整個原型鏈。


而且,當使用 for in 循環遍歷對象的屬性時,原型鏈上的全部屬性都將被訪問。


擴展內置類型的原型


一個錯誤特性被常用,那就是擴展 Object.prototype 或者其餘內置類型的原型對象。


這種技術被稱之爲 monkey patching 而且會破壞封裝。雖然它被普遍的應用到一些 JavaScript 類庫中好比 Prototype, 可是我仍然不認爲爲內置類型添加一些非標準的函數是個好主意。


擴展內置類型的惟一理由是爲了和新的 JavaScript 保持一致,好比 Array.forEach。


譯者注:這是編程領域經常使用的一種方式,稱之爲 Backport,也就是將新的補丁添加到老版本中。

總結


在寫複雜的 JavaScript 應用以前,充分理解原型鏈繼承的工做方式是每一個 JavaScript 程序員必修的功課。 要提防原型鏈過長帶來的性能問題,並知道如何經過縮短原型鏈來提升性能。 更進一步,絕對不要擴展內置類型的原型,除非是爲了和新的 JavaScript 引擎兼容。


hasOwnProperty 函數


爲了判斷一個對象是否包含自定義屬性而不是原型鏈上的屬性, 咱們須要使用繼承自 Object.prototype 的 hasOwnProperty 方法。


注意: 經過判斷一個屬性是否 undefined 是不夠的。 由於一個屬性可能確實存在,只不過它的值被設置爲 undefined。

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

結論


當檢查對象上某個屬性是否存在時,hasOwnProperty 是惟一可用的方法。 同時在使用 for in loop 遍歷對象時,推薦老是使用 hasOwnProperty 方法, 這將會避免原型對象擴展帶來的干擾。


for in 循環


和 in 操做符同樣,for in 循環一樣在查找對象屬性時遍歷原型鏈上的全部屬性。


注意: for in 循環不會遍歷那些 enumerable 設置爲 false 的屬性;好比數組的 length 屬性。

// 修改 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 函數來完成。


注意: 因爲 for in 老是要遍歷整個原型鏈,所以若是一個對象的繼承層次太深的話會影響性能。

使用 hasOwnProperty 過濾


// foo 變量是上例中的

for(var i in foo) {

    if (foo.hasOwnProperty(i)) {

        console.log(i);

    }

}

這個版本的代碼是惟一正確的寫法。因爲咱們使用了 hasOwnProperty,因此此次只輸出 moo。 若是不使用 hasOwnProperty,則這段代碼在原生對象原型(好比 Object.prototype)被擴展時可能會出錯。


一個普遍使用的類庫 Prototype 就擴展了原生的 JavaScript 對象。 所以,當這個類庫被包含在頁面中時,不使用 hasOwnProperty 過濾的 for in 循環不免會出問題。


總結


推薦老是使用 hasOwnProperty。不要對代碼運行的環境作任何假設,不要假設原生對象是否已經被擴展了。


函數

函數聲明與表達式


函數是JavaScript中的一等對象,這意味着能夠把函數像其它值同樣傳遞。 一個常見的用法是把匿名函數做爲回調函數傳遞到異步函數中。


函數聲明


function foo() {}

上面的方法會在執行前被 解析(hoisted),所以它存在於當前上下文的任意一個地方, 即便在函數定義體的上面被調用也是對的。


foo(); // 正常運行,由於foo在代碼運行前已經被建立

function foo() {}

函數賦值表達式


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 的 命名處理 所致, 函數名在函數內老是可見的。


注意:在IE8及IE8如下版本瀏覽器bar在外部也是可見的,是由於瀏覽器對命名函數賦值表達式進行了錯誤的解析, 解析成兩個函數 foo 和 bar

this 的工做原理


JavaScript 有一套徹底不一樣於其它語言的對 this 的處理機制。 在五種不一樣的狀況下 ,this 指向的各不相同。


全局範圍內


this;

當在所有範圍內使用 this,它將會指向全局對象。


譯者注:瀏覽器中運行的 JavaScript 腳本,這個全局對象是 window。

函數調用


foo();

這裏 this 也會指向全局對象。


ES5 注意: 在嚴格模式下(strict mode),不存在全局變量。 這種狀況下 this 將會是 undefined。

方法調用


test.foo(); 

這個例子中,this 指向 test 對象。


調用構造函數


new foo(); 

若是函數傾向於和 new 關鍵詞一塊使用,則咱們稱這個函數是 構造函數。 在函數內部,this 指向新建立的對象。


顯式的設置 this


function foo(a, b, c) {}


var bar = {};

foo.apply(bar, [1, 2, 3]); // 數組將會被擴展,以下所示

foo.call(bar, 1, 2, 3); // 傳遞到foo的參數是:a = 1, b = 2, c = 3

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


所以函數調用的規則在上例中已經不適用了,在foo 函數內 this 被設置成了 bar。


注意: 在對象的字面聲明語法中,this 不能用來指向對象自己。 所以 var obj = {me: this} 中的 me 不會指向 obj,由於 this 只可能出如今上述的五種狀況中。 譯者注:這個例子中,若是是在瀏覽器中運行,obj.me 等於 window 對象。

常見誤解


儘管大部分的狀況都說的過去,不過第一個規則(譯者注:這裏指的應該是第二個規則,也就是直接調用函數時,this 指向全局對象) 被認爲是JavaScript語言另外一個錯誤設計的地方,由於它歷來就沒有實際的用途。


Foo.method = function() {

    function test() {

        // this 將會被設置爲全局對象(譯者注:瀏覽器環境中也就是 window 對象)

    }

    test();

}

一個常見的誤解是 test 中的 this 將會指向 Foo 對象,實際上不是這樣子的。


爲了在 test 中獲取對 Foo 對象的引用,咱們須要在 method 函數內部建立一個局部變量指向 Foo 對象。


Foo.method = function() {

    var that = this;

    function test() {

        // 使用 that 來指向 Foo 對象

    }

    test();

}

that 只是咱們隨意起的名字,不過這個名字被普遍的用來指向外部的 this 對象。 在 閉包 一節,咱們能夠看到 that 能夠做爲參數傳遞。


方法的賦值表達式


另外一個看起來奇怪的地方是函數別名,也就是將一個方法賦值給一個變量。


var test = someObject.methodTest;

test();

上例中,test 就像一個普通的函數被調用;所以,函數內的 this 將再也不被指向到 someObject 對象。


雖然 this 的晚綁定特性彷佛並不友好,但這確實是基於原型繼承賴以生存的土壤。


function Foo() {}

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


function Bar() {}

Bar.prototype = Foo.prototype;


new Bar().method();

當 method 被調用時,this 將會指向 Bar 的實例對象。


閉包和引用


閉包是 JavaScript 一個很是重要的特性,這意味着當前做用域老是可以訪問外部做用域中的變量。 由於 函數 是 JavaScript 中惟一擁有自身做用域的結構,所以閉包的建立依賴於函數。


模擬私有變量


function Counter(start) {

    var count = start;

    return {

        increment: function() {

            count++;

        },


        get: function() {

            return count;

        }

    }

}


var foo = Counter(4);

foo.increment();

foo.get(); // 5

這裏,Counter 函數返回兩個閉包,函數 increment 和函數 get。 這兩個函數都維持着 對外部做用域 Counter 的引用,所以總能夠訪問此做用域內定義的變量 count.


爲何不能夠在外部訪問私有變量


由於 JavaScript 中不能夠對做用域進行引用或賦值,所以沒有辦法在外部訪問 count 變量。 惟一的途徑就是經過那兩個閉包。


var foo = new Counter(4);

foo.hack = function() {

    count = 1337;

};

上面的代碼不會改變定義在 Counter 做用域中的 count 變量的值,由於 foo.hack 沒有 定義在那個做用域內。它將會建立或者覆蓋全局變量 count。


循環中的閉包


一個常見的錯誤出如今循環中使用閉包,假設咱們須要在每次循環中調用循環序號


for(var i = 0; i < 10; i++) {

    setTimeout(function() {

        console.log(i);  

    }, 1000);

}

上面的代碼不會輸出數字 0 到 9,而是會輸出數字 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)

}

arguments 對象


JavaScript 中每一個函數內都能訪問一個特別變量 arguments。這個變量維護着全部傳遞到這個函數中的參數列表。


注意: 因爲 arguments 已經被定義爲函數內的一個變量。 所以經過 var 關鍵字定義 arguments 或者將 arguments 聲明爲一個形式參數, 都將致使原生的 arguments 不會被建立。

arguments 變量不是一個數組(Array)。 儘管在語法上它有數組相關的屬性 length,但它不從 Array.prototype 繼承,實際上它是一個對象(Object)。


所以,沒法對 arguments 變量使用標準的數組方法,好比 push, pop 或者 slice。 雖然使用 for 循環遍歷也是能夠的,可是爲了更好的使用數組方法,最好把它轉化爲一個真正的數組。


轉化爲數組


下面的代碼將會建立一個新的數組,包含全部 arguments 對象中的元素。


Array.prototype.slice.call(arguments);

這個轉化比較慢,在性能很差的代碼中不推薦這種作法。


傳遞參數


下面是將參數從一個函數傳遞到另外一個函數的推薦作法。


function foo() {

    bar.apply(null, arguments);

}

function bar(a, b, c) {

    // 幹活

}

另外一個技巧是同時使用 call 和 apply,建立一個快速的解綁定包裝器。


function Foo() {}


Foo.prototype.method = function(a, b, c) {

    console.log(this, a, b, c);

};


// 建立一個解綁定的 "method"

// 輸入參數爲: this, arg1, arg2...argN

Foo.method = function() {


    // 結果: Foo.prototype.method.call(this, arg1, arg2... argN)

    Function.call.apply(Foo.prototype.method, arguments);

};

譯者注:上面的 Foo.method 函數和下面代碼的效果是同樣的:


Foo.method = function() {

    var args = Array.prototype.slice.call(arguments);

    Foo.prototype.method.apply(args[0], args.slice(1));

};

自動更新


arguments 對象爲其內部屬性以及函數形式參數建立 getter 和 setter 方法。


所以,改變形參的值會影響到 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 對象總會被建立,除了兩個特殊狀況 - 做爲局部變量聲明和做爲形式參數。


arguments 的 getters 和 setters 方法總會被建立;所以使用 arguments 對性能不會有什麼影響。 除非是須要對 arguments 對象的屬性進行屢次訪問。


ES5 提示: 這些 getters 和 setters 在嚴格模式下(strict mode)不會被建立。

譯者注:在 MDC 中對 strict mode 模式下 arguments 的描述有助於咱們的理解,請看下面代碼:


// 闡述在 ES5 的嚴格模式下 `arguments` 的特性

function f(a) {

  "use strict";

  a = 42;

  return [a, arguments[0]];

}

var pair = f(17);

assert(pair[0] === 42);

assert(pair[1] === 17);

然而,的確有一種狀況會顯著的影響現代 JavaScript 引擎的性能。這就是使用 arguments.callee。


function foo() {

    arguments.callee; // do something with this function object

    arguments.callee.caller; // and the calling function object

}


function bigLoop() {

    for(var i = 0; i < 100000; i++) {

        foo(); // Would normally be inlined...

    }

}

上面代碼中,foo 再也不是一個單純的內聯函數 inlining(譯者注:這裏指的是解析器能夠作內聯處理), 由於它須要知道它本身和它的調用者。 這不只抵消了內聯函數帶來的性能提高,並且破壞了封裝,所以如今函數可能要依賴於特定的上下文。


所以強烈建議你們不要使用 arguments.callee 和它的屬性。


ES5 提示: 在嚴格模式下,arguments.callee 會報錯 TypeError,由於它已經被廢除了。

構造函數


JavaScript 中的構造函數和其它語言中的構造函數是不一樣的。 經過 new 關鍵字方式調用的函數都被認爲是構造函數。


在構造函數內部 - 也就是被調用的函數內 - this 指向新建立的對象 Object。 這個新建立的對象的 prototype 被指向到構造函數的 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 表達式將會影響返回結果,但僅限於返回的是一個對象。


function Bar() {

    return 2;

}

new Bar(); // 返回新建立的對象


function Test() {

    this.value = 2;


    return {

        foo: 1

    };

}

new Test(); // 返回的對象

譯者注:new Bar() 返回的是新建立的對象,而不是數字的字面值 2。 所以 new Bar().constructor === Bar,可是若是返回的是數字對象,結果就不一樣了,以下所示


function Bar() {

    return new Number(2);

}

new Bar().constructor === Number

譯者注:這裏獲得的 new Test()是函數返回的對象,而不是經過new關鍵字新建立的對象,所以:


(new Test()).value === undefined

(new Test()).foo === 1

若是 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 屬性的對象被返回, 其實這裏建立了一個閉包。


還須要注意, new Bar() 並不會改變返回對象的原型(譯者注:也就是返回對象的原型不會指向 Bar.prototype)。 由於構造函數的原型會被指向到剛剛建立的新對象,而這裏的 Bar 沒有把這個新對象返回(譯者注:而是返回了一個包含 method 屬性的自定義對象)。


在上面的例子中,使用或者不使用 new 關鍵字沒有功能性的區別。


譯者注:上面兩種方式建立的對象不能訪問 Bar 原型鏈上的屬性,以下所示:


var bar1 = new Bar();

typeof(bar1.method); // "function"

typeof(bar1.foo); // "undefined"


var bar2 = Bar();

typeof(bar2.method); // "function"

typeof(bar2.foo); // "undefined"

經過工廠模式建立新對象


咱們常聽到的一條忠告是不要使用 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 的調用方式不容易出錯,而且能夠充分利用私有變量帶來的便利, 可是隨之而來的是一些很差的地方。


會佔用更多的內存,由於新建立的對象不能共享原型上的方法。

爲了實現繼承,工廠方法須要從另一個對象拷貝全部屬性,或者把一個對象做爲新建立對象的原型。

放棄原型鏈僅僅是由於防止遺漏 new 帶來的問題,這彷佛和語言自己的思想相違背。

總結


雖然遺漏 new 關鍵字可能會致使問題,但這並非放棄使用原型鏈的藉口。 最終使用哪一種方式取決於應用程序的需求,選擇一種代碼書寫風格並堅持下去纔是最重要的。


做用域與命名空間


儘管 JavaScript 支持一對花括號建立的代碼段,可是並不支持塊級做用域; 而僅僅支持 函數做用域。


function test() { // 一個做用域

    for(var i = 0; i < 10; i++) { // 不是一個做用域

        // count

    }

    console.log(i); // 10

}

注意: 若是不是在賦值語句中,而是在 return 表達式或者函數參數中,{...} 將會做爲代碼段解析, 而不是做爲對象的字面語法解析。若是考慮到 自動分號插入,這可能會致使一些不易察覺的錯誤。

譯者注:若是 return 對象的左括號和 return 不在一行上就會出錯。


// 譯者注:下面輸出 undefined

function add(a, b) {

    return 

        a + b;

}

console.log(add(1, 2));

JavaScript 中沒有顯式的命名空間定義,這就意味着全部對象都定義在一個全局共享的命名空間下面。


每次引用一個變量,JavaScript 會向上遍歷整個做用域直到找到這個變量爲止。 若是到達全局做用域可是這個變量仍未找到,則會拋出 ReferenceError 異常。


隱式的全局變量


// 腳本 A

foo = '42';


// 腳本 B

var foo = '42'

上面兩段腳本效果不一樣。腳本 A 在全局做用域內定義了變量 foo,而腳本 B 在當前做用域內定義變量 foo。


再次強調,上面的效果徹底不一樣,不使用 var 聲明變量將會致使隱式的全局變量產生。


// 全局做用域

var foo = 42;

function test() {

    // 局部做用域

    foo = 21;

}

test();

foo; // 21

在函數 test 內不使用 var 關鍵字聲明 foo 變量將會覆蓋外部的同名變量。 起初這看起來並非大問題,可是當有成千上萬行代碼時,不使用 var 聲明變量將會帶來難以跟蹤的 BUG。


// 全局做用域

var items = [/* 數組 */];

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);

foo 和 i 是函數 test 內的局部變量,而對 bar 的賦值將會覆蓋全局做用域內的同名變量。


變量聲明提高(Hoisting)


JavaScript 會提高變量聲明。這意味着 var 表達式和 function 聲明都將會被提高到當前做用域的頂部。


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 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; // 賦值語句不會被提高規則(hoisting)影響

bar = function() {};


test();

沒有塊級做用域不只致使 var 表達式被從循環內移到外部,並且使一些 if 表達式更難看懂。


在原來代碼中,if 表達式看起來修改了全局變量 goo,實際上在提高規則被應用後,倒是在修改局部變量。


若是沒有提高規則(hoisting)的知識,下面的代碼看起來會拋出異常 ReferenceError。


// 檢查 SomeImportantThing 是否已經被初始化

if (!SomeImportantThing) {

    var SomeImportantThing = {};

}

實際上,上面的代碼正常運行,由於 var 表達式會被提高到全局做用域的頂部。


var SomeImportantThing;


// 其它一些代碼,可能會初始化 SomeImportantThing,也可能不會


// 檢查是否已經被初始化

if (!SomeImportantThing) {

    SomeImportantThing = {};

}

譯者注:在 Nettuts+ 網站有一篇介紹 hoisting 的文章,其中的代碼頗有啓發性。


// 譯者注:來自 Nettuts+ 的一段代碼,生動的闡述了 JavaScript 中變量聲明提高規則

var myvar = 'my value';  


(function() {  

    alert(myvar); // undefined  

    var myvar = 'local value';  

})();  

名稱解析順序


JavaScript 中的全部做用域,包括全局做用域,都有一個特別的名稱 this 指向當前對象。


函數做用域內也有默認的變量 arguments,其中包含了傳遞到函數中的參數。


好比,當訪問函數內的 foo 變量時,JavaScript 會按照下面順序查找:


當前做用域內是否有 var foo 的定義。

函數形式參數是否有使用 foo 名稱的。

函數自身是否叫作 foo。

回溯到上一級做用域,而後從 #1 從新開始。

注意: 自定義 arguments 參數將會阻止原生的 arguments 對象的建立。

命名空間


只有一個全局做用域致使的常見錯誤是命名衝突。在 JavaScript中,這能夠經過 匿名包裝器 輕鬆解決。


(function() {

    // 函數建立一個命名空間


    window.foo = function() {

        // 對外公開的函數,建立了閉包

    };


})(); // 當即執行此匿名函數

匿名函數被認爲是 表達式;所以爲了可調用性,它們首先會被執行。


( // 小括號內的函數首先被執行

function() {}

) // 而且返回函數對象

() // 調用上面的執行結果,也就是函數對象

有一些其餘的調用函數表達式的方法,好比下面的兩種方式語法不一樣,可是效果如出一轍。


// 另外兩種方式

+function(){}();

(function(){}());

結論


推薦使用匿名包裝器(譯者注:也就是自執行的匿名函數)來建立命名空間。這樣不只能夠防止命名衝突, 並且有利於程序的模塊化。


另外,使用全局變量被認爲是很差的習慣。這樣的代碼容易產生錯誤而且維護成本較高。


數組

數組遍歷與屬性


雖然在 JavaScript 中數組是對象,可是沒有好的理由去使用 for in 循環 遍歷數組。 相反,有一些好的理由不去使用 for in 遍歷數組。


注意: JavaScript 中數組不是 關聯數組。 JavaScript 中只有對象 來管理鍵值的對應關係。可是關聯數組是保持順序的,而對象不是。

因爲 for in 循環會枚舉原型鏈上的全部屬性,惟一過濾這些屬性的方式是使用 hasOwnProperty 函數, 所以會比普通的 for 循環慢上好多倍。


遍歷


爲了達到遍歷數組的最佳性能,推薦使用經典的 for 循環。


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 來緩存數組的長度。


雖然 length 是數組的一個屬性,可是在每次循環中訪問它仍是有性能開銷。 可能最新的 JavaScript 引擎在這點上作了優化,可是咱們無法保證本身的代碼是否運行在這些最近的引擎之上。


實際上,不使用緩存數組長度的方式比緩存版本要慢不少。


length 屬性


length 屬性的 getter 方式會簡單的返回數組的長度,而 setter 方式會截斷數組。


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

foo.length = 3;

foo; // [1, 2, 3]


foo.length = 6;

foo; // [1, 2, 3]

譯者注: 在 Firebug 中查看此時 foo 的值是: [1, 2, 3, undefined, undefined, undefined] 可是這個結果並不許確,若是你在 Chrome 的控制檯查看 foo 的結果,你會發現是這樣的: [1, 2, 3] 由於在 JavaScript 中 undefined 是一個變量,注意是變量不是關鍵字,所以上面兩個結果的意義是徹底不相同的。


// 譯者注:爲了驗證,咱們來執行下面代碼,看序號 5 是否存在於 foo 中。

5 in foo; // 無論在 Firebug 或者 Chrome 都返回 false

foo[5] = undefined;

5 in foo; // 無論在 Firebug 或者 Chrome 都返回 true

爲 length 設置一個更小的值會截斷數組,可是增大 length 屬性值不會對數組產生影響。


結論


爲了更好的性能,推薦使用普通的 for 循環並緩存數組的 length 屬性。 使用 for in 遍歷數組被認爲是很差的代碼習慣並傾向於產生錯誤和致使性能問題。


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, 4, 5); // 結果: [3, 4, 5] 

new Array(3) // 結果: [],此數組長度爲 3

譯者注:這裏的模棱兩可指的是數組的兩種構造函數語法

因爲只有一個參數傳遞到構造函數中(譯者注:指的是 new Array(3); 這種調用方式),而且這個參數是數字,構造函數會返回一個 length 屬性被設置爲此參數的空數組。 須要特別注意的是,此時只有 length 屬性被設置,真正的數組並無生成。


譯者注:在 Firebug 中,你會看到 [undefined, undefined, undefined],這實際上是不對的。在上一節有詳細的分析。

var arr = new Array(3);

arr[1]; // undefined

1 in arr; // false, 數組尚未生成

這種優先於設置數組長度屬性的作法只在少數幾種狀況下有用,好比須要循環字符串,能夠避免 for 循環的麻煩。


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

譯者注: new Array(3).join('#') 將會返回 ##

結論


應該儘可能避免使用數組構造函數建立新數組。推薦使用數組的字面語法。它們更加短小和簡潔,所以增長了代碼的可讀性。


類型

相等與比較


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 中的指針比較。


注意:爲了更直觀的看到==和===的區別,能夠參見JavaScript Equality Table

結論


強烈推薦使用嚴格等於操做符。若是類型須要轉換,應該在比較以前顯式的轉換, 而不是使用語言自己複雜的強制轉換規則。


typeof 操做符


typeof 操做符(和 instanceof 一塊兒)或許是 JavaScript 中最大的設計缺陷, 由於幾乎不可能從它們那裏獲得想要的結果。


儘管 instanceof 還有一些極少數的應用場景,typeof 只有一個實際的應用(譯者注:這個實際應用是用來檢測一個對象是否已經定義或者是否已經賦值), 而這個應用卻不是用來檢查對象的類型。


注意: 因爲 typeof 也能夠像函數的語法被調用,好比 typeof(obj),但這並非一個函數調用。 那兩個小括號只是用來計算一個表達式的值,這個返回值會做爲 typeof 操做符的一個操做數。 實際上不存在名爲 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 操做符的運算結果。能夠看到,這個值在大多數狀況下都返回 "object"。


Class 一列表示對象的內部屬性 `Class` 的值。


JavaScript 標準文檔中定義: `Class` 的值只多是下面字符串中的一個: Arguments, Array, Boolean, Date, Error, Function, JSON, Math, Number, Object, RegExp, String.

爲了獲取對象的 `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.toString 方法被調用,this 被設置爲了須要獲取 `Class` 值的對象。


譯者注:Object.prototype.toString 返回一種標準格式字符串,因此上例能夠經過 slice 截取指定位置的字符串,以下所示:


Object.prototype.toString.call([])    // "[object Array]"

Object.prototype.toString.call({})    // "[object Object]"

Object.prototype.toString.call(2)    // "[object Number]"

ES5 提示: 在 ECMAScript 5 中,爲了方便,對 null 和 undefined 調用 Object.prototype.toString 方法, 其返回值由 Object 變成了 Null 和 Undefined。

譯者注:這種變化能夠從 IE8 和 Firefox 4 中看出區別,以下所示:


// IE8

Object.prototype.toString.call(null)    // "[object Object]"

Object.prototype.toString.call(undefined)    // "[object Object]"


// Firefox 4

Object.prototype.toString.call(null)    // "[object Null]"

Object.prototype.toString.call(undefined)    // "[object Undefined]"

測試爲定義變量


typeof foo !== 'undefined'

上面代碼會檢測 foo 是否已經定義;若是沒有定義而直接使用會致使 ReferenceError 的異常。 這是 typeof 惟一有用的地方。


結論


爲了檢測一個對象的類型,強烈推薦使用 Object.prototype.toString 方法; 由於這是惟一一個可依賴的方式。正如上面表格所示,typeof 的一些返回值在標準文檔中並未定義, 所以不一樣的引擎實現可能不一樣。


除非爲了檢測一個變量是否已經定義,咱們應儘可能避免使用 typeof 操做符。


instanceof 操做符


instanceof 操做符用來比較兩個操做數的構造函數。只有在比較自定義的對象時纔有意義。 若是用來比較內置類型,將會和 typeof 操做符 同樣用處不大。


比較自定義對象


function Foo() {}

function Bar() {}

Bar.prototype = new Foo();


new Bar() instanceof Bar; // true

new Bar() instanceof Foo; // true


// 若是僅僅設置 Bar.prototype 爲函數 Foo 自己,而不是 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 用來比較屬於不一樣 JavaScript 上下文的對象(好比,瀏覽器中不一樣的文檔結構)時將會出錯, 由於它們的構造函數不會是同一個對象。


結論


instanceof 操做符應該僅僅用來比較來自同一個 JavaScript 上下文的自定義對象。 正如 typeof 操做符同樣,任何其它的用法都應該是避免的。


類型轉換


JavaScript 是弱類型語言,因此會在任何可能的狀況下應用強制類型轉換。


// 下面的比較結果是:true

new Number(10) == 10; // Number.toString() 返回的字符串被再次轉換爲數字


10 == '10';           // 字符串被轉換爲數字

10 == '+10 ';         // 同上

10 == '010';          // 同上 

isNaN(null) == false; // null 被轉換爲數字 0

                      // 0 固然不是一個 NaN(譯者注:否認之否認)


// 下面的比較結果是:false

10 == 010;

10 == '-10';

ES5 提示: 以 0 開頭的數字字面值會被做爲八進制數字解析。 而在 ECMAScript 5 嚴格模式下,這個特性被移除了。

爲了不上面複雜的強制類型轉換,強烈推薦使用嚴格的等於操做符。 雖然這能夠避免大部分的問題,但 JavaScript 的弱類型系統仍然會致使一些其它問題。


內置類型的構造函數


內置類型(好比 Number 和 String)的構造函數在被調用時,使用或者不使用 new 的結果徹底不一樣。


new Number(10) === 10;     // False, 對象與數字的比較

Number(10) === 10;         // True, 數字與數字的比較

new Number(10) + 0 === 10; // True, 因爲隱式的類型轉換

使用內置類型 Number 做爲構造函數將會建立一個新的 Number 對象, 而在不使用 new 關鍵字的 Number 函數更像是一個數字轉換器。


另外,在比較中引入對象的字面值將會致使更加複雜的強制類型轉換。


最好的選擇是把要比較的值顯式的轉換爲三種可能的類型之一。


轉換爲字符串


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

將一個值加上空字符串能夠輕鬆轉換爲字符串類型。


轉換爲數字


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

使用一元的加號操做符,能夠把字符串轉換爲數字。


譯者注:字符串轉換爲數字的經常使用方法:


+'010' === 10

Number('010') === 10

parseInt('010', 10) === 10  // 用來轉換爲整數


+'010.2' === 10.2

Number('010.2') === 10.2

parseInt('010.2', 10) === 10

轉換爲布爾型


經過使用 否 操做符兩次,能夠把一個值轉換爲布爾型。


!!'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,和下面兩種寫法效果同樣:


// 寫法一:直接調用全局做用域下的 foo 變量

var foo = 1;

function test() {

    var foo = 2;

    window.foo = 3;

    return foo;

}

test(); // 2

foo; // 3


// 寫法二:使用 call 函數修改 eval 執行的上下文爲全局做用域

var foo = 1;

function test() {

    var foo = 2;

    eval.call(window, 'foo = 3');

    return foo;

}

test(); // 2

foo; // 3

在任何狀況下咱們都應該避免使用 eval 函數。99.9% 使用 eval 的場景都有不使用 eval 的解決方案。


假裝的 eval


定時函數 setTimeout 和 setInterval 均可以接受字符串做爲它們的第一個參數。 這個字符串老是在全局做用域中執行,所以 eval 在這種狀況下沒有被直接調用。


安全問題


eval 也存在安全問題,由於它會執行任意傳給它的代碼, 在代碼字符串未知或者是來自一個不信任的源時,絕對不要使用 eval 函數。


結論


絕對不要使用 eval,任何使用它的代碼都會在它的工做方式,性能和安全性方面受到質疑。 若是一些狀況必須使用到 eval 才能正常工做,首先它的設計會受到質疑,這不該該是首選的解決方案, 一個更好的不使用 eval 的解決方案應該獲得充分考慮並優先採用。


undefined 和 null


JavaScript 有兩個表示‘空’的值,其中比較有用的是 undefined。


undefined 的值


undefined 是一個值爲 undefined 的類型。


這個語言也定義了一個全局變量,它的值是 undefined,這個變量也被稱爲 undefined。 可是這個變量不是一個常量,也不是一個關鍵字。這意味着它的值能夠輕易被覆蓋。


ES5 提示: 在 ECMAScript 5 的嚴格模式下,undefined 再也不是 可寫的了。 可是它的名稱仍然能夠被隱藏,好比定義一個函數名爲 undefined。

下面的狀況會返回 undefined 值:


訪問未修改的全局變量 undefined。

因爲沒有定義 return 表達式的函數隱式返回。

return 表達式沒有顯式的返回任何內容。

訪問不存在的屬性。

函數參數沒有被顯式的傳遞值。

任何被設置爲 undefined 值的變量。

處理 undefined 值的改變


因爲全局變量 undefined 只是保存了 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);

這裏惟一的區別是,在壓縮後而且函數內沒有其它須要使用 var 聲明變量的狀況下,這個版本的代碼會多出 4 個字節的代碼。


譯者注:這裏有點繞口,其實很簡單。若是此函數內沒有其它須要聲明的變量,那麼 var 總共 4 個字符(包含一個空白字符) 就是專門爲 undefined 變量準備的,相比上個例子多出了 4 個字節。

null 的用處


JavaScript 中的 undefined 的使用場景相似於其它語言中的 null,實際上 JavaScript 中的 null 是另一種數據類型。


它在 JavaScript 內部有一些使用場景(好比聲明原型鏈的終結 Foo.prototype = null),可是大多數狀況下均可以使用 undefined 來代替。


自動分號插入


儘管 JavaScript 有 C 的代碼風格,可是它不強制要求在代碼中使用分號,實際上能夠省略它們。


JavaScript 不是一個沒有分號的語言,偏偏相反上它須要分號來就解析源代碼。 所以 JavaScript 解析器在遇到因爲缺乏分號致使的解析錯誤時,會自動在源代碼中插入分號。


var foo = function() {

} // 解析錯誤,分號丟失

test()

自動插入分號,解析器從新解析。


var foo = function() {

}; // 沒有錯誤,解析繼續

test()

自動的分號插入被認爲是 JavaScript 語言最大的設計缺陷之一,由於它能改變代碼的行爲。


工做原理


下面的代碼沒有分號,所以解析器須要本身判斷須要在哪些地方插入分號。


(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); //<- 插入分號

注意: JavaScript 不能正確的處理 return 表達式緊跟換行符的狀況, 雖然這不能算是自動分號插入的錯誤,但這確實是一種不但願的反作用。

解析器顯著改變了上面代碼的行爲,在另一些狀況下也會作出錯誤的處理。


前置括號


在前置括號的狀況下,解析器不會自動插入分號。


log('testing!')

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

上面代碼被解析器轉換爲一行。


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

log 函數的執行結果極大可能不是函數;這種狀況下就會出現 TypeError 的錯誤,詳細錯誤信息多是 undefined is not a function。


結論


建議絕對不要省略分號,同時也提倡將花括號和相應的表達式放在一行, 對於只有一行代碼的 if 或者 else 表達式,也不該該省略花括號。 這些良好的編程習慣不只能夠提到代碼的一致性,並且能夠防止解析器改變代碼行爲的錯誤處理。


其它

setTimeout 和 setInterval


因爲 JavaScript 是異步的,可使用 setTimeout 和 setInterval 來計劃執行函數。


注意: 定時處理不是 ECMAScript 的標準,它們在 DOM (文檔對象模型) 被實現。

function foo() {}

var id = setTimeout(foo, 1000); // 返回一個大於零的數字

當 setTimeout 被調用時,它會返回一個 ID 標識而且計劃在未來大約 1000 毫秒後調用 foo 函數。 foo 函數只會被執行一次。


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


做爲第一個參數的函數將會在全局做用域中執行,所以函數內的 this 將會指向這個全局對象。


function Foo() {

    this.value = 42;

    this.method = function() {

        // this 指向全局對象

        console.log(this.value); // 輸出:undefined

    };

    setTimeout(this.method, 500);

}

new Foo();

注意: setTimeout 的第一個參數是函數對象,一個常犯的錯誤是這樣的 setTimeout(foo(), 1000), 這裏回調函數是 foo 的返回值,而不是foo自己。 大部分狀況下,這是一個潛在的錯誤,由於若是函數返回 undefined,setTimeout 也不會報錯。

setInterval 的堆調用


setTimeout 只會執行回調函數一次,不過 setInterval - 正如名字建議的 - 會每隔 X 毫秒執行函數一次。 可是卻不鼓勵使用這個函數。


當回調函數的執行被阻塞時,setInterval 仍然會發布更多的回調指令。在很小的定時間隔狀況下,這會致使回調函數被堆積起來。


function foo(){

    // 阻塞執行 1 秒

}

setInterval(foo, 100);

上面代碼中,foo 會執行一次隨後被阻塞了一秒鐘。


在 foo 被阻塞的時候,setInterval 仍然在組織未來對回調函數的調用。 所以,當第一次 foo 函數調用結束時,已經有 10 次函數調用在等待執行。


處理可能的阻塞調用


最簡單也是最容易控制的方案,是在回調函數內部使用 setTimeout 函數。


function foo(){

    // 阻塞執行 1 秒

    setTimeout(foo, 100);

}

foo();

這樣不只封裝了 setTimeout 回調函數,並且阻止了調用指令的堆積,能夠有更多的控制。 foo 函數如今能夠控制是否繼續執行仍是終止執行。


手工清空定時器


能夠經過將定時時產生的 ID 標識傳遞給 clearTimeout 或者 clearInterval 函數來清除定時, 至於使用哪一個函數取決於調用的時候使用的是 setTimeout 仍是 setInterval。


var id = setTimeout(foo, 1000);

clearTimeout(id);

清除全部定時器


因爲沒有內置的清除全部定時器的方法,能夠採用一種暴力的方式來達到這一目的。


// 清空"全部"的定時器

for(var i = 1; i < 1000; i++) {

    clearTimeout(i);

}

可能還有些定時器不會在上面代碼中被清除(譯者注:若是定時器調用時返回的 ID 值大於 1000), 所以咱們能夠事先保存全部的定時器 ID,而後一把清除。


隱藏使用 eval


setTimeout 和 setInterval 也接受第一個參數爲字符串的狀況。 這個特性絕對不要使用,由於它在內部使用了 eval。


注意: 因爲定時器函數不是 ECMAScript 的標準,如何解析字符串參數在不一樣的 JavaScript 引擎實現中可能不一樣。 事實上,微軟的 JScript 會使用 Function 構造函數來代替 eval 的使用。

function foo() {

    // 將會被調用

}


function bar() {

    function foo() {

        // 不會被調用

    }

    setTimeout('foo()', 1000);

}

bar();

因爲 eval 在這種狀況下不是被直接調用,所以傳遞到 setTimeout 的字符串會自全局做用域中執行; 所以,上面的回調函數使用的不是定義在 bar 做用域中的局部變量 foo。


建議不要在調用定時器函數時,爲了向回調函數傳遞參數而使用字符串的形式。


function foo(a, b, c) {}


// 不要這樣作

setTimeout('foo(1,2, 3)', 1000)


// 可使用匿名函數完成相同功能

setTimeout(function() {

    foo(1, 2, 3);

}, 1000)

注意: 雖然也可使用這樣的語法 setTimeout(foo, 1000, 1, 2, 3), 可是不推薦這麼作,由於在使用對象的屬性方法時可能會出錯。 (譯者注:這裏說的是屬性方法內,this 的指向錯誤)

結論


絕對不要使用字符串做爲 setTimeout 或者 setInterval 的第一個參數, 這麼寫的代碼明顯質量不好。當須要向回調函數傳遞參數時,能夠建立一個匿名函數,在函數內執行真實的回調函數。


另外,應該避免使用 setInterval,由於它的定時執行不會被 JavaScript 阻塞。


原文地址:http://bonsaiden.github.io/JavaScript-Garden/zh/#types.casting

相關文章
相關標籤/搜索