第22章 高级技巧
1.高级函数
(1)安全的类型检测:typeof 操作符由于一些无法预知的行为,经常会导致检测数据类型时得到不靠谱的结果;instanceof 操作符在存在多个全局作用域(像一个页面包含多个 frame)的情况下,也是问题多多。
在任何值上调用 Object 原生的 toString()方法,都会返回一个[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]"; }
(2)作用域安全的构造函数:避免因为忽略new 操作符在全局对象上意外设置属性。
-
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"
(3)惰性载入函数:函数执行的分支仅会发生一次。
第一种:在函数被调用时再处理函数。
-
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(); }
第二种:在声明函数时就指定适当的函数。
-
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."); }; } })();
(4)函数绑定:创建一个函数,可以在特定的 this 环境中以指定参数调用另一个函数。
ECMAScript 5 为所有函数定义了一个原生的 bind()方法,接受一个函数和一个环境,并返回一个在给定环境中调用给定函数的函数,并且将所有参数原封不动传递过去。主要用于事件处理程序以及 setTimeout() 和 setInterval(),因为性能的原因最好只在必要时使用。
-
var handler = { message: "Event handled", handleClick: function(event){ alert(this.message + ":" + event.type); } }; var btn = document.getElementById("my-btn"); EventUtil.addHandler(btn, "click", handler.handleClick.bind(handler));
(5)函数柯里化:用于创建已经设置好了一个或多个参数的函数。
-
//创建柯里化函数的通用方式,curry()函数的主要工作就是将被返回函数的参数进行排序,第一个参数是要进行柯里化的函数,其他参数是要传入的值。 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); }; } function add(num1, num2){ return num1 + num2; } //创建了第一个参数绑定为 5 的 add()的柯里化版本 var curriedAdd = curry(add, 5); alert(curriedAdd(3)); //8 //创建了绑定了两个参数的柯里化版本 var curriedAdd = curry(add, 5, 12); alert(curriedAdd()); //17
2.防篡改对象
(1) 不可扩展对象:使用 Object.preventExtensions()方法后不能再给对象添加属性和方法。
-
var person = { name: "Nicholas" }; Object.preventExtensions(person); person.age = 29; alert(person.age); //undefined
使用 Object.istExtensible()方法还可以确定对象是否可以扩展。
-
var person = { name: "Nicholas" }; alert(Object.isExtensible(person)); //true Object.preventExtensions(person); alert(Object.isExtensible(person)); //false
(2) 密封的对象:不可扩展,不能删除属性和方法
-
var person = { name: "Nicholas" }; Object.seal(person); person.age = 29; alert(person.age); //undefined delete person.name; alert(person.name); //"Nicholas"
使用 Object.isSealed()方法可以确定对象是否被密封了
-
var person = { name: "Nicholas" }; alert(Object.isExtensible(person)); //true alert(Object.isSealed(person)); //false Object.seal(person); alert(Object.isExtensible(person)); //false alert(Object.isSealed(person)); //true
(3) 冻结的对象:最严格的防篡改级别,既不可扩展,又是密封的,而且对象数据属性的[[Writable]]特性会被设置为 false。
-
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"
Object.isFrozen()方法用于检测冻结对象。
-
var person = { name: "Nicholas" }; alert(Object.isExtensible(person));//true alert(Object.isSealed(person));//false alert(Object.isFrozen(person));//false Object.freeze(person); alert(Object.isExtensible(person));//false alert(Object.isSealed(person));//true alert(Object.isFrozen(person));//true
3.高级定时器:JavaScript 是运行于单线程的环境中的,定时器仅仅只是计划代码在未来的某个时间执行,执行时机是不能保证的。浏览器负责进行排序,指派某段代码在某个时间点运行的优先级。
定时器对队列的工作方式:当特定时间过去后将代码插入,并不意味着对它立刻执行,而只能表示它会尽快执行。设定一个 150ms 后执行的定时器不代表到了 150ms 代码就立刻执行,它表示代码会在 150ms 后被加入到队列中。
关于定时器要记住的最重要的事情是,指定的时间间隔表示何时将定时器的代码添加到队列,而不是何时实际执行代码。
(1)重复的定时器:使用setInterval()创建的定时器确保了定时器代码规则地插入队列中,但是定时器代码可能在代码再次被添加到队列之前还没有完成执行,会导致定时器代码之后连续运行好几次。虽然JavaScript 引擎能避免这个问题,但是会导致新的两个问题:
- 某些间隔会被跳过。
- 多个定时器的代码执行之间的间隔可能会比预期的小。
可以使用链式 setTimeout() 调用避免这两个缺点。
-
/*在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。 保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续的运行。*/ 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);
(2)Yielding Processes:脚本长时间运行的问题通常是由两个原因之一造成的:过长的、过深嵌套的函数调用或者是进行大量处理的循环。在展开该循环之前,你需要回答以下两个重要的问题:
- 该处理是否必须同步完成?
- 数据是否必须按顺序完成?
-
当某个循环占用了大量时间,同时对于上述两个问题,回答都是“否”,可以使用数组分块(array chunking)的技术小块小块地处理数组。
在数组分块模式中,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); } 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); //使用concat()方法保证原数组不变 chunk(data.concat(), printValue);
(3)函数节流:某些代码不可以在没有间断的情况连续重复执行。只要代码是周期性执行的,都应该使用节流。
-
//接受两个参数:要执行的函数以及在哪个作用域中执行。 function throttle(method, context) { clearTimeout(method.tId); method.tId= setTimeout(function(){ method.call(context); }, 100); }
节流在 resize 事件中是最常用的。
-
function resizeDiv(){ var div = document.getElementById("myDiv"); div.style.height = div.offsetWidth + "px"; } window.onresize = function(){ throttle(resizeDiv); };
4.自定义事件:创建一个管理事件的对象,让其他对象监听那些事件。
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);
},
//触发一个事件,接受一个参数,一个至少包含 type 属性的对象。
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);
}
}
};
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!"});
5.拖放对于桌面和 Web 应用都是一个非常流行的用户界面范例,它能够让用户非常方便地以一种直 观的方式重新排列或者配置东西。
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;
}();
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 + ")";
});