十九、高級技巧

??JavaScript 是一種極其靈活的語言础废,具有多種使用風(fēng)格汛骂。

??一般來說,編寫 JavaScript 要么使用過程方式评腺,要么使用面向?qū)ο蠓绞搅辈t。然而,由于它天生的動態(tài)屬性蒿讥,這種語言還能使用更為復(fù)雜和有趣的模式蝶念。

??這些技巧要利用 ECMAScript 的語言特點(diǎn)、BOM 擴(kuò)展和 DOM 功能來獲得強(qiáng)大的效果芋绸。

1媒殉、高級函數(shù)

??函數(shù)是 JavaScript 中最有趣的部分之一。它們本質(zhì)上是十分簡單和過程化的摔敛,但也可以是非常復(fù)雜和動態(tài)的廷蓉。一些額外的功能可以通過使用閉包來實(shí)現(xiàn)。此外马昙,由于所有的函數(shù)都是對象桃犬,所以使用函數(shù)指針非常簡單的。
??這些令 JavaScript 函數(shù)不僅有趣而且強(qiáng)大行楞。

1.1疫萤、安全的類型檢測

??JavaScript 內(nèi)置的類型檢測機(jī)制并非完全可靠。事實(shí)上敢伸,發(fā)生錯誤否定及錯誤肯定的情況也不在少數(shù)扯饶。
??比如說 typeof 操作符,由于它有一些無法預(yù)知的行為池颈,經(jīng)常會導(dǎo)致檢測數(shù)據(jù)類型時得到不靠譜的結(jié)果尾序。Safari(直至第 4 版)在對正則表示式應(yīng)用 typeof 操作符時會返回"function",因此很難確定某個值到底是不是函數(shù)躯砰。
??再比如每币,instanceof 操作符在存在多個全局作用域(像一個頁面包含多個 frame)的情況下,也是問題多多琢歇。一個經(jīng)典的例子就是像下面這樣將對象標(biāo)識為數(shù)組兰怠。

var isArray = value instanceof Arrray;

??以上代碼要返回 true梦鉴,value 必須是一個數(shù)組,而且還必須與 Array 構(gòu)造函數(shù)在同個全局作用域中揭保。(別忘了肥橙,Array 是 window 的屬性。)如果 value 是在另個 frame 中定義的數(shù)組秸侣,那么以上代碼就會返回 false存筏。

??在檢測某個對象到底是原生對象還是開發(fā)人員自定義的對象的時候,也會有問題味榛。出現(xiàn)這個問題的原因是瀏覽器開始原生支持 JSON 對象了椭坚。因?yàn)楹芏嗳艘恢痹谑褂?DouglasCrockford 的 JSON 庫,而該庫定義了一個全局 JSON 對象搏色。于是開發(fā)人員很難確定頁面中的 JSON 對象到底是不是原生的善茎。

??解決上述問題的辦法都一樣。

??大家知道频轿,在任何值上調(diào)用 Object 原生的 toString() 方法垂涯,都會返回一個 [object NativeConstructorName] 格式的字符串。每個類在內(nèi)部都有一個 [[Class]] 屬性略吨,這個屬性中就指定了上述字符串中的構(gòu)造函數(shù)名集币。舉個例子吧。

var value1 = '123';
var value2 = 123;
var value3 = false;
var value4 = null;
var value5 = undefined;
var value6 = {};
var value7 = [];
var value8 = function(){}
var value9 = new RegExp;

console.log(Object.prototype.toString.call(value1)); // [object String]
console.log(Object.prototype.toString.call(value));  // [object Number]
console.log(Object.prototype.toString.call(value3)); // [object Boolean]
console.log(Object.prototype.toString.call(value4)); // [object Null]
console.log(Object.prototype.toString.call(value5)); // [object Undefined]
console.log(Object.prototype.toString.call(value6)); // [object Object]
console.log(Object.prototype.toString.call(value7)); // [object Array]
console.log(Object.prototype.toString.call(value8)); // [object Function]
console.log(Object.prototype.toString.call(value9)); // [object RegExp]

console.log(typeof Object.prototype.toString.call(value9) === 'string'); // true

??由于原生數(shù)組的構(gòu)造函數(shù)名與全局作用域無關(guān)翠忠,因此使用 toString() 就能保證返回一致的值鞠苟。利用這一點(diǎn)可以創(chuàng)建如下函數(shù):

function isArray(value) {
    return Object.prototype.toString.call(value) === '[object Array]';
}

??同樣,也可以基于這一思路來測試某個值是不是原生函數(shù)或正則表達(dá)式:

function isFunction(value) {
    return Object.prototype.toString.call(value) === '[object Function]';
}

function isRegExp(value) {
    return Object.prototype.toString.call(value) === '[object RegExp]';
}

??不過要注意秽之,對于在 IE 中以 COM 對象形式實(shí)現(xiàn)的任何函數(shù)当娱,isFunction() 都將返回 false(因?yàn)樗鼈儾⒎窃?JavaScript 函數(shù))。
??這一技巧也廣泛應(yīng)用于檢測原生 JSON 對象考榨。Object 的 toString() 方法不能檢測非原生構(gòu)造函數(shù)的構(gòu)造函數(shù)名跨细。因此,開發(fā)人員定義的任何構(gòu)造函數(shù)都將返回 [object Object]河质。有些 JavaScript 庫會包含與下面類似的代碼冀惭。


var isNativeJSON = window.JSON && Object.protptype.toString.call(JSON) === '[object JSON]';

??在 Web 開發(fā)中能夠區(qū)分原生與非原生 JavaScript 對象非常重要。只有這樣才能確切知道某個對象到底有哪些功能掀鹅。這個技巧可以對任何對象給出正確的結(jié)論散休。

??請注意,Object.prototype.toString() 本身也可能會被修改乐尊。上面討論的技巧假設(shè) Object.prototype.toString() 是未被修改過的原生版本戚丸。

1.2、作用域安全的構(gòu)造函數(shù)

??構(gòu)造函數(shù)其實(shí)就是一個使用 new 操作符調(diào)用的函數(shù)扔嵌。當(dāng)使用 new 調(diào)用時限府,構(gòu)造函數(shù)內(nèi)用到的 this 對象會指向新創(chuàng)建的對象實(shí)例夺颤,示例:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
}

var person = new Person('Nicholsa', 20, 'Software Engineer');

??上述例子中, Person 構(gòu)造函數(shù)使用 this 對象給三個屬性賦值:name胁勺、age 和 job世澜。當(dāng)和 new 操作符連用時,則會創(chuàng)建一個新的 Person 對象姻几,同時會給它分配這些屬性宜狐。
??問題出在當(dāng)沒有使用 new 操作符來調(diào)用該構(gòu)造函數(shù)的情況上势告。由于該 this 對象是在運(yùn)行時綁定的蛇捌,所以直接調(diào)用 Person(),this 會映射到全局對象 window 上咱台,導(dǎo)致錯誤對象屬性的意外增加络拌。示例:

var person = Person('Nicholas', 20, 'Software Engineer');

console.log(window.name); // 'Nicholas'
console.log(window.age); // 20
console.log(window.job); // 'Software Engineer'

??這里,原本針對 Person 實(shí)例的三個屬性被加到 window 對象上回溺,因?yàn)闃?gòu)造函數(shù)時作為普通函數(shù)調(diào)用的春贸,忽略了 new 操作符。這個問題是由 this 對象的晚綁定造成的遗遵,在這里 this 被解析成了 window 對象萍恕。
??由于 window 的 name 屬性是用于識別鏈接目標(biāo)和 frame 的,所以這里對該屬性的偶然覆蓋可能會導(dǎo)致該頁面上出現(xiàn)其他錯誤车要。
??這個問題的解決方法就是創(chuàng)建一個作用域安全的構(gòu)造函數(shù)允粤。
??作用域安全的構(gòu)造函數(shù)在進(jìn)行任何更改前,首先確認(rèn) this 對象是正確類型的實(shí)例翼岁。如果不是类垫,那么會創(chuàng)建新的實(shí)例并返回。示例:

function Person(name, age, job) {
    if (this instaceof Person) {
        this.name = name;
        this.age = age;
        this.job = job;
    } else {
        return new Person(name, age, job);
    }
}

var person1 = Person('Nicholas', 20, 'Software Engineer');
console.log(window.name); // ''
console.log(person1.name); // 'Nicholas'

var person2 = new Person('Shelby', 21, 'Ergonomist');
console.log(person2.name); // 'Shelby'

??上述代碼中 Person 構(gòu)造函數(shù)添加了一個檢查并確保 this 對象是 Person 實(shí)例的 if 語句琅坡,它表示要么使用 new 操作符悉患,要么在現(xiàn)有的 Person 實(shí)例環(huán)境中調(diào)用構(gòu)造函數(shù)。任何一種情況下榆俺,對象初始化都能正常運(yùn)行售躁。
??如果 this 并非 Person 的實(shí)例,那么會再次使用 new 操作符調(diào)用構(gòu)造函數(shù)并返回結(jié)果茴晋。最后的結(jié)果是陪捷,調(diào)用 Person 構(gòu)造函數(shù)時無論是否使用 new 操作符,都會返回一個 Person 的新實(shí)例晃跺,這就避免了在全局對象上意外設(shè)置屬性揩局。

??關(guān)于作用域安全的構(gòu)造函數(shù)的貼心提示。實(shí)現(xiàn)這個模式后掀虎,你就鎖定了可以調(diào)用構(gòu)造函數(shù)的環(huán)境凌盯。如果你使用構(gòu)造函數(shù)竊取模式的繼承且不使用原型鏈付枫,那么這個繼承很可能被破壞。這里有個例子:

function Polygon(sides) {
    if (this instanceof Polygon) {
        this.sides = sides;
        this.getArea = function () {
            return 0;
        };
    } else {
        return new Polygon(sides);
    }
}

function Rectangle(width, height) {
    Polygon.call(this, 2);
    this.width = width;
    this.height = height;
    this.function () {
        return this.width * this.height;
    };
}

var rect = new Rectangle(5, 10);
console.log(rect.sides); // undefined    

??在這段代碼中驰怎,Polygon 構(gòu)造函數(shù)是作用域安全的阐滩,然而 Rectangle 構(gòu)造函數(shù)則不是。新創(chuàng)建一個 Rectangle 實(shí)例之后县忌,這個實(shí)例應(yīng)該通過 Polygon.call() 來繼承 Polygon 的 sides 屬性掂榔。但是,由于 Polygon 構(gòu)造函數(shù)是作用域安全的症杏,this 對象并非 Polygon 的實(shí)例装获,所以會創(chuàng)建并返回一個新的 Polygon 對象。Rectangle 構(gòu)造函數(shù)中的 this 對象并沒有得到增長厉颤,同時 Polygon.call() 返回的值也沒有用到穴豫,所以 Rectangle 實(shí)例中就不會有 sides 屬性。
??如果構(gòu)造函數(shù)竊取結(jié)合使用原型鏈或者寄生組合則可以解決這個問題逼友【啵考慮以下例子:

function Polygon(sides) {
    if (this instanceof Polygon) {
        this.sides = sides;
        this.getArea = function () {
            return 0;
        };
    } else {
        return new Polygon(sides);
    }
}

function Rectangle(width, height) {
    Polygon.call(this, 2);
    this.width = width;
    this.height = height;
    this.function () {
        return this.width * this.height;
    };
}

Rectangle.prototype = new Polygon();

var rect = new Rectangle(5, 10);
console.log(rect.sides); // 2

??上面這段重寫的代碼中,一個 Rectangle 實(shí)例也同時是一個Polygon實(shí)例帜乞,所以 Polygon.call() 會照原意執(zhí)行司抱,最終為 Rectangle 實(shí)例添加了 sides 屬性。
??多個程序員在同一個頁面上寫 JavaScript 代碼的環(huán)境中黎烈,作用域安全構(gòu)造函數(shù)就很有用了习柠。屆時,對全局對象意外的更改可能會導(dǎo)致一些常常難以追蹤的錯誤怨喘。除非你單純基于構(gòu)造函數(shù)竊取來實(shí)現(xiàn)繼承津畸,推薦作用域安全的構(gòu)造函數(shù)作為最佳實(shí)踐。

1.3必怜、惰性載入函數(shù)

??因?yàn)闉g覽器之間行為的差異肉拓,多數(shù) JavaScript 代碼包含了大量的 if 語句,將執(zhí)行引導(dǎo)到正確的代碼中梳庆∨荆看看下面來自上一章的 createXHR() 函數(shù)。

function createXHR() {
    if (typeof XMLHttpReauest != 'undefined') {
        return new XMLHttpReauest();
    } else if (typeof ActiveXObject != 'undefined') {
        if (typof arguments.callee.activeXString != 'string') {
            var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'],
                i,
                len;

            for(i = 0, len = versions.length; i < len; i++) {
                try {
                    new ActiveXObject(versions[i]);
                    arguments.callee.activeXString = versions[i];
                    break;
                } catch(ex) {
                    // 跳過
                }
            }
        }

        return new ActiveXObject(arguments.callee,activeXString);
    } else {
        throw new Error('No XHR object available');
    }
}

??每次調(diào)用 createXHR() 的時候膏执,它都要對瀏覽器所支持的能力仔細(xì)檢查驻售。首先檢查內(nèi)置的 XHR,然后測試有沒有基于 ActiveX 的 XHR更米,最后如果都沒有發(fā)現(xiàn)的話就拋出一個錯誤欺栗。
??每次調(diào)用該函數(shù)都是這樣,即使每次調(diào)用時分支的結(jié)果都不變:如果瀏覽器支持內(nèi)置 XHR,那么它就一直支持了迟几,那么這
種測試就變得沒必要了消请。即使只有一個 if 語句的代碼,也肯定要比沒有 if 語句的慢类腮,所以如果 if 語句不必每次執(zhí)行臊泰,那么代碼可以運(yùn)行地更快一些。
??解決方案就是稱之為惰性載入的技巧蚜枢。

??惰性載入表示函數(shù)執(zhí)行的分支僅會發(fā)生一次缸逃。

??有兩種實(shí)現(xiàn)惰性載入的方式,第一種就是在函數(shù)被調(diào)用時再處理函數(shù)厂抽。在第一次調(diào)用的過程中需频,該函數(shù)會被覆蓋為另外一個按合適方式執(zhí)行的函數(shù),這樣任何對原函數(shù)的調(diào)用都不用再經(jīng)過執(zhí)行的分支了修肠。例如贺辰,可以用下面的方式使用惰性載入重寫 createXHR()户盯。

function createXHR() {
    if (typeof XMLHttpReauest != 'undefined') {
        createXHR = function () {
            return new XMLHttpReauest();
        };
    } else if (typeof ActiveXObject != 'undefined') {
        createXHR = function () {
            if (typof arguments.callee.activeXString != 'string') {
                var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'],
                    i,
                    len;

                for(i = 0, len = versions.length; i < len; i++) {
                    try {
                        new ActiveXObject(versions[i]);
                        arguments.callee.activeXString = versions[i];
                        break;
                    } catch(ex) {
                        // 跳過
                    }
                }
            }
            return new ActiveXObject(arguments.callee,activeXString);
        };
    } else {
        createXHR = function () {
            throw new Error('No XHR object available');
        }
    }
    return createXHR();
}

??在這個惰性載入的 createXHR() 中嵌施,if 語句的每一個分支都會為 createXHR 變量賦值,有效覆蓋了原有的函數(shù)莽鸭。最后一步便是調(diào)用新賦的函數(shù)吗伤。下一次調(diào)用 createXHR() 的時候,就會直接調(diào)用被分配的函數(shù)硫眨,這樣就不用再次執(zhí)行 if 語句了足淆。

??第二種實(shí)現(xiàn)惰性載入的方式是在聲明函數(shù)時就指定適當(dāng)?shù)暮瘮?shù)。這樣礁阁,第一次調(diào)用函數(shù)時就不會損失性能了巧号,而在代碼首次加載時會損失一點(diǎn)性能。以下就是按照這一思路重寫前面例子的結(jié)果姥闭。

var createXHR = (function () {
    if (typeof XMLHttpReauest != 'undefined') {
        return function () {
            return new XMLHttpReauest();
        };
    } else if (typeof ActiveXObject != 'undefined') {
        return function () {
            if (typof arguments.callee.activeXString != 'string') {
                var versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'],
                    i,
                    len;

                for(i = 0, len = versions.length; i < len; i++) {
                    try {
                        new ActiveXObject(versions[i]);
                        arguments.callee.activeXString = versions[i];
                        break;
                    } catch(ex) {
                        // 跳過
                    }
                }
            }
            return new ActiveXObject(arguments.callee,activeXString);
        };
    } else {
        return function () {
            throw new Error('No XHR object available');
        }
    }
    return createXHR();
})();

??這個例子中使用的技巧是創(chuàng)建一個匿名丹鸿、自執(zhí)行的函數(shù),用以確定應(yīng)該使用哪一個函數(shù)實(shí)現(xiàn)棚品。實(shí)際的邏輯都一樣靠欢。不一樣的地方就是第一行代碼(使用 var 定義函數(shù))、新增了自執(zhí)行的匿名函數(shù)铜跑,另外
每個分支都返回正確的函數(shù)定義门怪,以便立即將其賦值給 createXHR()。

??惰性載入函數(shù)的優(yōu)點(diǎn)是只在執(zhí)行分支代碼時犧牲一點(diǎn)兒性能锅纺。至于哪種方式更合適掷空,就要看你的具體需求而定了。不過這兩種方式都能避免執(zhí)行不必要的代碼。

1.4坦弟、函數(shù)綁定

??另一個日益流行的高級技巧叫做函數(shù)綁定疼电。函數(shù)綁定要創(chuàng)建一個函數(shù),可以在特定的 this 環(huán)境中以指定參數(shù)調(diào)用另一個函數(shù)减拭。該技巧常常和回調(diào)函數(shù)與事件處理程序一起使用蔽豺,以便在將函數(shù)作為變量傳遞的同時保留代碼執(zhí)行環(huán)境。請看以下例子:

var handler = {
    message: 'Event handled',

    handleClick: function (event) {
        alert(this.message);
    }
}

var btn = document.getElementById('my-btn');
EventUtil.addHandler(btn, 'click', handler.handleClick);

??在上面這個例子中拧粪,創(chuàng)建了一個叫做 handler 的對象修陡。handler.handleClick() 方法被分配為一個 DOM 按鈕的事件處理程序。當(dāng)按下該按鈕時可霎,就調(diào)用該函數(shù)魄鸦,顯示一個警告框。雖然貌似警告框應(yīng)該顯示 Event handled 癣朗,然而實(shí)際上顯示的是 undefiend 拾因。
??這個問題在于沒有保存 handler.handleClick() 的環(huán)境,所以 this 對象最后是指向了 DOM 按鈕而非 handler(在 IE8 中旷余,this 指向 window绢记。)可以如下面例子所示,使用一個閉包來修正這個問題正卧。

var handler = {
    message: 'Event handled',

    handleClick: function (event) {
        console.log(this.message);
    }
}

var btn = document.getElementById('my-btn');
EventUtil.addHandler(btn, 'click', function (event) {
    handler.handleClick(event);
});

??這個解決方案在 onclick 事件處理程序內(nèi)使用了一個閉包直接調(diào)用 handler.handleClick()蠢熄。

??當(dāng)然修档,這是特定于這段代碼的解決方案梯影。創(chuàng)建多個閉包可能會令代碼變得難于理解和調(diào)試访锻。因此红氯,很多 JavaScript 庫實(shí)現(xiàn)了一個可以將函數(shù)綁定到指定環(huán)境的函數(shù)发魄。這個函數(shù)一般都叫 bind()莹捡。

??一個簡單的 bind() 函數(shù)接受一個函數(shù)和一個環(huán)境纲刀,并返回一個在給定環(huán)境中調(diào)用給定函數(shù)的函數(shù)雅潭,并且將所有參數(shù)原封不動傳遞過去罐盔。語法如下:

function bind(fn, context) {
    return function () {
        return fn.apply(context, arguments)
    }
}

??這個函數(shù)似乎簡單但绕,但其功能是非常強(qiáng)大的。在 bind() 中創(chuàng)建了一個閉包翘骂,閉包使用 apply() 調(diào)用傳入的函數(shù)壁熄,并給 apply() 傳遞 context 對象和參數(shù)。注意這里使用的 arguments 對象是內(nèi)部函數(shù)的碳竟,而非 bind() 的草丧。當(dāng)調(diào)用返回的函數(shù)時,它會在給定環(huán)境中執(zhí)行被傳入的函數(shù)并給出所有參數(shù)莹桅。

??bind() 函數(shù)按如下方式使用:

var handler = {
    message: 'Event handled',

    handleClick: function (event) {
        console.log(this.message);
    }
}

var btn = document.getElementById('my-btn');

EventUtil.addHandler(btn, 'click', bind(handler.handleClick, handler));

??在這個例子中昌执,我們用 bind() 函數(shù)創(chuàng)建了一個保持了執(zhí)行環(huán)境的函數(shù)烛亦,并將其傳給 EventUtil.addHandler()。event 對象也被傳給了該函數(shù)懂拾,如下所示:

var handler = {
    message: 'Event handled',

    handleClick: function (event) {
        console.log(this.message + ':' + event.type);
    }
}

var btn = document.getElementById('my-btn');
EventUtil.addHandler(btn, 'click', bind(handler.handleClick, handler));

??handler.handleClick() 方法和平時一樣獲得了 event 對象煤禽,因?yàn)樗械膮?shù)都通過被綁定的函數(shù)直接傳給了它。
??ECMAScript 5 為所有函數(shù)定義了一個原生的 bind() 方法岖赋,進(jìn)一步簡單了操作檬果。換句話說,你不用再自己定義 bind() 函數(shù)了唐断,而是可以直接在函數(shù)上調(diào)用這個方法选脊。例如:

var handler = {
    message: 'Event handled',

    handleClick: function (event) {
        console.log(this.message + ':' + event.type);
    }
}

var btn = document.getElementById('my-btn');
EventUtil.addHandler(btn, 'click', handler.handleClick.bind(handler));

??原生的 bind() 方法與前面介紹的自定義 bind() 方法類似,都是要傳入作為 this 值的對象脸甘。支持原生 bind() 方法的瀏覽器有 IE9+恳啥、Firefox 4+和 Chrome。

??只要是將某個函數(shù)指針以值的形式進(jìn)行傳遞丹诀,同時該函數(shù)必須在特定環(huán)境中執(zhí)行钝的,被綁定函數(shù)的效用就突顯出來了。它們主要用于事件處理程序以及 setTimeout() 和 setInterval()铆遭。
??然而硝桩,被綁定函數(shù)與普通函數(shù)相比有更多的開銷,它們需要更多內(nèi)存疚脐,同時也因?yàn)槎嘀睾瘮?shù)調(diào)用稍微慢一點(diǎn)亿柑,所以最好只在必要時使用。

1.5棍弄、函數(shù)柯里化

??與函數(shù)綁定緊密相關(guān)的主題是函數(shù)柯里化(function currying),它用于創(chuàng)建已經(jīng)設(shè)置好了一個或多個參數(shù)的函數(shù)疟游。
??函數(shù)柯里化的基本方法和函數(shù)綁定是一樣的:使用一個閉包返回一個函數(shù)呼畸。兩者的區(qū)別在于,當(dāng)函數(shù)被調(diào)用時颁虐,返回的函數(shù)還需要設(shè)置一些傳入的參數(shù)蛮原。請看以下例子。

function add(num1, num2) {
    return num1 + num2;
}

function curriedAdd(num2) {
    return add(5, num2);
}

console.log(add(2, 3)); // 5
console.log(curriedAdd(3)); // 8

??這段代碼定義了兩個函數(shù):add() 和 curriedAdd()另绩。后者本質(zhì)上是在任何情況下第一個參數(shù)為 5 的 add() 版本儒陨。盡管從技術(shù)上來說 curriedAdd() 并非柯里化的函數(shù),但它很好地展示了其概念笋籽。

??柯里化函數(shù)通常由以下步驟動態(tài)創(chuàng)建:調(diào)用另一個函數(shù)并為它傳入要柯里化的函數(shù)和必要參數(shù)蹦漠。下面是創(chuàng)建柯里化函數(shù)的通用方式。

function curry(fn) {
    var args = Array.prototype.slice.call(arguments, 1);
    return function () {
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return fn.apply(null, finalArgs);
    };
}

??curry() 函數(shù)的主要工作就是將被返回函數(shù)的參數(shù)進(jìn)行排序车海。
??curry() 的第一個參數(shù)是要進(jìn)行柯里化的函數(shù)笛园,其他參數(shù)是要傳入的值。為了獲取第一個參數(shù)之后的所有參數(shù),在 arguments 對象上調(diào)用了 slice() 方法研铆,并傳入?yún)?shù) 1 表示被返回的數(shù)組包含從第二個參數(shù)開始的所有參數(shù)埋同。然后 args 數(shù)組包含了來自外部函數(shù)的參數(shù)。
??在內(nèi)部函數(shù)中棵红,創(chuàng)建了 innerArgs 數(shù)組用來存放所有傳入的參數(shù)(又一次用到了 slice())凶赁。有了存放來自外部函數(shù)和內(nèi)部函數(shù)的參數(shù)數(shù)組后,就可以使用 concat() 方法將它們組合為 finalArgs逆甜,然后使用 apply() 將結(jié)果傳遞給該函數(shù)哟冬。注意這個函數(shù)并沒有考慮到執(zhí)行環(huán)境,所以調(diào)用 apply() 時第一個參數(shù)是 null忆绰。curry() 函數(shù)可以按以下方式應(yīng)用浩峡。

function add(num1, num2) {
    return num1 + num2;
}

var curriedAdd = curry(add, 5);
console.log(curriedAdd(3)); // 8

??在這個例子中,創(chuàng)建了第一個參數(shù)綁定為 5 的 add() 的柯里化版本错敢。當(dāng)調(diào)用 curriedAdd() 并傳入 3 時翰灾,3 會成為 add() 的第二個參數(shù),同時第一個參數(shù)依然是 5稚茅,最后結(jié)果便是和 8纸淮。你也可以像下面例子這樣給出所有的函數(shù)參數(shù):

function add(num1, num2) {
    return num1 + num2;
}

var curriedAdd = curry(add, 5, 12);
console.log(curriedAdd()); // 17

??在這里,柯里化的 add() 函數(shù)兩個參數(shù)都提供了亚享,所以以后就無需再傳遞它們了咽块。

??函數(shù)柯里化還常常作為函數(shù)綁定的一部分包含在其中,構(gòu)造出更為復(fù)雜的 bind() 函數(shù)欺税。例如:

function bind(fun, context) {
    var args = Array.prototype.slice.call(arguments, 2);
    return function () {
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return fn.apply(context, finalArgs);
    };
}

??對 curry() 函數(shù)的主要更改在于傳入的參數(shù)個數(shù)侈沪,以及它如何影響代碼的結(jié)果。curry() 僅僅接受一個要包裹的函數(shù)作為參數(shù)晚凿,而 bind() 同時接受函數(shù)和一個 object 對象亭罪。
??這表示給被綁定的函數(shù)的參數(shù)是從第三個開始而不是第二個,這就要更改 slice() 的第一處調(diào)用歼秽。另一處更改是在倒數(shù)第 3 行將 object 對象傳給 apply()应役。當(dāng)使用 bind() 時,它會返回綁定到給定環(huán)境的函數(shù)燥筷,并且可能它其中某些函數(shù)參數(shù)已經(jīng)被設(shè)好箩祥。當(dāng)你想除了 event 對象再額外給事件處理程序傳遞參數(shù)時,這非常有用肆氓,例如:

var handler = {
    message: 'Event handled',

    handleClick: function (name, event) {
        console.log(this.message + ':' + name + ':' + event.type);
    }
}

var btn = document.getElementById('my-btn');
EventUtil.addHandler(btn, 'click', bind(handler.handleClick, handler, 'my-btn'));

??在這個更新過的例子中袍祖,handler.handleClick() 方法接受了兩個參數(shù):要處理的元素的名字和 event 對象。作為第三個參數(shù)傳遞給 bind() 函數(shù)的名字做院,又被傳遞給了 handler.handleClick()盲泛,而 handler.handleClick() 也會同時接收到 event 對象濒持。

??ECMAScript 5 的 bind() 方法也實(shí)現(xiàn)函數(shù)柯里化,只要在 this 的值之后再傳入另一個參數(shù)即可寺滚。

var handler = {
    message: 'Event handled',

    handleClick: function (name, event) {
        console.log(this.message + ':' + name + ':' + event.type);
    }
}

var btn = document.getElementById('my-btn');
EventUtil.addHandler(btn, 'click', handler.handleClick.bind(handler, 'my-btn'));

??JavaScript 中的柯里化函數(shù)和綁定函數(shù)提供了強(qiáng)大的動態(tài)函數(shù)創(chuàng)建功能柑营。
??使用 bind() 還是 curry() 要根據(jù)是否需要 object 對象響應(yīng)來決定。它們都能用于創(chuàng)建復(fù)雜的算法和功能村视,當(dāng)然兩者都不應(yīng)濫用官套,因?yàn)槊總€函數(shù)都會帶來額外的開銷。

2蚁孔、防篡改對象

??JavaScript 共享的本質(zhì)一直是開發(fā)人員心頭的痛奶赔。因?yàn)槿魏螌ο蠖伎梢员辉谕画h(huán)境中運(yùn)行的代碼修改。開發(fā)人員很可能會意外地修改別人的代碼杠氢,甚至更糟糕地站刑,用不兼容的功能重寫原生對象。
??ECMAScript 5 致力于解決這個問題鼻百,可以讓開發(fā)人員定義防篡改對象(tamper-proof object)绞旅。ECMAScript 5 增加了幾個方法,通過它們可以指定對象的行為温艇。
??不過請注意:一旦把對象定義為防篡改因悲,就無法撤銷了。

2.1勺爱、不可擴(kuò)展對象

??默認(rèn)情況下晃琳,所有對象都是可以擴(kuò)展的。也就是說琐鲁,任何時候都可以向?qū)ο笾刑砑訉傩院头椒ㄎ篮怠@纾梢韵裣旅孢@樣先定義一個對象绣否,后來再給它添加一個屬性誊涯。

var person = { name: "Nicholas" };
person.age = 29; 

??即使第一行代碼已經(jīng)完整定義 person 對象,但第二行代碼仍然能給它添加屬性∷獯椋現(xiàn)在,使用 Object.preventExtensions() 方法可以改變這個行為跪呈,讓你不能再給對象添加屬性和方法段磨。例如:

var person = { name: "Nicholas" };
Object.preventExtensions(person);

person.age = 29;
console.log(person.age); //undefined 

??在調(diào)用了 Object.preventExtensions() 方法后,就不能給 person 對象添加新屬性和方法了耗绿。

??在非嚴(yán)格模式下苹支,給對象添加新成員會導(dǎo)致靜默失敗,因此 person.age 將是 undefined误阻。而在嚴(yán)格模式下债蜜,嘗試給不可擴(kuò)展的對象添加新成員會導(dǎo)致拋出錯誤晴埂。

??雖然不能給對象添加新成員,但已有的成員則絲毫不受影響寻定。你仍然還可以修改和刪除已有的成員儒洛。

??另外,使用 Object.isExtensible() 方法還可以確定對象是否可以擴(kuò)展狼速。

var person = { name: "Nicholas" };
console.log(Object.isExtensible(person)); // true

Object.preventExtensions(person);
console.log(Object.isExtensible(person)); // false 

2.2琅锻、密封對象

??ECMAScript 5 為對象定義的第二個保護(hù)級別是密封對象(sealed object)。密封對象不可擴(kuò)展向胡,而且已有成員的[[Configurable]]特性將被設(shè)置為 false恼蓬。這就意味著不能添加、刪除屬性和方法僵芹,因?yàn)椴荒苁褂?Object.defineProperty() 把數(shù)據(jù)屬性修改為訪問器屬性处硬,或者相反。屬性值是可以修改的拇派。
??要密封對象荷辕,可以使用 Object.seal()方法。

var person = { name: "Nicholas" };
Object.seal(person);

person.age = 29;
console.log(person.age); // undefined

delete person.name;
console.log(person.name); // "Nicholas" 

??在這個例子中攀痊,添加 age 屬性的行為被忽略了桐腌。而嘗試刪除 name 屬性的操作也被忽略了,因此這個屬性沒有受任何影響苟径。這是在非嚴(yán)格模式下的行為案站。在嚴(yán)格模式下,嘗試添加或刪除對象成員都會導(dǎo)致拋出錯誤棘街。

??使用 Object.isSealed() 方法可以確定對象是否被密封了蟆盐。因?yàn)楸幻芊獾膶ο蟛豢蓴U(kuò)展,所以用 Object.isExtensible() 檢測密封的對象也會返回 false遭殉。

var person = { name: "Nicholas" };
console.log(Object.isExtensible(person)); // true
console.log(Object.isSealed(person)); // false

Object.seal(person);
console.log(Object.isExtensible(person)); // false
console.log(Object.isSealed(person)); // true

2.3石挂、 凍結(jié)的對象

??最嚴(yán)格的防篡改級別是凍結(jié)對象(frozen object)。凍結(jié)的對象既不可擴(kuò)展险污,又是密封的痹愚,而且對象數(shù)據(jù)屬性的[[Writable]]特性會被設(shè)置為 false。如果定義[[Set]]函數(shù)蛔糯,訪問器屬性仍然是可寫的拯腮。

??ECMAScript 5 定義的 Object.freeze() 方法可以用來凍結(jié)對象。

var person = { name: "Nicholas" };
Object.freeze(person);

person.age = 29;
console.log(person.age); // undefined

delete person.name;
console.log(person.name); // "Nicholas"

person.name = "Greg";
console.log(person.name); // "Nicholas"

??與密封和不允許擴(kuò)展一樣蚁飒,對凍結(jié)的對象執(zhí)行非法操作在非嚴(yán)格模式下會被忽略动壤,而在嚴(yán)格模式下會拋出錯誤。

??當(dāng)然淮逻,也有一個 Object.isFrozen() 方法用于檢測凍結(jié)對象琼懊。因?yàn)閮鼋Y(jié)對象既是密封的又是不可擴(kuò)展的阁簸,所以用 Object.isExtensible() 和 Object.isSealed() 檢測凍結(jié)對象將分別返回 false 和 true。

var person = { name: "Nicholas" };
console.log(Object.isExtensible(person)); // true
console.log(Object.isSealed(person)); // false
console.log(Object.isFrozen(person)); // false

Object.freeze(person);
console.log(Object.isExtensible(person)); // false
console.log(Object.isSealed(person)); // true
console.log(Object.isFrozen(person)); // true 

??對 JavaScript 庫的作者而言哼丈,凍結(jié)對象是很有用的启妹。因?yàn)?JavaScript 庫最怕有人意外(或有意)地修改了庫中的核心對象。凍結(jié)(或密封)主要的庫對象能夠防止這些問題的發(fā)生削祈。

3翅溺、高級定時器

??使用 setTimeout() 和 setInterval() 創(chuàng)建的定時器可以用于實(shí)現(xiàn)有趣且有用的功能。雖然人們對 JavaScript 的定時器存在普遍的誤解髓抑,認(rèn)為它們是線程咙崎,其實(shí) JavaScript 是運(yùn)行與單線程的環(huán)境中的,而定時器僅僅只是計(jì)劃代碼在未來的某個時間執(zhí)行吨拍。
??執(zhí)行時機(jī)是不能保證的褪猛,因?yàn)樵陧撁娴纳芷谥校煌瑫r間可能有其他代碼在控制 JavaScript 進(jìn)程羹饰。在頁面下載完成后的代碼運(yùn)行伊滋、事件處理程序、Ajax 回調(diào)函數(shù)都必須使用同樣的線程來執(zhí)行队秩。
??實(shí)際上笑旺,瀏覽器負(fù)責(zé)進(jìn)行排序,指派某段代碼在某個時間點(diǎn)運(yùn)行的優(yōu)先級馍资。

??可以把 JavaScript 想象成在時間線上運(yùn)行的筒主。
??當(dāng)頁面載入時,首先執(zhí)行的是任何包含在<script>元素中的代碼鸟蟹,通常是頁面生命周期后面要用到的一些簡單的函數(shù)和變量的聲明乌妙,不過有時候也包含一些初始數(shù)據(jù)的處理。
??在這之后建钥,JavaScript 進(jìn)程將等待更多代碼執(zhí)行藤韵。
??當(dāng)進(jìn)程空閑的時候,下一個代碼會被觸發(fā)并立刻執(zhí)行熊经。例如泽艘,當(dāng)點(diǎn)擊某個按鈕時,onclick 事件處理程序會立刻執(zhí)行镐依,只要 JavaScript 進(jìn)程處于空閑狀態(tài)悉盆。
??這樣一個頁面的時間線類似于下圖:

??除了主 JavaScript 執(zhí)行進(jìn)程外,還有一個需要再進(jìn)程下一次空閑時執(zhí)行的代碼隊(duì)列馋吗。隨著頁面在其生命周期中的推移,代碼會按照執(zhí)行順序添加入隊(duì)列秋秤。
??例如宏粤,當(dāng)某個按鈕被按下時脚翘,它的事件處理程序代碼就會被添加到隊(duì)列中,并在下一個可能的時間里執(zhí)行绍哎。
??當(dāng)接收到某個 Ajax 響應(yīng)時来农,回調(diào)函數(shù)的代碼會被添加到隊(duì)列總。在 JavaScript 中沒有任何代碼是立刻執(zhí)行的崇堰,但一旦進(jìn)程空閑則盡快執(zhí)行沃于。

??定時器對隊(duì)列的工作方式是,當(dāng)特定時間過去后將代碼插入海诲。注意繁莹,給隊(duì)列添加代碼并不意味著對它立刻執(zhí)行,而只能表示它會盡快執(zhí)行特幔。
??設(shè)定一個 150ms 后執(zhí)行的定時器不代表到了 150ms 代碼就立刻執(zhí)行咨演,它表示代碼會在 150ms 后被加入到隊(duì)列中,如果在這個時間點(diǎn)上蚯斯,隊(duì)列中沒有其它東西薄风,那么這段代碼就會被執(zhí)行,表面上看上去好像代碼就在精確指定的時間點(diǎn)上執(zhí)行了拍嵌。其他情況下遭赂,代碼可能明顯地等待更長時間才執(zhí)行。
??請看一下代碼:

var btn = document.getElementById('my-btn');
btn.onclick = function() {
    setTimeout(function() {
        document.getElementById('message').style.visibility = 'visible';
    }, 250);
    // 其他代碼
};

??在這里給一個按鈕設(shè)置了一個事件處理程序横辆。事件處理程序設(shè)置了一個 250ms 后調(diào)用的定時器撇他。點(diǎn)擊該按鈕后,首先將 onclick 事件處理程序加入隊(duì)列龄糊。該程序執(zhí)行后才設(shè)置定時器逆粹,再有 250ms 后,指定的代碼才被添加到隊(duì)列中等待執(zhí)行炫惩。實(shí)際上僻弹,對 setTimeout() 的調(diào)用表示要晚點(diǎn)執(zhí)行某些代碼。

??關(guān)于定時器要記住的最重要的事情是他嚷,指定的時間間隔表示何時將定時器的代碼添加到隊(duì)列蹋绽,而不是何時實(shí)際執(zhí)行代碼。
??如果前面例子中的 onclick 事件處理程序執(zhí)行了 300ms筋蓖,那么定時器的代碼至少要在定時器設(shè)置之后的 300ms 后才會被執(zhí)行卸耘。隊(duì)列中所有的代碼都要等到 JavaScript 進(jìn)程空閑之后才能執(zhí)行,而不管它們是如何添加到隊(duì)列中的粘咖。

??如上圖所示蚣抗,盡管在 255ms 處添加了定時器代碼,但這時候還不能執(zhí)行瓮下,因?yàn)?onclick 事件處理程序仍在運(yùn)行翰铡。定時器代碼最早能執(zhí)行的時機(jī)是在 300ms 處钝域,即 onclick 事件處理程序結(jié)束之后。

??實(shí)際上 Firefox 中定時器的實(shí)現(xiàn)還能讓你確定定時器過了多久才執(zhí)行锭魔,這需要傳遞一個實(shí)際執(zhí)行的時間與指定的間隔的差值例证。示例:

// 僅 Firefox 中
setTimeout(function(diff) {
    if (diff > 0) {
        // 晚調(diào)用
    } else if (diff < 0) {
        // 早調(diào)用
    } else {
        // 調(diào)用及時
    }
}, 250);

??執(zhí)行完一套代碼后,JavaScript 進(jìn)程返回一段很短的時間迷捧,這樣頁面上的其他處理就可以進(jìn)行了织咧。由于 JavaScript 進(jìn)程會阻塞其他頁面處理,所以必須有這些小間隔來防止用戶界面被鎖定(代碼長時間運(yùn)行中還有可能出現(xiàn))漠秋。這樣設(shè)置一個定時器笙蒙,可以確保在定時器代碼執(zhí)行前至少有一個進(jìn)程間隔。

3.1膛堤、重復(fù)的定時器

??使用 setInterval() 創(chuàng)建的定時器確保了定時器代碼規(guī)則地插入隊(duì)列中手趣。這個方式的問題在于,定時器代碼可能在代碼再次被添加到隊(duì)列之前還沒有完成執(zhí)行肥荔,結(jié)果導(dǎo)致定時器代碼連續(xù)運(yùn)行好幾次绿渣,而之間沒有任何停頓。幸好燕耿,JavaScript 引擎夠聰明中符,能避免這個問題。
??當(dāng)使用 setInterval() 時誉帅,僅當(dāng)沒有該定時器的任何其他代碼實(shí)例時淀散,才將定時器代碼添加到隊(duì)列中。這確保了定時器代碼加入到隊(duì)列中的最小時間間隔為指定間隔蚜锨。
??這種重復(fù)定時器的規(guī)則有兩個問題:
????(1) 某些間隔會被跳過档插;
????(2) 多個定時器的代碼執(zhí)行之間的間隔可能會比預(yù)期的小。

??假設(shè)亚再,某個 onclick 事件處理程序使用 setInterval() 設(shè)置了一個 200ms 間隔的重復(fù)定時器郭膛。如果事件處理程序花了 300ms 多一點(diǎn)的時間完成,同時定時器代碼也花了差不多的時間氛悬,就會同時出現(xiàn)跳過間隔且連續(xù)運(yùn)行定時器代碼的情況则剃。如下入所示:

??這個例子中的第 1 個定時器是在 205ms 處添加到隊(duì)列中的,但是直到過了 300ms 處才能夠執(zhí)行如捅。當(dāng)執(zhí)行這個定時器代碼時棍现,在 405ms 處又給隊(duì)列添加了另外一個副本。在下一個間隔镜遣,即 605ms 處己肮,第一個定時器代碼仍在運(yùn)行,同時在隊(duì)列中已經(jīng)有了一個定時器代碼的實(shí)例。結(jié)果是朴肺,在這個時間點(diǎn)上的定時器代碼不會被添加到隊(duì)列中窖剑。結(jié)果在 5ms 處添加的定時器代碼結(jié)束之后,405ms 處添加的定時器代碼就立刻執(zhí)行戈稿。
??為了避免setInterval() 的重復(fù)定時器的這2個缺點(diǎn),你可以用如下模式使用鏈?zhǔn)?setTimeout() 調(diào)用讶舰。

setTimeout(function(){
    // 處理中
    setTimeout(arguments.callee, interval);
}, interval);

??這個模式鏈?zhǔn)秸{(diào)用了 setTimeout()鞍盗,每次函數(shù)執(zhí)行的時候都會創(chuàng)建一個新的定時器。第二個 setTimeout() 調(diào)用使用了arguments.callee 來獲取對當(dāng)前執(zhí)行的函數(shù)的引用跳昼,并為其設(shè)置另外一個定時器般甲。
??這樣做的好處是,在前一個定時器代碼執(zhí)行完之前鹅颊,不會向隊(duì)列插入新的定時器代碼敷存,確保不會有任何缺失的間隔。而且堪伍,它可以保證在下一次定時器代碼執(zhí)行之前锚烦,至少要等待指定的間隔,避免了連續(xù)的運(yùn)行帝雇。這個模式主要用于重復(fù)定時器涮俄,如下例所示。

setTimeout(function(){
    var div = document.getElementById("myDiv");
    left = parseInt(div.style.left) + 5;
    div.style.left = left + "px";

    if (left < 200){ 
        setTimeout(arguments.callee, 50); 
    }
}, 50);

??這段定時器代碼每次執(zhí)行的時候?qū)⒁粋€<div>元素向右移動尸闸,當(dāng)左坐標(biāo)在 200 像素的時候停止彻亲。

??JavaScript 動畫中使用這個模式很常見。每個瀏覽器窗口吮廉、標(biāo)簽頁苞尝、或者 frame 都有其各自的代碼執(zhí)行隊(duì)列。這意味著宦芦,進(jìn)行跨 frame 或者跨窗口的定時調(diào)用宙址,當(dāng)代碼同時執(zhí)行的時候可能會導(dǎo)致競爭條件。無論何時需要使用這種通信類型踪旷,最好是在接收 frame 或者窗口中創(chuàng)建一個定時器來執(zhí)行代碼曼氛。

3.2、Yielding Processes

??運(yùn)行在瀏覽器中的 JavaScript 都被分配了一個確定數(shù)量的資源令野。不同于桌面應(yīng)用往往能夠隨意控制他們要的內(nèi)存大小和處理器時間舀患,JavaScript 被嚴(yán)格限制了,以防止惡意的 Web 程序員把用戶的計(jì)算機(jī)
搞掛了气破。
??其中一個限制是長時間運(yùn)行腳本的制約聊浅,如果代碼運(yùn)行超過特定的時間或者特定語句數(shù)量就不讓它繼續(xù)執(zhí)行。如果代碼達(dá)到了這個限制,會彈出一個瀏覽器錯誤的對話框低匙,告訴用戶某個腳本會用過長的時間執(zhí)行旷痕,詢問是允許其繼續(xù)執(zhí)行還是停止它。所有 JavaScript 開發(fā)人員的目標(biāo)就是顽冶,確保用戶永遠(yuǎn)不會在瀏覽器中看到這個令人費(fèi)解的對話框朽寞。定時器是繞開此限制的方法之一。

??腳本長時間運(yùn)行的問題通常是由兩個原因之一造成的:過長的胆萧、過深嵌套的函數(shù)調(diào)用或者是進(jìn)行大量處理的循環(huán)纱意。
??這兩者中,后者是較為容易解決的問題间景。長時間運(yùn)行的循環(huán)通常遵循以下模式:

for (var i=0, len=data.length; i < len; i++){
    process(data[i]);
}

??這個模式的問題在于要處理的項(xiàng)目的數(shù)量在運(yùn)行前是不可知的佃声。如果完成 process() 要花 100ms,只有 2 個項(xiàng)目的數(shù)組可能不會造成影響倘要,但是 10 個的數(shù)組可能會導(dǎo)致腳本要運(yùn)行一秒鐘才能完成圾亏。數(shù)組中的項(xiàng)目數(shù)量直接關(guān)系到執(zhí)行完該循環(huán)的時間長度。
??同時由于 JavaScript 的執(zhí)行是一個阻塞操作封拧,腳本運(yùn)行所花時間越久志鹃,用戶無法與頁面交互的時間也越久。
??在展開該循環(huán)之前哮缺,你需要回答以下兩個重要的問題弄跌。

  • 該處理是否必須同步完成?如果這個數(shù)據(jù)的處理會造成其他運(yùn)行的阻塞尝苇,那么最好不要改動它铛只。不過,如果你對這個問題的回答確定為“否”糠溜,那么將某些處理推遲到以后是個不錯的備選項(xiàng)淳玩。
  • 數(shù)據(jù)是否必須按順序完成?通常非竿,數(shù)組只是對項(xiàng)目的組合和迭代的一種簡便的方法而無所謂順序蜕着。如果項(xiàng)目的順序不是非常重要,那么可能可以將某些處理推遲到以后红柱。

??當(dāng)你發(fā)現(xiàn)某個循環(huán)占用了大量時間承匣,同時對于上述兩個問題,你的回答都是“否”锤悄,那么你就可以使用定時器分割這個循環(huán)韧骗。
??這是一種叫做 數(shù)組分塊(array chunking)的技術(shù),小塊小塊地處理數(shù)組零聚,通常每次一小塊袍暴⌒┦蹋基本的思路是為要處理的項(xiàng)目創(chuàng)建一個隊(duì)列,然后使用定時器取出下一個要處理的項(xiàng)目進(jìn)行處理政模,接著再設(shè)置另一個定時器岗宣。基本的模式如下淋样。

setTimeout(function(){
    // 取出下一個條目并處理
    var item = array.shift();
    process(item);

    // 若還有條目耗式,再設(shè)置另一個定時器
    if(array.length > 0){
        setTimeout(arguments.callee, 100);
    }
}, 100);

??在數(shù)組分塊模式中,array 變量本質(zhì)上就是一個“待辦事宜”列表习蓬,它包含了要處理的項(xiàng)目纽什。使用 shift() 方法可以獲取隊(duì)列中下一個要處理的項(xiàng)目,然后將其傳遞給某個函數(shù)躲叼。如果在隊(duì)列中還有其他
項(xiàng)目,則設(shè)置另一個定時器企巢,并通過 arguments.callee 調(diào)用同一個匿名函數(shù)枫慷。要實(shí)現(xiàn)數(shù)組分塊非常簡單,可以使用以下函數(shù)浪规。

function chunk(array, process, context){
    setTimeout(function(){
        var item = array.shift();
        process.call(context, item);

        if (array.length > 0){
            setTimeout(arguments.callee, 100);
        }
    }, 100);
}

??chunk() 方法接受三個參數(shù):要處理的項(xiàng)目的數(shù)組或听,用于處理項(xiàng)目的函數(shù),以及可選的運(yùn)行該函數(shù)的環(huán)境笋婿。函數(shù)內(nèi)部用了之前描述過的基本模式誉裆,通過 call() 調(diào)用的 process() 函數(shù),這樣可以設(shè)置一個合適的執(zhí)行環(huán)境(如果必須)缸濒。
??定時器的時間間隔設(shè)置為了 100ms足丢,使得 JavaScript 進(jìn)程有時間在處理項(xiàng)目的事件之間轉(zhuǎn)入空閑。你可以根據(jù)你的需要更改這個間隔大小庇配,不過 100ms 在大多數(shù)情況下效果不錯斩跌。可以按如下所示使用該函數(shù):

var data = [12, 123, 1234, 453, 436, 23, 23, 5, 4123, 45, 346, 5634, 2234, 345, 342];

function printValue(item){
    var div = document.getElementById("myDiv");
    div.innerHTML += item + "<br>";
}

chunk(data, printValue);

??這個例子使用 printValue() 函數(shù)將 data 數(shù)組中的每個值輸出到一個<div>元素捞慌。由于函數(shù)處在全局作用域內(nèi)耀鸦,因此無需給 chunk() 傳遞一個 context 對象。
??必須當(dāng)心的地方是啸澡,傳遞給 chunk() 的數(shù)組是用作一個隊(duì)列的袖订,因此當(dāng)處理數(shù)據(jù)的同時,數(shù)組中的條目也在改變嗅虏。如果你想保持原數(shù)組不變洛姑,則應(yīng)該將該數(shù)組的克隆傳遞給 chunk(),如下例所示:

chunk(data.concat(), printValue);

??當(dāng)不傳遞任何參數(shù)調(diào)用某個數(shù)組的 concat() 方法時旋恼,將返回和原來數(shù)組中項(xiàng)目一樣的數(shù)組吏口。這樣你就可以保證原數(shù)組不會被該函數(shù)更改奄容。

??數(shù)組分塊的重要性在于它可以將多個項(xiàng)目的處理在執(zhí)行隊(duì)列上分開,在每個項(xiàng)目處理之后产徊,給予其他的瀏覽器處理機(jī)會運(yùn)行昂勒,這樣就可能避免長時間運(yùn)行腳本的錯誤。
??一旦某個函數(shù)需要花 50ms 以上的時間完成舟铜,那么最好看看能否將任務(wù)分割為一系列可以使用定時器的小任務(wù)戈盈。

3.3、函數(shù)節(jié)流

??瀏覽器中某些計(jì)算和處理要比其他的昂貴很多谆刨。例如塘娶,DOM 操作比起非 DOM 交互需要更多的內(nèi)存和 CPU 時間。
??連續(xù)嘗試進(jìn)行過多的 DOM 相關(guān)操作可能會導(dǎo)致瀏覽器掛起痊夭,有時候甚至?xí)罎⒌蟀丁S绕湓?IE 中使用 onresize 事件處理程序的時候容易發(fā)生,當(dāng)調(diào)整瀏覽器大小的時候她我,該事件會連續(xù)觸發(fā)虹曙。
??在 onresize 事件處理程序內(nèi)部如果嘗試進(jìn)行 DOM 操作,其高頻率的更改可能會讓瀏覽器崩潰番舆。
??為了繞開這個問題酝碳,你可以使用定時器對該函數(shù)進(jìn)行節(jié)流。

??函數(shù)節(jié)流背后的基本思想是指恨狈,某些代碼不可以在沒有間斷的情況連續(xù)重復(fù)執(zhí)行疏哗。
??第一次調(diào)用函數(shù),創(chuàng)建一個定時器禾怠,在指定的時間間隔之后運(yùn)行代碼返奉。當(dāng)?shù)诙握{(diào)用該函數(shù)時,它會清除前一次的定時器并設(shè)置另一個刃宵。如果前一個定時器已經(jīng)執(zhí)行過了衡瓶,這個操作就沒有任何意義。然而牲证,如果前一個定時器尚未執(zhí)行哮针,其實(shí)就是將其替換為一個新的定時器。目的是只有在執(zhí)行函數(shù)的請求停止了一段時間之后才執(zhí)行坦袍。以下是該模式的基本形式:

var processor = {
    timeoutId: null,

    // 實(shí)際進(jìn)行處理的方法
    performProcessing: function(){
        // 實(shí)際執(zhí)行的代碼
    },

    // 初始處理調(diào)用的方法
    process: function(){
        clearTimeout(this.timeoutId);

        var that = this;
        this.timeoutId = setTimeout(function(){
            that.performProcessing();
        }, 100);
    }
};
// 嘗試開始執(zhí)行
processor.process();

??在這段代碼中十厢,創(chuàng)建了一個叫做 processor 對象。這個對象還有 2 個方法:process() 和 performProcessing()捂齐。前者是初始化任何處理所必須調(diào)用的蛮放,后者則實(shí)際進(jìn)行應(yīng)完成的處理。
??當(dāng)調(diào)用了 process()奠宜,第一步是清除存好的 timeoutId包颁,來阻止之前的調(diào)用被執(zhí)行瞻想。然后,創(chuàng)建一個新的定時器調(diào)用 performProcessing()娩嚼。由于 setTimeout() 中用到的函數(shù)的環(huán)境總是 window蘑险,所以有必要保存 this 的引用以方便以后使用。
??時間間隔設(shè)為了 100ms岳悟,這表示最后一次調(diào)用 process() 之后至少 100ms 后才會調(diào)用 performProcessing()佃迄。所以如果 100ms 之內(nèi)調(diào)用了 process() 共 20 次,performanceProcessing() 仍只會被調(diào)用一次贵少。
??這個模式可以使用 throttle() 函數(shù)來簡化呵俏,這個函數(shù)可以自動進(jìn)行定時器的設(shè)置和清除,如下例所示:

function throttle(method, context) {
    clearTimeout(method.tId);
    method.tId= setTimeout(function(){
        method.call(context);
    }, 100);
}

??throttle() 函數(shù)接受兩個參數(shù):要執(zhí)行的函數(shù)以及在哪個作用域中執(zhí)行滔灶。上面這個函數(shù)首先清除之前設(shè)置的任何定時器普碎。定時器 ID 是存儲在函數(shù)的 tId 屬性中的,第一次把方法傳遞給 throttle() 的時候录平,這個屬性可能并不存在随常。接下來,創(chuàng)建一個新的定時器萄涯,并將其 ID 儲存在方法的 tId 屬性中。
??如果這是第一次對這個方法調(diào)用 throttle() 的話唆鸡,那么這段代碼會創(chuàng)建該屬性涝影。定時器代碼使用 call() 來確保方法在適當(dāng)?shù)沫h(huán)境中執(zhí)行。如果沒有給出第二個參數(shù)争占,那么就在全局作用域內(nèi)執(zhí)行該方法燃逻。

??前面提到過,節(jié)流在 resize 事件中是最常用的臂痕。如果你基于該事件來改變頁面布局的話伯襟,最好控制處理的頻率,以確保瀏覽器不會在極短的時間內(nèi)進(jìn)行過多的計(jì)算握童。例如姆怪,假設(shè)有一個<div/>元素需要保持它的高度始終等同于寬度。那么實(shí)現(xiàn)這一功能的 JavaScript 可以如下編寫:

window.onresize = function(){
    var div = document.getElementById("myDiv");
    div.style.height = div. offsetWidth + "px";
};

??這段非常簡單的例子有兩個問題可能會造成瀏覽器運(yùn)行緩慢澡绩。首先稽揭,要計(jì)算 offsetWidth 屬性,如果該元素或者頁面上其他元素有非常復(fù)雜的 CSS 樣式肥卡,那么這個過程將會很復(fù)雜溪掀。其次,設(shè)置某個元素的高度需要對頁面進(jìn)行回流來令改動生效步鉴。如果頁面有很多元素同時應(yīng)用了相當(dāng)數(shù)量的 CSS 的話揪胃,這又需要很多計(jì)算璃哟。這就可以用到 throttle() 數(shù),如下例所示:

function resizeDiv(){
    var div = document.getElementById("myDiv");
    div.style.height = div.offsetWidth + "px";
}

window.onresize = function(){
    throttle(resizeDiv);
};

??這里喊递,調(diào)整大小的功能被放入了一個叫做 resizeDiv() 的單獨(dú)函數(shù)中随闪。然后 onresize 事件處理程序調(diào)用 throttle() 并傳入 resizeDiv 函數(shù),而不是直接調(diào)用 resizeDiv()册舞。
??多數(shù)情況下蕴掏,用戶是感覺不到變化的,雖然給瀏覽器節(jié)省的計(jì)算可能會非常大调鲸。
??只要代碼是周期性執(zhí)行的盛杰,都應(yīng)該使用節(jié)流,但是你不能控制請求執(zhí)行的速率藐石。這里展示的 throttle()函數(shù)用了 100ms 作為間隔即供,你當(dāng)然可以根據(jù)你的需要來修改它。

4于微、自定義事件

??事件是一種叫做觀察者的設(shè)計(jì)模式逗嫡,這是一種創(chuàng)建松散耦合代碼的技術(shù)。

??對象可以發(fā)布事件株依,用來表示在該對象生命周期中某個有趣的時刻到了驱证。然后其他對象可以觀察該對象,等待這些有趣的時刻到來并通過運(yùn)行代碼來響應(yīng)恋腕。

??觀察者模式由兩類對象組成:主體觀察者抹锄。主體負(fù)責(zé)發(fā)布事件,同時觀察者通過訂閱這些事件來觀察該主體荠藤。
??該模式的一個關(guān)鍵概念是主體并不知道觀察者的任何事情伙单,也就是說它可以獨(dú)自存在并正常運(yùn)作即使觀察者不存在。從另一方面來說哈肖,觀察者知道主體并能注冊事件的回調(diào)函數(shù)(事件處理程序)吻育。
??涉及 DOM 上時,DOM 元素便是主體淤井,你的事件處理代碼便是觀察者布疼。

??事件是與 DOM 交互的最常見的方式,但它們也可以用于非 DOM 代碼中——通過實(shí)現(xiàn)自定義事件庄吼。
??自定義事件背后的概念是創(chuàng)建一個管理事件的對象缎除,讓其他對象監(jiān)聽那些事件。實(shí)現(xiàn)此功能的基本模式可以如下定義:

function EventTarget(){
    this.handlers = {};
}

EventTarget.prototype = {
    constructor: EventTarget,
    addHandler: function(type, handler){
        if (typeof this.handlers[type] == "undefined"){
            this.handlers[type] = [];
        }

        this.handlers[type].push(handler);
    },

    fire: function(event){
        if (!event.target){
            event.target = this;
        }
        if (this.handlers[event.type] instanceof Array){
            var handlers = this.handlers[event.type];
            for (var i=0, len=handlers.length; i < len; i++){
                handlers[i](event);
            }
        }
    },
    removeHandler: function(type, handler){
        if (this.handlers[type] instanceof Array){
            var handlers = this.handlers[type];
            for (var i=0, len=handlers.length; i < len; i++){
                if (handlers[i] === handler){
                    break;
                }
            }
            handlers.splice(i, 1);
        }
    }
};

??EventTarget 類型有一個單獨(dú)的屬性 handlers总寻,用于儲存事件處理程序器罐。還有三個方法:
??addHandler() ,用于注冊給定類型事件的事件處理程序渐行;
??fire() 轰坊,用于觸發(fā)一個事件铸董;
??removeHandler(),用于注銷某個事件類型的事件處理程序肴沫。

??addHandler() 方法接受兩個參數(shù):事件類型和用于處理該事件的函數(shù)粟害。當(dāng)調(diào)用該方法時,會進(jìn)行一次檢查颤芬,看看 handlers 屬性中是否已經(jīng)存在一個針對該事件類型的數(shù)組悲幅;如果沒有,則創(chuàng)建一個新的站蝠。然后使用 push() 將該處理程序添加到數(shù)組的末尾汰具。
??如果要觸發(fā)一個事件,要調(diào)用 fire() 函數(shù)菱魔。該方法接受一個單獨(dú)的參數(shù)留荔,是一個至少包含 type 屬性的對象。fire() 方法先給 event 對象設(shè)置一個 target 屬性澜倦,如果它尚未被指定的話聚蝶。然后它就查找對應(yīng)該事件類型的一組處理程序,調(diào)用各個函數(shù)藻治,并給出 event 對象碘勉。因?yàn)檫@些都是自定義事件,所以 event 對象上還需要的額外信息由你自己決定桩卵。
??removeHandler() 方法是 addHandler() 的輔助恰聘,它們接受的參數(shù)一樣:事件的類型和事件處理程序。這個方法搜索事件處理程序的數(shù)組找到要刪除的處理程序的位置吸占。如果找到了,則使用 break 操作符退出 for 循環(huán)凿宾。然后使用 splice() 方法將該項(xiàng)目從數(shù)組中刪除矾屯。然后,使用 EventTarget 類型的自定義事件可以如下使用:

function handleMessage(event){
    alert("Message received: " + event.message);
}

// 創(chuàng)建一個新對象
var target = new EventTarget();

// 添加一個事件處理程序
target.addHandler("message", handleMessage);

// 觸發(fā)事件
target.fire({ type: "message", message: "Hello world!"});

// 刪除事件處理程序
target.removeHandler("message", handleMessage);

// 再次初厚,應(yīng)沒有處理程序
target.fire({ type: "message", message: "Hello world!"});

??在這段代碼中件蚕,定義了 handleMessage() 函數(shù)用于處理 message 事件。它接受 event 對象并輸出 message 屬性产禾。調(diào)用 target 對象的 addHandler() 方法并傳給"message"以及 handleMessage() 函數(shù)排作。
??在接下來的一行上,調(diào)用了 fire()函數(shù)亚情,并傳遞了包含 2 個屬性妄痪,即 type 和 message 的對象直接量。它會調(diào)用 message 事件的事件處理程序楞件,這樣就會顯示一個警告框(來自 handleMessage())衫生。
??然后刪除了事件處理程序裳瘪,這樣即使事件再次觸發(fā),也不會顯示任何警告框罪针。

??因?yàn)檫@種功能是封裝在一種自定義類型中的彭羹,其他對象可以繼承 EventTarget 并獲得這個行為,如下例所示:

function Person(name, age){
    EventTarget.call(this);
    this.name = name;
    this.age = age;
}

inheritPrototype(Person, EventTarget);

Person.prototype.say = function(message){
    this.fire({type: "message", message: message});
};

??cPerson 類型使用了寄生組合繼承方法來繼承 EventTarget泪酱。一旦調(diào)用了 say() 方法派殷,便觸發(fā)了事件,它包含了消息的細(xì)節(jié)墓阀。在某種類型的另外的方法中調(diào)用 fire() 方法是很常見的毡惜,同時它通常不是公開調(diào)用的。這段代碼可以照如下方式使用:

function handleMessage(event){
    alert(event.target.name + " says: " + event.message);
}

//創(chuàng)建新 person
var person = new Person("Nicholas", 29);

//添加一個事件處理程序
person.addHandler("message", handleMessage);

//在該對象上調(diào)用 1 個方法岂津,它觸發(fā)消息事件
person.say("Hi there.");

??這個例子中的 handleMessage() 函數(shù)顯示了某人名字(通過 event.target.name 獲得)的一個警告框和消息正文虱黄。當(dāng)調(diào)用 say() 方法并傳遞一個消息時,就會觸發(fā) message 事件吮成。接下來橱乱,它又會調(diào)用 handleMessage() 函數(shù)并顯示警告框。

??當(dāng)代碼中存在多個部分在特定時刻相互交互的情況下粱甫,自定義事件就非常有用了泳叠。這時,如果每個對象都有對其他所有對象的引用茶宵,那么整個代碼就會緊密耦合危纫,同時維護(hù)也變得很困難,因?yàn)閷δ硞€對象的修改也會影響到其他對象乌庶。
??使用自定義事件有助于解耦相關(guān)對象种蝶,保持功能的隔絕。在很多情況中瞒大,觸發(fā)事件的代碼和監(jiān)聽事件的代碼是完全分離的螃征。

5、拖放

??拖放是一種非常流行的用戶界面模式透敌。它的概念很簡單:單擊某個對象盯滚,并按住鼠標(biāo)按鈕不放,將鼠標(biāo)移動到另一個區(qū)域酗电,然后釋放鼠標(biāo)按鈕將對象“放”在這里魄藕。
??拖放功能也流行到了 Web 上,成為了一些更傳統(tǒng)的配置界面的一種候選方案撵术。
??拖放的基本概念很簡單:創(chuàng)建一個絕對定位的元素背率,使其可以用鼠標(biāo)移動。這個技術(shù)源自一種叫做“鼠標(biāo)拖尾”的經(jīng)典網(wǎng)頁技巧。
??鼠標(biāo)拖尾是一個或者多個圖片在頁面上跟著鼠標(biāo)指針移動退渗。
??單元素鼠標(biāo)拖尾的基本代碼需要為文檔設(shè)置一個 onmousemove 事件處理程序移稳,它總是將指定元素移動到鼠標(biāo)指針的位置,如下面的例子所示:

EventUtil.addHandler(document, 'mousemove', function (event) {
    var myDiv = document.getElementById('myDiv');
    myDiv.style.left = event.clientX + 'px';
    myDiv.style.top = event.clientY + 'px';
});

??在上述例子中会油,元素的 left 和 top 坐標(biāo)設(shè)置為了 event 對象的 clientX 和 clientY 屬性个粱,這就將元素放到了視口中指針的位置上。它的效果是一個元素始終跟隨者指針在頁面上移動翻翩。只要正確的時刻(當(dāng)鼠標(biāo)按鈕按下的時候)實(shí)現(xiàn)該功能都许,并在之后刪除它(當(dāng)釋放鼠標(biāo)按鈕時),就可以實(shí)現(xiàn)拖放了嫂冻。最簡單的拖放界面可用以下代碼實(shí)現(xiàn):

var DragDrop = function () {
    var dragging = null;

    function handleEvent(event) {
        
        // 獲取事件和目標(biāo)
        event = EventUtil.getEvent(event);
        var target = EventUtil.getTarget(event);

        // 確定事件類型
        switch(event.type) {
            case 'mousedown': 
                if (target.className,indexOf('draggable') > -1) {
                    dragging = target;
                }
                break;

            case 'mousemove': 
                if (dragging !== null) {

                    // 指定位置
                    dragging.style.left = event.clientX + 'px';
                    dragging.style.top = event.clientY + 'px';
                }
                break;

            case 'mouseup':
                dragging = null;
                break;
        }
    };

    // 公共接口
    return {
        enable: function () {
            EventUtil.addHandler(document, 'mousedown', handleEvent);
            EventUtil.addHandler(document, 'mousemove', handleEvent);
            EventUtil.addHandler(document, 'mouseup', handleEvent);
        },
        disable: function () {
            EventUtil.removeHandler(document, 'mousedown', handleEvent);
            EventUtil.removeHandler(document, 'mousemove', handleEvent);
            EventUtil.removeHandler(document, 'mouseup', handleEvent);
        }
    }
}();

??DragDrop 對象封裝了拖放的所有基本功能胶征。這是一個單例對象,并使用了模塊模式來隱藏某些實(shí)現(xiàn)細(xì)節(jié)桨仿。dragging 變量起初是 null睛低,將會存放被拖放的元素,所以當(dāng)變量不為 null 時服傍,就知道正在拖動某個東西钱雷。
??handleEvent() 函數(shù)處理拖放功能中的所有的三個鼠標(biāo)事件。它首先獲取 event 對象和事件目標(biāo)的引用吹零。之后罩抗,用一個 switch 語句確定要觸發(fā)哪個事件樣式。當(dāng) mousedown 事件發(fā)生時灿椅,會檢查 target 的 class 是否包含"draggable"類套蒂,如果是,那么將 target 存放到 dragging 中茫蛹。這個技巧可以很方便地通過標(biāo)記語言而非 JavaScript 腳本來確定可拖動的元素操刀。
??handleEvent() 的 mousemove 情況和前面的代碼一樣,不過要檢查 dragging 是否為 null婴洼。當(dāng)它不是 null馍刮,就知道 dragging 就是要拖動的元素,這樣就會把它放到恰當(dāng)?shù)奈恢蒙稀?br> ??mouseup 情況就僅僅是將 dragging 重置為 null窃蹋,讓 mousemove 事件中的判斷失效。
??DragDrop 還有兩個公共方法:enable() 和 disabled()静稻,它們只是相應(yīng)添加和刪除所有的事件處理程序警没。這兩個函數(shù)提供了額外的對拖放功能的控制手段。
??要使用 DragDrop 對象振湾,只要在頁面哈桑包含這些代碼并調(diào)用 enable()杀迹。拖放會自動針對所有包含"draggable"類的元素啟用,示例押搪;

<div class="draggable" style="position: absolute; background: red"></div>

??注意為了元素能被拖放树酪,它必須是絕對定位的浅碾。

5.1、修繕拖動功能

??當(dāng)你試了上面的例子之后续语,你會發(fā)現(xiàn)元素的左上角總是和指針在一起垂谢。這個結(jié)果對用戶來說有一點(diǎn)不爽,因?yàn)楫?dāng)鼠標(biāo)開始移動的時候疮茄,元素好像是突然跳了一下滥朱。
??理想情況是,這個動作應(yīng)該看上去好像這個元素是被指針“拾起”的力试,也就是說當(dāng)在拖動元素的時候徙邻,用戶點(diǎn)擊的那一點(diǎn)就是指針應(yīng)該保持的位置,如下圖所示:

??要達(dá)到需要的效果畸裳,必須做一些額外的計(jì)算缰犁。你需要計(jì)算元素左上角和指針位置之間的差值。這個差值應(yīng)該在 mousedown 事件發(fā)生的時候確定怖糊,并且一直保持帅容,直到 mouseup 事件發(fā)生。通過將 event 的 clientX 和 clientY 屬性與該元素的 offsetLeft 和 offsetTop 屬性進(jìn)行比較蓬抄,就可以算出水平方向和垂直方向上需要多少空間丰嘉,如下圖所示:

??為了保存 x 和 y 坐標(biāo)上的差值,還需要幾個變量嚷缭。diffX 和 diffY 這些變量需要在 onmousemove 事件處理程序中用到饮亏,來對元素進(jìn)行適當(dāng)?shù)亩ㄎ唬缦旅娴睦铀尽?/p>

var DragDrop = function(){
    var dragging = null,
        diffX = 0,
        diffY = 0;

    function handleEvent(event){

        // 獲取事件和目標(biāo)
        event = EventUtil.getEvent(event);
        var target = EventUtil.getTarget(event);

        // 確定事件類型
        switch(event.type){
            case "mousedown":
                if (target.className.indexOf("draggable") > -1){
                    dragging = target;
                    diffX = event.clientX - target.offsetLeft;
                    diffY = event.clientY - target.offsetTop;
                }
                break;

            case "mousemove":
                if (dragging !== null){
                    // 指定位置
                    dragging.style.left = (event.clientX - diffX) + "px";
                    dragging.style.top = (event.clientY - diffY) + "px";
                }
                break;

            case "mouseup": 
                dragging = null;
                break;
        }
    };

    // 公共接口
    return {
        enable: function(){
            EventUtil.addHandler(document, "mousedown", handleEvent);
            EventUtil.addHandler(document, "mousemove", handleEvent);
            EventUtil.addHandler(document, "mouseup", handleEvent);
        },
        disable: function(){
            EventUtil.removeHandler(document, "mousedown", handleEvent);
            EventUtil.removeHandler(document, "mousemove", handleEvent);
            EventUtil.removeHandler(document, "mouseup", handleEvent);
        }
    }
}();

??diffX 和 diffY 變量是私有的阅爽,因?yàn)橹挥?handleEvent() 函數(shù)需要用到它們路幸。當(dāng) mousedown 事件發(fā)生時,通過 clientX 減去目標(biāo)的 offsetLeft付翁,clientY 減去目標(biāo)的 offsetTop简肴,可以計(jì)算到這兩個變量的值。當(dāng)觸發(fā)了 mousemove 事件后百侧,就可以使用這些變量從指針坐標(biāo)中減去砰识,得到最終的坐標(biāo)。最后得到一個更加平滑的拖動體驗(yàn)佣渴,更加符合用戶所期望的方式辫狼。

5.2、添加自定義事件

??拖放功能還不能真正應(yīng)用起來辛润,除非能知道什么時候拖動開始了膨处。從這點(diǎn)上看,前面的代碼沒有提供任何方法表示拖動開始、正在拖動或者已經(jīng)結(jié)束真椿。
??這時鹃答,可以使用自定義事件來指示這幾個事件的發(fā)生,讓應(yīng)用的其他部分與拖動功能進(jìn)行交互突硝。

??由于 DragDrop 對象是一個使用了模塊模式的單例测摔,所以需要進(jìn)行一些更改來使用 EventTarget 類型。首先狞换,創(chuàng)建一個新的 EventTarget 對象避咆,然后添加 enable() 和 disable() 方法,最后返回這個對象修噪〔榭猓看以下內(nèi)容。

var DragDrop = function(){

    var dragdrop = new EventTarget(),

        dragging = null,
        diffX = 0,
        diffY = 0;

    function handleEvent(event){

        // 獲取事件和目標(biāo)
        event = EventUtil.getEvent(event);
        var target = EventUtil.getTarget(event);

        // 確定事件類型
        switch(event.type){
            case "mousedown":
                if (target.className.indexOf("draggable") > -1){
                    dragging = target;
                    diffX = event.clientX - target.offsetLeft;
                    diffY = event.clientY - target.offsetTop;

                    dragdrop.fire({
                        type: 'dragstart', 
                        target: dragging, 
                        x: event.clientX, 
                        y: event.clientY
                    });
                }
                break;

            case "mousemove":
                if (dragging !== null){
                    // 指定位置
                    dragging.style.left = (event.clientX - diffX) + "px";
                    dragging.style.top = (event.clientY - diffY) + "px";

                    // 觸發(fā)自定義事件
                    dragdrop.fire({
                        type: 'drag', 
                        target: dragging, 
                        x: event.clientX, 
                        y: event.clientY
                    });
                }
                break;

            case "mouseup": 

                dragdrop.fire({
                    type: 'dragend', 
                    target: dragging, 
                    x: event.clientX, 
                    y: event.clientY
                });

                dragging = null;
                break;
        }
    };

    // 公共接口
    dragdrop.enable = function(){
        EventUtil.addHandler(document, "mousedown", handleEvent);
        EventUtil.addHandler(document, "mousemove", handleEvent);
        EventUtil.addHandler(document, "mouseup", handleEvent);
    };
    dragdrop.disable = function(){
        EventUtil.removeHandler(document, "mousedown", handleEvent);
        EventUtil.removeHandler(document, "mousemove", handleEvent);
        EventUtil.removeHandler(document, "mouseup", handleEvent);
    };
    
    return dragdrop;
}();

??這段代碼定義了三個事件:dragstart黄琼、drag 和 dragend樊销。它們都將被拖動的元素設(shè)置為了 target,并給出了 x 和 y 屬性來表示當(dāng)前的位置脏款。
??它們觸發(fā)于 dragdrop 對象上围苫,之后在返回對象前給對象增加 enable() 和 disable() 方法。這些模塊模式中的細(xì)小更改令 DragDrop 對象支持了事件撤师,如下:

DragDrop.addHandler('dragstart', function (event) {
    var status = document.getElementById('status');
    status.innerHTML = 'Started dragging' + event.target.id;
});

DragDrop.addHandler('drag', function (event) {
    var status = document.getElementById('status');
    status.innerHTML += '<br/> Dragged ' + event.target.id + ' to (' + event.x + ',' + event.y + ')';
});

DragDrop.addHandler('dragend', function (event) {
    var status = document.getElementById('status');
    status.innerHTML += '<br/> Dropped ' + event.target.id + ' at (' + event.x + ',' + event.y + ')';
});

??這里剂府,為 DragDrop 對象的每個事件添加了事件處理程序。還使用了一個元素來實(shí)現(xiàn)被拖動的元素當(dāng)前的狀態(tài)和位置剃盾。一旦元素被放下了腺占,就可以看到從它一開始被拖動之后經(jīng)過的所有的中間步驟。
??為 DragDrop 添加自定義事件可以使這個對象更健壯痒谴,它將可以在網(wǎng)絡(luò)應(yīng)用中處理復(fù)雜的拖放功能衰伯。

小結(jié)

??JavaScript 中的函數(shù)非常強(qiáng)大,因?yàn)樗鼈兪堑谝活悓ο蠡怠J褂瞄]包和函數(shù)環(huán)境切換意鲸,還可以有很多使用函數(shù)的強(qiáng)大方法。
??可以創(chuàng)建作用域安全的構(gòu)造函數(shù)尽爆,確保在缺少 new 操作符時調(diào)用構(gòu)造函數(shù)不會改變錯誤的環(huán)境對象怎顾。

  • 可以使用惰性載入函數(shù),將任何代碼分支推遲到第一次調(diào)用函數(shù)的時候漱贱。
  • 函數(shù)綁定可以讓你創(chuàng)建始終在指定環(huán)境中運(yùn)行的函數(shù)槐雾,同時函數(shù)柯里化可以讓你創(chuàng)建已經(jīng)填了某些參數(shù)的函數(shù)签夭。
  • 將綁定和柯里化組合起來趟畏,就能夠給你一種在任意環(huán)境中以任意參數(shù)執(zhí)行任意函數(shù)的方法赴蝇。

??ECMAScript 5 允許通過以下幾種方式來創(chuàng)建防篡改對象牡肉。

  • 不可擴(kuò)展的對象彪笼,不允許給對象添加新的屬性或方法钻注。
  • 密封的對象,也是不可擴(kuò)展的對象配猫,不允許刪除已有的屬性和方法幅恋。
  • 凍結(jié)的對象,也是密封的對象泵肄,不允許重寫對象的成員捆交。

??JavaScript 中可以使用 setTimeout() 和 setInterval() 如下創(chuàng)建定時器。

  • 定時器代碼是放在一個等待區(qū)域腐巢,直到時間間隔到了之后品追,此時將代碼添加到 JavaScript 的處理隊(duì)列中,等待下一次 JavaScript 進(jìn)程空閑時被執(zhí)行冯丙。
  • 每次一段代碼執(zhí)行結(jié)束之后肉瓦,都會有一小段空閑時間進(jìn)行其他瀏覽器處理。
  • 這種行為意味著胃惜,可以使用定時器將長時間運(yùn)行的腳本切分為一小塊一小塊可以在以后運(yùn)行的代碼段泞莉。這種做法有助于 Web 應(yīng)用對用戶交互有更積極的響應(yīng)。

??JavaScript 中經(jīng)常以事件的形式應(yīng)用觀察者模式船殉。雖然事件常常和 DOM 一起使用鲫趁,但是你也可以通過實(shí)現(xiàn)自定義事件在自己的代碼中應(yīng)用。使用自定義事件有助于將不同部分的代碼相互之間解耦利虫,讓維護(hù)更加容易挨厚,并減少引入錯誤的機(jī)會。

??拖放對于桌面和 Web 應(yīng)用都是一個非常流行的用戶界面范例列吼,它能夠讓用戶非常方便地以一種直觀的方式重新排列或者配置東西幽崩。
??在 JavaScrip 中可以使用鼠標(biāo)事件和一些簡單的計(jì)算來實(shí)現(xiàn)這種功能類型。將拖放行為和自定義事件結(jié)合起來可以創(chuàng)建一個可重復(fù)使用的框架寞钥,它能應(yīng)用于各種不同的情況下慌申。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市理郑,隨后出現(xiàn)的幾起案子蹄溉,更是在濱河造成了極大的恐慌,老刑警劉巖您炉,帶你破解...
    沈念sama閱讀 206,013評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件柒爵,死亡現(xiàn)場離奇詭異,居然都是意外死亡赚爵,警方通過查閱死者的電腦和手機(jī)棉胀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評論 2 382
  • 文/潘曉璐 我一進(jìn)店門法瑟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人唁奢,你說我怎么就攤上這事霎挟。” “怎么了麻掸?”我有些...
    開封第一講書人閱讀 152,370評論 0 342
  • 文/不壞的土叔 我叫張陵酥夭,是天一觀的道長。 經(jīng)常有香客問我脊奋,道長熬北,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,168評論 1 278
  • 正文 為了忘掉前任诚隙,我火速辦了婚禮讶隐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘最楷。我一直安慰自己整份,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,153評論 5 371
  • 文/花漫 我一把揭開白布籽孙。 她就那樣靜靜地躺著烈评,像睡著了一般。 火紅的嫁衣襯著肌膚如雪犯建。 梳的紋絲不亂的頭發(fā)上讲冠,一...
    開封第一講書人閱讀 48,954評論 1 283
  • 那天,我揣著相機(jī)與錄音适瓦,去河邊找鬼竿开。 笑死,一個胖子當(dāng)著我的面吹牛玻熙,可吹牛的內(nèi)容都是我干的否彩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,271評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼嗦随,長吁一口氣:“原來是場噩夢啊……” “哼列荔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起枚尼,我...
    開封第一講書人閱讀 36,916評論 0 259
  • 序言:老撾萬榮一對情侶失蹤贴浙,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后署恍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體崎溃,經(jīng)...
    沈念sama閱讀 43,382評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,877評論 2 323
  • 正文 我和宋清朗相戀三年盯质,在試婚紗的時候發(fā)現(xiàn)自己被綠了袁串。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片概而。...
    茶點(diǎn)故事閱讀 37,989評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖囱修,靈堂內(nèi)的尸體忽然破棺而出到腥,到底是詐尸還是另有隱情,我是刑警寧澤蔚袍,帶...
    沈念sama閱讀 33,624評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站配名,受9級特大地震影響啤咽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜渠脉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,209評論 3 307
  • 文/蒙蒙 一宇整、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧芋膘,春花似錦鳞青、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至习寸,卻和暖如春胶惰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背霞溪。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評論 1 260
  • 我被黑心中介騙來泰國打工孵滞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鸯匹。 一個月前我還...
    沈念sama閱讀 45,401評論 2 352
  • 正文 我出身青樓坊饶,卻偏偏與公主長得像,于是被迫代替她去往敵國和親殴蓬。 傳聞我的和親對象是個殘疾皇子匿级,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,700評論 2 345

推薦閱讀更多精彩內(nèi)容

  • ??JavaScript 與 HTML 之間的交互是通過事件實(shí)現(xiàn)的根蟹。 ??事件,就是文檔或?yàn)g覽器窗口中發(fā)生的一些特...
    霜天曉閱讀 3,473評論 1 11
  • 第3章 基本概念 3.1 語法 3.2 關(guān)鍵字和保留字 3.3 變量 3.4 數(shù)據(jù)類型 5種簡單數(shù)據(jù)類型:Unde...
    RickCole閱讀 5,096評論 0 21
  • 第2章 基本語法 2.1 概述 基本句法和變量 語句 JavaScript程序的執(zhí)行單位為行(line)糟秘,也就是一...
    悟名先生閱讀 4,116評論 0 13
  • python使用例子: 如果經(jīng)常需要高亮顯示輸出简逮,上面代碼編寫比較低效,可以封裝成裝飾器或者類
    摸不去的塵閱讀 2,945評論 0 1
  • 二年級 陳雨潯 我的家是一個幸改蜃快樂的一家四口(這句話有點(diǎn)拗口散庶,可以改成:“我的家是一個幸附堆撸快樂的四...
    綠沁2閱讀 453評論 0 0