这一篇讲解一下js中乱起八糟却特别有用的东西:
1、闭包
2、this作用域
3、arguments对象
4、懒加载
闭包:
闭包这个东西,或者说这个概念,如果要在网上找到特别清晰明朗的解释是不容易的,因为自从jQuery把闭包发扬光大后,大家才逐渐开始使用起这个技术。
这是我从一本书上摘抄的一句话:在函数内部定义了其他函数时,就创建了闭包。
是的,简单、直接却很明了的解释了什么是闭包。有人会提自执行的匿名函数了,我们暂时把它称之为"较为特殊的闭包"或"块级作用域"。
来看一段代码明白我们为什么要使用这个技术:
function foo(){
var results=[];
for(var i=0;i<3;i++){
results[i]=function(){
console.log(i);
}
}
return results;
}
var results=foo();
results[0]();//3
results[1]();//3
results[2]();//3
可以看出console.log(i);中的i保存的仅仅是一个引用,而不是真正的值。
解决的办法是构建一个闭包:
function foo(){
var results=[];
for(var i=0;i<3;i++){
results[i]=(function(i){
return function(){
console.log(i);
}
})(i);
}
return results;
}
var results=foo();
results[0]();//0
results[1]();//1
results[2]();//2
我们把重点放在这一句话:
results[i]=(function(i){
return function(){
console.log(i);
}
})(i);
(function(i){})(i);就是一个闭包,或者说是一个块级作用域,(i)代表调用,i是传入的参数,所以这是一个自执行的函数。
return function(){...}是返回一个function,将一个function赋给results[i]。同时将传入的i参数保存下来。
闭包的原理如下:
1、在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域。
2、通常,函数的作用域及其变量都会在函数执行完成后销毁。但是,当函数返回了一个闭包时,这个函数的作用域会一直在内存中保存到闭包不存在为止。
顺便多提以下闭包模仿js中的块级作用域:
1、创建自执行的函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用。
2、结果就是函数内部的所有变量都被销毁——除非把变量赋值给了包含作用域中的变量。
使用闭包时需要注意:闭包是极其有用的特性。但是创建闭包必须维护额外的作用域,所以过度使用它们可能会占用大量内存。
this作用域&arguments对象:
js方法中的this可以参考这一篇文章:js-静态、原型、实例属性。
看下面的例子:
function foo(){
console.log(this==window);
}
var obj=new Object();
Object.foo=function(){
return this;
}
obj.foo=function(){
this.fn=function(){
console.log(this==obj);
};
}
obj.fun=function(){
return function(){
console.log(this==window);
}
}
var name="window";
obj.myfun=function(){
this.name="myfun";
function closure(){
console.log(this.name);
}
return closure.call(this);
}
var obj1=new Object();
obj1.foo=Object.foo;
console.log(obj1.foo()==obj1);//true 1
console.log(Object.foo()==Object);//true 2
foo();//true 3
obj.foo();//给obj添加fn方法 4
obj.fn();//true 5
obj.fun()();//true 6
obj.myfun();//myfun 7
由1、2、3句的结果可以得出this对象取决于调用方式的不同:
1、当作为obj1的实例方法调用时,this指向的是obj1
2、当作为静态方法调用时,this指向的是Object
3、当在window作用域下执行时,this执行的是window
5的执行结果可以看出给对象添加方法时,this执行的是当前对象。
6句返回window,可以看出返回函数时,函数的定义不是在当前对象上,而是在window中,所以this指向window。
最后一句obj.myfun函数展示了将函数绑定到特定作用域下执行的方式:使用call方法,第一个参数指定执行的作用域。
arguments对象:
刚才提到了call方法,同样函数还存在一个apply方法,他们的作用都是将函数绑定到指定的作用域,也就是纠正this指针,唯一不同的是传参的方式不同:call接收多个参数,第一个参数是指定的作用域,后面可以指定多个参数。而apply只接受两个参数,第一个参数也是作用域,第二个参数是一个数组。
来看一个例子:
function bind(fn,context){
if(arguments.length<2)return fn;
context=context||window;
var args=Array.prototype.slice.call(arguments,2);
return function(){
var innerArgs=Array.prototype.slice.call(arguments,0),
finalArgs=args.concat(innerArgs);
return fn.apply(context,finalArgs);
}
}
这是个经典的函数绑定的方式,将函数fn绑定到context上执行。
Array.prototype.slice.call(arguments,2)
这句话可能会有很多人搞不懂为什么用这样的写法。
首先arguments对象并不是一个真正的数组,只不过它的数据格式比较像数组而已,也有length属性。而Array.protoype.slice方法可以根据对象的length属性进行处理,然后返回一个数组。
arguments对象还有一个成员:callee
function count(n){
if(n==0)return 0;
return arguments.callee(n-1)+n;
}
上面是一个典型的递归,arguments.callee代表对自身的调用,所以这种写法也常见于递归。但是这种写法更加解耦,同样可以对匿名函数进行更好的处理。
arguments.callee.caller表示了当前函数的调用者,如果没有调用者返回null。
arguments、call、apply、callee、caller这些可能经常被混淆,但他们都有各自的应用场合,只要明白各自的概念也就可以很好的使用。
懒加载:
来看一下一个创建xhr的函数:
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];
} catch (ex){
//skip
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
} else {
throw new Error("No XHR object available.");
}
}
可以看出,每次调用createXHR方法的时候,它都要对浏览器的支持能力进行检查。首先检查内置的XHR,然后检查有没有基于ActiveX的XHR,最后都没有就抛出一个异常。
每次调用都做相同的检查,如果浏览器内置支持XHR,就一直支持,那么这样检查就没有必要了。解决方法就称之为惰性加载的技巧。
懒加载表示函数执行的分支仅执行一次。
有两种懒加载的实现方式:
1、在函数被调用时再处理函数——在第一次执行的过程中,该函数被覆盖成另一个按合适方式执行的函数,这样对原函数的执行就不需要再经过任何分支了。
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();
}
在这个懒加载的createXHR函数中,每一个if分支都会为变量createXHR赋值,有效覆盖了原有函数。下一次再调用该函数时,就会直接调用被分配好的函数,这样就不用再执行if语句了。
2、在声明函数时就指定适当的函数——这样在首次调用函数时就不会损失性能了,而在代码首次加载时损失一点性能。
var createXHR=(function(){
if (typeof XMLHttpRequest != "undefined"){
return function(){
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];
} catch (ex){
//skip
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
};
} else {
return function(){
throw new Error("No XHR object available.");
};
}
})();
该方式使用的技巧是创建一个匿名、自执行的函数,用以确认改实现哪一个函数实现。
懒加载函数的优点是只在执行分支代码时牺牲一点性能。至于哪种方式更合适,就需要根据你的具体需求而定了。不过两种方式都能避免执行不必要的代码。
js的大杂烩到此结束了,有疑问或错误之处欢迎指出,请大家多多赐教。
预告:下一篇待定,这次留点神秘感。好吧,我这次还没想好