定义
函数可以访问它被创建时的上下文环境,称为闭包
内部函数比它的外部函数具有更长的生命周期
function closure(name) {
var status = 1;
return {
getName: function() {
return name;
},
getStatus: function() {
return status++;
}
}
}
var a = closure('w3ctech');
alert(a.getName()); // w3ctech
alert(a.getStatus()); // 1
alert(a.getStatus()); // 2
函数作用域:
- 变量和参数在函数外不可见
- 变量可以在函数内任何位置定义,并在函数内任何地方可见
- 嵌套函数可以访问外部函数的参数和变量
闭包应用场景:
- 实现私有成员
- 保护命名空间
- 避免污染全局变量
- 变量需要长期驻留在内存
对闭包的理解
闭包return的是一个funciton
函数A中return了一个函数B,所以外界C在调用A时,相当于调用了B,C是不可以直接访问A中的变量的,但是通过B,C就访问了A中的变量
闭包的目的有两个:
- 从外部得到函数内部的局部变量
- 让这些局部变量的值始终存在内存当中
function f1(){
var n=999;
function f2(){
alert(n);//获得f1的内部变量
}
return f2;
}
f1()();//f1()返回值是f2,需要对f2再次引用
闭包的定义:闭包(closure)是能读取他函数内部变量的函数(即定义在函数内部的函数),上面的例子中f2就是一个闭包,f2在f1内部需要作为返回值被输出。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
f1()()的引用相当于一个全局变量,所以f2式中存在于全局变量中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
使用闭包要注意:变量式中存在于内存中,内存消耗大。
思考题
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function() {
return function() {
return this.name;
};
}
};
alert(object.getNameFunc()()); //结果 The Window
由于this
始终表示对调用者的引用,object.getNameFunc()
的返回值是object
对象内部的匿名函数,这个匿名函数的调用者是window
,所以this
的指向就是window
,最后一句相当于打印的就是window.name
,结果是'The Window'
那么为什么返回的匿名函数没有取得其包含作用域的this
对象呢?
每个函数在被调用时都会自动取得两个特殊变量,
this
和arguments
。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数的这两个变量。不过把外部作用域的this
对象保存在一个闭包能搞访问的变量里,就可以让闭包访问该对象了。 – 《JavaScript高级程序设计》 P182
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function() {
var that = this;
return function() {
return that.name;
};
}
};
alert(object.getNameFunc()()); //结果My Object
that是对象内部的this的值,所以结果是My Object
for循环赋值的例子
引入
下面的这个例子,本意是点击不同的li,打印出对应的索引,但是结果是无论点击哪个li,结果都是3
<body>
<p title="选择你最喜欢的水果?">你最喜欢的水果是?</p>
<ul>
<li id="li1" title="苹果">苹果</li>
<li title="橘子">橘子</li>
<li title="菠萝">菠萝</li>
</ul>
<script>
var li = document.getElementsByTagName('li');
for(var i= 0; i<li.length; i++){
li[i].onclick = function(){
alert(i)
}
}
</script>
JavaScript的事件处理机制
一般而言,操作分为:发出调用和得到结果两步。发出调用,立即得到结果是为同步。发出调用,但无法立即得到结果,需要额外的操作才能得到预期的结果是为异步。同步就是调用之后一直等待,直到返回结果。异步则是调用之后,不能直接拿到结果,通过一系列的手段才最终拿到结果(调用之后,拿到结果中间的时间可以介入其他任务)
JavaScript语言的一大特点就是单线程,对于用户操作事件都是异步调用的,比如onclick事件,当onlick事件被定义后,它会调用浏览器对应的API,监听对应对象的click行为,当行为产生后,浏览器就会将这个事件对应的回调函数放到任务队列中。
当引擎解析完所有语句与dom之后(也就是栈中的代码执行完毕之后),主线程就会去读取任务队列,依次执行那些事件对应的回调函数。
执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行。
原因分析
会产生这种原因有两个:
- 作用域,JS中是没有块作用域的,只有全局作用域和函数作用域
- JS的事件处理机制
上面的for循环的执行过程大致如下:
- for循环每执行一次,调用dom中的onclick事件,将事件加入到任务队列中,总共加入了3个click事件
- 包括for在内的所有语句执行完毕,此时i=3(当i=2时for内部语句执行最后一次,i++后i=3)
- event loop开始起作用,轮询任务队列,分别执行
- 此时函数中的i都指向全局环境中的i,所以都为3
解决方法
利用闭包创建局部的作用域,将每个i都传入到函数内部并保存起来,具体有两种方法
方法1:创建一个自执行的匿名函数,将变化的i传入函数内部并保存起来(自执行是关键,这样就不需要放到队列中去等待进行,而是在循环的过程中执行)
/***利用自执行匿名函数创造一个闭包域空间,将i的值储存在函数内部
for(var i= 0; i<li.length; i++){
(function(j){
li[j].onclick = function(){
alert(j)
}
})(i)
}
****/
方法二:也是利用闭包,办法一是在新增的匿名闭包空间内完成事件的绑定,而此例是将事件绑定在新增的匿名函数返回的函数上
for(var i= 0; i<li.length; i++){
li[i].onclick = (function(j){
return function(){
alert(j)
}
})(i)
}
方法三:利用ES6的let声明块级变量
for(let i= 0; i<li.length; i++){
li[i].onclick = function(){
alert(i)
}
}
原因是:变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。
用闭包隐藏数据
闭包经常用于创建含有隐藏数据的函数(但并不总是这样)。
var db = (function() {
// 创建一个隐藏的object, 这个object持有一些数据
// 从外部是不能访问这个object的
var data = {};
// 创建一个函数, 这个函数提供一些访问data的数据的方法
return function(key, val) {
if (val === undefined) { return data[key] } // get
else { return data[key] = val } // set
}
// 我们可以调用这个匿名方法
// 返回这个内部函数,它是一个闭包
})();
db('x'); // 返回 undefined
db('x', 1); // 设置data['x']为1
db('x'); // 返回 1
// 我们不可能访问data这个object本身
// 但是我们可以设置它的成员