一、闭包
闭包就是指有权访问 另一个函数作用域 中的变量 的函数 !!!
好处:灵活方便,可封装
缺点:空间浪费、内存泄露、性能消耗
1、简单例子
一般函数执行完后局部变量释放,有闭包则局部变量不能在函数执行完释放。
调用outer()返回匿名函数,这个匿名函数仍然可以访问外部outer的局部变量localVal,所以outer执行完成后localVal不能被释放。
outer()调用结束,func()再次调用的时候仍然能访问到外层的outer()这个外函数的局部变量。这种情况就是通常所说的闭包。
2、前端闭包
点击事件里面用到外层的局部变量,有了闭包在数据的传递上更为灵活。
!function(){ var localData="localData here"; document.addEventListener('click', function(){ console.log(localData); }); }();
异步请求,用$.ajax()方法,在success回调中,用到外层的这些变量。在前端编程中,经常直接或间接,有意或无意用到闭包。
!function(){ var localData="localData here"; var url="http://www.baidu.com"; $.ajax({ url:url, success:function(){ //do sth console.log(localData); } }); }();
3、常见错误—循环闭包
期望结果:点击aaa弹出1,点击bbb弹出2,点击ccc弹出3。
<script> document.body.innerHTML="<div id=div1>aaa</div>"+"<div id=div2>bbb</div><div id=div3>ccc</div>"; for(var i=1;i<4;i++){ document.getElementById('div'+i).addEventListener('click',function(){ alert(i);//all are 4!!! }); } </script>
这段代码执行后无论点击哪个,弹出的永远是4。
addEventListener里面是个回到函数, 当点击的时候,这个回调函数才会动态的拿到i的值,在整个初始化完成之后i的值就已经是4了。
正确做法:
在每次循环的时候用一个立即执行的匿名函数包装起来,每次循环的时候把i的值传到匿名函数里面,在匿名函数里面再去引用i。这样的话,在每次点击alert的函数i会取自每一个闭包环境下的i,这个i来源于每次循环时的赋值i,这样的话才能实现点击弹出1,2,3的次序。
document.body.innerHTML="<div id=div1>aaa</div>"+"<div id=div2>bbb</div><div id=div3>ccc</div>"; for(var i=1;i<4;i++){ !function(i){ document.getElementById('div'+i).addEventListener('click',function(){ alert(i);//all are 4!!! }); }(i); }
4、闭包的好处—封装
(function(){})() 里面定义一些想让外部无法直接获取的变量_userId,_typeId,最后通过window.export=export把最终想输出的对象输出出去。
<script> (function(){ var _userId=23492; var _typeId='item'; var myExport={}; function converter(userId){ return +userId; } myExport.getUserId=function(){ return converter(_userId); } myExport.getTypeId=function(){ return _typeId; } window.myExport=myExport; })(); console.log(myExport.getUserId()); //23492 console.log(myExport.getTypeId()); //item console.log(myExport._userId);//undefined console.log(myExport._typeId);//undefined console.log(myExport.converter);//undefined </script>
对应外部使用export对象上的getUserId()方法的人来说,只能通过export上提供的方法来间接访问到具体的函数里面的变量,利用了闭包的特性,getUserId在函数执行完了后仍然能访问到里面的自由变量。
在函数外面无法通过myExport._userId直接访问变量,也没法去改写变量。
二、作用域
1、全局\函数\eval作用域
比较简单。有时候也经常引起误解。有哪几种作用域:全局、函数和eval作用域。
2、作用域链
闭包outer1里可以访问到自由变量local2也可以访问到global3。
function outer2(){ var local2=1; function outer1(){ var local1=1; //可以访问到 local1,local2 or global3 console.log(local1+','+local2+','+global3); } outer1(); } var global3=1; outer2();//1,1,1
3、利用函数作用域封装
如果没有一些模块化的工具的话,经常看到很多类库或者代码最外层,去写一个匿名函数如下:
(function(){ //do sth here var a,b; })();
或者
!function(){ //do sth here var a,b; }();
或者
+function(){ //do sth here var a,b; }();
好处:把函数内部的变量变成函数的局部变量,而不是全局变量,防止大量的全局变量和其他代码或者类库冲突。
用!或者+目的是把函数变成函数表达式而不是函数声明。如果省略掉!,把一个完整的语句以function开头的话,会被理解为函数声明,会被前置处理掉,最后留下一对括号或者函数声明省略了名字的话都会报语法错误。
三、ES3执行上下文(可选)
每一次函数调用的时候,都有一套执行环境(execution context)。
抽象概念:执行上下文,变量对象
1、执行上下文
类似一个栈的概念。
函数调用1万次就会有1万个Execution context执行上下文。
console.log('EC0'); function funcEC1(){ console.log('EC1'); var funcEC2=function(){ console.log('EC2'); var funcEC3=function(){ console.log('EC3'); } funcEC3(); } funcEC2(); } funcEC1(); //EC0 EC1 EC2 EC3
控制权从EC0到EC1到EC2到EC3,EC3执行完后控制权退回到EC2,EC2执行完之后控制权退回到EC1,EC1执行完后退回到EC0
2、变量对象
JavaScript解释器如何找到我们定义的函数和变量?
需要引入一个抽象名词:变量对象。
变量对象(Variable Object,缩写为VO)是一个抽象概念中的“对象”,它用于存储执行上下文中的:1、变量2、函数声明3、函数参数。
即
例子:比如有一段javaScript代码
var a=10; function test(x){ var b=20; } test(30);
全局作用域下的VO等于window,等于this。
3、全局执行上下文(浏览器)
在JavaScript第一行就可以调用Math,String,isNaN等方法,在浏览器里也可以拿到window,为什么?
因为在全局作用域下,背后就有一个变量对象VO(globalContext)===[[global]];
在第一行代码执行之前,浏览器js引擎会把一些全局的东西初始化到VO里面,比如[[global]]里面有Math方法,String对象,isNaN函数,等,也会有一个window,这个window会指向它这个全局对象本身。
VO对象是一个标准抽象的概念,对应javascript语言本身,是不可见的,没办法直接访问到,
比如函数对象的VO是没任何办法拿到的;但是在浏览器里面有一个全局的window会指向它自己,所以在控制台里用window.window.window.window...可以一直嵌套下去可以证明这是个无限循环。
String(10)背后就是会访问对应的VO对象,也就是[[global]]对象,拿到[[global]]对象的属性String。
4、函数中的激活对象
函数稍微特殊一点,函数中还有一个概念叫激活对象。
函数在执行的时候会把arguments放在AO激活对象中。
初始化auguments之后呢,这个AO对象又会被叫做VO对象。
和全局的VO一样,进行其他一些初始化,比如说初始化函数的形参,初始化变量的声明,或者是函数的声明。
4.1、变量初始化阶段
目的主要是理解一点:为什么函数和变量的声明会被前置?为什么匿名函数表达式的名字不可以在外面调用?
对于函数对象的VO来说,分为2个阶段,第一个阶段为变量初始化阶段。
上面说了全局作用域下VO变量初始化会把Math,String等一些全局的东西放进去。在第二个阶段才能更好的执行代码。
函数的变量初始化阶段会把arguments的初始化,会把变量声明和函数声明放进去。
具体操作:
VO按照如下顺序填充: 1、函数参数(若未传入,初始化该参数值为undefined) 2、函数声明(若发生命名冲突,会覆盖) 3、变量声明(初始化变量值为undefined,若发生命名冲突,会忽略)
注意一点:函数表达式不会影响VO
比如上面,var e=function _e(){};中_e是不会放到AO中的。这也是为什么在外面不能通过_e拿到函数对象。
函数变量初始化的阶段把函数声明d放到了AO中,这也就解释了为什么函数声明会被前置。
函数声明冲突会覆盖,变量什么冲突会忽略。
4.2代码执行阶段
这段代码:
第一阶段:变量初始化阶段AO如下
第二阶段:代码执行阶段
得到
5、测试一下
<script> console.log(x); //function x(){} var x=10; console.log(x);//10 x=20; function x(){} console.log(x); //20 if(true){ var a=1; }else{ var b=true; } console.log(a); //1 console.log(b); //undefined </script>
本文作者starof,因知识本身在变化,作者也在不断学习成长,文章内容也不定时更新,为避免误导读者,方便追根溯源,请诸位转载注明出处:http://www.cnblogs.com/starof/p/4904929.html有问题欢迎与我讨论,共同进步。