闭包及其作用
闭包是JavaScript中的一大难点,在学习闭包之前,首先我们必须清楚高阶函数、变量的作用域、作用域、作用域链和执行上下文,如果对高阶函数、变量的作用域、作用域、作用域链和执行上下文不是很清楚的同学,我建议先去学习相关的理论基础再过来学习闭包,这样理解的会更加透彻。
高阶函数
高阶函数是对其他函数进行操作的函数,它接收函数作为参数或将函数作为返回值输出(函数作为返回值,函数作为参数);JavaScript的回调函数是以实参形式传入其他函数中,这也是高阶函数(在函数式编程中回调函数被称为 lambda表达式)
//第一种情况,将函数作为参数(回调函数)
function fn(callback) {
callback && callback();
}
fn(function () {
alert('hello world');
});
//第二种情况,将函数作为返回值输出
function fn() {
return function () {
alert('hello world');
}
}
fn()();
变量作用域
- 函数内部可以使用全局变量
- 函数外部不可以使用局部变量
- 当函数执行完毕,本作用域的局部变量会销毁
闭包(closure)
在很多时候我们要获取函数的局部变量,在正常情况下是不允许的(上述我们已经说过),这个时候就要用到我们的闭包了,闭包:指有权访问另一个函数作用域中变量的函数;闭包是允许函数访问局部作用域之外的数据。即使外部函数已经退出,外部函数的变量仍可以被内部函数访问到;闭包的主要作用:延伸了变量的作用范围
闭包实现的三个条件:
- 内部函数访问了外部函数的变量
- 外部函数已经退出
- 内部函数仍可以访问
function fun() {
var x = 0;
return function (y) {
x = x + y;
console.log(x);
}
}
var a = fun();
a(1); //1
a(1); //2
上述代码在执行的时候,a得到的是闭包对象的引用,虽然fun函数执行完毕,但是fun的活动对象由于闭包的存在并没有被销毁,在执行a(1)
的时候,仍然访问到了x变量,并将其加1再赋值给x变量,若再执行a(1)
,则x变量值为
2
,因为闭包的引用a并没有消除。(闭包返回了函数,函数可以创建独立的作用域)
闭包的核心内容:有些情况下(函数调用返回一个函数),函数调用完成之后,其执行上下文环境不会接着被销毁。
//全局执行上下文
function fun() {
var max = 10;
// fun函数执行上下文
return function bar(x) {
if (x > max) {
console.log(x);
}
}; //bar函数执行上下文
}
var f = fun();
f(15);
上面代码在执行到调用fun()函数后,按道理应该会销毁掉fun()函数执行上下文和其中的数据,但是这里没有销毁,因为在fun()函数执行完毕之后返回的是一个函数,函数能够创建独立的作用域,而正巧合的是,返回的这个函数体中,还有一个自由变量max
要引用fun函数
作用域下的fun()函数执行
上下文环境中的max。因此,这个max不能被销毁,销毁了之后bar函数中的max就找不到值了。因此,这里的fun()函数上下文环境不能被销毁,还存在执行上下文栈中,没有出栈,所以使用闭包会增加内容开销,在IE中可能导致内存泄露。解决方法:在退出函数之前,将不使用的局部变量全部清除(变量赋值为null)。
闭包案例
循环注册点击事例(点击li输出当前的li的索引号)
1、利用动态添加属性的方式
for (var i = 0; i < lis.length; i++) {
lis[i].index = i;//动态添加index属性
lis[i].onclick = function () {
console.log(this.index);
}
}
2、利用闭包的方式得到当前li的索引号
//闭包不符合预期
var lis = document.querySelectorAll('li');
for (var i = 0; i < 5; i++) {
lis[i].onclick = function () {//函数执行是在循环之后才运行,变量i是全局作用域
console.log(i);
}
}
上面代码输出结果都是 5,这不符合预期,由于作用域链的一个副作用,闭包只能取得包含函数中任何变量的最后一个值。闭包保存的是整个变量对象,而不是某个特殊的变量。
解决办法:
//方法一:利用立即执行函数强制让闭包符合预期
for (var i = 0; i < 5; i++) {
//利用for循环创建了4个立即执行函数
(function (i) {
lis[i].onclick = function () {
console.log(i);
}
})(i)
}
//立即函数也称为小闭包,因为在立即函数里面的任何一个函数都可以访问i这个变量
//方法二:使用let声明变量
var lis = document.querySelectorAll('li');
for (let i = 0; i < 5; i++) {
//每次循环会产生一个块级作用域
lis[i].onclick = function () {
console.log(i);
}
}