《JavaScript 模式》讀書筆記

簡介

在軟件開發過程當中,模式是指一個通用問題的解決方案。一個模式不只僅是一個能夠用來複制粘貼的代碼解決方案,更多地是提供了一個更好的實踐經驗、有用的抽象化表示和解決一類問題的模板。html

對象有兩大類:git

  • 本地對象(Native):由ECMAScript標準定義的對象github

  • 宿主對象(Host):由宿主環境建立的對象(好比瀏覽器環境)web

本地對象也能夠被歸類爲內置對象(好比Array、Date)或自定義對象(var o = {})。
宿主對象包含window和全部DOM對象。若是你想知道你是否在使用宿主對象,將你的代碼遷移到一個非瀏覽器環境中運行一下,若是正常工做,那麼你的代碼就只用到了本地對象。 數組

「GoF」的書中提到一條通用規則,「組合優於繼承」。瀏覽器

console 對象緩存

基本技巧

易維護的代碼具備以下特性:安全

  • 可讀的閉包

  • 風格一致app

  • 可預測的

  • 看起來像一我的寫的

  • 有文檔

儘可能少用全局變量

全局變量的問題

隱式建立所有變量有兩種狀況:

  • 未經聲明的變量就爲全局對象全部。

    function sum(x, y){
           result = x + y; // result 是全局變量;
       }
  • 帶有 var 聲明的鏈式賦值

    function foo(){
           var a = b = 0; // b 是全局變量
       }

    因爲 = 的運算順序是從右到左。即 var a = b = 0; 等價於 var a = (b = 0)
    所以,對於鏈式賦值建議作法以下:

    function foo(){
           var a, b;
           a = b = 0;
       }

隱式建立的全局變量與明肯定義的全局變量的不一樣之處在於:是否能被 delete 操做符刪除。

  • 使用 var 建立的全局變量(在函數外建立)不能刪除

  • 不使用 var 建立的隱式全局變量(就算在函數內建立)能夠刪除

var a = 1;
b = 2;
(function(){
    c = 3;
})();

delete a; // false;
delete b; // true;
delete c; // true

typeof a; // number
typeof b; // undefined
typeof c; // undefined

在ES5 strict模式下,爲未聲明的變量賦值會拋出錯誤。

單一var模式

在函數頂部對全部變量經過一個 var 進行聲明。好處以下:

  • 能夠在同一個位置找到函數所需的全部變量

  • 避免在變量聲明以前使用這個變量時產生的邏輯錯誤

  • 提醒你不要忘記聲明變量,順便減小潛在的全局變量

  • 代碼量更少

例子:

function func(){
    var a = 1,
        b = 2,
        sum = a + b,
        myobject = {},
        i,
        j;
    console.log(sum)
    // 函數體
}
func()

使用逗號操做符能夠在一條語句中執行多個操做。多用於聲明多個變量,但還能夠用於賦值,總會返回表達式的最後一項。

(1) 逗號表達式的運算過程爲:從左往右逐個計算表達式。
(2) 逗號表達式做爲一個總體,它的值爲最後一個表達式的值。var num = (5,4,1,0); // num 爲 0
(3) 逗號運算符的優先級別在全部運算符中最低。

避免使用隱式類型轉換

使用 ===!== 進行比較。加強代碼可閱讀性,避免猜想。
另外,switch 語句的 case 進行比較時,使用的是 ===

避免使用 eval()

new Functin() 與 eval()的不一樣:

第一點:
new Function()中的代碼將在局部函數空間運行,所以代碼中任何採用var定義的變量不會自動成爲全局變量(即在函數內)。
eval()則會自動成爲全局變量,但可經過當即調用函數對其進行封裝。

console.log(typeof un);// "undefined"
console.log(typeof deux); // "undefined"
console.log(typeof trois); // "undefined"

var jsstring = "var un = 1; console.log(un);";
eval(jsstring); // 打印出 "1"

jsstring = "var deux = 2; console.log(deux);";
new Function(jsstring)(); // 打印出 "2"

jsstring = "var trois = 3; console.log(trois);";
(function () {
    eval(jsstring);
}()); // 打印出 "3"

console.log(typeof un); // "number"
console.log(typeof deux); // "undefined"
console.log(typeof trois); // "undefined"

第二點:
eval()會影響到做用域鏈,而Function則像一個沙盒,不管在哪裏執行Function,它都僅能看到全局做用域鏈。所以對局部變量的影響比較小。

(function () {
    var local = 1;
    eval("local = 3; console.log(local)"); // 打印出 3
    console.log(local); // 打印出 3
}());

(function () {
    var local = 1;
    Function("console.log(typeof local);")(); // 打印出 undefined
}());

使用parseInt()進行數字轉換

ECMAScript3中以0爲前綴的字符串會被看成八進制數處理,這一點在ES5中已經有了改變。爲了不轉換類型不一致而致使的意外結果,應當老是指定第二個參數:

var month = "06",
year = "09";
month = parseInt(month, 10);
year = parseInt(year, 10);

字符串轉換爲數字還有兩種方法:

+"08" // 結果爲8,隱式調用Number()
Number("08") // 結果爲8

這兩種方法要比parseInt()更快一些,由於顧名思義parseInt()是一種「解析」而不是簡單的「轉換」。但當你指望將「08 hello」這類字符串轉換爲數字,則必須使用parseInt(),其餘方法都會返回NaN。

命名約定

  • 構造函數首字母大寫

  • 函數用小駝峯式(getFirstName),變量用「全部單詞小寫,並用下劃線分隔各個單詞」(first_name)。這樣就能區分函數和變量了。

  • 常量和全局變量的全部字符大寫

  • 私有成員函數用下劃線(_)前綴命名

固然,還要正確編寫註釋和更新註釋。最好能編寫 API 文檔。

字面量與構造函數

// 一種方法,使用字面量
var car = {goes: "far"};

// 另外一種方法,使用內置構造函數
// 注意:這是一種反模式
var car = new Object();
car.goes = "far";

字面量寫法的一個明顯優點是,它的代碼更少。「建立對象的最佳模式是使用字面量」還有一個緣由,它能夠強調對象就是一個簡單的可變的散列表,而沒必要必定派生自某個類。
另一個使用字面量而不是Object()構造函數建立實例對象的緣由是,對象字面量不須要「做用域解析」(scope resolution)。由於可能存在一個同名的構造函數Object(),當你調用Object()的時候,解析器須要順着做用域鏈從當前做用域開始查找,直到找到全局Object()構造函數爲止。

Object()構造函數僅接受一個參數,且依賴於傳遞的值,該Object()根據值委派另外一個內置構造函數來建立對象,並返回另一個對象實例,而這每每不是你想要的。

// 空對象
var o = new Object();
console.log(o.constructor === Object); // true

// 數值對象
var o = new Object(1);
console.log(o.constructor === Number); // true
console.log(o.toFixed(2)); // "1.00"

// 字符串對象
var o = new Object("I am a string");
console.log(o.constructor === String); // true
// 普通對象沒有substring()方法,但字符串對象有
console.log(typeof o.substring); // "function"

// 布爾值對象
var o = new Object(true);
console.log(o.constructor === Boolean); // true

強制使用new的模式

對於構造函數,若忘記使用 new 操做符,會致使構造函數中的this指向全局對象(嚴格模式下,指向undeinfed)。

爲了防止忘記 new,咱們使用下面的方法:在構造函數中首先檢查this是不是構造函數的實例,若是不是,則經過new再次調用本身

function Waffle() {

// Waffle 可換成 arguments.callee(指向當前執行的函數)
if (!(this instanceof Waffle)) { 
    return new Waffle();
}
this.tastes = "yummy";

}
Waffle.prototype.wantAnother = true;

// 測試
var first = new Waffle(),
    second = Waffle();

console.log(first.tastes); // "yummy"
console.log(second.tastes); // "yummy"

console.log(first.wantAnother); // true
console.log(second.wantAnother); // true

函數

重定義函數

函數能夠被動態定義,也能夠被賦值給變量。若是將你定義的函數賦值給已經存在的函數變量的話,則新函數會覆蓋舊函數。這樣作的結果是,舊函數的引用被丟棄掉,變量中所存儲的引用值替換成了新的函數。這樣看起來這個變量指代的函數邏輯就發生了變化,或者說函數進行了「從新定義」或「重寫」。

var scareMe = function () {
    alert("Boo!");
    scareMe = function () {
        alert("Double boo!");
    };
};
// 使用重定義函數
scareMe(); // Boo!
scareMe(); // Double boo!

當函數中包含一些初始化操做,並但願這些初始化操做只執行一次,那麼這種模式是很是合適的,由於咱們要避免重複執行不須要的代碼。在這個場景中,函數執行一次後就被重寫爲另一個函數了。

使用這種模式能夠幫助提升應用的執行效率,由於從新定義的函數執行的代碼量更少。

這種模式的另一個名字是「函數的懶惰定義」,由於直到函數執行一次後才從新定義,能夠說它是「某個時間點以後才存在」,簡稱「懶惰定義」。

這種模式有一個明顯的缺陷,就是以前給原函數添加的功能在重定義以後都丟失了。同時,若是這個函數被重定義爲不一樣的名字,被賦值給不一樣的變量,或者是做爲對象的方法使用,那麼重定義的部分並不會生效,原來的函數依然會被執行。

條件初始化

條件初始化(也叫條件加載)是一種優化模式。當你知道某種條件在整個程序生命週期中都不會變化的時候,那麼對這個條件的探測只作一次就頗有意義。瀏覽器探測(或者特徵檢測)是一個典型的例子。

// 接口
var utils = {
    addListener: null,
    removeListener: null
};

// 實現
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;
    };
}

固然,重定義函數也能實現這種效果。

函數屬性——記憶模式(Memoization)

任什麼時候候均可以給函數添加自定義屬性。添加自定義屬性的一個有用場景是緩存函數的執行結果(返回值),這樣下次一樣的函數被調用的時候就不須要再作一次那些可能很複雜的計算。緩存一個函數的運行結果也就是爲你們所熟知的記憶模式。

var myFunc = function (param) {
    if (!myFunc.cache[param]) {
        var result = {};
        // ……複雜的計算……
        myFunc.cache[param] = result;
    }
    return myFunc.cache[param];
};

// 緩存
myFunc.cache = {};

上面的代碼假設函數只接受一個參數param,而且這個參數是原始類型(好比字符串)。若是你有更多更復雜的參數,則一般須要對它們進行序列化。好比,你須要將arguments對象序列化爲JSON字符串,而後使用JSON字符串做爲cache對象的key:

var myFunc = function () {

    var cachekey = JSON.stringify(Array.prototype.slice.call(arguments)),
        result;

    if (!myFunc.cache[cachekey]) {
        result = {};
        // ……複雜的計算……
        myFunc.cache[cachekey] = result;
    }
    return myFunc.cache[cachekey];
};

// 緩存
myFunc.cache = {};

前面代碼中的函數名還可使用arguments.callee來替代,這樣就不用將函數名硬編碼。不過儘管現階段這個辦法可行,可是仍然須要注意,arguments.callee在ECMAScript5的嚴格模式中是不被容許的。

柯里化

是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數並且返回結果的新函數的技術。
下面是通用的柯里化函數:

function cury(fn){
    var slice = Array.prototype.slice;
    var stored_args = slice.call(arguments, 1);

    return function(){
        var new_args = slice.call(arguments),
            args = stored_args.concat(new_args);
        return fn.apply(null, args);
    }
}

// 測試
function sum(){
    var result = 0;
    for(var i = 0, len = arguments.length; i < len; i++){
        result += arguments[i];
    }
    return result;
}

var newSum = cury(sum, 1,2,3,4,5,6);
console.log(newSum(2,3,5,4)); // 35

何時使用柯里化

當你發現本身在調用一樣的函數而且傳入的參數大部分都相同的時候,就是考慮柯里化的理想場景了。你能夠經過傳入一部分的參數動態地建立一個新的函數。這個新函數會存儲那些重複的參數(因此你不須要再每次都傳入),而後再在調用原始函數的時候將整個參數列表補全。

apply()接受兩個參數:第一個是在函數內部綁定到this上的對象,第二個是一個參數數組,參數數組會在函數內部變成一個相似數組的arguments對象。若是第一個參數爲 null,那麼this將指向全局對象,這正是當你調用一個函數(且這個函數不是某個對象的方法)時發生的事情。

小結

在介紹完背景和函數的語法後,介紹了一些有用的模式,按分類列出:

  1. API模式,它們幫助咱們爲函數給出更乾淨的接口,包括:

    • 回調模式

      傳入一個函數做爲參數
    • 配置對象

      幫助保持函數的參數數量可控
    • 返回函數

      函數的返回值是另外一個函數
    • 柯里化

      新函數在已有函數的基礎上再加上一部分參數構成
  2. 初始化模式,這些模式幫助咱們用一種乾淨的、結構化的方法來作一些初始化工做(在web頁面和應用中很是常見),經過一些臨時變量來保證不污染全局命名空間。這些模式包括:

    • 即時函數

      當它們被定義後當即執行
    • 對象即時初始化

      初始化工做被放入一個匿名對象,這個對象提供一個能夠當即被執行的方法
    • 條件初始化

      使分支代碼只在初始化的時候執行一次,而不是在整個程序生命週期中反覆執行
  3. 性能模式,這些模式幫助提升代碼的執行速度,包括:

    • 記憶模式

      利用函數的屬性,使已經計算過的值不用再次計算
    • 自定義函數

      重寫自身的函數體,使第二次及後續的調用作更少的工做

對象建立模式

JavaScript語言自己很簡單、直觀,也沒有其餘語言的一些語言特性:命名空間、模塊、包、私有屬性以及靜態成員。本章將介紹一些經常使用的模式,以此實現這些語言特性。

咱們將對命名空間、依賴聲明、模塊模式以及沙箱模式進行初探——它們能夠幫助咱們更好地組織應用程序的代碼,有效地減小全局污染的問題。除此以外,還會討論私有和特權成員、靜態和私有靜態成員、對象常量、鏈式調用以及一種像類式語言同樣定義構造函數的方法等話題。

命名空間模式

使用命名空間能夠減小全局變量的數量,與此同時,還能有效地避免命名衝突和前綴的濫用。

本章後續要介紹的沙箱模式則能夠避免這些缺點。

// 重構前:5個全局變量
// 注意:反模式
// 構造函數
function Parent() {} 
function Child() {}
// 一個變量
var some_var = 1;

// 一些對象
var module1 = {}; 
module1.data = {a: 1, b: 2}; 
var module2 = {};

能夠經過建立一個全局對象(一般表明應用名)好比MYAPP來重構上述這類代碼,而後將上述例子中的函數和變量都變爲該全局對象的屬性:

// 重構後:一個全局變量
// 全局對象
var MYAPP = {};

// 構造函數
MYAPP.Parent = function () {}; 
MYAPP.Child = function () {};

// 一個變量
MYAPP.some_var = 1;

// 一個對象容器
MYAPP.modules = {};

// 嵌套的對象
MYAPP.modules.module1 = {}; 
MYAPP.modules.module1.data = {a: 1, b: 2}; 
MYAPP.modules.module2 = {};

這種模式在大多數狀況下很是適用,但也有它的缺點:

  • 代碼量稍有增長;在每一個函數和變量前加上這個命名空間對象的前綴,會增長代碼量,增大文件大小

  • 該全局實例能夠被隨時修改

  • 命名的深度嵌套會減慢屬性值的查詢

本章後續要介紹的沙箱模式則能夠避免這些缺點。

通用命名空間函數

隨着程序複雜度的提升,代碼會被分拆在不一樣的文件中以按照頁面須要來加載,這樣一來,就不能保證你的代碼必定是第一個定義命名空間或者某個屬性的,甚至會發生屬性覆蓋的問題。因此,在建立命名空間或者添加屬性的時候,最好先檢查下是否存在,以下所示:

// 不安全的作法
var MYAPP = {};
// 更好的作法
if (typeof MYAPP === "undefined") {
    var MYAPP = {}; 
}
// 簡寫
var MYAPP = MYAPP || {};

如上所示,若是每次作相似操做都要這樣檢查一下就會有不少重複的代碼。例如,要聲明MYAPP.modules.module2,就要重複三次這樣的檢查。因此,咱們須要一個可複用的namespace()函數來專門處理這些檢查工做,而後用它來建立命名空間,以下所示:

// 使用命名空間函數
MYAPP.namespace('MYAPP.modules.module2');

// 等價於:
// var MYAPP = {
//  modules: {
//      module2: {}
//  }
// };

下面是上述namespace函數的實現示例。這種實現是非破壞性的,意味着若是要建立的命名空間已經存在,則不會再重複建立:

var MYAPP = MYAPP || {};

MYAPP.namespace = function(ns_string){
    var parts = ns_string.split("."),
        parent = MYAPP;

    if(parts[0] === "MYAPP"){
        parts.shift();
    }

    for(var i = 0, len = parts.length; i < len; i++){
        if(parent[parts[i]] === undefined){
            parent[parts[i]] = {}
        }
        parent = parent[parts[i]]
    }
    return parent;
}
var module2 = MYAPP.namespace("MYAPP.modules.module2");
console.log(module2 === MYAPP.modules.module2); // true

var modules = MYAPP.namespace("modules");
console.log(modules === MYAPP.modules); // true

依賴聲明

JavaScript庫每每是模塊化並且有用到命名空間的,這使得你能夠只使用你須要的模塊。好比在YUI2中,全局變量YAHOO就是一個命名空間,各個模塊都是全局變量的屬性,好比YAHOO.util.Dom(DOM模塊)、YAHOO.util.Event(事件模塊)。

將你的代碼依賴在函數或者模塊的頂部進行聲明是一個好主意。聲明就是建立一個本地變量,指向你須要用到的模塊:

var myFunction = function () {
    // 依賴
    var event = YAHOO.util.Event,
        dom = YAHOO.util.Dom;

    // 在函數後面的代碼中使用event和dom……
};

這是一個至關簡單的模式,可是有不少的好處:

  • 明確的依賴聲明是告知使用你代碼的開發者,須要保證指定的腳本文件被包含在頁面中。

  • 將聲明放在函數頂部使得依賴很容易被查找和解析。

  • 本地變量(如dom)永遠會比全局變量(如YAHOO)要快,甚至比全局變量的屬性(如YAHOO.util.Dom)還要快,這樣會有更好的性能。使用了依賴聲明模式以後,全局變量的解析在函數中只會進行一次,在此以後將會使用更快的本地變量(備註:本地變量直接指向最後一級對象,event)。

  • 一些高級的代碼壓縮工具好比YUI Compressor和Google Closure compiler會重命名本地變量(好比event可能會被壓縮成一個字母,如A),這會使代碼更精簡,但這個操做不會對全局變量進行,由於這樣作不安全。

私有屬性和方法

私有成員

經過閉包實現:

function Gadget() {
    // 私有成員
    var name = 'iPod';
    // 公有函數
    this.getName = function () {
        return name;
    };
}
var toy = new Gadget();

// name是是私有的
console.log(toy.name); // undefined
// 公有方法能夠訪問到name
console.log(toy.getName()); // "iPod"

特權方法

特權方法的概念不涉及到任何語法,它只是一個給能夠訪問到私有成員的公有方法的名字(就好像它們有更多權限同樣)。
在前面的例子中,getName()就是一個特權方法,由於它有訪問name屬性的特殊權限。

私有成員失效

當你直接經過特權方法返回一個私有變量,而這個私有變量剛好是一個對象或者數組時,外部的代碼能夠修改這個私有變量,由於它是按引用傳遞的。

function Gadget() {
    // 私有成員
    var specs = {
        screen_width: 320,
        screen_height: 480,
        color: "white"
    };

    // 公有函數
    this.getSpecs = function () {
        return specs; // 直接返回對象(數組也是對象),會致使私有對象能在外面被修改
    };
}

解決方法:返回精簡後新對象(返回須要用到的部分屬性),或對私有對象進行復制(返回副本)。

衆所周知的「最低受權原則」(Principle of Least Authority,簡稱POLA),指永遠不要給出比真實須要更多的東西。

原型和私有成員

使用構造函數建立私有成員的一個弊端是,每一次調用構造函數建立對象時這些私有成員都會被建立一次。

function Gadget() {
    // 私有成員
    var name = 'iPod';
    // 公有函數
    this.getName = function () {
        return name;
    };
}

Gadget.prototype = (function () {
    // 私有成員
    var browser = "Mobile Webkit";
    // 公有函數
    return {
        getBrowser: function () {
            return browser;
        }
    };
}());

var toy = new Gadget();
console.log(toy.getName()); // 自有的特權方法 
console.log(toy.getBrowser()); // 來自原型的特權方法

將私有函數暴露爲公有方法

「暴露模式」是指將已經有的私有函數暴露爲公有方法。

咱們來看一個例子,它創建在對象字面量的私有成員模式之上:

var myarray;

(function () {

    var astr = "[object Array]",
        toString = Object.prototype.toString;

    function isArray(a) {
        return toString.call(a) === astr;
    }

    function indexOf(haystack, needle) {
        var i = 0,
            max = haystack.length;
        for (; i < max; i += 1) {
            if (haystack[i] === needle) {
                return i;
            }
        }
        return −1;
    }

    myarray = {
        isArray: isArray,
        indexOf: indexOf,
        inArray: indexOf
    };

}());

模塊模式

模塊模式使用得很普遍,由於它能夠爲代碼提供特定的結構,幫助組織日益增加的代碼。不像其它語言,JavaScript沒有專門的「包」(package)的語法,但模塊模式提供了用於建立獨立解耦的代碼片斷的工具,這些代碼能夠被當成黑盒,當你正在寫的軟件需求發生變化時,這些代碼能夠被添加、替換、移除。

模塊模式是咱們目前討論過的好幾種模式的組合,即:

  • 命名空間模式

  • 即時函數模式

  • 私有和特權成員模式

  • 依賴聲明模式

第一步是初始化一個命名空間。咱們使用本章前面部分的namespace()函數,建立一個提供數組相關方法的套件模塊:

MYAPP.namespace('MYAPP.utilities.array');

下一步是定義模塊。使用一個即時函數來提供私有做用域供私有成員使用。即時函數返回一個對象,也就是帶有公有接口的真正的模塊,能夠供其它代碼使用:

MYAPP.utilities.array = (function () {
    return {
        // todo...
    };
}());

下一步,給公有接口添加一些方法:

MYAPP.utilities.array = (function () {
    return {
        inArray: function (needle, haystack) {
            // ...
        },
        isArray: function (a) {
            // ...
        }
    };
}());

若是須要的話,你能夠在即時函數提供的閉包中聲明私有屬性和私有方法。一樣,依賴聲明放置在函數頂部,在變量聲明的下方能夠選擇性地放置輔助初始化模塊的一次性代碼。函數最終返回的是一個包含模塊公共API的對象:

MYAPP.namespace('MYAPP.utilities.array');
MYAPP.utilities.array = (function () {

        // 依賴聲明
    var uobj = MYAPP.utilities.object,
        ulang = MYAPP.utilities.lang,

        // 私有屬性
        array_string = "[object Array]",
        ops = Object.prototype.toString;

        // 私有方法
        // ……

        // 結束變量聲明

    // 選擇性放置一次性初始化的代碼
    // ……

    // 公有API
    return {

        inArray: function (needle, haystack) {
            for (var i = 0, max = haystack.length; i < max; i += 1) {
                if (haystack[i] === needle) {
                    return true;
                }
            }
        },

        isArray: function (a) {
            return ops.call(a) === array_string;
        }
        // ……更多的方法和屬性
    };
}());

模塊模式被普遍使用,是一種值得強烈推薦的模式,它能夠幫助咱們組織代碼,尤爲是代碼量在不斷增加的時候。

暴露模塊模式

咱們在本章中討論私有成員模式時已經討論過暴露模式。模塊模式也能夠用相似的方法來組織,將全部的方法保持私有,只在最後暴露須要使用的方法來初始化API。

上面的例子能夠變成這樣:

MYAPP.utilities.array = (function () {

        // 私有屬性
    var array_string = "[object Array]",
        ops = Object.prototype.toString,

        // 私有方法
        inArray = function (haystack, needle) {
            for (var i = 0, max = haystack.length; i < max; i += 1) {
                if (haystack[i] === needle) {
                    return i;
                }
            }
            return −1;
        },
        isArray = function (a) {
            return ops.call(a) === array_string;
        };
        // 結束變量定義

    // 暴露公有API
    return {
        isArray: isArray,
        indexOf: inArray
    };
}());

在模塊中引入全局上下文

做爲這種模式的一個常見的變種,你能夠給包裹模塊的即時函數傳遞參數。你能夠傳遞任何值,但一般狀況下會傳遞全局變量甚至是全局對象自己。引入全局上下文能夠加快函數內部的全局變量的解析,由於引入以後會做爲函數的本地變量:

MYAPP.utilities.module = (function (app, global) {

    // 全局對象和全局命名空間都做爲本地變量存在

}(MYAPP, this));

代碼複用模式

在作代碼複用的工做的時候,謹記Gang of Four在書中給出的關於對象建立的建議:「優先使用對象建立而不是類繼承」。

類式(傳統)繼承(classical inheritance) vs 現代繼承模式
類式繼承:按照類的方式考慮JavaScript,併產生了一些假定在類的基礎上的開發思路和繼承模式。
現代繼承模式:其餘任何不須要以類的方式考慮的模式。

當須要給項目選擇一個繼承模式時,有很多的備選方案。你應該儘可能選擇那些現代繼承模式,除非團隊已經以爲「無類不歡」。

跳過類繼承..

經過複製屬性實現繼承

在這種模式中,一個對象經過簡單地複製另外一個對象來得到功能。下面是一個簡單的實現這種功能的extend()函數:

function extend(parent, child) {
    var i;
    child = child || {};
    for (i in parent) {
        if (parent.hasOwnProperty(i)) {
            child[i] = parent[i];
        }
    }
    return child;
}

上面給出的實現叫做對象的「淺拷貝」(shallow copy),與之相對,「深拷貝」是指檢查準備複製的屬性自己是不是對象或者數組,若是是,也遍歷它們的屬性並複製。若是使用淺拷貝的話(由於在JavaScript中對象是按引用傳遞),若是你改變子對象的一個屬性,而這個屬性剛好是一個對象,那麼你也會改變父對象。實際上這對方法來講可能很好(由於函數也是對象,也是按引用傳遞),可是當遇到其它的對象和數組的時候可能會有些意外狀況。

如今讓咱們來修改一下extend()函數以便實現深拷貝。你須要作的事情只是檢查一個屬性的類型是不是對象,若是是,則遞歸遍歷它的屬性。另一個須要作的檢查是這個對象是真的對象仍是數組,可使用第三章討論過的數組檢查方式。最終深拷貝版的extend()是這樣的:

function extendDeep(parent, child) {
    var i,
        toStr = Object.prototype.toString,
        astr = "[object Array]";

    child = child || {};

    for (i in parent) {
        if (parent.hasOwnProperty(i)) {
            if (typeof parent[i] === "object") {
                child[i] = (toStr.call(parent[i]) === astr) ? [] : {};
                extendDeep(parent[i], child[i]);
            } else {
                child[i] = parent[i];
            }
        }
    }
    return child;
}

這種模式並不高深,由於根本沒有原型牽涉進來,而只跟對象和它們的屬性有關。

混元(Mix-ins)

「混元」模式,從任意多數量的對象中複製屬性,而後將它們混在一塊兒組成一個新對象。

function mix() {
    var arg, prop, child = {};
    for (arg = 0; arg < arguments.length; arg += 1) {
        for (prop in arguments[arg]) {
            if (arguments[arg].hasOwnProperty(prop)) {
                child[prop] = arguments[arg][prop];
            }
        }
    }
    return child;
}

這裏咱們只是簡單地遍歷、複製自有屬性,並無與父對象有任何連接。

借用方法

apply、call、bind(ES5)。
apply是接受數組,而call是接受一個一個的參數。

在低於ES5的環境中時如何實現Function.prototype.bind():

if (typeof Function.prototype.bind === "undefined") {
    Function.prototype.bind = function (thisArg) {
        var fn = this,
        slice = Array.prototype.slice,
        args = slice.call(arguments, 1);

        return function () {
            return fn.apply(thisArg, args.concat(slice.call(arguments)));
        };
    };
}

小結

在JavaScript中,繼承有不少種方案能夠選擇,在本章中你看到了不少類式繼承和現代繼承的方案。學習和理解不一樣的模式是有好處的,由於這能夠加強你對這門語言的掌握能力。

可是,也許在開發過程當中繼承並非你常常面對的一個問題。一部分是由於這個問題已經被使用某種方式或者某個你使用的類庫解決了,另外一部分是由於你不須要在JavaScript中創建很長很複雜的繼承鏈。在靜態強類型語言中,繼承多是惟一能夠複用代碼的方法,但在JavaScript中有更多更簡單更優化的方法,包括借用方法、綁定、複製屬性、混元等。

記住,代碼複用纔是目標,繼承只是達成這個目標的一種手段。

DOM與瀏覽器模式

延遲加載

所謂的延遲加載是指在頁面的load事件以後再加載外部文件。一般,將一個大的合併後的文件分紅兩部分是有好處的:

  • 一部分是頁面初始化和綁定UI元素的事件處理函數必須的

  • 第二部分是隻在用戶交互或者其它條件下才會用到的

分紅兩部分的目標就是逐步加載頁面,讓用戶儘快能夠進行一些操做。剩餘的部分在用戶能夠看到頁面的時候再在後臺加載。

加載第二部分JavaScript的方法也是使用動態script元素,將它加在head或者body中:

……頁面主體部分……

    <!-- 第二塊結束 -->
    <script src="all_20100426.js"></script>
    <script>
    window.onload = function () {
        var script = document.createElement("script");
        script.src = "all_lazy_20100426.js";
        document.documentElement.firstChild.appendChild(script);
    };
    </script>
</body>
</html>

### 按需加載
建立一個require()函數或者方法,它接受一個須要被加載的腳本文件的文件名,還有一個在腳本被加載完畢後執行的回調函數。

require("extra.js", function () {
    functionDefinedInExtraJS();
});
function require(file, callback) {

    var script = document.getElementsByTagName('script')[0], newjs = document.createElement('script');

    // IE
    newjs.onreadystatechange = function () {
        if (newjs.readyState === 'loaded' || newjs.readyState === 'complete') {
            newjs.onreadystatechange = null;
            callback();
        }
    };

    // 其它瀏覽器
    newjs.onload = function () {
        callback();
    };

    newjs.src = file;
    script.parentNode.insertBefore(newjs, script);
}

這個實現的幾點說明:

  • 在IE中須要監聽readystatechange事件,而後判斷狀態是否爲"loaded"或者"complete"。其它的瀏覽器會忽略這裏。

  • 在Firefox,Safari和Opera中,經過onload屬性監聽load事件。

  • 這個方法在Safari 2中無效。若是必需要處理這個瀏覽器,須要設一個定時器,週期性地去檢查某個指定的變量(在腳本中定義的)是否有定義。當它變成已定義時,就意味着新的腳本已經被加載並執行。

預加載JavaScript

在延遲加載模式和按需加載模式中,咱們加載了當前頁面須要用到的腳本,除此以外,咱們也能夠加載當前頁面不須要但可能在接下來的頁面中須要的腳本。這樣的話,當用戶進入第二個頁面時,腳本已經被預加載過,總體體驗會變得更快。

預加載能夠簡單地經過動態腳本模式實現,但這也意味着腳本會被解析和執行。解析僅僅會在頁面加載時間中增長預加載消耗的時間,但執行卻可能致使JavaScript錯誤,由於預加載的腳本會假設本身運行在第二個頁面上,好比找一個特定的DOM節點就可能出錯。

僅加載腳本而不解析和執行是可能的,這也一樣適用於CSS和圖像。

在IE中,你可使用熟悉的圖片信標模式來發起請求:

new Image().src = "preloadme.js";

在其它的瀏覽器中,你可使用<object>替代script元素,而後將它的data屬性指向腳本的URL:

var obj = document.createElement('object');
obj.data = "preloadme.js";
document.body.appendChild(obj);

爲了阻止object可見,你應該設置它的width和height屬性爲0。

你能夠建立一個通用的preload()函數或者方法,使用條件初始化模式(第四章)來處理瀏覽器差別:

var preload;
if (/*@cc_on!@*/false) { // IE支持條件註釋
    preload = function (file) {
        new Image().src = file;
    };
} else {
    preload = function (file) {
        var obj = document.createElement('object'),
            body = document.body;

        obj.width = 0;
        obj.height = 0;
        obj.data = file;
        body.appendChild(obj);
    };
}

使用這個新函數:

preload('my_web_worker.js');

這種模式的壞處在於存在用戶代理(瀏覽器)嗅探,但這裏沒法避免,由於特性檢測沒有辦法告滿足夠的瀏覽器行爲信息。好比在這個模式中,理論上你能夠測試typeof Image是不是"function"來代替嗅探,但這種方法其實沒有做用,由於全部的瀏覽器都支持new Image();只是有一些瀏覽器會爲圖片單獨作緩存,意味着做爲圖片緩存下來的組件(文件)在第二個頁面中不會被做爲腳本取出來,而是會從新下載。

瀏覽器嗅探中使用條件註釋頗有意思,這明顯比在navigator.userAgent中找字符串要安全得多,由於用戶能夠很容易地修改這些字符串。 好比: var isIE = /*@cc_on!@*/false; 會在其它的瀏覽器中將isIE設爲false(由於忽略了註釋),但在IE中會是true,由於在條件註釋中有取反運算符!。在IE中就像是這樣: var isIE = !false; // true

預加載模式能夠被用於各類組件(文件),而不只僅是腳本。好比在登陸頁就頗有用,當用戶開始輸入用戶名時,你可使用打字的時間開始預加載(非敏感的東西),由於用戶極可能會到第二個也就是登陸後的頁面。

完~

Github: https://github.com/JChehe/blog
博客:http://jchehe.github.io/

相關文章
相關標籤/搜索