高級函數(shù)
函數(shù)本質(zhì)上是很簡單且過程化的,但是由于JS天生的動(dòng)態(tài)的特性,從使用方式上可以很復(fù)雜。
安全的類型檢測
雖然JS中是有類型檢測的,但是由于瀏覽器實(shí)現(xiàn)等它們并不完全可靠。比如typeof在Safari中對正則表達(dá)式也返回function。
instanceof在存在多個(gè)全局作用域時(shí)也會(huì)把同種卻不同作用域中構(gòu)造函數(shù)的實(shí)例識別為不同的實(shí)例:
var isArray = value instanceof Array;
這個(gè)表達(dá)式要是想返回true,value必須是個(gè)數(shù)組,且必須與Array構(gòu)造函數(shù)在同一個(gè)全局作用域中,如果value是另一個(gè)全局作用域中定義的數(shù)組,那這個(gè)表達(dá)式返回false。
檢測某個(gè)對象是原生的還是開發(fā)人員自定義的對象時(shí)也會(huì)有問題。因?yàn)闉g覽器開始原生支持JSON了,而有些開發(fā)人員還是在用第三方庫來實(shí)現(xiàn)JSON,這個(gè)庫里會(huì)有全局的JSON對象,這樣想確定JSON對象是不是原生的就麻煩了。
解決這些問題的辦法就是使用Object的toString方法,這個(gè)方法會(huì)返回一個(gè)[object NativeConstructorName]格式的字符串。
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形式實(shí)現(xiàn)的函數(shù),isFunction()都會(huì)返回false。
對于JSON是否為原生的問題可以這樣:
var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON) == "[object JSON]";
作用域安全的構(gòu)造函數(shù)
之前我們說的構(gòu)造函數(shù)是這么使用的:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
}
var person = new Person("Nicholas", 29, "Software Engineer");
在這里,由于使用了new操作符,this被綁定在了新創(chuàng)建的Person對象上,如果不用new操作符直接調(diào)用Person(),this就會(huì)被綁定到window上,這顯然是不行的。
function Person(name, age, job){
if (this instanceof Person){
this.name = name;
this.age = age;
this.job = job;
} else {
return new Person(name, age, job);
}
}
var person1 = Person("Nicholas", 29, "Software Engineer");
alert(window.name); //""
alert(person1.name); //"Nicholas"
var person2 = new Person("Shelby", 34, "Ergonomist");
alert(person2.name); //"Shelby"
不過在使用了這樣作用域安全的構(gòu)造函數(shù)后,如果使用基于構(gòu)造函數(shù)竊取的繼承,就會(huì)有問題:
function Polygon(sides){
if (this instanceof Polygon) {
this.sides = sides;
this.getArea = function(){
return 0;
};
} else {
return new Polygon(sides);
}
}
function Rectangle(width, height){
//這里調(diào)用時(shí),傳進(jìn)去的this是Rectangle類型的,沒辦法拓展side屬性
Polygon.call(this, 2);
this.width = width;
this.height = height;
this.getArea = function(){
return this.width * this.height;
};
}
var rect = new Rectangle(5, 10);
alert(rect.sides); //undefined
解決方法就是使Rectangle也是Polygon的一個(gè)實(shí)例就好啦
Rectangle.prototype = new Polygon();
var rect = new Rectangle(5, 10);
alert(rect.sides); //2
惰性載入函數(shù)
由于瀏覽器差異,大量的判斷瀏覽器能力的函數(shù)需要被使用(通常是大量的if),然而這些判斷一般其實(shí)不必每次都執(zhí)行,在執(zhí)行一次后,瀏覽器的能力就確定了,以后就應(yīng)該不用在判斷了。比如:
function createXHR(){
if (typeof XMLHttpRequest != "undefined"){
return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined"){
if (typeof 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.");
}
}
這里的創(chuàng)建XHR對象的函數(shù),每次創(chuàng)建對象時(shí)都會(huì)判斷一次瀏覽器能力,這是不必要的。
惰性載入有兩種方式,第一種就是在函數(shù)第一次被調(diào)用時(shí),根據(jù)不同情況,用不同的新函數(shù)把這個(gè)函數(shù)覆蓋掉,以后調(diào)用就不需要再判斷而是直接執(zhí)行該執(zhí)行的操作。
function createXHR(){
if (typeof XMLHttpRequest != "undefined"){
createXHR = function(){
return new XMLHttpRequest();
};
} else if (typeof ActiveXObject != "undefined"){
createXHR = function(){
if (typeof 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){
//skip
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
};
} else {
createXHR = function(){
throw new Error("No XHR object available.");
};
}
return createXHR();
}
createXHR();
alert(createXHR);
第二種思路一樣,只不過是在聲明函數(shù)時(shí)就指定新函數(shù),不在第一次調(diào)用時(shí)再指定。兩種辦法其實(shí)本質(zhì)上是一樣的,看你想怎么用了。
var createXHR = (function(){
if (typeof XMLHttpRequest != "undefined"){
return function(){
return new XMLHttpRequest();
};
} else if (typeof ActiveXObject != "undefined"){
return function(){
if (typeof 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){
//skip
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
};
} else {
return function(){
throw new Error("No XHR object available.");
};
}
})();
alert(createXHR);
函數(shù)綁定
函數(shù)綁定解決的問題是調(diào)用函數(shù)時(shí)函數(shù)的this對象被改為我們不想要的對象的問題。這種情況經(jīng)常出現(xiàn)在指定事件處理函數(shù)時(shí)。看個(gè)例子:
var handler = {
message: "Event handled",
handleClick: function(event){
alert(this);
alert(this.message);
}
};
var btn = document.getElementById("myButton");
EventUtil.addHandler(btn, "click", handler.handleClick); //[object HTMLButtonElement] undefined
handler.handleClick(); // [object Object] Event handled
這里的this被改成了按鈕元素。
解決辦法就是將真正的函數(shù)套在一個(gè)閉包中,以此來保存著個(gè)函數(shù)的環(huán)境
var handler = {
message: "Event handled",
handleClick: function(event){
alert(this); // [object Object]
alert(this.message); // Event handled
} };
var btn = document.getElementById("myButton");
EventUtil.addHandler(btn, "click", function(event){
alert(this); //[object HTMLButtonElement]
handler.handleClick(event);
});
可以看到,閉包的this被改為了button,而因?yàn)檫@個(gè)閉包的保護(hù),我們的處理函數(shù)的環(huán)境保存住了。
為了不每次都手動(dòng)創(chuàng)建一個(gè)閉包,我們可以創(chuàng)建一個(gè)工具函數(shù)bind:
function bind(fn, context){
alert(arguments[0]); //fn本身
return function(){
alert(arguments[0]); //event
return fn.apply(context, arguments);
};
}
這里就是把一個(gè)函數(shù)使用apply在特定的環(huán)境下調(diào)用,注意一下arguments對象,最后應(yīng)該使用的是return過去的匿名函數(shù)(閉包)的arguments才對,這樣事件給事件處理函數(shù)傳遞的參數(shù)才能穿到我們的函數(shù)里。
var handler = {
message: "Event handled",
handleClick: function(event){
alert(this); // [object Object]
alert(this.message); // Event handled
} };
var btn = document.getElementById("myButton");
EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler));
ES5中為所有函數(shù)都定義了一個(gè)原生的bind()方法。直接使用就行。
EventUtil.addHandler(btn, "click", handler.handleClick.bind(handler));
只要是將某個(gè)函數(shù)指針以值的形式進(jìn)行傳遞,同時(shí)該函數(shù)必須在特定環(huán)境中執(zhí)行,被綁定函數(shù)的效果就顯現(xiàn)了
函數(shù)柯里化
這個(gè)是用來創(chuàng)建已經(jīng)設(shè)置好一個(gè)或多個(gè)參數(shù)的函數(shù),用一個(gè)例子看看基本思想:
function add(num1, num2){
return num1 + num2;
}
function curriedAdd(num2){
return add(5, num2);
}
alert(add(2, 3)); //5
alert(curriedAdd(3)); //8
創(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);
};
}
這個(gè)函數(shù)主要的工作就是將外部函數(shù)和內(nèi)部函數(shù)的參數(shù)都獲取到傳遞給了返回的函數(shù)中。
function add(a,b) {
alert(a);
alert(b);
alert(a+b);
}
var curriedAdd = curry(add, 5);
curriedAdd(3); //8
curriedAdd = curry(add, 3);
curriedAdd(5); //8
curriedAdd = curry(add, 5,3);
curriedAdd(); //8
可以將其利用在bind函數(shù)中來給事件處理函數(shù)傳入多個(gè)參數(shù)。
function bind(fn, 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);
};
}
var handler = {
message: "Event handled",
handleClick: function(name, event){
alert(this.message + ":"+ name + ":"+ event.type);
}
};
var btn = document.getElementById("myButton");
EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler, "myButton"));
這里要注意的是參數(shù)的順序,比如這里name和event反了可就不對了呦。
ES5中的bind也實(shí)現(xiàn)了柯里化,直接使用就可以。
EventUtil.addHandler(btn, "click", handler.handleClick.bind(handler, "myButton"));
防篡改對象
JS共享的本質(zhì)使任意對象都可被隨意修改。這樣有時(shí)很不方便。ES5增加了幾個(gè)方法來設(shè)置對象的行為。一旦將對象設(shè)置為防篡改就不能撤銷了。
不可拓展對象
不可以添加新的屬性和方法
var person = { name: "Nicholas" };
Object.preventExtensions(person);
person.age = 29;
alert(person.age); //undefined
alert(Object.isExtensible(person)); //false
person.name = "hahah";
alert(person.name); //hahah
密封的對象
不可以添加或刪除屬性,已有成員的[[Configurable]]被設(shè)置為false。
var person = { name: "Nicholas" };
Object.seal(person);
person.age = 29;
alert(person.age); //undefined
delete person.name;
alert(person.name); //"Nicholas"
alert(Object.isExtensible(person)); //false
alert(Object.isSealed(person)); //true
凍結(jié)的對象
不可拓展,密封,且對象數(shù)據(jù)屬性的[[Writable]]將被設(shè)為false。如果定義了[[Set]],訪問器屬性依然可寫。
var person = { name: "Nicholas" };
Object.freeze(person);
person.age = 29; alert(person.age); //undefined
delete person.name; alert(person.name); //"Nicholas"
person.name = "Greg"; alert(person.name); //"Nicholas"
alert(Object.isExtensible(person));//false
alert(Object.isSealed(person));//true
alert(Object.isFrozen(person));//true
高級定時(shí)器
setTimeout()和setInterval()是很實(shí)用的功能,不過有些事情是要注意的。
JS是單線程的,這就意味著定時(shí)器實(shí)際上是很有可能被阻塞的。我們在這兩個(gè)函數(shù)中所設(shè)置的定時(shí),其實(shí)是代表將代碼加入到執(zhí)行隊(duì)列的事件,如果在加入時(shí)恰巧JS是空閑的,那么這段代碼會(huì)立即被執(zhí)行,也就是說這個(gè)定時(shí)被準(zhǔn)時(shí)的執(zhí)行了。相反,如果這時(shí)JS并不空閑或隊(duì)列中還有別的優(yōu)先級更高的代碼,那就意味著你的定時(shí)器會(huì)被延時(shí)執(zhí)行。
重復(fù)的定時(shí)器
使用setInterval創(chuàng)建定時(shí)器的目的是使代碼規(guī)則的插入到隊(duì)列中。這個(gè)方式的問題在于,存在這樣一種可能,在上次代碼還沒執(zhí)行完的時(shí)候代碼再次被添加到隊(duì)列。JS引擎會(huì)解決這個(gè)問題,在將代碼添加到隊(duì)列時(shí)會(huì)檢查隊(duì)列中有沒有代碼實(shí)例,如果有就不添加,這確保了定時(shí)器代碼被加入隊(duì)列中的最小間隔是規(guī)定間隔。但是在某些特殊情況下還是會(huì)出現(xiàn)兩個(gè)問題,某些間隔因?yàn)镴S的處理被跳過,代碼之間的間隔比預(yù)期的小。
所以盡量使用setTimeout()模擬間隔調(diào)用。
setTimeout(function(){
setTimeout(arguments.callee, interval);
}, interval);
Yielding Processes
如果你的頁面中要進(jìn)行大量的循環(huán)處理,每次循環(huán)會(huì)消耗大量的時(shí)間,那就會(huì)阻塞用戶的操作。這時(shí)分塊處理數(shù)據(jù)就是個(gè)好辦法。
這個(gè)例子每100ms取一個(gè)數(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);
}
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);
函數(shù)節(jié)流
這個(gè)是為了避免某個(gè)操作連續(xù)不停的觸發(fā),比如涉及到DOM操作,連續(xù)大量的DOM操作非常耗資源。
函數(shù)節(jié)流的基本思想是,每一次調(diào)用其實(shí)是設(shè)置一個(gè)真正調(diào)用的setTimeout操作,每次調(diào)用都會(huì)先清除當(dāng)前的setTimeout再設(shè)置一個(gè)新的。如果短時(shí)間內(nèi)大量調(diào)用,就回一直設(shè)置新的setTimeout而不執(zhí)行setTimeout內(nèi)的操作。只有停止調(diào)用足夠長的時(shí)間,直到setTimeout時(shí)間到了,內(nèi)部的真正操作才會(huì)執(zhí)行一次。這個(gè)對onresize事件特別有用。
function throttle(method, context) {
clearTimeout(method.tId);
method.tId= setTimeout(function(){
method.call(context);
}, 100);
}
function reDiv(){
var div = document.getElementById("myDiv");
div.innerHTML += "qqqqqq" + "<br>";
}
window.onresize = function(){
throttle(reDiv);
};
這樣只有當(dāng)你停下調(diào)整窗口大小100ms后才會(huì)執(zhí)行reDiv操作。
自定義事件
事件這樣的交互其實(shí)就是觀察者模式,這類模式由兩類對象組成:主體和觀察者。主體負(fù)責(zé)發(fā)布事件,觀察者通過訂閱這些事件來觀察該主體。
創(chuàng)建自定義事件實(shí)際上就是創(chuàng)建一個(gè)管理事件的對象,并在里面存入各種事件類型的處理函數(shù),觸發(fā)事件時(shí),只要你給出事件類型,這個(gè)對象就會(huì)找到相應(yīng)的事件處理程序并執(zhí)行。
下面是一個(gè)事件管理對象的大體形式:
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);
}
}
};
添加事件處理程序時(shí),addHandler會(huì)按照事件的類型將處理函數(shù)存入handlers屬性中對應(yīng)的數(shù)組里(如果還沒有則新建)。
觸發(fā)事件時(shí)使用fire,傳入一個(gè)至少有type屬性的對象。
使用時(shí)就像這樣:
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!"});
自定義事件經(jīng)常用來解耦對象之間的交互,使用事件就不需要有對象與對象之間的引用,使事件處理和事件觸發(fā)保持隔離。
拖放
使用原始的鼠標(biāo)事件
創(chuàng)建一個(gè)單例,使用模塊模式來創(chuàng)建一個(gè)拖動(dòng)的插件,返回兩個(gè)方法,分別用來添加和移除所有的事件處理程序。
var DragDrop = function(){
var dragging = null;
var diffX = 0;
var 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);
}
}
}();
DragDrop.enable();
這樣看來拖動(dòng)的功能是實(shí)現(xiàn)了,不過有個(gè)問題。比如說這是我寫的一個(gè)插件,使用的人想在拖動(dòng)開始的時(shí)候做一些事情,那么他就不得不在起的源碼里做出修改。他需要把所有要執(zhí)行的代碼和函數(shù)加到case "mousedown"里。如果這個(gè)插件我加密了呢,那想在這個(gè)時(shí)間點(diǎn)做些事情就更麻煩了。這樣的做法顯然并不科學(xué)。
這時(shí)如果使用了自定義事件,就可以很好的解決這個(gè)問題。
添加自定義事件
我們在這里新定義一個(gè)dragdrop變量,它是EventTarget類型的對象,在它上面我們可以添加事件處理函數(shù)或觸發(fā)事件。在拖動(dòng)開始時(shí),過程中,結(jié)束時(shí),都觸發(fā)了自定義事件,這樣有人想在這幾個(gè)節(jié)點(diǎn)做什么就直接添加事件處理函數(shù)就可以了。
var DragDrop = function(){
//這里的dragdrop是之前的EventTarget類型,可以用來保存和觸發(fā)事件
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;
//觸發(fā)自定義事件
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;
}();
DragDrop.addHandler("dragstart", function(event){
var status = document.getElementById("myDiv");
status.innerHTML = "Started dragging " + event.target.id;
});
DragDrop.addHandler("drag", function(event){
var status = document.getElementById("myDiv");
status.innerHTML += "<br/> Dragged " + event.target.id + " to (" + event.x +
"," + event.y + ")";
});
DragDrop.addHandler("dragend", function(event){
var status = document.getElementById("myDiv");
status.innerHTML += "<br/> Dropped " + event.target.id + " at (" + event.x +
"," + event.y + ")";
});
DragDrop.enable();