Javascript高級編程

    原文引用地址:http://bonsaiden.github.io/JavaScript-Garden/zh/javascript

    對象java

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

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

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

    一個常見的誤解時數字的字面值不是對象。這是由於javascript解析器的一個錯誤,它試圖將點操做符解析爲浮點數字面值的一部分。程序員

2.toString();//SyntaxError

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

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

    對象做爲數據類型ajax

    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的關鍵字)

    刪除屬性

    刪除屬性的惟一辦法是使用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解析器的另外一個錯誤設計,上面的第二種聲明方式在ECMAScript5以前會拋出SyntaxError的錯誤。

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

    原型

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

    雖然這常常被看成是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() // 建立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 屬性。

    屬性查找

    當查找一個對象的屬性時,javascript會向上遍歷原型鏈,知道找到給定名稱的屬性爲止。
    到查找到達原型鏈的頂部-也就是Object.prototype - 可是仍然沒有找到指定的屬性,就會返回undefined。

    原型屬性

    當圓形屬性用來建立原型鏈時,能夠把任何類型的值賦給它(prototype)。

    然而將原子類型賦給prototype的操做將會被忽略。

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

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

    性能

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

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

    擴展內置類型的原型

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

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

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

    總結

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

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

    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 循環一樣在查找對象屬性時遍歷原型鏈上的全部屬性。

// 修改 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。若是不使用hasOwnProperty ,則這段代碼在原生對象原型被擴展時可能會出錯。

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

    總結

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

    函數

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

    函數聲明

function foo() {}

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

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

    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]); // 數組將會被擴展,以下所示
foo.call(bar, 1, 2, 3); // 傳遞到foo的參數是:a = 1, b = 2, c = 3

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

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

    常見誤解

    儘管大部分的狀況說的過去,不過第一個規則(這裏指的是應該是第二個規則,也就是直接調用函數時,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。這兩個函數都維持着對外部做用域count的引用,所以總能夠訪問到此做用域內定義的變量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的一個拷貝。

    當傳遞給setTimeount的匿名函數執行時,它就擁有了對e的引用,而這個值是不會被循環改變的。

    有另外一個方法完成一樣的工做;那就是從匿名包裝器中返回一個函數。這和上面的代碼效果同樣。

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

    arguments對象

    JavaScript 中每一個函數內都能訪問一個特別變量 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) {
    // do stuff here
}

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

    上面的 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 對象的屬性進行屢次訪問。
    在 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 和它的屬性。
    構造函數

    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 關鍵字來調用函數,由於若是忘記使用它就會致使錯誤。對象不能訪問 Bar 原型鏈上的屬性,以下所示:
    爲了建立新對象,咱們能夠建立一個工廠方法,而且在方法內構造一個新對象。

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++) { // 不是一個做用域
        // count
    }
    console.log(i); // 10
}

    若是 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 的賦值將會覆蓋全局做用域內的同名變量。

    變量聲明提高

    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 會按照下面順序查找:

  1. 當前做用域內是否有 var foo 的定義。
  2. 函數形式參數是否有使用 foo 名稱的。
  3. 函數自身是否叫作 foo
  4. 回溯到上一級做用域,而後從 #1 從新開始。
    命名空間
    只有一個全局做用域致使的常見錯誤是命名衝突。在 JavaScript中,這能夠經過 匿名包裝器 輕鬆解決。
(function() {
    // 函數建立一個命名空間

    window.foo = function() {
        // 對外公開的函數,建立了閉包
    };

})(); // 當即執行此匿名函數
    匿名函數被認爲是  表達式;所以爲了可調用性,它們首先會被執行。
( // 小括號內的函數首先被執行
function() {}
) // 而且返回函數對象
() // 調用上面的執行結果,也就是函數對象
    有一些其餘的調用函數表達式的方法,好比下面的兩種方式語法不一樣,可是效果如出一轍。
// 另外兩種方式
+function(){}();
(function(){}());
    結論
    推薦使用匿名包裝器( 譯者注:也就是自執行的匿名函數)來建立命名空間。這樣不只能夠防止命名衝突, 並且有利於程序的模塊化。
    另外,使用全局變量被認爲是很差的習慣。這樣的代碼傾向於產生錯誤和帶來高的維護成本。
    數組
    數組遍歷與屬性
    雖然在 JavaScript 中數組是對象,可是沒有好的理由去使用  for in 循環 遍歷數組。 相反,有一些好的理由不去使用  for in 遍歷數組。
    因爲  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 屬性被設置,真正的數組並無生成。
var arr = new Array(3);
arr[1]; // undefined
1 in arr; // false, 數組尚未生成
    這種優先於設置數組長度屬性的作法只在少數幾種狀況下有用,好比須要循環字符串,能夠避免  for 循環的麻煩。
new Array(count + 1).join(stringToRepeat);
    結論
    應該儘可能避免使用數組構造函數建立新數組。推薦使用數組的字面語法。它們更加短小和簡潔,所以增長了代碼的可讀性。
    類型
    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 操做符的運算結果。能夠看到,這個值在大多數狀況下都返回 "object"。
    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.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]"
    這種變化能夠從 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]"
    測試爲定義變量
    上面代碼會檢測  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';
    爲了不上面複雜的強制類型轉換,強烈推薦使用 嚴格的等於操做符。 雖然這能夠避免大部分的問題,但 JavaScript 的弱類型系統仍然會致使一些其它問題。
    內置類型的構造函數
    內置類型(好比  Number 和  String)的構造函數在被調用時,使用或者不使用  new 的結果徹底不一樣。
new Number(10) === 10;     // False, 對象與數字的比較
Number(10) === 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 值:
  • 訪問未修改的全局變量 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 個字節的代碼。
    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); //<- 插入分號
    解析器顯著改變了上面代碼的行爲,在另一些狀況下也會作出錯誤的處理。
    前置括號
    在前置括號的狀況下,解析器不會自動插入分號。
log('testing!')
(options.list || []).forEach(function(i) {})
    上面代碼被解析器轉換爲一行。 
log('testing!')(options.list || []).forEach(function(i) {})
    結論
    建議絕對不要省略分號,同時也提倡將花括號和相應的表達式放在一行, 對於只有一行代碼的  if 或者 else 表達式,也不該該省略花括號。 這些良好的編程習慣不只能夠提到代碼的一致性,並且能夠防止解析器改變代碼行爲的錯誤處理。
    其餘
    setTimeout和setInterval
    因爲 JavaScript 是異步的,可使用  setTimeout 和  setInterval 來計劃執行函數。
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();
    setInterval的堆調用
     setTimeout 只會執行回調函數一次,不過  setInterval - 正如名字建議的 - 會每隔 X 毫秒執行函數一次。 可是卻不鼓勵使用這個函數。
    當回調函數的執行被阻塞時, setInterval 仍然會發布更多的回調指令。在很小的定時間隔狀況下,這會致使回調函數被堆積起來。
function foo(){
    // 阻塞執行 1 秒
}
setInterval(foo, 1000);
    上面代碼中, foo 會執行一次隨後被阻塞了一分鐘。
    在  foo 被阻塞的時候, setInterval 仍然在組織未來對回調函數的調用。 所以,當第一次  foo 函數調用結束時,已經有 10 次函數調用在等待執行。
    處理可能的阻塞調用
    最簡單也是最容易控制的方案,是在回調函數內部使用  setTimeout 函數。  
function foo(){
    // 阻塞執行 1 秒
    setTimeout(foo, 1000);
}
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
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(a, b, c);
}, 1000)
    結論
    絕對不要使用字符串做爲  setTimeout 或者  setInterval 的第一個參數, 這麼寫的代碼明顯質量不好。當須要向回調函數傳遞參數時,能夠建立一個匿名函數,在函數內執行真實的回調函數。
    另外,應該避免使用  setInterval,由於它的定時執行不會被 JavaScript 阻塞。
相關文章
相關標籤/搜索