《JavaScript模式》精要

P25. 如何避免eval()定義全局變量?

如:javascript

var jsstring = "var un = 1;";
eval(jsstring);
console.log(typeof un);  // "number"

方法1:使用new Function()。new Function()中的代碼將在局部函數空間中運行。css

var jsstring = "var un2 = 1;";
new Function(jsstring)();
console.log(typeof un2);   // "undefined"

方法2:將eval()封裝到一個即時函數中。html

var jsstring = "var un3 = 1;";
(function() {
    eval(jsstring);
})()
console.log(typeof un3);   // "undefined"

P26. eval()對做用域鏈的影響

  • eval()會影響到做用域鏈,eval()能夠訪問和修改它外部做用域的變量。
  • Function更相似於一個沙盒,不管哪裏執行Function,它都僅僅能看到全局做用域,看不到外部做用域。
(function() {
    var local = 1;
    eval("local = 3; console.log(local); ");  // 3
    console.log(local);  // 3
})();

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

P49. 自調用構造函數

做用:java

  • 調用構造函數時,及時沒有使用new操做符,也能夠正常工做。
  • 使得原型屬性可在對象實例中使用。
function Waffle() {
    if (!(this instanceof Waffle)) {    // 或者: if (!(this instanceof arguments.callee)) {
        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

P51. 數組構造函數的特殊性

  • 向Array()構造函數傳遞單個數字時,它並不會成爲第一個數組元素的值。相反,它設定了數組的長度,且每一個元素都是undefined。
  • 向Array()構造函數傳遞浮點數時,會引起RangeError。
var a = new Array(3);
console.log(a.length);  // 3
console.log(a[0]);  // undefined

var a = new Array(3.14);  // RangeError: invalid array length
console.log(typeof a);  // undefined;

使用Array()構造函數,返回一個具備255個空白字符的字符串:數組

var white = new Array(256).join(' ');

P52. 檢查數組的性質

ES5定義了Array.isArray()方法。或調用Object.prototype.toString()方法。瀏覽器

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

P70. 自定義函數/惰性函數定義

函數體內部,以一個新函數覆蓋了舊函數,回收了舊函數指針以指向一個新函數。即,該函數以一個新的實現覆蓋並從新定義了自身。緩存

  • 優勢:適合舊函數有一些初始化準備工做要作,而且僅需執行一次。使用自定義函數模式,能夠提高應用程序的性能。
  • 缺點:
    • 重定義自身時,已經添加到原始函數的任何屬性都會丟失。
    • 若是該函數使用了不一樣的名稱,好比分配給不一樣的變量、以對象的方法來使用,那麼重定義部分將永遠不會發生,而且將會執行原始函數體。
var scareMe = function() {
    alert("Boo!");
    scareMe = function() {
        alert("Double boo!");
    }
}

// 使用自定義函數
scareMe();  // Boo!
scareMe();  // Double boo!

P74. 即時函數

做用1:存儲私有數據。即時函數能夠返回函數,所以能夠利用即時函數的做用域存儲一些私有數據,而這特定於返回函數的內部函數。安全

var getResult = (function() {
    var res = 2+2;
    return function() {
        return res;
    }
})();
console.log(getResult());  // 4

做用2:定義對象屬性。數據結構

var o = {
    message: (function() {
         var who = "me",
             what = "call";
         return what + " " + who;
    })(),
    getMsg: function() {
         return this.message;
     }
}

// 用法
console.log(o.getMsg());  // call me
console.log(o.message);  // call me

P75. 即時對象初始化

使用帶有init()方法的對象,該方法在建立對象後將會當即執行。init()函數負責全部的初始化任務。閉包

優勢:能夠在執行一次初始化任務時保護全局命名空間。

這種模式主要適用於一次性任務,並且init()完畢後也沒有對該對象的訪問。若是想在init()完畢後保存對該對象的一個引用,能夠在init()尾部添加return this

({
    max: 60,
    getMax: function() {
        return this.max;
    },

    // 初始化
    init: function() {
        console.log(this.getMax());  // 60
        // 更多初始化任務
    }
}).init();

P78. 函數備忘

函數能夠在任什麼時候候將自定義屬性添加在函數中。自定義屬性的其中一個用例是緩存函數結果(即返回值),所以,在下一次調用該函數時就不用重作潛在的繁重計算。

能夠爲函數建立一個屬性cache,該屬性是一個對象,其中使用傳遞給函數的參數做爲鍵,而計算結果做爲值。計算結果能夠是須要的任意複雜數據結構。對於有更多以及更復雜的參數,通用的解決方案是將它們序列化。例如,能夠將參數對象序列化爲一個JSON字符串,並使用該字符串做爲cache對象的鍵。

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

    if (!myFunc.cache[cachekey]) {
         result = {};
         // ``` 開銷很大的操做```
         console.log('Do Job');
         myFunc.cache[cachekey] = result;
     }

    return myFunc.cache[cachekey];
}

// 緩存存儲
myFunc.cache = {};

// 測試
myFunc(0);  // Do Job
myFunc(0);  // 無

P85. 函數curry化/函數部分應用

function curry(fn) {
    var slice = Array.prototype.slice,
        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 add(a, b, c, d, e) {
    return a + b + c + d + e;
}

// 可運行於任意數量的參數
curry(add, 1, 2, 3)(5, 5);  // 16

// 兩步curry化
var addOne = curry(add, 1);
addOne(10, 10, 10, 10);  // 41
var addSix = curry(addOne, 2, 3);
addSix(5, 5);  // 16

P91. 命名空間函數

添加到命名空間的一些屬性可能已經存在,這致使可能會覆蓋它們。所以,在添加一個屬性或者建立一個命名空間以前,最好是首先檢查它是否已經存在:

if (typeof MYAPP === 'undefined') {
    var MYAPP = {};
}
// 或者:
var MYAPP = MYAPP || {};

能夠定義一個處理命名空間細節的可重用的函數。這個實現是非破壞性的,也就是說,若是已經存在一個命名空間,便不會再從新建立它。

var MYAPP = MYAPP || {};
MYAPP.namespace = function(ns_string) {
    var parts = ns_string.split('.'),
        parent = MYAPP,
        i;

    // 剝離最前面的冗餘全局變量
    if (parts[0] === "MYAPP") {
        parts = parts.slice(1);
    }

    for (i=0; i<parts.length; i++) {
        // 若是不存在,就建立一個屬性
        parent[parts[i]] = parent[parts[i]] || {};
        parent = parent[parts[i]];
    }
    return parent;
}

// 測試
var module1 = MYAPP.namespace('MYAPP.modules.module1');
module1 === MYAPP.modules.module1;

// 忽略最前面的MYAPP
var module2 = MYAPP.namespace('modules.module2');
module2 === MYAPP.modules.module2;

// 長命名空間
MYAPP.namespace('one.two.three.four.five.six.seven.eight');

P93. 聲明依賴

var myFunction() {
    // 依賴
    var event = YAHOO.util.Event,
          dom = YAHOO.util.Dom;
    // 使用事件和DOM變量
    // ...
}

P94.對象的私有性

構造函數的私有性

構造函數建立一個閉包,而在閉包範圍內部的任意變量都不會暴露給構造函數之外的代碼。然而,這些私有變量仍然能夠用於公共方法中,也稱爲特權方法。

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

// 測試
var toy = new Gadget();
console.log(toy.name)  // undefined
console.log(toy.getName());  // iPod

私有性失效:當直接從一個特權方法中返回一個私有變量,且該變量剛好是一個對象或者數組,那麼外面的代碼仍然能夠訪問該私有變量。由於這是經過引用傳遞的。

function Gadget() {
   // 私有成員
   var specs = {
       screen_width: 320,
       screen_height: 480,
       color: "white"
   };
   // 公有函數或特權方法
   this.getSpecs = function() {
       return specs;
   }
}

// 測試
var toy = new Gadget(),
    specs = toy.getSpecs();
specs.color = "black";
specs.price = "free";
console.dir(toy.getSpecs());

私有性失效的解決方法:

  • 使getSpecs()返回一個新對象,該對象僅包含客戶關注的原對象中的數據。即最低受權原則。

  • 使用一個通用性的對象克隆函數,以建立specs對象的副本。如淺複製、深複製。

對象字面量的私有性

可使用一個額外的匿名即時函數建立閉包來實現私有性。

方法1:

var myobj;
(function(){
    // 私有成員
    var name = "Peter";

    // 公有部分
    myobj = {
        // 特權方法
        getName: function() {
            return name;
        }
    }
})();

// 測試
console.log(myobj.name)  // undefined
console.log(myobj.getName());  // iPod

方法2:模塊模式

var myobj = (function(){
    // 私有成員
    var name = "Peter";

    // 公有部分
    return {
        getName: function() {
            return name;
        }
    }
})();

// 測試
console.log(myobj.name)  // undefined
console.log(myobj.getName());  // iPod

原型的私有性

當私有成員與構造函數一塊兒使用時,一個缺點在於每次調用構造函數以建立對象時,這些私有成員都會被從新建立。爲了不復制工做以節省內存,能夠將經常使用屬性和方法添加到構造函數的prototype屬性中。這樣,經過同一個構造函數建立的多個實例,能夠共享常見的部分數據。

可使用如下兩個模式的組合:構造函數中的私有屬性,以及對象字面量中的私有屬性。因爲prototype屬性僅是一個對象,所以可使用對象字面量建立該對象。

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());  // iPod
console.log(toy.getBrowser());  // Mobile Webkit

P99. 模塊模式

模塊模式是如下模式的組合:

  • 命名空間
  • 即時函數
  • 私有和特權成員
  • 聲明依賴

方法1:返回對象

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;

    // 私有方法
    // ...

    // 可選的一次性初始化過程
    console.log('Creating namespace: array');

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

方法2:返回構造函數

MYAPP.namespace('MYAPP.utilities.array');
MYAPP.utilities.array = (function(){
    // 依賴
    var uobj = MYAPP.utilities.object,
        ulang = MYAPP.utilities.lang,

    // 私有屬性和方法
       Constr;

    // 可選的一次性初始化過程
    console.log('Creating namespace: array');

    // 公有API——構造函數
    Constr = function(o) {
        this.elements = this.toArray(o);
    };

    // 公有API——原型
    Constr.prototype = {
        constructor: MYAPP.utiltities.array,
        version: "2.0",
        toArray: function(obj) {
           for (var i=0, a=[], len=obj.length; i<len; i++) {
               a[i] = obj[i];
            }
            return a;
        }
    }

    // 返回新的構造函數
    return Constr;
})();

// 測試
var arr = new MYAPP.utilities.array(obj);

P108. 靜態成員

靜態屬性和方法,是指從一個實例到另外一個實例都不會發生改變的屬性和方法。
靜態成員的優勢:能夠包含非實例相關的方法和屬性,而且不會爲每一個實例從新建立靜態屬性。

公有靜態成員

可使用構造函數,而且向其添加屬性這種方式。靜態成員,不須要特定的對象就可以運行。同時爲了使實例對象也能夠調用靜態成員,只須要向原型中添加一個新的成員便可,其中該新成員做爲一個指向原始靜態成員的外觀。

// 構造函數
var Gadget = function() {};

// 靜態方法
Gadget.isShiny = function() {
    return 'you bet';
};
// 向原型中添加一個普通方法
Gadget.prototype.setPrice = function(price) {
    this.price = price;
}
// 向原型中添加一個外觀
Gadget.prototype.isShiny = Gadget.isShiny;

// 測試
// 調用靜態方法
Gadget.isShiny();  // you bet;

// 實例調用普通方法
var iphone = new Gadget();
iphone.setPrice(500);

// 實例調用靜態方法
iphone.isShiny();  // you bet

私有靜態成員

私有靜態成員具備以下屬性:

  • 以同一個構造函數建立的全部對象,共享該成員。
  • 構造函數外部不可訪問該成員。

實現方法:閉包+即時函數。可參考模塊模式的返回構造函數。

// 構造函數
var Gadget = (function() {
    // 靜態變量
    var counter = 0,
        NewGadget;

    // 新的構造函數的實現
    NewGadget = function() {
        counter ++;
    };

    // 特權方法
    NewGadget.prototype.getLastId = function() {
        return counter;
    };

    // 覆蓋該構造函數
    return NewGadget;

})();

// 測試
var iphone = new Gadget();
console.log(iphone.getLastId());
var ipod = new Gadget();
console.log(ipod.getLastId());
var ipad = new Gadget();
console.log(ipad.getLastId());

P115. method()方法

做用:向構造函數的原型中添加新方法。

if (typeof Function.prototype.method !== "function") {
    Function.prototype.method = function(name, implementation) {
        this.prototype[name] = implementation;
        return this;    // 返回this(指向構造函數),支持鏈式調用
    }
}

// 測試
// 構造函數
var Person = function(name) {
    this.name = name;
}.
    method('getName', function() {
        return this.name;
    }).
    method('setName', function(name) {
        this.name = name;
        return this;
    });

// 對象
var a = new Person('Adam');
a.getName();  // Adam;
a.setName('Eve').getName();  // Eve

P119. 繼承模式1——設置原型(默認模式)

實現方式:

// 父構造函數
function Parent(name) {
    this.name = name || 'Adam';
}

// 向原型中添加方法
Parent.prototype.say = function() {
    return this.name;
}

// 子構造函數(空白)
function Child(name) {}

// 繼承:設置原型
Child.prototype = new Parent();

// 測試
var kid = new Child();
kid.say();  // Adam

原型鏈:

注意:

  • __proto__屬性僅用來解釋原型鏈,不可用於開發中。
  • 若子對象#3定義屬性name,並不會修改父對象#2的name屬性,而是直接在子對象#3上建立一個自身屬性。若是使用delete刪除子對象#3的name屬性,那麼父對象#2的name屬性將表現出來。
  • 優勢:
    • 子對象繼承了:父構造函數中的this屬性、父原型中的屬性。
  • 缺點:
    • 不支持將參數傳遞到子構造函數中,而子構造函數而後又將參數傳遞到父構造函數中。
    • 若是父類構造函數中的this屬性爲引用類型,可能存在子對象意外覆蓋父對象屬性的風險。
// 演示缺點1

var s = new Child('Seth');
s.say();  // Adam
// 演示缺點2

// 父構造函數
function Article() {
    this.tags = ['js', 'css'];
}
var article = new Article();

// 子構造函數及繼承
function Blog() {}
Blog.prototype = article;

// 子對象意外修改父對象的引用屬性
var blog = new Blog();
blog.tags.push('html');
console.log(article.tags.join(' '));  // js css html

P122. 繼承模式2——借用構造函數

實現方式:

// 父構造函數
function Parent(name) {
    this.name = name || 'Adam';
}

// 向原型中添加方法
Parent.prototype.say = function() {
    return this.name;
}

// 子構造函數
function Child(name) {
    // 繼承:借用構造函數
    Parent.apply(this, arguments);
}

// 測試
var kid = new Child('Partrick');
kid.name;  // Partick
typeof kid.say;  // undefined

原型鏈:

注意:

  • 缺點:只能繼承父構造函數中的this屬性,不能繼承父原型中的屬性。
  • 優勢:
    • 本模式解決了從子構造函數到父構造函數的參數傳遞問題。
    • 子對象能夠得到父對象自身成員的副本(而非引用),而且不會存在子對象意外覆蓋父對象屬性的風險。

P125. 繼承模式3——設置原型&借用構造函數

實現方式:

// 父構造函數
function Parent(name) {
    this.name = name || 'Adam';
}

// 向原型中添加方法
Parent.prototype.say = function() {
    return this.name;
}

// 子構造函數
function Child(name) {
    // 繼承:借用構造函數
    Parent.apply(this, arguments);
}

// 繼承:設置原型
Child.prototype = new Parent();

// 測試
var kid = new Child('Partrick');
kid.name;  // Partick
kid.say();  // Partick
delete kid.name;
kid.say();  // Adam

原型鏈:

注意:

  • 優勢:
    • 可以得到父對象自身成員的副本。子對象能夠安全地修改自身屬性,且不會帶來修改其父對象的風險。
    • 子對象繼承了:父構造函數中的this屬性、父原型中的屬性。
    • 子構造函數可將任意參數傳遞到父構造函數中。
  • 缺點:
    • 父構造函數被調用了兩次,致使其效率低下。自身的屬性(name)被繼承了兩次,刪除了子類自己的name屬性的副本後,原型鏈上的name屬性將表現出來。

P126. 繼承模式4——共享原型

實現方式:

// 父構造函數
function Parent(name) {
    this.name = name || 'Adam';
}

// 向原型中添加方法
Parent.prototype.say = function() {
    return this.name;
}

// 子構造函數
function Child() {}

// 繼承:共享原型
child.prototype = Parent.prototype;

原型鏈:

注意:

  • 本模式適用於:可複用成員應轉移到原型中,而不是放置在父類this中。任何值得繼承的東西都應該放置在原型中實現。
  • 不能繼承父構造函數中的this屬性,只能繼承父原型中的屬性。
  • 缺點:
    • 若是在繼承鏈下方的某處存在一個子對象或孫子對象修改了原型,將會影響到全部父對象和祖先對象。

P128. 繼承模式5——臨時構造函數

實現方式:

// 父構造函數
function Parent(name) {
    this.name = name || 'Adam';
}

// 向原型中添加方法
Parent.prototype.say = function() {
    return this.name;
}

// 子構造函數
function Child(name) {}

// 繼承:設置原型
inherit(Child, Parent);

// 實現:
function inherit(C, P) {
    var F = function() {};
    F.prototype = P.prototype;
    C.prototype = new F();
    C.prototype.constructor = C;
}

// 優化:避免在每次須要繼承時,都建立臨時(代理)構造函數。
// 實現:即時函數+閉包
var inherit2 = (function() {
    var F = function() {};
    return function(C, P) {
        F.prototype = P.prototype;
        C.prototype = new F();
        C.prototype.constructor = C;
    }
})();

// 測試
var kid = new Child();
kid.say();  // undefined
kid.name = "Peter";
kid.say();  // Peter

原型鏈:

注意:

  • 子對象僅繼承了原型的屬性。原型僅用來放置可複用的功能。父構造函數的this中的任何成員都不會被繼承。
  • 須要重置子構造函數的指針:C.prototype.constructor = C

P133. 繼承模式6——原型繼承

實現方式:

function object(P) {
    var F = function() {};
    F.prototype = P;
    return new F();
}

對象字面量方式建立父對象

var parent = {
    name: "papa"
}
var child = object(parent);

// 測試
console.log(child.name);

構造函數方式建立父對象

// 父構造函數
function Parent() {
    this.name = "papa";
}
Parent.prototype.getName = function() {
    return this.name;
}

// 建立一個父對象
var papa = new Parent();

// 繼承方式1:父構造函數中的this屬性、父原型的屬性都被繼承
var kid = object(papa);
console.log(typeof kid.name);  // string
console.log(typeof kid.getName);  // function

// 繼承方式2:僅繼承父原型的屬性
var kid = object(Parent.prototype);
console.log(typeof kid.name);  // undefined
console.log(typeof kid.getName);  // function

ES5: Object.create()


P136. 繼承模式7——複製屬性

淺複製

在使用淺複製時,若是改變了子對象的屬性,而且該屬性剛好是一個對象,那麼這種操做也將修改父對象。

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

// 測試
var dad = {
    counts: [1, 2, 3],
    reads: { paper: true }
};
var kid = extend(dad);
kid.counts.push(4);
dad.counts.toString();  // 1,2,3,4
dad.reads === kid.reads;  // true

深複製

檢查父對象的某個屬性是否爲對象,若是是,則須要遞歸複製出該對象的屬性。

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

// 測試
var dad = {
    counts: [1, 2, 3],
    reads: { paper: true }
};
var kid = extendDeep(dad);
kid.counts.push(4);
dad.counts.toString();  // 1,2,3
dad.reads === kid.reads;  // false

混入

從多個對象中複製出任意成員,並將這些成員組合成一個新的對象。
遇到同名屬性,老是使用靠後對象的值,即越日後優先級越高。

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

// 測試
var cake = mix(
    { eggs: 2, large: true },
    { buter: 1, saleted: true },
    { flour: "3 cups" },
    { sugar: "sure!" },
    { eggs: 3 }  // 同名屬性,越日後優先級越高
);
console.dir(cake);

P141. 綁定

function bind(obj, fn) {
    return function() {
        return fn.apply(obj, Array.prototype.slice.call(arguments));
    };
}

ES5: Funtion.prototype.bind()

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

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

P204. 延遲加載

function lazyload(file) {
    var script = document.createElement("script");
    script.src = file;
    document.getElementsByTagName("head")[0].appenChild(script);
}

// 使用
window.onload = function() {
    lazyload('extra.js');
}

P205. 按需加載

function require(file, callback){
   var script = document.createElement ("script");
   script.type = "text/javascript";
   if (script.readyState){ //IE
     script.onreadystatechange = function(){
       if (script.readyState === "loaded" || script.readyState === "complete"){
         script.onreadystatechange = null;
         callback();
       }
     };
   } else { //Others
     script.onload = function(){
       callback();
     };
   }
  script.src = file;
    document.getElementsByTagName("head")[0].appendChild(script);
}

// 使用
require("extra.js", function() {
    // 執行extra.js中定義的函數
});

P207. 預加載

  • IE下使用圖像燈塔模式方法請求。
  • 其餘瀏覽器中,使用<object>來代替腳本元素,並向其data屬性指向腳本的URL。
var preload;
if (/*@cc_on!@*/false) {  // 使用條件註釋的IE嗅探
    preload = function(file) {
        new Image().src = file;
    };
} else {
    preload = function(file) {
        var obj = document.createElement('object');
        obj.width = 0;
        obj.height = 0;
        obj.data = file;
        document.body.appendChild(obj);
    };
}

// 使用:
preload('extra.js');
相關文章
相關標籤/搜索