1.高级函数
①var isArray = value instanceof Array;
上述代码要返回true,value必须是一个数组,而且还必须与Array构造函数在同个全局作用域中。(别忘了,Array是window的属性)。如果value是在另个frame里定义的数组,那么以上代码就会返回false
在任何值上调用Object原生的toString()方法,都会返回[object NativeConstructorName]格式的字符串。每个类在内部都有一个[[Class]]属性,这个属性就指定了上述字符串的构造函数名。
alert(Object.prototype.toString.call(value)); //"[object Array]"
由于原生数组的构造函数名与全局作用域无关,所以使用toString()都能保证返回一致的值。因此验证是否是一个数组:
function isArray(value){
return Object.prototyoe.toString.call(value) == [object Array];
}
②作用域安全的构造函数
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"
③惰性载入函数
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){
//skip
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
} else {
throw new Error("No XHR object available.");
}
}
像上面那样,每次调用createXHR(),它都要对浏览器所支持的能力仔细检查,所以如果if语言不必每次都执行,那么代码可以运行地更快一些,解决方案就是称之为惰性载入的技巧
惰性函数表示函数执行的分支仅会发生一次。有两种实现惰性载入的方式。
第一种就是在函数被调用时再处理函数。在第一次调用的过程中,该函数会被覆盖为另外一个按合适方式执行的函数,这样任何对原函数的调用都不用再经过执行的分支。
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];
} catch (ex){
//skip
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
};
} else {
createXHR = function(){
throw new Error("No XHR object available.");
};
}
return createXHR();
}
var xhr1 = createXHR();
var xhr2 = createXHR();
这个堕入载入的createXHR()钟,if语句的每一个分支都会为creatXHR()变量赋值,有效覆盖了原有的函数。最后一步便是调用新赋的函数。下次调用createXHR()的时候,就会直接调用被分配的函数,这样就不用再次执行if语句了。
第二种实现惰性载入的方式是在声明函数时就指定适当的函数。这样,第一次调用函数时就不会损失性能了,而在代码首次加载时会损失一点性能。
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.");
};
}
})();
var xhr1 = createXHR();
var xhr2 = createXHR();
总结:惰性载入函数的优点是只在执行分支代码时牺牲一点而性能。至于两种方式哪种更适合,就要看你的具体需求而定。不过这两种方式都能避免执行不必要的代码。
4.函数绑定
该技巧常常和回调函数与事件处理程序一起使用,以便在将函数作为变量传递的同时保留代码执行环境
函数绑定的bind方法是:fun.bind(binedObject),binedObject是为fun函数绑定的对象,但这原生的bind()方法的兼容性为:IE9+、Firefox、Chrome,得自己写一个bind()函数
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));
这个this对象指向了DOM按钮而非handler(在IE中,this指向window),所以可以使用闭包来修改这个问题。
function bind(fn, context){
return function(){
return fn.apply(context, arguments);
};
}
var handler = {
message: "Event handled",
handleClick: function(event){
alert(this.message + ":" + event.type);
}
};
var btn = document.getElementById("my-btn");
EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler));
不过不懂的是为什么要在bind()函数的闭包函数也return,明明不写return也能正常执行的,而且返回的还是undefined
这个函数看似简单,但其功能是非常强大的。注意这里使用的arguments对象是内部函数的,而非bind()的。
函数绑定主要用于事件处理程序以及setTimeout()和setInterval()。然而,被绑定函数与普通函数相比有更多的开销,它们需要更多内存(闭包),同时也因为多重函数调用稍微慢一些,所以最好只在必要时使用。
⑤函数柯里化(function currying)(但说实话,我还是没搞懂要怎么用,好扯淡。。)
它用于创建已经设置好了一个或多个参数的函数。基本方法和函数绑定是一样的:使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入的函数
柯里化函数通常由以下步骤动态创建:调用另一个函数并为它传入要柯里化的函数和必要参数。其简单概念如下:
function add(num1, num2){
return num1 + num2;
}
function curriedAdd(num2){
return add(5, num2);
]
alert(add(2, 3)); //5
alert(curriedAdd(3)); //8
function curry(fn){
var args = Array.prototype.slice.call(arguments, 1); //args存放外部函数参数,除了fn这个参数
return function(){
var innerArgs = Array.prototype.slice.call(arguments), //存放内部函数参数
finalArgs = args.concat(innerArgs);
return fn.apply(null, finalArgs);
};
}
function add(num1, num2){
return num1 + num2;
}
var curriedAdd = curry(add, 5);
alert(curriedAdd(3)); //8
var curriedAdd2 = curry(add, 5, 12);
alert(curriedAdd2()); //17
function bind(fn, context){
var args = Array.prototype.slice.call(arguments, 2); //注意这里要传除了函数参数fn和对象参数context
return function(){
var innerArgs = Array.prototype.slice.call(arguments),
finalArgs = args.concat(innerArgs);
return fn.apply(context, finalArgs); //注意这里要传入context参数
};
}
var handler = {
message: "Event handled",
handleClick: function(name, event){
alert(this.message + ":" + name + ":" + event.type);
}
};
var btn = document.getElementById("my-btn");
EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler, "my-btn"));
JavaScript中的柯里化函数和绑定函数提供了强大的动态函数创建功能。使用bind()还是curry()要根据是否需要object对象响应来决定。它们都能用于创建复杂的算法和功能,当然两者都不应滥用,因为每个函数都会带来额外开销。
2.防篡改对象
注意的是:一旦把对象定义为防篡改,就无法撤销了。ECMAScript增加了几个方法,通过它们可以指定对象的行为
①不可扩展对象:第一保护级别
使用Object.preventExtensions(object)使你不能再给对象添加属性和方法,object为你要改变的对象。在非严格模式下,尝试,给对象添加新成员会导致静默失败,因此object.property将是undefined。而在严格模式下,尝试给不可扩展的对象添加新成员会导致错误。但不会对已有的对象成员造成应该。
另外可使用Object.isExtensible()方法确定对象是否可扩展
②密封的对象:第二保护级别
密封对象不可扩展,而且已有成员的[[Configurable]]特性将被设置为false,这将意味着不能删除属性和方法。但已有属性值是可修改的。在严格模式和非严格模式下的影响和不可扩展对象一样,导致错误或被忽略。
另外可使用Object.isSealed()可确定对象是否被密封了。
③冻结的对象:第三保护级别
最严格的防篡改级别。冻结的对象既不可扩展,又是密封的,而且对象数据属性的[ [ Writable ]]特性会被设置为false。如果设置[ [ Set] ]函数,访问器属性仍然是可写的。在严格模式和非严格模式下的影响和不可扩展对象一样,导致错误或被忽略。
可使用Object.isFrozen()方法确定是否检测冻结对象
对JavaScript库的作者而言,冻结对象是很很有用的。因为JavaScript库最怕有人意外(或有意)地修改了库中的核心对象。冻结(或密封)主要的库对象能够防止这些问题的发生。
3.高级定时器
虽然人们对JavaScript的定时器存在普遍的误解,认为它们是线程,其实JavaScript是运行在单线程的环境中的,而定时器仅仅只是计划在未来的某个时间执行。执行实际是不能保证的,因为在页面的声明周期内,不同时间可能有其他代码在控制进程。(如设定一个150ms的定时器不代表到了150ms代码就立刻执行,它表示会在150ms后会被加入到执行队列中)在页面下载完后的代码运行、时间处理程序、Ajax回调函数都必须使用同样的线程来执行。实际上,浏览器负责进行排序,指派某段代码在某个时间点运行的优先级。
①重复的定时器:setInterval()
重复的定时器问题在于,定时器代码可能在代码再次被添加到队列之前还没完全执行,结果导致定时器代码连续执行好几次,而之间没有停顿。幸好,JavaScript引擎够聪明,能避免这个问题。当使用setInterval()时,仅当没有该定时器代码实例时,才将定时器代码代码添加到队列中。这确保定时器代码加入到队列中的最小时间间隔为指定间隔。
这个规则有两个问题:(1)某些间隔会被跳过;(2)多个定时器的代码执行之间的间隔可能会被预期的小。
为了避免setInterval()的重复定时器的这两个缺点,你可以用如下模式使用链式setTimeout()调用。
setTimeout(function(){
//处理中
setTimeout(argumrnts.callee, interval);
}, interval);
这么做的好处是,在前一个定时器代码执行完之前,不会向队列中插入新的定时器代码,确保不会有任何缺失的间隔。而且它可以保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续的运行。应用如下:
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);
注意:每个浏览器窗口、标签页、或者frame都有各自的代码执行队列。这意味着,进行跨frame或者跨串口的定时调用,当代码同时执行的时候可能会导致竞争条件。无论何时需要使用这种通信类型,最好是在接受frame或者窗口中创建一个定时器来执行代码。
3.Yielding Processes(让出进程)
背景:运行在浏览器中的JavaScript都被分配了一个确定数量的资源。不同于桌面应该往往能够随意控制他们要的内存大小和处理器时间,JavaScript被严格限制了,以防止恶意的Web程序员把用户的计算机搞挂了。其中一个限制是长时间运行脚本的限制,如果代码运行超过特定的时间或特定语句数量就不让它继续执行。如果代码达到了这个限制,会弹出一个浏览器错误的对话框,告诉用户某个脚本会用过长的时间执行,询问是否其继续执行还是停止它。所有JavaScript开发人员的目标就是,确保用户永远不会在浏览器中看到这个令人费解的对话框。定时器是绕开此限制的方法之一。
脚本长时间运行的问题通常是由两个原因之一造成的:①过长的、过深嵌套的函数调用②进行大量处理的循环
长时间运行的循环通常遵循以下模式:
for(var i =0, len=data.length; i
如果process()要处理很久,那意味着这段时间内用户无法与页面交互的时间会很久
在展开该循环之前,你需要回答以下两个问题
【】该处理是否必须同步完成?
【】数据是否必须按顺序完成?
因此,当你发现某个循环占用了大量时间,同时对于上述两个问题,你的回答都是否,那么你就可以使用定时器分隔这个循环。这是一种叫做数据分组(array chunking)的技术,小块小块地处理数组,通常每次处理一块。
var data = [12,123,1234,453,436,23,23,5,4123,45,346,5634,2234,345,342];
function chunk(array, process, context){
setTimeout(function(){
var item = array.shift();
process.call(context, item);
if (array.length > 0){
setTimeout(arguments.callee, 100);
}
}, 100);
}
function printValue(item){
var div = document.getElementById("myDiv");
div.innerHTML += item + "
";
}
chunk(data, printValue); //如果想保持原数组,chunck(data.concat(), printValue);
函数节流
背景:浏览器中某些计算和处理比其他的昂贵很多,例如DOM操作,消耗更多的内存和CPU时间
基本思想:某些代码不可以在没有间断的情况连续重复执行
实现:定时器
应用:只要代码是周期性执行,都应该使用节流,但是你不能控制请求执行的频率。
function throttle(method, scope) {
clearTimeout(method.tId);
method.tId= setTimeout(function(){
method.call(scope); //没有传入scope时,那么就在全局作用域内执行该方法
}, 100);
}
function resizeDiv(){
var div = document.getElementById("myDiv");
div.style.height = div.offsetWidth + "px";
}
window.onresize = function(){
throttle(resizeDiv);
};
但要注意这函数节流在onscroll事件并不合适,因为onscroll事件要求滚动条每变化1像素就要求有所反应,要是有些滚动条变化没被响应,效果就没那么好了。
自定义事件
背景:事件是一种叫做观察者的设计模式,这是一种创建松散耦合代码的技术。对象可以发布事件,用来表示在该对象生命周期中某个有趣的时刻到了。然后其他对象可以观察该对象,等待这些有趣的时刻到来并通过运行代码来响应
观察者模式:由两类对象组成,主体和观察者。主题负责发布事件,同时观察者通过订阅这些事件来观察该主体。该模式的一个关键概念是主体并不知道观察者的任何事情,也就是说它可以独自存在并正常运作即使观察者不存在。从另一方面来说,观察者知道主体并能注册事件的回调函数(事件处理程序)。涉及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);
}
}
};
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!"});
应用:当代码中存在多个部分在特定时刻相互交互的情况,自定义事件就非常有用了。这时,如果每个对象都有对其他所有对象引用。那么整个代码就会紧密耦合,同时维护也变得很困难,因此对某个对象的修改也会影响其他对象。使用自定义事件有助于解耦相关对象,保护功能的隔绝。在很多情况中,触发事件的代码和监听事件代码是完全分离的。
拖放
因为没什么好说的,就直接贴代码了,下面是一个单例对象,并使用了模块模式来隐藏某些实现细节
var DragDrop = function(){
var dragging = null,
diffX = 0,
diffY = 0;
function handleEvent(event){
//get event and target
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
//determine the type of event
switch(event.type){
case "mousedown":
if (target.className.indexOf("draggable") > -1){
dragging = target;
diffX = event.clientX - target.offsetLeft; //注意这里的event和target
diffY = event.clientY - target.offsetTop;
}
break;
case "mousemove":
if (dragging !== null){
//assign location
dragging.style.left = (event.clientX - diffX) + "px";
dragging.style.top = (event.clientY - diffY) + "px";
}
break;
case "mouseup":
dragging = null;
break;
}
};
//public interface
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();
拖放功能还不能真正应用起来,除非能知道什么时候拖动开始了。从这点看,前面的代码没有提供任何方法表示拖动开始、正在拖动或者已经结束。这是,可以使用自定义事件来指示这几个事件的发生,让应用的其他部分与拖动功能进行交互。
var DragDrop = function(){
var dragdrop = new EventTarget(),
dragging = null,
diffX = 0,
diffY = 0;
function handleEvent(event){
//get event and target
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
//determine the type of 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){
//assign location
dragging.style.left = (event.clientX - diffX) + "px";
dragging.style.top = (event.clientY - diffY) + "px";
//fire custom event
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;
}
};
//public interface
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.enable();
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 += "
Dragged " + event.target.id + " to (" + event.x + "," + event.y + ")";
});
DragDrop.addHandler("dragend", function(event){
var status = document.getElementById("status");
status.innerHTML += "
Dropped " + event.target.id + " at (" + event.x + "," + event.y + ")";
});
总结:①可以创建作用域安全的构造函数,确保在缺少new操作符时调用构造函数不会改变错误的环境对象
②可以使用惰性载入函数,将任何代码分支推迟到第一次调用函数的时候
③函数绑定可以让你创建始终在制定环境中运行的函数,同时函数函数柯里化可以让你创建已经填了某些参数的函数
④将绑定和函数柯里化结合起来,就能够给你一种在任意环境中以任意参数执行任意函数的方法
⑤定时器原理可以让你实现数组分块和函数节流的技术
⑥使用自定义事件有助于将不同部分的代码相互之间解耦,让维护更加容易,并减少引入错误的机会。
⑦将拖放行为和自定义事件结合起来可以创建一个可重复使用的框架,它能应用于各种不同的情况下。
这一章真的能学到很多,而且并不是空洞的知识,本人亲测,这些知识都能应用在实际中,棒棒哒。PS:看不懂就先放下,这本书第一次看不懂很正常,等有了一些项目实践后再看,会更加清晰,而且必须看很多遍,等书上的例子都应用到实际中用烂了,这本书你才算真正的看懂了。