??JavaScript 是一種極其靈活的語言,具有多種使用風格。
??一般來說,編寫 JavaScript 要么使用過程方式,要么使用面向對象方式。然而,由于它天生的動態屬性,這種語言還能使用更為復雜和有趣的模式。
??這些技巧要利用 ECMAScript 的語言特點、BOM 擴展和 DOM 功能來獲得強大的效果。
1、高級函數
??函數是 JavaScript 中最有趣的部分之一。它們本質上是十分簡單和過程化的,但也可以是非常復雜和動態的。一些額外的功能可以通過使用閉包來實現。此外,由于所有的函數都是對象,所以使用函數指針非常簡單的。
??這些令 JavaScript 函數不僅有趣而且強大。
1.1、安全的類型檢測
??JavaScript 內置的類型檢測機制并非完全可靠。事實上,發生錯誤否定及錯誤肯定的情況也不在少數。
??比如說 typeof 操作符,由于它有一些無法預知的行為,經常會導致檢測數據類型時得到不靠譜的結果。Safari(直至第 4 版)在對正則表示式應用 typeof 操作符時會返回"function",因此很難確定某個值到底是不是函數。
??再比如,instanceof 操作符在存在多個全局作用域(像一個頁面包含多個 frame)的情況下,也是問題多多。一個經典的例子就是像下面這樣將對象標識為數組。
var isArray = value instanceof Arrray;
??以上代碼要返回 true,value 必須是一個數組,而且還必須與 Array 構造函數在同個全局作用域中。(別忘了,Array 是 window 的屬性。)如果 value 是在另個 frame 中定義的數組,那么以上代碼就會返回 false。
??在檢測某個對象到底是原生對象還是開發人員自定義的對象的時候,也會有問題。出現這個問題的原因是瀏覽器開始原生支持 JSON 對象了。因為很多人一直在使用 DouglasCrockford 的 JSON 庫,而該庫定義了一個全局 JSON 對象。于是開發人員很難確定頁面中的 JSON 對象到底是不是原生的。
??解決上述問題的辦法都一樣。
??大家知道,在任何值上調用 Object 原生的 toString() 方法,都會返回一個 [object NativeConstructorName] 格式的字符串。每個類在內部都有一個 [[Class]] 屬性,這個屬性中就指定了上述字符串中的構造函數名。舉個例子吧。
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
??由于原生數組的構造函數名與全局作用域無關,因此使用 toString() 就能保證返回一致的值。利用這一點可以創建如下函數:
function isArray(value) {
return Object.prototype.toString.call(value) === '[object Array]';
}
??同樣,也可以基于這一思路來測試某個值是不是原生函數或正則表達式:
function isFunction(value) {
return Object.prototype.toString.call(value) === '[object Function]';
}
function isRegExp(value) {
return Object.prototype.toString.call(value) === '[object RegExp]';
}
??不過要注意,對于在 IE 中以 COM 對象形式實現的任何函數,isFunction() 都將返回 false(因為它們并非原生的 JavaScript 函數)。
??這一技巧也廣泛應用于檢測原生 JSON 對象。Object 的 toString() 方法不能檢測非原生構造函數的構造函數名。因此,開發人員定義的任何構造函數都將返回 [object Object]。有些 JavaScript 庫會包含與下面類似的代碼。
var isNativeJSON = window.JSON && Object.protptype.toString.call(JSON) === '[object JSON]';
??在 Web 開發中能夠區分原生與非原生 JavaScript 對象非常重要。只有這樣才能確切知道某個對象到底有哪些功能。這個技巧可以對任何對象給出正確的結論。
??請注意,Object.prototype.toString() 本身也可能會被修改。上面討論的技巧假設 Object.prototype.toString() 是未被修改過的原生版本。
1.2、作用域安全的構造函數
??構造函數其實就是一個使用 new 操作符調用的函數。當使用 new 調用時,構造函數內用到的 this 對象會指向新創建的對象實例,示例:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
}
var person = new Person('Nicholsa', 20, 'Software Engineer');
??上述例子中, Person 構造函數使用 this 對象給三個屬性賦值:name、age 和 job。當和 new 操作符連用時,則會創建一個新的 Person 對象,同時會給它分配這些屬性。
??問題出在當沒有使用 new 操作符來調用該構造函數的情況上。由于該 this 對象是在運行時綁定的,所以直接調用 Person(),this 會映射到全局對象 window 上,導致錯誤對象屬性的意外增加。示例:
var person = Person('Nicholas', 20, 'Software Engineer');
console.log(window.name); // 'Nicholas'
console.log(window.age); // 20
console.log(window.job); // 'Software Engineer'
??這里,原本針對 Person 實例的三個屬性被加到 window 對象上,因為構造函數時作為普通函數調用的,忽略了 new 操作符。這個問題是由 this 對象的晚綁定造成的,在這里 this 被解析成了 window 對象。
??由于 window 的 name 屬性是用于識別鏈接目標和 frame 的,所以這里對該屬性的偶然覆蓋可能會導致該頁面上出現其他錯誤。
??這個問題的解決方法就是創建一個作用域安全的構造函數。
??作用域安全的構造函數在進行任何更改前,首先確認 this 對象是正確類型的實例。如果不是,那么會創建新的實例并返回。示例:
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 構造函數添加了一個檢查并確保 this 對象是 Person 實例的 if 語句,它表示要么使用 new 操作符,要么在現有的 Person 實例環境中調用構造函數。任何一種情況下,對象初始化都能正常運行。
??如果 this 并非 Person 的實例,那么會再次使用 new 操作符調用構造函數并返回結果。最后的結果是,調用 Person 構造函數時無論是否使用 new 操作符,都會返回一個 Person 的新實例,這就避免了在全局對象上意外設置屬性。
??關于作用域安全的構造函數的貼心提示。實現這個模式后,你就鎖定了可以調用構造函數的環境。如果你使用構造函數竊取模式的繼承且不使用原型鏈,那么這個繼承很可能被破壞。這里有個例子:
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 構造函數是作用域安全的,然而 Rectangle 構造函數則不是。新創建一個 Rectangle 實例之后,這個實例應該通過 Polygon.call() 來繼承 Polygon 的 sides 屬性。但是,由于 Polygon 構造函數是作用域安全的,this 對象并非 Polygon 的實例,所以會創建并返回一個新的 Polygon 對象。Rectangle 構造函數中的 this 對象并沒有得到增長,同時 Polygon.call() 返回的值也沒有用到,所以 Rectangle 實例中就不會有 sides 屬性。
??如果構造函數竊取結合使用原型鏈或者寄生組合則可以解決這個問題。考慮以下例子:
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 實例也同時是一個Polygon實例,所以 Polygon.call() 會照原意執行,最終為 Rectangle 實例添加了 sides 屬性。
??多個程序員在同一個頁面上寫 JavaScript 代碼的環境中,作用域安全構造函數就很有用了。屆時,對全局對象意外的更改可能會導致一些常常難以追蹤的錯誤。除非你單純基于構造函數竊取來實現繼承,推薦作用域安全的構造函數作為最佳實踐。
1.3、惰性載入函數
??因為瀏覽器之間行為的差異,多數 JavaScript 代碼包含了大量的 if 語句,將執行引導到正確的代碼中。看看下面來自上一章的 createXHR() 函數。
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');
}
}
??每次調用 createXHR() 的時候,它都要對瀏覽器所支持的能力仔細檢查。首先檢查內置的 XHR,然后測試有沒有基于 ActiveX 的 XHR,最后如果都沒有發現的話就拋出一個錯誤。
??每次調用該函數都是這樣,即使每次調用時分支的結果都不變:如果瀏覽器支持內置 XHR,那么它就一直支持了,那么這
種測試就變得沒必要了。即使只有一個 if 語句的代碼,也肯定要比沒有 if 語句的慢,所以如果 if 語句不必每次執行,那么代碼可以運行地更快一些。
??解決方案就是稱之為惰性載入的技巧。
??惰性載入表示函數執行的分支僅會發生一次。
??有兩種實現惰性載入的方式,第一種就是在函數被調用時再處理函數。在第一次調用的過程中,該函數會被覆蓋為另外一個按合適方式執行的函數,這樣任何對原函數的調用都不用再經過執行的分支了。例如,可以用下面的方式使用惰性載入重寫 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 變量賦值,有效覆蓋了原有的函數。最后一步便是調用新賦的函數。下一次調用 createXHR() 的時候,就會直接調用被分配的函數,這樣就不用再次執行 if 語句了。
??第二種實現惰性載入的方式是在聲明函數時就指定適當的函數。這樣,第一次調用函數時就不會損失性能了,而在代碼首次加載時會損失一點性能。以下就是按照這一思路重寫前面例子的結果。
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();
})();
??這個例子中使用的技巧是創建一個匿名、自執行的函數,用以確定應該使用哪一個函數實現。實際的邏輯都一樣。不一樣的地方就是第一行代碼(使用 var 定義函數)、新增了自執行的匿名函數,另外
每個分支都返回正確的函數定義,以便立即將其賦值給 createXHR()。
??惰性載入函數的優點是只在執行分支代碼時犧牲一點兒性能。至于哪種方式更合適,就要看你的具體需求而定了。不過這兩種方式都能避免執行不必要的代碼。
1.4、函數綁定
??另一個日益流行的高級技巧叫做函數綁定。函數綁定要創建一個函數,可以在特定的 this 環境中以指定參數調用另一個函數。該技巧常常和回調函數與事件處理程序一起使用,以便在將函數作為變量傳遞的同時保留代碼執行環境。請看以下例子:
var handler = {
message: 'Event handled',
handleClick: function (event) {
alert(this.message);
}
}
var btn = document.getElementById('my-btn');
EventUtil.addHandler(btn, 'click', handler.handleClick);
??在上面這個例子中,創建了一個叫做 handler 的對象。handler.handleClick() 方法被分配為一個 DOM 按鈕的事件處理程序。當按下該按鈕時,就調用該函數,顯示一個警告框。雖然貌似警告框應該顯示 Event handled ,然而實際上顯示的是 undefiend 。
??這個問題在于沒有保存 handler.handleClick() 的環境,所以 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 事件處理程序內使用了一個閉包直接調用 handler.handleClick()。
??當然,這是特定于這段代碼的解決方案。創建多個閉包可能會令代碼變得難于理解和調試。因此,很多 JavaScript 庫實現了一個可以將函數綁定到指定環境的函數。這個函數一般都叫 bind()。
??一個簡單的 bind() 函數接受一個函數和一個環境,并返回一個在給定環境中調用給定函數的函數,并且將所有參數原封不動傳遞過去。語法如下:
function bind(fn, context) {
return function () {
return fn.apply(context, arguments)
}
}
??這個函數似乎簡單,但其功能是非常強大的。在 bind() 中創建了一個閉包,閉包使用 apply() 調用傳入的函數,并給 apply() 傳遞 context 對象和參數。注意這里使用的 arguments 對象是內部函數的,而非 bind() 的。當調用返回的函數時,它會在給定環境中執行被傳入的函數并給出所有參數。
??bind() 函數按如下方式使用:
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() 函數創建了一個保持了執行環境的函數,并將其傳給 EventUtil.addHandler()。event 對象也被傳給了該函數,如下所示:
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 對象,因為所有的參數都通過被綁定的函數直接傳給了它。
??ECMAScript 5 為所有函數定義了一個原生的 bind() 方法,進一步簡單了操作。換句話說,你不用再自己定義 bind() 函數了,而是可以直接在函數上調用這個方法。例如:
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。
??只要是將某個函數指針以值的形式進行傳遞,同時該函數必須在特定環境中執行,被綁定函數的效用就突顯出來了。它們主要用于事件處理程序以及 setTimeout() 和 setInterval()。
??然而,被綁定函數與普通函數相比有更多的開銷,它們需要更多內存,同時也因為多重函數調用稍微慢一點,所以最好只在必要時使用。
1.5、函數柯里化
??與函數綁定緊密相關的主題是函數柯里化(function currying),它用于創建已經設置好了一個或多個參數的函數。
??函數柯里化的基本方法和函數綁定是一樣的:使用一個閉包返回一個函數。兩者的區別在于,當函數被調用時,返回的函數還需要設置一些傳入的參數。請看以下例子。
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
??這段代碼定義了兩個函數:add() 和 curriedAdd()。后者本質上是在任何情況下第一個參數為 5 的 add() 版本。盡管從技術上來說 curriedAdd() 并非柯里化的函數,但它很好地展示了其概念。
??柯里化函數通常由以下步驟動態創建:調用另一個函數并為它傳入要柯里化的函數和必要參數。下面是創建柯里化函數的通用方式。
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() 函數的主要工作就是將被返回函數的參數進行排序。
??curry() 的第一個參數是要進行柯里化的函數,其他參數是要傳入的值。為了獲取第一個參數之后的所有參數,在 arguments 對象上調用了 slice() 方法,并傳入參數 1 表示被返回的數組包含從第二個參數開始的所有參數。然后 args 數組包含了來自外部函數的參數。
??在內部函數中,創建了 innerArgs 數組用來存放所有傳入的參數(又一次用到了 slice())。有了存放來自外部函數和內部函數的參數數組后,就可以使用 concat() 方法將它們組合為 finalArgs,然后使用 apply() 將結果傳遞給該函數。注意這個函數并沒有考慮到執行環境,所以調用 apply() 時第一個參數是 null。curry() 函數可以按以下方式應用。
function add(num1, num2) {
return num1 + num2;
}
var curriedAdd = curry(add, 5);
console.log(curriedAdd(3)); // 8
??在這個例子中,創建了第一個參數綁定為 5 的 add() 的柯里化版本。當調用 curriedAdd() 并傳入 3 時,3 會成為 add() 的第二個參數,同時第一個參數依然是 5,最后結果便是和 8。你也可以像下面例子這樣給出所有的函數參數:
function add(num1, num2) {
return num1 + num2;
}
var curriedAdd = curry(add, 5, 12);
console.log(curriedAdd()); // 17
??在這里,柯里化的 add() 函數兩個參數都提供了,所以以后就無需再傳遞它們了。
??函數柯里化還常常作為函數綁定的一部分包含在其中,構造出更為復雜的 bind() 函數。例如:
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() 函數的主要更改在于傳入的參數個數,以及它如何影響代碼的結果。curry() 僅僅接受一個要包裹的函數作為參數,而 bind() 同時接受函數和一個 object 對象。
??這表示給被綁定的函數的參數是從第三個開始而不是第二個,這就要更改 slice() 的第一處調用。另一處更改是在倒數第 3 行將 object 對象傳給 apply()。當使用 bind() 時,它會返回綁定到給定環境的函數,并且可能它其中某些函數參數已經被設好。當你想除了 event 對象再額外給事件處理程序傳遞參數時,這非常有用,例如:
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() 方法接受了兩個參數:要處理的元素的名字和 event 對象。作為第三個參數傳遞給 bind() 函數的名字,又被傳遞給了 handler.handleClick(),而 handler.handleClick() 也會同時接收到 event 對象。
??ECMAScript 5 的 bind() 方法也實現函數柯里化,只要在 this 的值之后再傳入另一個參數即可。
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 中的柯里化函數和綁定函數提供了強大的動態函數創建功能。
??使用 bind() 還是 curry() 要根據是否需要 object 對象響應來決定。它們都能用于創建復雜的算法和功能,當然兩者都不應濫用,因為每個函數都會帶來額外的開銷。
2、防篡改對象
??JavaScript 共享的本質一直是開發人員心頭的痛。因為任何對象都可以被在同一環境中運行的代碼修改。開發人員很可能會意外地修改別人的代碼,甚至更糟糕地,用不兼容的功能重寫原生對象。
??ECMAScript 5 致力于解決這個問題,可以讓開發人員定義防篡改對象(tamper-proof object)。ECMAScript 5 增加了幾個方法,通過它們可以指定對象的行為。
??不過請注意:一旦把對象定義為防篡改,就無法撤銷了。
2.1、不可擴展對象
??默認情況下,所有對象都是可以擴展的。也就是說,任何時候都可以向對象中添加屬性和方法。例如,可以像下面這樣先定義一個對象,后來再給它添加一個屬性。
var person = { name: "Nicholas" };
person.age = 29;
??即使第一行代碼已經完整定義 person 對象,但第二行代碼仍然能給它添加屬性。現在,使用 Object.preventExtensions() 方法可以改變這個行為,讓你不能再給對象添加屬性和方法。例如:
var person = { name: "Nicholas" };
Object.preventExtensions(person);
person.age = 29;
console.log(person.age); //undefined
??在調用了 Object.preventExtensions() 方法后,就不能給 person 對象添加新屬性和方法了。
??在非嚴格模式下,給對象添加新成員會導致靜默失敗,因此 person.age 將是 undefined。而在嚴格模式下,嘗試給不可擴展的對象添加新成員會導致拋出錯誤。
??雖然不能給對象添加新成員,但已有的成員則絲毫不受影響。你仍然還可以修改和刪除已有的成員。
??另外,使用 Object.isExtensible() 方法還可以確定對象是否可以擴展。
var person = { name: "Nicholas" };
console.log(Object.isExtensible(person)); // true
Object.preventExtensions(person);
console.log(Object.isExtensible(person)); // false
2.2、密封對象
??ECMAScript 5 為對象定義的第二個保護級別是密封對象(sealed object)。密封對象不可擴展,而且已有成員的[[Configurable]]特性將被設置為 false。這就意味著不能添加、刪除屬性和方法,因為不能使用 Object.defineProperty() 把數據屬性修改為訪問器屬性,或者相反。屬性值是可以修改的。
??要密封對象,可以使用 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 屬性的操作也被忽略了,因此這個屬性沒有受任何影響。這是在非嚴格模式下的行為。在嚴格模式下,嘗試添加或刪除對象成員都會導致拋出錯誤。
??使用 Object.isSealed() 方法可以確定對象是否被密封了。因為被密封的對象不可擴展,所以用 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、 凍結的對象
??最嚴格的防篡改級別是凍結對象(frozen object)。凍結的對象既不可擴展,又是密封的,而且對象數據屬性的[[Writable]]特性會被設置為 false。如果定義[[Set]]函數,訪問器屬性仍然是可寫的。
??ECMAScript 5 定義的 Object.freeze() 方法可以用來凍結對象。
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"
??與密封和不允許擴展一樣,對凍結的對象執行非法操作在非嚴格模式下會被忽略,而在嚴格模式下會拋出錯誤。
??當然,也有一個 Object.isFrozen() 方法用于檢測凍結對象。因為凍結對象既是密封的又是不可擴展的,所以用 Object.isExtensible() 和 Object.isSealed() 檢測凍結對象將分別返回 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 庫的作者而言,凍結對象是很有用的。因為 JavaScript 庫最怕有人意外(或有意)地修改了庫中的核心對象。凍結(或密封)主要的庫對象能夠防止這些問題的發生。
3、高級定時器
??使用 setTimeout() 和 setInterval() 創建的定時器可以用于實現有趣且有用的功能。雖然人們對 JavaScript 的定時器存在普遍的誤解,認為它們是線程,其實 JavaScript 是運行與單線程的環境中的,而定時器僅僅只是計劃代碼在未來的某個時間執行。
??執行時機是不能保證的,因為在頁面的生命周期中,不同時間可能有其他代碼在控制 JavaScript 進程。在頁面下載完成后的代碼運行、事件處理程序、Ajax 回調函數都必須使用同樣的線程來執行。
??實際上,瀏覽器負責進行排序,指派某段代碼在某個時間點運行的優先級。
??可以把 JavaScript 想象成在時間線上運行的。
??當頁面載入時,首先執行的是任何包含在<script>元素中的代碼,通常是頁面生命周期后面要用到的一些簡單的函數和變量的聲明,不過有時候也包含一些初始數據的處理。
??在這之后,JavaScript 進程將等待更多代碼執行。
??當進程空閑的時候,下一個代碼會被觸發并立刻執行。例如,當點擊某個按鈕時,onclick 事件處理程序會立刻執行,只要 JavaScript 進程處于空閑狀態。
??這樣一個頁面的時間線類似于下圖:
??除了主 JavaScript 執行進程外,還有一個需要再進程下一次空閑時執行的代碼隊列。隨著頁面在其生命周期中的推移,代碼會按照執行順序添加入隊列。
??例如,當某個按鈕被按下時,它的事件處理程序代碼就會被添加到隊列中,并在下一個可能的時間里執行。
??當接收到某個 Ajax 響應時,回調函數的代碼會被添加到隊列總。在 JavaScript 中沒有任何代碼是立刻執行的,但一旦進程空閑則盡快執行。
??定時器對隊列的工作方式是,當特定時間過去后將代碼插入。注意,給隊列添加代碼并不意味著對它立刻執行,而只能表示它會盡快執行。
??設定一個 150ms 后執行的定時器不代表到了 150ms 代碼就立刻執行,它表示代碼會在 150ms 后被加入到隊列中,如果在這個時間點上,隊列中沒有其它東西,那么這段代碼就會被執行,表面上看上去好像代碼就在精確指定的時間點上執行了。其他情況下,代碼可能明顯地等待更長時間才執行。
??請看一下代碼:
var btn = document.getElementById('my-btn');
btn.onclick = function() {
setTimeout(function() {
document.getElementById('message').style.visibility = 'visible';
}, 250);
// 其他代碼
};
??在這里給一個按鈕設置了一個事件處理程序。事件處理程序設置了一個 250ms 后調用的定時器。點擊該按鈕后,首先將 onclick 事件處理程序加入隊列。該程序執行后才設置定時器,再有 250ms 后,指定的代碼才被添加到隊列中等待執行。實際上,對 setTimeout() 的調用表示要晚點執行某些代碼。
??關于定時器要記住的最重要的事情是,指定的時間間隔表示何時將定時器的代碼添加到隊列,而不是何時實際執行代碼。
??如果前面例子中的 onclick 事件處理程序執行了 300ms,那么定時器的代碼至少要在定時器設置之后的 300ms 后才會被執行。隊列中所有的代碼都要等到 JavaScript 進程空閑之后才能執行,而不管它們是如何添加到隊列中的。
??如上圖所示,盡管在 255ms 處添加了定時器代碼,但這時候還不能執行,因為 onclick 事件處理程序仍在運行。定時器代碼最早能執行的時機是在 300ms 處,即 onclick 事件處理程序結束之后。
??實際上 Firefox 中定時器的實現還能讓你確定定時器過了多久才執行,這需要傳遞一個實際執行的時間與指定的間隔的差值。示例:
// 僅 Firefox 中
setTimeout(function(diff) {
if (diff > 0) {
// 晚調用
} else if (diff < 0) {
// 早調用
} else {
// 調用及時
}
}, 250);
??執行完一套代碼后,JavaScript 進程返回一段很短的時間,這樣頁面上的其他處理就可以進行了。由于 JavaScript 進程會阻塞其他頁面處理,所以必須有這些小間隔來防止用戶界面被鎖定(代碼長時間運行中還有可能出現)。這樣設置一個定時器,可以確保在定時器代碼執行前至少有一個進程間隔。
3.1、重復的定時器
??使用 setInterval() 創建的定時器確保了定時器代碼規則地插入隊列中。這個方式的問題在于,定時器代碼可能在代碼再次被添加到隊列之前還沒有完成執行,結果導致定時器代碼連續運行好幾次,而之間沒有任何停頓。幸好,JavaScript 引擎夠聰明,能避免這個問題。
??當使用 setInterval() 時,僅當沒有該定時器的任何其他代碼實例時,才將定時器代碼添加到隊列中。這確保了定時器代碼加入到隊列中的最小時間間隔為指定間隔。
??這種重復定時器的規則有兩個問題:
????(1) 某些間隔會被跳過;
????(2) 多個定時器的代碼執行之間的間隔可能會比預期的小。
??假設,某個 onclick 事件處理程序使用 setInterval() 設置了一個 200ms 間隔的重復定時器。如果事件處理程序花了 300ms 多一點的時間完成,同時定時器代碼也花了差不多的時間,就會同時出現跳過間隔且連續運行定時器代碼的情況。如下入所示:
??這個例子中的第 1 個定時器是在 205ms 處添加到隊列中的,但是直到過了 300ms 處才能夠執行。當執行這個定時器代碼時,在 405ms 處又給隊列添加了另外一個副本。在下一個間隔,即 605ms 處,第一個定時器代碼仍在運行,同時在隊列中已經有了一個定時器代碼的實例。結果是,在這個時間點上的定時器代碼不會被添加到隊列中。結果在 5ms 處添加的定時器代碼結束之后,405ms 處添加的定時器代碼就立刻執行。
??為了避免setInterval() 的重復定時器的這2個缺點,你可以用如下模式使用鏈式 setTimeout() 調用。
setTimeout(function(){
// 處理中
setTimeout(arguments.callee, interval);
}, interval);
??這個模式鏈式調用了 setTimeout(),每次函數執行的時候都會創建一個新的定時器。第二個 setTimeout() 調用使用了arguments.callee 來獲取對當前執行的函數的引用,并為其設置另外一個定時器。
??這樣做的好處是,在前一個定時器代碼執行完之前,不會向隊列插入新的定時器代碼,確保不會有任何缺失的間隔。而且,它可以保證在下一次定時器代碼執行之前,至少要等待指定的間隔,避免了連續的運行。這個模式主要用于重復定時器,如下例所示。
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);
??這段定時器代碼每次執行的時候將一個<div>元素向右移動,當左坐標在 200 像素的時候停止。
??JavaScript 動畫中使用這個模式很常見。每個瀏覽器窗口、標簽頁、或者 frame 都有其各自的代碼執行隊列。這意味著,進行跨 frame 或者跨窗口的定時調用,當代碼同時執行的時候可能會導致競爭條件。無論何時需要使用這種通信類型,最好是在接收 frame 或者窗口中創建一個定時器來執行代碼。
3.2、Yielding Processes
??運行在瀏覽器中的 JavaScript 都被分配了一個確定數量的資源。不同于桌面應用往往能夠隨意控制他們要的內存大小和處理器時間,JavaScript 被嚴格限制了,以防止惡意的 Web 程序員把用戶的計算機
搞掛了。
??其中一個限制是長時間運行腳本的制約,如果代碼運行超過特定的時間或者特定語句數量就不讓它繼續執行。如果代碼達到了這個限制,會彈出一個瀏覽器錯誤的對話框,告訴用戶某個腳本會用過長的時間執行,詢問是允許其繼續執行還是停止它。所有 JavaScript 開發人員的目標就是,確保用戶永遠不會在瀏覽器中看到這個令人費解的對話框。定時器是繞開此限制的方法之一。
??腳本長時間運行的問題通常是由兩個原因之一造成的:過長的、過深嵌套的函數調用或者是進行大量處理的循環。
??這兩者中,后者是較為容易解決的問題。長時間運行的循環通常遵循以下模式:
for (var i=0, len=data.length; i < len; i++){
process(data[i]);
}
??這個模式的問題在于要處理的項目的數量在運行前是不可知的。如果完成 process() 要花 100ms,只有 2 個項目的數組可能不會造成影響,但是 10 個的數組可能會導致腳本要運行一秒鐘才能完成。數組中的項目數量直接關系到執行完該循環的時間長度。
??同時由于 JavaScript 的執行是一個阻塞操作,腳本運行所花時間越久,用戶無法與頁面交互的時間也越久。
??在展開該循環之前,你需要回答以下兩個重要的問題。
- 該處理是否必須同步完成?如果這個數據的處理會造成其他運行的阻塞,那么最好不要改動它。不過,如果你對這個問題的回答確定為“否”,那么將某些處理推遲到以后是個不錯的備選項。
- 數據是否必須按順序完成?通常,數組只是對項目的組合和迭代的一種簡便的方法而無所謂順序。如果項目的順序不是非常重要,那么可能可以將某些處理推遲到以后。
??當你發現某個循環占用了大量時間,同時對于上述兩個問題,你的回答都是“否”,那么你就可以使用定時器分割這個循環。
??這是一種叫做 數組分塊(array chunking)的技術,小塊小塊地處理數組,通常每次一小塊。基本的思路是為要處理的項目創建一個隊列,然后使用定時器取出下一個要處理的項目進行處理,接著再設置另一個定時器。基本的模式如下。
setTimeout(function(){
// 取出下一個條目并處理
var item = array.shift();
process(item);
// 若還有條目,再設置另一個定時器
if(array.length > 0){
setTimeout(arguments.callee, 100);
}
}, 100);
??在數組分塊模式中,array 變量本質上就是一個“待辦事宜”列表,它包含了要處理的項目。使用 shift() 方法可以獲取隊列中下一個要處理的項目,然后將其傳遞給某個函數。如果在隊列中還有其他
項目,則設置另一個定時器,并通過 arguments.callee 調用同一個匿名函數。要實現數組分塊非常簡單,可以使用以下函數。
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() 方法接受三個參數:要處理的項目的數組,用于處理項目的函數,以及可選的運行該函數的環境。函數內部用了之前描述過的基本模式,通過 call() 調用的 process() 函數,這樣可以設置一個合適的執行環境(如果必須)。
??定時器的時間間隔設置為了 100ms,使得 JavaScript 進程有時間在處理項目的事件之間轉入空閑。你可以根據你的需要更改這個間隔大小,不過 100ms 在大多數情況下效果不錯。可以按如下所示使用該函數:
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() 函數將 data 數組中的每個值輸出到一個<div>元素。由于函數處在全局作用域內,因此無需給 chunk() 傳遞一個 context 對象。
??必須當心的地方是,傳遞給 chunk() 的數組是用作一個隊列的,因此當處理數據的同時,數組中的條目也在改變。如果你想保持原數組不變,則應該將該數組的克隆傳遞給 chunk(),如下例所示:
chunk(data.concat(), printValue);
??當不傳遞任何參數調用某個數組的 concat() 方法時,將返回和原來數組中項目一樣的數組。這樣你就可以保證原數組不會被該函數更改。
??數組分塊的重要性在于它可以將多個項目的處理在執行隊列上分開,在每個項目處理之后,給予其他的瀏覽器處理機會運行,這樣就可能避免長時間運行腳本的錯誤。
??一旦某個函數需要花 50ms 以上的時間完成,那么最好看看能否將任務分割為一系列可以使用定時器的小任務。
3.3、函數節流
??瀏覽器中某些計算和處理要比其他的昂貴很多。例如,DOM 操作比起非 DOM 交互需要更多的內存和 CPU 時間。
??連續嘗試進行過多的 DOM 相關操作可能會導致瀏覽器掛起,有時候甚至會崩潰。尤其在 IE 中使用 onresize 事件處理程序的時候容易發生,當調整瀏覽器大小的時候,該事件會連續觸發。
??在 onresize 事件處理程序內部如果嘗試進行 DOM 操作,其高頻率的更改可能會讓瀏覽器崩潰。
??為了繞開這個問題,你可以使用定時器對該函數進行節流。
??函數節流背后的基本思想是指,某些代碼不可以在沒有間斷的情況連續重復執行。
??第一次調用函數,創建一個定時器,在指定的時間間隔之后運行代碼。當第二次調用該函數時,它會清除前一次的定時器并設置另一個。如果前一個定時器已經執行過了,這個操作就沒有任何意義。然而,如果前一個定時器尚未執行,其實就是將其替換為一個新的定時器。目的是只有在執行函數的請求停止了一段時間之后才執行。以下是該模式的基本形式:
var processor = {
timeoutId: null,
// 實際進行處理的方法
performProcessing: function(){
// 實際執行的代碼
},
// 初始處理調用的方法
process: function(){
clearTimeout(this.timeoutId);
var that = this;
this.timeoutId = setTimeout(function(){
that.performProcessing();
}, 100);
}
};
// 嘗試開始執行
processor.process();
??在這段代碼中,創建了一個叫做 processor 對象。這個對象還有 2 個方法:process() 和 performProcessing()。前者是初始化任何處理所必須調用的,后者則實際進行應完成的處理。
??當調用了 process(),第一步是清除存好的 timeoutId,來阻止之前的調用被執行。然后,創建一個新的定時器調用 performProcessing()。由于 setTimeout() 中用到的函數的環境總是 window,所以有必要保存 this 的引用以方便以后使用。
??時間間隔設為了 100ms,這表示最后一次調用 process() 之后至少 100ms 后才會調用 performProcessing()。所以如果 100ms 之內調用了 process() 共 20 次,performanceProcessing() 仍只會被調用一次。
??這個模式可以使用 throttle() 函數來簡化,這個函數可以自動進行定時器的設置和清除,如下例所示:
function throttle(method, context) {
clearTimeout(method.tId);
method.tId= setTimeout(function(){
method.call(context);
}, 100);
}
??throttle() 函數接受兩個參數:要執行的函數以及在哪個作用域中執行。上面這個函數首先清除之前設置的任何定時器。定時器 ID 是存儲在函數的 tId 屬性中的,第一次把方法傳遞給 throttle() 的時候,這個屬性可能并不存在。接下來,創建一個新的定時器,并將其 ID 儲存在方法的 tId 屬性中。
??如果這是第一次對這個方法調用 throttle() 的話,那么這段代碼會創建該屬性。定時器代碼使用 call() 來確保方法在適當的環境中執行。如果沒有給出第二個參數,那么就在全局作用域內執行該方法。
??前面提到過,節流在 resize 事件中是最常用的。如果你基于該事件來改變頁面布局的話,最好控制處理的頻率,以確保瀏覽器不會在極短的時間內進行過多的計算。例如,假設有一個<div/>元素需要保持它的高度始終等同于寬度。那么實現這一功能的 JavaScript 可以如下編寫:
window.onresize = function(){
var div = document.getElementById("myDiv");
div.style.height = div. offsetWidth + "px";
};
??這段非常簡單的例子有兩個問題可能會造成瀏覽器運行緩慢。首先,要計算 offsetWidth 屬性,如果該元素或者頁面上其他元素有非常復雜的 CSS 樣式,那么這個過程將會很復雜。其次,設置某個元素的高度需要對頁面進行回流來令改動生效。如果頁面有很多元素同時應用了相當數量的 CSS 的話,這又需要很多計算。這就可以用到 throttle() 數,如下例所示:
function resizeDiv(){
var div = document.getElementById("myDiv");
div.style.height = div.offsetWidth + "px";
}
window.onresize = function(){
throttle(resizeDiv);
};
??這里,調整大小的功能被放入了一個叫做 resizeDiv() 的單獨函數中。然后 onresize 事件處理程序調用 throttle() 并傳入 resizeDiv 函數,而不是直接調用 resizeDiv()。
??多數情況下,用戶是感覺不到變化的,雖然給瀏覽器節省的計算可能會非常大。
??只要代碼是周期性執行的,都應該使用節流,但是你不能控制請求執行的速率。這里展示的 throttle()函數用了 100ms 作為間隔,你當然可以根據你的需要來修改它。
4、自定義事件
??事件是一種叫做觀察者的設計模式,這是一種創建松散耦合代碼的技術。
??對象可以發布事件,用來表示在該對象生命周期中某個有趣的時刻到了。然后其他對象可以觀察該對象,等待這些有趣的時刻到來并通過運行代碼來響應。
??觀察者模式由兩類對象組成:主體 和 觀察者。主體負責發布事件,同時觀察者通過訂閱這些事件來觀察該主體。
??該模式的一個關鍵概念是主體并不知道觀察者的任何事情,也就是說它可以獨自存在并正常運作即使觀察者不存在。從另一方面來說,觀察者知道主體并能注冊事件的回調函數(事件處理程序)。
??涉及 DOM 上時,DOM 元素便是主體,你的事件處理代碼便是觀察者。
??事件是與 DOM 交互的最常見的方式,但它們也可以用于非 DOM 代碼中——通過實現自定義事件。
??自定義事件背后的概念是創建一個管理事件的對象,讓其他對象監聽那些事件。實現此功能的基本模式可以如下定義:
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 類型有一個單獨的屬性 handlers,用于儲存事件處理程序。還有三個方法:
??addHandler() ,用于注冊給定類型事件的事件處理程序;
??fire() ,用于觸發一個事件;
??removeHandler(),用于注銷某個事件類型的事件處理程序。
??addHandler() 方法接受兩個參數:事件類型和用于處理該事件的函數。當調用該方法時,會進行一次檢查,看看 handlers 屬性中是否已經存在一個針對該事件類型的數組;如果沒有,則創建一個新的。然后使用 push() 將該處理程序添加到數組的末尾。
??如果要觸發一個事件,要調用 fire() 函數。該方法接受一個單獨的參數,是一個至少包含 type 屬性的對象。fire() 方法先給 event 對象設置一個 target 屬性,如果它尚未被指定的話。然后它就查找對應該事件類型的一組處理程序,調用各個函數,并給出 event 對象。因為這些都是自定義事件,所以 event 對象上還需要的額外信息由你自己決定。
??removeHandler() 方法是 addHandler() 的輔助,它們接受的參數一樣:事件的類型和事件處理程序。這個方法搜索事件處理程序的數組找到要刪除的處理程序的位置。如果找到了,則使用 break 操作符退出 for 循環。然后使用 splice() 方法將該項目從數組中刪除。然后,使用 EventTarget 類型的自定義事件可以如下使用:
function handleMessage(event){
alert("Message received: " + event.message);
}
// 創建一個新對象
var target = new EventTarget();
// 添加一個事件處理程序
target.addHandler("message", handleMessage);
// 觸發事件
target.fire({ type: "message", message: "Hello world!"});
// 刪除事件處理程序
target.removeHandler("message", handleMessage);
// 再次,應沒有處理程序
target.fire({ type: "message", message: "Hello world!"});
??在這段代碼中,定義了 handleMessage() 函數用于處理 message 事件。它接受 event 對象并輸出 message 屬性。調用 target 對象的 addHandler() 方法并傳給"message"以及 handleMessage() 函數。
??在接下來的一行上,調用了 fire()函數,并傳遞了包含 2 個屬性,即 type 和 message 的對象直接量。它會調用 message 事件的事件處理程序,這樣就會顯示一個警告框(來自 handleMessage())。
??然后刪除了事件處理程序,這樣即使事件再次觸發,也不會顯示任何警告框。
??因為這種功能是封裝在一種自定義類型中的,其他對象可以繼承 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。一旦調用了 say() 方法,便觸發了事件,它包含了消息的細節。在某種類型的另外的方法中調用 fire() 方法是很常見的,同時它通常不是公開調用的。這段代碼可以照如下方式使用:
function handleMessage(event){
alert(event.target.name + " says: " + event.message);
}
//創建新 person
var person = new Person("Nicholas", 29);
//添加一個事件處理程序
person.addHandler("message", handleMessage);
//在該對象上調用 1 個方法,它觸發消息事件
person.say("Hi there.");
??這個例子中的 handleMessage() 函數顯示了某人名字(通過 event.target.name 獲得)的一個警告框和消息正文。當調用 say() 方法并傳遞一個消息時,就會觸發 message 事件。接下來,它又會調用 handleMessage() 函數并顯示警告框。
??當代碼中存在多個部分在特定時刻相互交互的情況下,自定義事件就非常有用了。這時,如果每個對象都有對其他所有對象的引用,那么整個代碼就會緊密耦合,同時維護也變得很困難,因為對某個對象的修改也會影響到其他對象。
??使用自定義事件有助于解耦相關對象,保持功能的隔絕。在很多情況中,觸發事件的代碼和監聽事件的代碼是完全分離的。
5、拖放
??拖放是一種非常流行的用戶界面模式。它的概念很簡單:單擊某個對象,并按住鼠標按鈕不放,將鼠標移動到另一個區域,然后釋放鼠標按鈕將對象“放”在這里。
??拖放功能也流行到了 Web 上,成為了一些更傳統的配置界面的一種候選方案。
??拖放的基本概念很簡單:創建一個絕對定位的元素,使其可以用鼠標移動。這個技術源自一種叫做“鼠標拖尾”的經典網頁技巧。
??鼠標拖尾是一個或者多個圖片在頁面上跟著鼠標指針移動。
??單元素鼠標拖尾的基本代碼需要為文檔設置一個 onmousemove 事件處理程序,它總是將指定元素移動到鼠標指針的位置,如下面的例子所示:
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 坐標設置為了 event 對象的 clientX 和 clientY 屬性,這就將元素放到了視口中指針的位置上。它的效果是一個元素始終跟隨者指針在頁面上移動。只要正確的時刻(當鼠標按鈕按下的時候)實現該功能,并在之后刪除它(當釋放鼠標按鈕時),就可以實現拖放了。最簡單的拖放界面可用以下代碼實現:
var DragDrop = function () {
var dragging = null;
function handleEvent(event) {
// 獲取事件和目標
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 對象封裝了拖放的所有基本功能。這是一個單例對象,并使用了模塊模式來隱藏某些實現細節。dragging 變量起初是 null,將會存放被拖放的元素,所以當變量不為 null 時,就知道正在拖動某個東西。
??handleEvent() 函數處理拖放功能中的所有的三個鼠標事件。它首先獲取 event 對象和事件目標的引用。之后,用一個 switch 語句確定要觸發哪個事件樣式。當 mousedown 事件發生時,會檢查 target 的 class 是否包含"draggable"類,如果是,那么將 target 存放到 dragging 中。這個技巧可以很方便地通過標記語言而非 JavaScript 腳本來確定可拖動的元素。
??handleEvent() 的 mousemove 情況和前面的代碼一樣,不過要檢查 dragging 是否為 null。當它不是 null,就知道 dragging 就是要拖動的元素,這樣就會把它放到恰當的位置上。
??mouseup 情況就僅僅是將 dragging 重置為 null,讓 mousemove 事件中的判斷失效。
??DragDrop 還有兩個公共方法:enable() 和 disabled(),它們只是相應添加和刪除所有的事件處理程序。這兩個函數提供了額外的對拖放功能的控制手段。
??要使用 DragDrop 對象,只要在頁面哈桑包含這些代碼并調用 enable()。拖放會自動針對所有包含"draggable"類的元素啟用,示例;
<div class="draggable" style="position: absolute; background: red"></div>
??注意為了元素能被拖放,它必須是絕對定位的。
5.1、修繕拖動功能
??當你試了上面的例子之后,你會發現元素的左上角總是和指針在一起。這個結果對用戶來說有一點不爽,因為當鼠標開始移動的時候,元素好像是突然跳了一下。
??理想情況是,這個動作應該看上去好像這個元素是被指針“拾起”的,也就是說當在拖動元素的時候,用戶點擊的那一點就是指針應該保持的位置,如下圖所示:
??要達到需要的效果,必須做一些額外的計算。你需要計算元素左上角和指針位置之間的差值。這個差值應該在 mousedown 事件發生的時候確定,并且一直保持,直到 mouseup 事件發生。通過將 event 的 clientX 和 clientY 屬性與該元素的 offsetLeft 和 offsetTop 屬性進行比較,就可以算出水平方向和垂直方向上需要多少空間,如下圖所示:
??為了保存 x 和 y 坐標上的差值,還需要幾個變量。diffX 和 diffY 這些變量需要在 onmousemove 事件處理程序中用到,來對元素進行適當的定位,如下面的例子所示。
var DragDrop = function(){
var dragging = null,
diffX = 0,
diffY = 0;
function handleEvent(event){
// 獲取事件和目標
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 變量是私有的,因為只有 handleEvent() 函數需要用到它們。當 mousedown 事件發生時,通過 clientX 減去目標的 offsetLeft,clientY 減去目標的 offsetTop,可以計算到這兩個變量的值。當觸發了 mousemove 事件后,就可以使用這些變量從指針坐標中減去,得到最終的坐標。最后得到一個更加平滑的拖動體驗,更加符合用戶所期望的方式。
5.2、添加自定義事件
??拖放功能還不能真正應用起來,除非能知道什么時候拖動開始了。從這點上看,前面的代碼沒有提供任何方法表示拖動開始、正在拖動或者已經結束。
??這時,可以使用自定義事件來指示這幾個事件的發生,讓應用的其他部分與拖動功能進行交互。
??由于 DragDrop 對象是一個使用了模塊模式的單例,所以需要進行一些更改來使用 EventTarget 類型。首先,創建一個新的 EventTarget 對象,然后添加 enable() 和 disable() 方法,最后返回這個對象。看以下內容。
var DragDrop = function(){
var dragdrop = new EventTarget(),
dragging = null,
diffX = 0,
diffY = 0;
function handleEvent(event){
// 獲取事件和目標
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";
// 觸發自定義事件
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。它們都將被拖動的元素設置為了 target,并給出了 x 和 y 屬性來表示當前的位置。
??它們觸發于 dragdrop 對象上,之后在返回對象前給對象增加 enable() 和 disable() 方法。這些模塊模式中的細小更改令 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 對象的每個事件添加了事件處理程序。還使用了一個元素來實現被拖動的元素當前的狀態和位置。一旦元素被放下了,就可以看到從它一開始被拖動之后經過的所有的中間步驟。
??為 DragDrop 添加自定義事件可以使這個對象更健壯,它將可以在網絡應用中處理復雜的拖放功能。
小結
??JavaScript 中的函數非常強大,因為它們是第一類對象。使用閉包和函數環境切換,還可以有很多使用函數的強大方法。
??可以創建作用域安全的構造函數,確保在缺少 new 操作符時調用構造函數不會改變錯誤的環境對象。
- 可以使用惰性載入函數,將任何代碼分支推遲到第一次調用函數的時候。
- 函數綁定可以讓你創建始終在指定環境中運行的函數,同時函數柯里化可以讓你創建已經填了某些參數的函數。
- 將綁定和柯里化組合起來,就能夠給你一種在任意環境中以任意參數執行任意函數的方法。
??ECMAScript 5 允許通過以下幾種方式來創建防篡改對象。
- 不可擴展的對象,不允許給對象添加新的屬性或方法。
- 密封的對象,也是不可擴展的對象,不允許刪除已有的屬性和方法。
- 凍結的對象,也是密封的對象,不允許重寫對象的成員。
??JavaScript 中可以使用 setTimeout() 和 setInterval() 如下創建定時器。
- 定時器代碼是放在一個等待區域,直到時間間隔到了之后,此時將代碼添加到 JavaScript 的處理隊列中,等待下一次 JavaScript 進程空閑時被執行。
- 每次一段代碼執行結束之后,都會有一小段空閑時間進行其他瀏覽器處理。
- 這種行為意味著,可以使用定時器將長時間運行的腳本切分為一小塊一小塊可以在以后運行的代碼段。這種做法有助于 Web 應用對用戶交互有更積極的響應。
??JavaScript 中經常以事件的形式應用觀察者模式。雖然事件常常和 DOM 一起使用,但是你也可以通過實現自定義事件在自己的代碼中應用。使用自定義事件有助于將不同部分的代碼相互之間解耦,讓維護更加容易,并減少引入錯誤的機會。
??拖放對于桌面和 Web 應用都是一個非常流行的用戶界面范例,它能夠讓用戶非常方便地以一種直觀的方式重新排列或者配置東西。
??在 JavaScrip 中可以使用鼠標事件和一些簡單的計算來實現這種功能類型。將拖放行為和自定義事件結合起來可以創建一個可重復使用的框架,它能應用于各種不同的情況下。