JavaScript的小祕密

對象


JavaScript 中所喲變量均可以看成對象使用,除了兩個例外 null 和 undefined。程序員

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

funciton Fun(){}
Fun.num = 1;
Fun.num; // 1
複製代碼

一個常見的誤解是數字的字面值不能看成對象使用。這是由於JavaScript解析器的一個錯誤,他試圖將點操做符解析爲浮點數字字面值的一部分。數組

7.toString(); // SyntaxError
複製代碼

有一些變通的方法可讓數字的字面值看起來像對象。瀏覽器

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

對象做爲數據類型

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

使用對象的字面語法 {} 能夠建立一個簡單對象。這個新建立的對象從 Object.prototype 繼承下面,沒有任何自定義屬性。bash

var obj = {} // 一個空對象

// 一個對象,擁有12的自定義屬性'test'
var obj2 = {
    test: 12
}
複製代碼

訪問屬性

有兩種方式來訪問對象的屬性,點操做符或者中括號操做符。閉包

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

var name = 'name'
obj[name]; // kitten
複製代碼

兩種語法是等價的,可是中括號操做符在下面兩種狀況下依然有效app

  • 動態設置屬性
  • 屬性名不是一個有效的變量名(好比:屬性名中包含空格,或者屬性名是 JS 的關鍵詞)

刪除屬性

刪除屬性的惟一方法是使用 delete 操做符;設置屬性爲 undefined 或者 null 並不能真正的刪除屬性, 而僅僅是移除了屬性和值的關聯。異步

var obj = {
    a: 1,
    b: 2,
    c: 3
};
obj.a = undefined;
obj.b = null;
delete obj.c;
for (var i in obj) {
    if (obj.hasOwnProperty(i)) {
        console.log(i, '' + obj[i]);
    }
}
複製代碼

上面的輸出結果有 a undefined 和 b null ,只有 c 被真正的刪除了,因此從輸出結果中消失。模塊化

屬性名的語法

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 使用 原型鏈 的繼承方式。

function Fun() {
    this.value  = 97;
}
Fun.prorotype = {
    method: function() {}
};
function Demo() {}

// 設置Demo的prototype屬性爲Fun的示例對象
Demo.prototype = new Fun();
Demo.prototype.fun = 'Hello啊';

// 修正Demo.prototype.constructor 爲 Demo 自己
Demo.prototype.constructor = Demo;
var test = new Demo(); // 建立一個Demo的一個新實例

// 原型鏈
test [Demo的示例]
    Demo.prototype [Fun的實例]
        { fun: 'Hello啊' }
        Foo.prototype
            { method: ...}
                Object.prototype
                    {toString:... /* etc. */}
複製代碼

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

屬性查找

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

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

原型屬性

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

function Fun() {}
Fun.prototype = 1; // 無效
複製代碼

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

性能

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

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

擴展內置類型的原型

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

這樣會破壞封裝。雖然它被普遍的應用到一些 JavaScript 類庫中好比P rototype ,可是我不認爲爲內置類型添加一些非標準的函數是個好主意。

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

總結

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

hasOwnProperty 函數

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

hasOwnProperty 是 JavaScript 中惟一一個處理屬性可是不查找原型鏈的函數。

// 修改Object.prototype
Object.prototype.value = 1
var obj = {
    s: undefined
};
obj.value; // 1
's' in obj; // true

obj.hasOwnProperty('s'); // true
obj.hasOwnProperty('value'); // false
複製代碼

只有 hasOwnProperty 能夠給出正確和指望的結果,這在遍歷對象的屬性時會頗有用。 沒有其它方法能夠用來排除原型鏈上的屬性,而不是定義在對象自身上的屬性。

hasOwnProperty 做爲屬性

JavaScript 不會保護 hasOwnProperty 被非法佔用,所以若是一個對象碰巧存在這個屬性, 就須要使用外部的 hasOwnProperty 函數來獲取正確的結果。

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

obj.hasOwnProperty('str'); // 老是返回 false

// 使用其它對象的 hasOwnProperty,並將其上下文設置爲obj
({}).hasOwnProperty.call(obj, 'str'); // true
複製代碼

結論

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


for in 循環

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

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

var obj = {num: 2};
for(var i in obj) {
    console.log(i); // 輸出兩個屬性:value 和 num
}
複製代碼

因爲不可能改變 for in 自身的行爲,所以有必要過濾出那些不但願出如今循環體中的屬性, 這能夠經過 Object.prototype 原型上的 hasOwnProperty 函數來完成。

使用 hasOwnProperty 過濾

// obj 變量是上例中的
for(var i in obj) {
    if (foo.hasOwnProperty(i)) {
        console.log(i);
    }
}
複製代碼

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

總結

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


函數f(x)


函數聲明與表達式

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

函數聲明

function fun() {}
複製代碼

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

fun(); // 正常運行,由於fun在代碼運行前已經被建立
function fun() {}
複製代碼

函數賦值表達式

var fun = function() {};
複製代碼

這個例子把一個匿名的函數賦值給變量 fun。

fun; // 'undefined'
fun(); // 出錯:TypeError
var fun = function() {};
複製代碼

因爲 var 定義了一個聲明語句,對變量 fun 的解析是在代碼運行以前,所以 fun 變量在代碼運行時已經被定義過了。

可是因爲賦值語句只在運行時執行,所以在相應代碼執行以前, fun 的值缺省爲 undefined。

命名函數的賦值表達式

另一個特殊的狀況是將命名函數賦值給一個變量。

var demo = function fun() {
    fun(); // 正常運行
}
fun(); // 出錯:ReferenceError
複製代碼

fun 函數聲明外是不可見的,這是由於咱們已經把函數賦值給了 demo; 然而在 fun 內部依然可見。這是因爲 JavaScript 的 命名處理所致, 函數名在函數內老是可見的。

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


this 的工做原理

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

全局範圍內

this;
複製代碼

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

函數調用

fun();
複製代碼

這裏 this 也會指向全局對象。(瀏覽器中運行的 JavaScript 腳本,這個全局對象是 window。)

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

方法調用

test.foo();
複製代碼

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

調用構造函數

new foo(); 
複製代碼

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

顯式的設置 this

function fun(a, b, c) {}

var obj = {};
fun.apply(obj, [1, 2, 3]); // 數組將會被擴展,以下所示
fun.call(obj, 1, 2, 3); // 傳遞到fun的參數是:a = 1, b = 2, c = 3
複製代碼

常見誤解

儘管大部分的狀況都說的過去,可是直接調用函數時,this 指向全局對象。被認爲是JavaScript語言另外一個錯誤設計的地方,由於它歷來就沒有實際的用途。

Fun.method = function() {
    function test() {
        // this 將會被設置爲全局對象(瀏覽器環境中也就是 window 對象)
    }
    test();
}
複製代碼

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

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

Fun.method = function() {
    var _this = this;
    function test() {
        // 使用 _this 來指向 Fun 對象
    }
    test();
}
複製代碼

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

方法的賦值表達式

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

var test = someObject.methodTest;
test();
複製代碼

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

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

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

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

new Test().method();
複製代碼

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


閉包和引用

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

模擬私有變量

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

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

var obj = Counter(4);
obj.increment();
obj.get(); // 5
複製代碼

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

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

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

var obj = new Counter(4);
obj.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)
}
複製代碼

固然,使用ES6中的let關鍵字是十分方便的,由於 let 關鍵字所在的代碼塊內有效,並且有暫時性死區的約束。

for(let i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);  
    }, 1000);
}
複製代碼

arguments 對象

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

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

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

轉化爲數組

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

Array.prototype.slice.call(arguments);
複製代碼

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

使用ES6提供的Array.from()將arguments 變量這種僞數組轉化爲數組也是十分方便的。

Array.from(arguments);
複製代碼

傳遞參數

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

function fun() {
    demo.apply(null, arguments);
}
function demo(a, b, c) {
    // 幹活
}
複製代碼

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

function Fun() {}

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

// 建立一個解綁定的 "method"
// 輸入參數爲: this, arg1, arg2...argN
Fun.method = function() {

    // 結果: Fun.prototype.method.call(this, arg1, arg2... argN)
    Function.call.apply(Fun.prototype.method, arguments);
};
複製代碼

上面的 Fun.method 函數和下面代碼的效果是同樣的:

Fun.method = function() {
    var args = Array.prototype.slice.call(arguments);
    Fun.prototype.method.apply(args[0], args.slice(1));
};
複製代碼

自動更新

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

所以,改變形參的值會影響到 arguments 對象的值,反之亦然。

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

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

    var d = c;
    d = 9;
    c; // 3
}
fun(1, 2, 3);
複製代碼

性能真相

無論它是否有被使用,arguments 對象總會被建立,除了兩個特殊狀況 - 做爲局部變量聲明和做爲形式參數。

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

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

// 闡述在 ES5 的嚴格模式下 `arguments` 的特性
function f(a) {
  "use strict";
  a = 42;
  return [a, arguments[0]];
}
var pair = f(17);
console.assert(pair[0] === 42);
console.assert(pair[1] === 17);
複製代碼

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

function fun() {
    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++) {
        fun(); // Would normally be inlined...
    }
}
複製代碼

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

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


構造函數

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

在構造函數內部 - 也就是被調用的函數內 - this 指向新建立的對象 Object。 這個 新建立 的對象的 prototype 被指向到構造函數的 prototype。

若是被調用的函數沒有顯式的 return 表達式,則隱式的會返回 this 對象 - 也就是新建立的對象。

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

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

var test = new Fun();
複製代碼

上面代碼把 Fun 做爲構造函數調用,並設置新建立對象的 prototype 爲 Fun.prototype。

顯式的 return 表達式將會影響返回結果,但僅限於返回的是一個對象。

function Demo() {
    return 2;
}
new Demo(); // 返回新建立的對象

function Test() {
    this.value = 2;

    return {
        foo: 1
    };
}
new Test(); // 返回的對象
複製代碼

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

function Demo() {
    return new Number(2);
}
new Demo().constructor === Number
複製代碼

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

(new Test()).value === undefined
(new Test()).foo === 1
複製代碼

若是 new 被遺漏了,則函數不會返回新建立的對象。

function Fun() {
    this.bla = 1; // 獲取設置全局參數
}
Fun(); // undefined
複製代碼

雖然上例在有些狀況下也能正常運行,可是因爲 JavaScript 中 this 的工做原理, 這裏的 this 指向***全局對象***。


做用域與命名空間

儘管 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);
複製代碼

變量聲明提高(Hoisting)

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

Demo();
var Demo = 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 = {};
}
複製代碼

名稱解析順序

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

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

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

  1. 當前做用域內是否有 var obj 的定義。
  2. 函數形式參數是否有使用 obj 名稱的。
  3. 函數自身是否叫作 obj。
  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 arr = [1, 2, 3, 4, 5, 6];
arr.length = 3;
arr; // [1, 2, 3]

arr.length = 6;
arr; // [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);
複製代碼

結論

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

相關文章
相關標籤/搜索