高级函数
函数本质上是很简单且过程化的,但是由于JS天生的动态的特性,从使用方式上可以很复杂。
安全的类型检测
虽然JS中是有类型检测的,但是由于浏览器实现等它们并不完全可靠。比如typeof在Safari中对正则表达式也返回function。
instanceof在存在多个全局作用域时也会把同种却不同作用域中构造函数的实例识别为不同的实例:
var isArray = value instanceof Array;
这个表达式要是想返回true,value必须是个数组,且必须与Array构造函数在同一个全局作用域中,如果value是另一个全局作用域中定义的数组,那这个表达式返回false。
检测某个对象是原生的还是开发人员自定义的对象时也会有问题。因为浏览器开始原生支持JSON了,而有些开发人员还是在用第三方库来实现JSON,这个库里会有全局的JSON对象,这样想确定JSON对象是不是原生的就麻烦了。
解决这些问题的办法就是使用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]";
}
不过要注意的是,对于在IE中任何以COM形式实现的函数,isFunction()都会返回false。
对于JSON是否为原生的问题可以这样:
var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON) == "[object JSON]";
作用域安全的构造函数
之前我们说的构造函数是这么使用的:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
}
var person = new Person("Nicholas", 29, "Software Engineer");
在这里,由于使用了new操作符,this被绑定在了新创建的Person对象上,如果不用new操作符直接调用Person(),this就会被绑定到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"
不过在使用了这样作用域安全的构造函数后,如果使用基于构造函数窃取的继承,就会有问题:
function Polygon(sides){
if (this instanceof Polygon) {
this.sides = sides;
this.getArea = function(){
return 0;
};
} else {
return new Polygon(sides);
}
}
function Rectangle(width, height){
//这里调用时,传进去的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的一个实例就好啦
Rectangle.prototype = new Polygon();
var rect = new Rectangle(5, 10);
alert(rect.sides); //2
惰性载入函数
由于浏览器差异,大量的判断浏览器能力的函数需要被使用(通常是大量的if),然而这些判断一般其实不必每次都执行,在执行一次后,浏览器的能力就确定了,以后就应该不用在判断了。比如:
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.");
}
}
这里的创建XHR对象的函数,每次创建对象时都会判断一次浏览器能力,这是不必要的。
惰性载入有两种方式,第一种就是在函数第一次被调用时,根据不同情况,用不同的新函数把这个函数覆盖掉,以后调用就不需要再判断而是直接执行该执行的操作。
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);
第二种思路一样,只不过是在声明函数时就指定新函数,不在第一次调用时再指定。两种办法其实本质上是一样的,看你想怎么用了。
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);
函数绑定
函数绑定解决的问题是调用函数时函数的this对象被改为我们不想要的对象的问题。这种情况经常出现在指定事件处理函数时。看个例子:
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被改成了按钮元素。
解决办法就是将真正的函数套在一个闭包中,以此来保存着个函数的环境
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,而因为这个闭包的保护,我们的处理函数的环境保存住了。
为了不每次都手动创建一个闭包,我们可以创建一个工具函数bind:
function bind(fn, context){
alert(arguments[0]); //fn本身
return function(){
alert(arguments[0]); //event
return fn.apply(context, arguments);
};
}
这里就是把一个函数使用apply在特定的环境下调用,注意一下arguments对象,最后应该使用的是return过去的匿名函数(闭包)的arguments才对,这样事件给事件处理函数传递的参数才能穿到我们的函数里。
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中为所有函数都定义了一个原生的bind()方法。直接使用就行。
EventUtil.addHandler(btn, "click", handler.handleClick.bind(handler));
只要是将某个函数指针以值的形式进行传递,同时该函数必须在特定环境中执行,被绑定函数的效果就显现了
函数柯里化
这个是用来创建已经设置好一个或多个参数的函数,用一个例子看看基本思想:
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);
return function(){
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return fn.apply(null, finalArgs);
};
}
这个函数主要的工作就是将外部函数和内部函数的参数都获取到传递给了返回的函数中。
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函数中来给事件处理函数传入多个参数。
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"));
这里要注意的是参数的顺序,比如这里name和event反了可就不对了呦。
ES5中的bind也实现了柯里化,直接使用就可以。
EventUtil.addHandler(btn, "click", handler.handleClick.bind(handler, "myButton"));
防篡改对象
JS共享的本质使任意对象都可被随意修改。这样有时很不方便。ES5增加了几个方法来设置对象的行为。一旦将对象设置为防篡改就不能撤销了。
不可拓展对象
不可以添加新的属性和方法
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]]被设置为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
冻结的对象
不可拓展,密封,且对象数据属性的[[Writable]]将被设为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
高级定时器
setTimeout()和setInterval()是很实用的功能,不过有些事情是要注意的。
JS是单线程的,这就意味着定时器实际上是很有可能被阻塞的。我们在这两个函数中所设置的定时,其实是代表将代码加入到执行队列的事件,如果在加入时恰巧JS是空闲的,那么这段代码会立即被执行,也就是说这个定时被准时的执行了。相反,如果这时JS并不空闲或队列中还有别的优先级更高的代码,那就意味着你的定时器会被延时执行。
重复的定时器
使用setInterval创建定时器的目的是使代码规则的插入到队列中。这个方式的问题在于,存在这样一种可能,在上次代码还没执行完的时候代码再次被添加到队列。JS引擎会解决这个问题,在将代码添加到队列时会检查队列中有没有代码实例,如果有就不添加,这确保了定时器代码被加入队列中的最小间隔是规定间隔。但是在某些特殊情况下还是会出现两个问题,某些间隔因为JS的处理被跳过,代码之间的间隔比预期的小。
所以尽量使用setTimeout()模拟间隔调用。
setTimeout(function(){
setTimeout(arguments.callee, interval);
}, interval);
Yielding Processes
如果你的页面中要进行大量的循环处理,每次循环会消耗大量的时间,那就会阻塞用户的操作。这时分块处理数据就是个好办法。
这个例子每100ms取一个数组元素并添加到页面。
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);
函数节流
这个是为了避免某个操作连续不停的触发,比如涉及到DOM操作,连续大量的DOM操作非常耗资源。
函数节流的基本思想是,每一次调用其实是设置一个真正调用的setTimeout操作,每次调用都会先清除当前的setTimeout再设置一个新的。如果短时间内大量调用,就回一直设置新的setTimeout而不执行setTimeout内的操作。只有停止调用足够长的时间,直到setTimeout时间到了,内部的真正操作才会执行一次。这个对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);
};
这样只有当你停下调整窗口大小100ms后才会执行reDiv操作。
自定义事件
事件这样的交互其实就是观察者模式,这类模式由两类对象组成:主体和观察者。主体负责发布事件,观察者通过订阅这些事件来观察该主体。
创建自定义事件实际上就是创建一个管理事件的对象,并在里面存入各种事件类型的处理函数,触发事件时,只要你给出事件类型,这个对象就会找到相应的事件处理程序并执行。
下面是一个事件管理对象的大体形式:
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);
}
}
};
添加事件处理程序时,addHandler会按照事件的类型将处理函数存入handlers属性中对应的数组里(如果还没有则新建)。
触发事件时使用fire,传入一个至少有type属性的对象。
使用时就像这样:
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;
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();
这样看来拖动的功能是实现了,不过有个问题。比如说这是我写的一个插件,使用的人想在拖动开始的时候做一些事情,那么他就不得不在起的源码里做出修改。他需要把所有要执行的代码和函数加到case "mousedown"里。如果这个插件我加密了呢,那想在这个时间点做些事情就更麻烦了。这样的做法显然并不科学。
这时如果使用了自定义事件,就可以很好的解决这个问题。
添加自定义事件
我们在这里新定义一个dragdrop变量,它是EventTarget类型的对象,在它上面我们可以添加事件处理函数或触发事件。在拖动开始时,过程中,结束时,都触发了自定义事件,这样有人想在这几个节点做什么就直接添加事件处理函数就可以了。
var DragDrop = function(){
//这里的dragdrop是之前的EventTarget类型,可以用来保存和触发事件
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("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();