狗日的Javascript中的闭包
前面的话:
闭包,是 javascript 中重要的一个概念,对于初学者来讲,闭包是一个特别抽象的概念,特别是ECMA规范给的定义,如果没有实战经验,你很难从定义去理解它。下面是作者从作用域链慢慢讲到闭包以及在后面提到了一些闭包的高级用法。下面大家一起来学习Javascript中的闭包。
谈一谈JavaScript作用域链
当执行一段JavaScript代码(全局代码或函数)时,JavaScript引擎会创建为其创建一个作用域又称为执行上下文(Execution Context),在页面加载后会首先创建一个全局的作用域,然后每执行一个函数,会建立一个对应的作用域,从而形成了一条作用域链。每个作用域都有一条对应的作用域链,链头是全局作用域,链尾是当前函数作用域。
作用域链的作用是用于解析标识符,当函数被创建时(不是执行),会将this、arguments、命名参数和该函数中的所有局部变量添加到该当前作用域中,当JavaScript需要查找变量X的时候(这个过程称为变量解析),它首先会从作用域链中的链尾也就是当前作用域进行查找是否有X属性,如果没有找到就顺着作用域链继续查找,直到查找到链头,也就是全局作用域链,仍未找到该变量的话,就认为这段代码的作用域链上不存在x变量,并抛出一个引用错误(ReferenceError)的异常。
看下面的例子:
//定义全局变量color,对于全局都适用,即在任何地方都可以使用全局变量color var color = "red"; function changeColor(){ //在changeColor()函数内部定义局部变量anotherColor,只在函数changeColor()里面有效 var anotherColor = "blue"; function swapColor(){ //在swapColor()函数内部定义局部变量tempColor,只在函数swapColor()里面有效 var tempColor = anotherColor; anotherColor = color; color = tempColor; //这里可以访问color、anotherColor和tempColor console.log(color); //blue console.log(anotherColor); //red console.log(tempColor); //blue } swapColor(); //这里只能访问color,不能访问anotherColor、tempColor console.log(color); //blue console.log(anotherColor); //anotherColor is not defined console.log(tempColor); //tempColor is not defined } changeColor(); //这里只能访问color console.log(color); //blue console.log(anotherColor); //anotherColor is not defined console.log(tempColor); //tempColor is not defined
还有几个坑需要注意一下:
1、var和函数的提前声明
var color = "red"; function changeColor(){ var color = "yellow"; return color; } var result = changeColor(); console.log(result);
再如:
function fn(a) { console.log(a); var a = 2; function a() {} console.log(a); } fn(1); //输出:function a() {} ,2
2、Javascript中没有块级作用域,但是有词法作用域,比如:
function f1(){var a=1;f2();} function f2(){return a;} var result = f1(); console.log(result); //输出结果:a is not defined
3、在函数内部不用var关键字申明变量,则默认该变量为全局变量,比如:
function add(a,b){ var sum = a+b;//次世代sum为add函数内部的变量,仅限在函数内部使用,在函数外面不可以使用 return sum; } var result = add(1,2); console.log(result); //3 console.log(sum); //sum is not defined //不使用var关键字声明变量 function add(a,b){ sum = a+b;//此时的sum为全局变量,在函数之外也可以调用 return sum; } var result = add(1,2); console.log(result); //3 console.log(sum); //3
补充:
在JavaScript中如果不创建变量,直接去使用,则报错:
1
2
|
console.log(xxoo); // 报错:Uncaught ReferenceError: xxoo is not defined |
JavaScript中如果创建值而不赋值,则该值为 undefined,如:
1
2
3
|
var xxoo; console.log(xxoo); // 输出:undefined |
在函数内如果这么写:
1
2
3
4
5
6
7
|
function Foo(){ console.log(xo); var xo = 'seven' ; } Foo(); // 输出:undefined |
上述代码,不报错而是输出 undefined,其原因是:JavaScript的函数在被执行之前,会将其中的变量全部声明,而不赋值。所以,相当于上述实例中,函数在“预编译”时,已经执行了var xo;所以上述代码中输出的是undefined。
注意:我们平时在声明变量时一定要注意!!!还有不要滥用全局变量(在forin循环的时候特别注意)!!!
4、词法作用域是不可逆的,我们可以从下面的例子中看到结果:
// name = undefined var scope1 = function () { // name = undefined var scope2 = function () { // name = undefined var scope3 = function () { var name = 'Todd'; // locally scoped }; }; };
前面我们了解了作用域的一些基本知识,我们发现有作用域的存在能帮我们省去不少事,但是于此同时,也给我们带来了很多麻烦,比如说我们想在下面的函数A中,调用函数B,我们该怎么办呢?
function A(){
function B(){
//
}
}
思路:我们给函数B设一个返回值,然后在函数A中调用,代码如下:
function A(){ function B(){ console.log("Hello foodoir!"); } return B; } var c = A(); c();//Hello foodoir!
这样我们就可以得到我们想要的结果。这样,我们基本上到了一个最简单的闭包形式。我们再回过头分析代码:
1
2
3
4
5
|
(1)定义了一个普通函数A (2)在A中定义了普通函数B (3)在A中返回B(确切的讲,在A中返回B的引用) (4)执行A(),把A的返回结果赋值给变量 c (5)执行 c() |
把这5步操作总结成一句话:函数A的内部函数B被函数A外的一个变量 c 引用。当一个内部函数被其外部函数之外的变量引用时,就形成了一个闭包。
思考:我们还有没有其他的方法?
思路:使用匿名函数
function A(){ //匿名函数 var B = function(x,y) { return x+y; } console.log(B(1,2));//3 return B(1,2); } var c = A(); console.log(c);//3
然而,在Javascript高级程序设计中是这样描述闭包的“闭包是指有权访问另一个函数作用域中的变量的函数”,但是我们看匿名函数的例子,很明显,这种方法不可取!
通过这个例子,能让我们更好的理解闭包。
下面我们再来看下面的几种闭包
demo1:
function fn(){ var b = "foodoir"; return function(){ console.log(b);//foodoir return b; } } //console.log(b);//b is not defined var result = fn(); console.log(result());//foodoir
demo2:
var n; function f(){ var b = "foodoir"; n = function(){ return b; } } f(); console.log(n());//foodoir
demo3:
//相关定义与闭包 function f(arg){ var n = function(){ return arg; }; arg++; return n; } var m = f(123); console.log(m());//124 //注意,当我们返回函数被调用时,arg++已经执行过一次递增操作了,所以m()返回的是更新后的值。
demo4:闭包中的读取与修改
//闭包中的设置与修改 var getValue,setValue; (function(){ var n = 0; getValue = function(){ return n; }; setValue = function(x){ n = x; } })(); //console.log(n); console.log(getValue());//0 console.log(setValue());//undefined setValue(123); console.log(getValue());//123
demo5:用闭包实现迭代效果
//用闭包实现迭代器效果 function test(x){ //得到一个数组内部指针的函数 var i=0; return function(){ return x[i++]; }; } var next = test(["a","b","c","d"]); console.log(next());//a console.log(next());//b console.log(next());//c console.log(next());//d
demo6:循环中的闭包
//循环中的闭包 function fn(){ var a = []; for(var i=0;i<3;i++){ a[i] = function(){ return i; } } return a; } var a = fn(); console.log(a[0]());//3 console.log(a[1]());//3 console.log(a[2]());//3 /* * 我们这里创建的三个闭包,结果都指向一个共同的局部变量i。 * 但是闭包并不会记录它们的值,它们所拥有的只是一个i的连接,因此只能返回i的当前值。 * 由于循环结束时i的值为3,所以这三个函数都指向了3这一个共同值。 * */
思考:如何使结果输出分别为0、1、2呢?
思路一:我们可以尝试使用自调用函数
function fn(){ var a = []; for(var i=0;i<3;i++){ a[i] = (function(x){ return function(){ return x; } })(i); } return a; } var a = fn(); console.log(a[0]());//0 console.log(a[1]());//1 console.log(a[2]());//2
思路二:我们将i值本地化
function fa(){ function fb(x){ return function(){ return x; } } var a = []; for(var i=0;i<3;i++){ a[i] = fb(i) } return a; } console.log(a[0]());//0 console.log(a[1]());//1 console.log(a[2]());//2
------------------------------------------------------分界线-------------------------------------------------------
在这里,我们来对闭包进行更深一步的操作
我们再将demo1的例子进行扩展
代码示例如下:
function funcTest(){ var tmpNum=100; //私有变量 //在函数funcTest内 //定义另外的函数作为funcTest的方法函数 function innerFuncTest( { alert(tmpNum); //引用外层函数funcTest的临时变量tmpNum } return innerFuncTest; //返回内部函数 } //调用函数 var myFuncTest=funcTest(); myFuncTest();//弹出100
到样,我们对闭包的概念和用法有更加熟悉
闭包和this相关
闭包应用举例,模拟类的私有属性,利用闭包的性质,局部变量只有在sayAge方法中才可以访问,而name在外部也访问,从而实现了类的私有属性。
function User(){ this.name = "foodoir"; //共有属性 var age = 21; //私有属性 this.sayAge=function(){ console.log("my age is " + age); } } var user = new User(); console.log(user.name); //"foodoir" console.log(user.age); //"undefined" user.sayAge(); //"my age is 21"
关于闭包更深入的了解
前面在demo6中,我们了解了用自调用方法来实现闭包,下面我们用这种方法来进行更复杂的操作(写一个简单的组件)。
(function(document){ var viewport; var obj = { init:function(id){ viewport = document.querySelector("#"+id); }, addChild:function(child){ viewport.appendChild(child); }, removeChild:function(child){ viewport.removeChild(child); } } window.jView = obj; })(document);
这个组件的作用是:初始化一个容器,然后可以给这个容器添加子容器,也可以移除一个容器。功能很简单,但这里涉及到了另外一个概念:立即执行函数。 简单了解一下就行。主要是要理解这种写法是怎么实现闭包功能的。
闭包并不是万能的,它也有它的缺点
1、闭包会使得函数中的变量都保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页性能问题。另外在IE下有可能引发内存泄漏 (内存泄漏指当你的页面跳转的时候 内存不会释放 一直占用你的CPU 只有当你关闭了浏览器才会被释放);
2、闭包会在父函数外部改变父函数内部的变量的值,所以不要随便改动父函数内部的值。
更多参考资料:
《Javascript高级程序设计(第三版)》第四章、第七章
《Javascript面向对象编程指南》第三章
作者的话:
这篇文章主要先是通过几个简单的例子介绍作用域链(顺便补充了几个和作用域链相关的易出错的小知识),然后通过提问慢慢过渡到闭包(在闭包这部分介绍了几种常见闭包的例子),后面又进一步讲到了关于闭包的更高级的用法。后面遇到关于闭包的较好的用法会继续更新。