高级函数
安全类型的检测
JavaScript 内置的类型检测机制并非完全可靠。事实上,发生错误否定及错误肯定的情况也不在少数。比如说 typeof 操作符吧,由于它有一些无法预知的行为,经常会导致检测数据类型时得到不靠谱的结果。
var isArray = value instanceof Array;
这个表达式要是想返回true,value必须是个数组,且必须与Array构造函数在同一个全局作用域中,如果value是另一个全局作用域(其他frame)中定义的数组,那这个表达式返回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运算符, 那么name, age, job三个属性会被直接挂在到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"
加了这个判断之后,看起来更叫稳妥.
不过又产生了新的问题, 假如Person函数调用call/apply实现继承的话, 那么结果可能不是我们想要的:
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传的是Rectangle的实例
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.prototype = new Polygon(); //原型链继承, 这样this就是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();//第一次调用的时候会执行if语句
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.");
};
}
})(); //这里是一个立即执行函数, 执行完毕后createXHR就可以直接调用, 无需再检测
函数绑定
函数绑定是为了解决this的指向问题:
var handler = {
message: "Event handled",
handleClick: function(event){
console.log(this);
console.log(this.message);
}
};
var btn = document.getElementById("myButton");
btn.addEventListener('click', handler.handleClick, false); //这里会输出 dom 和 undefined, 表面上handler.handleClick是挂载在handler上, 但是它里面的this指向会发生改变
// 为了解决上面的问题, 我们有如下两个方法:
// 方法1: 新增匿名函数
btn.addEventListener('click', function(evt){
handler.handleClick(evt) // 通过新增一个匿名函数可以实期待的输出
}, false);
// 方法2: 使用Es5 bind方法
btn.addEventListener('click', handler.handleClick.bind(handler), false);
// 如果浏览器不支持bind方法, 我们可以利用apply实现一个
if(!Function.prototype.bind){
Function.prototype.bind = function(fn,context){
return function(){
fn.apply(context,arguments)
}
}
}
函数curry化
函数curry化, 中文翻译柯里化, 个人觉得在大多数情况下不是很有必要.书上讲得也不好, 请直接观看 这篇文章讲解什么是curry化
防篡改对象
不可扩展对象
JS共享的本质使任意对象都可被随意修改。这样有时很不方便。ES5增加了几个方法来设置对象的行为。一旦将对象设置为防篡改就不能撤销了。
var person = { name: "Nicholas" };
Object.preventExtensions(person); //ES5新增的Object.preventExtensions方法
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
冻结的对象
Object.freeze, 比前面两个更加严格
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并不空闲或队列中还有别的优先级更高的代码,那就意味着你的定时器会被延时执行。
记住: 在JS中, 没有任何代码是立即执行的, 只有一旦进程空闲就执行.
重复的定时器
使用setInterval创建定时器的目的是使代码规则的插入到队列中。这个方式的问题在于,存在这样一种可能,在上次代码还没执行完的时候代码再次被添加到队列。JS引擎会解决这个问题,在将代码添加到队列时会检查队列中有没有代码实例,如果有就不添加,这确保了定时器代码被加入队列中的最小间隔是规定间隔。但是在某些特殊情况下还是会出现两个问题,某些间隔因为JS的处理被跳过,代码之间的间隔比预期的小。
所以尽量使用setTimeout()模拟间隔调用。
setTimeout(function(){
setTimeout(arguments.callee, interval);
}, interval);
yielding processes
浏览器中的js被分配了一个确定数量的资源,所以会限制js脚本的运行时间,不能过长。
如果达到这个限制,会弹出一个浏览器错误的对话框,询问是否继续执行。定时器时绕开此限制的方法之一。
脚本长时间运行的原因有两个:
- 过长的、过深嵌套的函数调用
- 进行大量处理的循环
通常我们是处理第二个因素, 但是要记住, 如果你的循环不必同步,或者结果不必按顺序, 那么么就可以采用yielding processes思想.
我们看这例子:
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);
函数节流
举个例子 , 页面有一个长度为3的轮播图, 你鼠标放到(hover)对应轮播点的时候自动显示该张图, 如果你在非常短时间(比如10ms)内快速来回hover, 那图片自然也会也会快速闪烁, 这样会操作性能的浪费. 我们就可以利用setTimeout来限制用户的hover频率
自定义事件
事件是一种叫做观察者模的设计模式(也叫发布订阅模式), 这是一种创建松散耦合的代码技术.
观察者模式有两类对象组成: 主体和观察者, 主体发布时间, 同时观察者通过订阅这些事件来观察主体. 涉及到DOM上, DOM元素就是主体, 你的事件处理程序就是观察者.
我们来实现一个简单的观察者模式:
var Pubsub = function (argument) {
this.hub = {};
}
Pubsub.prototype.on = function(type,fn){
if (!this.hub[type]) {
this.hub[type] = [];
}
this.hub[type].push(fn);
};
Pubsub.prototype.off = function(type){
this.hub[type] = [];
};
Pubsub.prototype.fire = function(type,fn){
var fns = this.hub[type]; //有可能存了多个事件
if (!fns.length) {
console.log('无'+type+'订阅')
}
for (var i = 0; i < fns.length; i++) {
fns[i]();
}
};
var user = new Pubsub;
function read(){
console.log("I'm reading");
}
function read2(){
console.log("I'm recording");
}
user.on('update',read);
user.on('update',read2);
user.fire('update');
user.off('update');
user.fire('update');
拖放
不太清楚为何
拖放
这节内容会放在高级技巧中, 这里不再讲解.
JS实现拖放的思路就是对一个DOM元素设置绝对定位, 然后根据鼠标的位置, 配合mouseDown/mouseUp/mouseMove事件来动态设置DOM元素的top/left值. 代码略
本章完