經常使用的JavaScript模式

模式是解決或者避免一些問題的方案。html

在JavaScript中,會用到一些經常使用的編碼模式。下面就列出了一些經常使用的JavaScript編碼模式,有的模式是爲了解決特定的問題,有的則是幫助咱們避免一些JavaScript中容易出現的錯誤。數組

單一var模式

所謂「單一var模式」(Single var pattern)就是指在函數頂部,只使用一個var進行變量聲明的模式。例如:瀏覽器

function func() {
    var a = 1,
        b = 2,
        sum = a + b,
        myObject = {},
        i, 
        j;
        
    // other code
}

使用這個模式的好處:安全

  • 在函數頂部展現了全部函數中使用的局部變量
  • 防止變量提高引發的問題

變量提高

JavaScript容許在函數的任意地方聲明變量,可是效果都等同於在函數頂部進行聲明,這個是所謂的變量提高(Hoisting)。app

看一個例子:dom

var num = 10;

function func() {
    alert(num); // undefined
    var num = 1;
    alert(num); // 1
}

func();

從這個例子能夠看到,第一次alert的值並非10,而是undefined。因此,應該儘可能使用「單一var模式」來避免相似的問題。
關於變量提高的細節,請參考我前面一篇JavaScript的執行上下文模塊化

for-in循環

在JavaScript中,for-in循環主要用來枚舉對象的屬性。函數

可是,因爲JavaScript中原型鏈的存在,通常都會結合hasOwnProperty()來使用for-in循環,從而過濾原型鏈上的非該對象的屬性。工具

var wilber = {
    name: "Wilber",
    age: 28,
    gender: "male"
};

Object.prototype.printPersonalInfo = function() {
    console.log(this.name, "is", this.age, "years old");
};

for(var prop in wilber) {
    if(wilber.hasOwnProperty(prop)) {
        console.log(prop, ":", wilber[prop]);
    }
}

開放的大括號位置

根據開發人員的習慣,開放大括號的位置會有不一樣的選擇,能夠和語句放在同一行,也能夠放在新的一行:性能

var total = 10;

if(tatal > 5) {
    console.log("bigger than 5");
}

if(tatal > 5)
{
    console.log("bigger than 5");
}

兩種形式的代碼都能實現一樣的邏輯,可是,JavaScript容許開發人員省略分號,JavaScript的分號插入機制(semicolon insertion mechanism)會負責加上省略的分號,這時開放大括號的位置不一樣就可能產生不一樣的結果。

看一個例子:

function func() {
    return 
    {
        name: "Wilber"
    };
}

alert(func());
// undefined

之因此獲得的結果是undefined就是由於JavaScript的分號插入機制,在return語句以後自動添加了分號。

調整一下開放的大括號的位置就能夠避免這個問題:

function func() {
    return {
        name: "Wilber"
    };
}

alert(func());
// [object]

因此,關於開放的大括號位置,建議將開放的大括號放置在前面語句的同一行

強制new模式

JavaScript中,經過new關鍵字,能夠用構造函數來建立對象,例如:

function Person(name, city) {
    this.name = name;
    this.city = city;
    
    this.getInfo = function() {
        console.log(this.name, "lives at", this.city);
    }
}

var will = new Person("Will", "Shanghai");

will.getInfo();
// Will lives at Shanghai

可是,若是開發人員忘記了new關鍵字,那麼構造函數中的this將表明全局對象(瀏覽器中就是window對象),全部的屬性將會變成全局對象的屬性。

function Person(name, city) {
    this.name = name;
    this.city = city;
    
    this.getInfo = function() {
        console.log(this.name, "lives at", this.city);
    }
}

var will = Person("Will", "Shanghai");
console.log(will.name);
// Uncaught TypeError: Cannot read property 'name' of undefined
console.log(window.name);
// Will
console.log(window.city);
// Shanghai
window.getInfo();
// Will lives at Shanghai

因此,爲了不這類問題的方式,首先是從代碼規範上下手。建議對於全部的JavaScript構造函數的命名方式都遵循,構造函數使用首字母大寫的命名方式
這樣當咱們看到首字母大寫的函數,就要考慮是否是漏掉了new關鍵字。

自調用構造函數

固然除了規範以外,還能夠經過代碼的方式來避免上面的問題。

具體的作法就是,在構造函數中檢查this是否爲構造函數的一個實例,若是不是,構造函數能夠經過new關鍵字進行自調用。

下面就是使用自調用構造函數對上面的例子進行改進:

function Person(name, city) {
    if(!(this instanceof Person)) {
        return new Person(name, city);
    }
    
    this.name = name;
    this.city = city;
    
    this.getInfo = function() {
        console.log(this.name, "lives at", this.city);
    }
}

var will = Person("Will", "Shanghai");
console.log(will.name);
// Will
console.log(will.city);
// Shanghai
will.getInfo();
// Will lives at Shanghai
window.getInfo();
// Uncaught TypeError: window.getInfo is not a function

結合構造函數的命名約定和自調用的構造函數,這下就不用擔憂漏掉new關鍵字的狀況了。

數組性質檢查

當在JavaScript中判斷一個對象是否是數組的時候,不能直接使用typeof,由於咱們會獲得object

在ECMA5中提出了Array.isArray()這個函數,咱們能夠直接使用來判斷一個對象是否是數組類型。

對於不支持ECMA5的環境,咱們能夠經過下面的方式本身實現Array.isArray()這個函數。

if(typeof Array.isArray === "undefined") {
    Array.isArray = function(arg){
        return Object.prototype.toString.call(arg) === "[object Array]";
    };
}

var arr = [];
console.log(Array.isArray(arr));
// true

當即執行函數

當即執行函數是JavaScript中很是經常使用的一種模式,形式以下:

(function() {
    // other code
}());

經過這個模式能夠提供一個局部的做用域,因此函數代碼都會在局部做用域中執行,不會污染其餘做用域。

如今的不少JavaScript庫都直接使用了這種模式,例如JQuery、underscore等等。

當即執行函數的參數

關於當即執行函數另一點須要注意的地方就是當即執行函數的參數。

咱們能夠像正常的函數調用同樣進行參數傳遞:

(function(name, city) {
    console.log(name, "lives at", city);
}("Wilber", "Shanghai"));
// Wilber lives at Shanghai

在當即執行函數中,是能夠訪問外部做用域的(固然包括全局對象),例如:

var name = "Wilber";
var city = "Shanghai";

(function() {
    console.log(name, "lives at", city);
}());
// Wilber lives at Shanghai

可是,若是當即執行函數須要訪問全局對象,經常使用的模式就是將全局對象以參數的方式傳遞給當即執行函數

var name = "Wilber";
var city = "Shanghai";

(function(global) {
    console.log(global.name, "lives at", global.city);
}(this));
// Wilber lives at Shanghai

這樣作的好處就是,在當即執行函數中訪問全局變量的屬性的時候就不用進行做用域鏈查找了,關於更多JavaScript做用域鏈的內容,能夠參考理解JavaScript的做用域鏈

初始化時分支

初始化時分支(Init-time Branching)是一種經常使用的優化模式,就是說當某個條件在整個程序聲明週期內都不會發生改變的時候,不用每次都對條件進行判斷,僅僅一次判斷就足夠了。

這裏最多見的例子就是對瀏覽器的檢測,在下面的例子中,每次使用utils.addListener1屬性的時候都要進行瀏覽器判斷,效率比較低下:

var utils = {
    addListener: function(el, type, fn) {
        if (typeof window.addEventListener === 'function') {
            el.addEventListener(type, fn, false);
        } else if (typeof document.attachEvent === 'function') { // IE
            el.attachEvent('on' + type, fn);
        } else { // older browsers
            el['on' + type] = fn;
        }
    },
    removeListener: function(el, type, fn) {
        // pretty much the same...
    }
};

因此,根據初始化時分支模式,能夠在腳本初始化的時候進行一次瀏覽器檢測,這樣在之後使用utils的時候就沒必要進行瀏覽器檢測了:

// the interface
var utils = {
    addListener: null,
    removeListener: null
};

// the implementation
if (typeof window.addEventListener === 'function') {
    utils.addListener = function(el, type, fn) {
        el.addEventListener(type, fn, false);
    };
    utils.removeListener = function(el, type, fn) {
        el.removeEventListener(type, fn, false);
    };
} else if (typeof document.attachEvent === 'function') { // IE
    utils.addListener = function(el, type, fn) {
        el.attachEvent('on' + type, fn);
    };
    utils.removeListener = function(el, type, fn) {
        el.detachEvent('on' + type, fn);
    };
} else { // older browsers
    utils.addListener = function(el, type, fn) {
        el['on' + type] = fn;
    };
    utils.removeListener = function(el, type, fn) {
        el['on' + type] = null;
    };
}

命名空間模式

JavaScript代碼中,過多的全局變量常常會引起一些問題,好比命名衝突。

結合命名空間模式就能夠必定程度上減小代碼中全局變量的個數。

下面就看一個通用命名空間函數的實現:

var MYAPP = MYAPP || {};
MYAPP.namespace = function (ns_string) {
    var parts = ns_string.split('.'),
        parent = MYAPP,
        i;
    // strip redundant leading global
    if (parts[0] === "MYAPP") {
        parts = parts.slice(1);
    }
    for (i = 0; i < parts.length; i += 1) {
        // create a property if it doesn't exist
        if (typeof parent[parts[i]] === "undefined") {
            parent[parts[i]] = {};
        }
        parent = parent[parts[i]];
    }
    return parent;
};

結合這個通用命名空間函數的,就能夠實現代碼的模塊化:

// assign returned value to a local var
var module2 = MYAPP.namespace('MYAPP.modules.module2');
module2 === MYAPP.modules.module2; // true

// skip initial `MYAPP`
MYAPP.namespace('modules.module51');

// long namespace
MYAPP.namespace('once.upon.a.time.there.was.this.long.nested.property');

聲明依賴關係

JavaScirpt庫一般是經過命名空間來進行模塊化,當咱們在代碼中使用第三方的庫的時候,能夠只引入咱們代碼依賴的模塊。

所謂聲明依賴關係,就是指在函數或者模塊的頂部是聲明代碼須要依賴哪些模塊,這個聲明包括建立一個局部變量,並將它們指向你須要的模塊:

var myFunction = function () {
    // dependencies
    var event = YAHOO.util.Event,
        dom = YAHOO.util.Dom;
    // use event and dom variables
    // for the rest of the function...
};

經過聲明依賴關係這種模式,會給咱們帶來不少好處:

  • 明確的依賴聲明能夠向你的代碼的使用者代表這些特殊的腳本文件須要被確保包含進頁面
  • 數頭部的聲明解,讓發現和處理依賴關係更加簡單
  • 局部變量(好比:dom)一般比使用全局變量(好比:YAHOO)快,比訪問全局對象的屬性(好比:YAHOO.util.Do)更快,能夠獲得更好的性能,全局符號只會在函數中出現一次,而後就可使用局部變量,後者速度更快。
  • 壓縮工具好比YUICompressor 和 Google Closure compiler會重命名局部變量,產生更小的體積的代碼,但歷來不會重命名全局變量,由於那樣是不安全的

代碼複用模式

下面就看看JavaScript中的代碼複用模式。通常來講,一般使用下面的方式來實現代碼的複用:

  • 繼承
  • 借用方法

繼承

在JavaScript中能夠很方便的經過原型來實現繼承。

關於原型式繼承,ECMA5經過新增Object.create()方法規範化了原型式繼承。這個方法接收兩個參數:

  • 一個用做新對象原型的對象
  • 一個爲新對象定義額外屬性的對象(可選的)

看一個使用Object.create()的例子:

utilsLibC = Object.create(utilsLibA, {
    sub: {
        value: function(){
            console.log("sub method from utilsLibC");
        }
    },
    mult: {
        value: function(){
            console.log("mult method from utilsLibC");
        }
    },
})

utilsLibC.add();
// add method from utilsLibA
utilsLibC.sub();
// sub method from utilsLibC
utilsLibC.mult();
// mult method from utilsLibC
console.log(utilsLibC.__proto__);
// Object {add: (), sub: (), __proto__: Object}
console.log(utilsLibC.__proto__.constructor);
// function Object() { [native code] }

關於JavaScript繼承的更多信息,能夠參考關於JavaScript繼承的那些事

借用方法

有時候可能只須要一個已經存在的對象的一個或兩個方法,可是又不想經過繼承,來創建額外的父子(parent-child)關係。

這時就能夠考慮使用借用方法模式完成一些函數的複用。借用方法模式得益於function的方法call()和apply()。

這種模式一個常見用法就是借用數組方法。
數組擁有有用的方法,那些類數組對象(array-like objects)好比arguments類數組對象(array-like objects)好比arguments沒有的方法。因此arguments能夠借用數組的方法,好比slice()方法,看一個例子:

function f() {
    var args = [].slice.call(arguments, 1, 3);
    return args;
}
// example
f(1, 2, 3, 4, 5, 6); // returns [2,3]

總結

本文主要介紹了JavaScript中經常使用的編碼模式,經過這些模式可使代碼健壯、可讀。

主要參考《JavaScript patterns》。

相關文章
相關標籤/搜索