在学习js的过程中,我们都会遇到闭包和立即执行函数的相关概念,今天就这两个概念做一个大致的整理。
本文结构:
-
闭包
- 闭包的概念
-
立即执行函数
- 什么是立即执行函数
- 一个经典面试题
- es6 中的 let
一、闭包
我们先来看闭包的作用:闭包通常用来创建内部变量,使得这些变量不能被外部随意修改,同时又可以通过指定的函数接口来操作。
对于不理解闭包的朋友来说,上面这句话并没有什么用。所以我们通过一个简单的例子来理解这个概念。
比如说我们现在要写一个游戏,每个人初始都有5点精力,赢了就可以加1点精力,输了就要减1点精力。
var amount=5; //初始有5点精力
function add(){ //增加精力
amount+=1;
return amount;
}
function minus(){ //减少精力
amount-=1;
return amount;
}
//第一局赢了,调用增加精力函数
add();console.log(amount); //结果为6
//第二局输了,调用减少精力函数
minus();console.log(amount); //结果为5
这样就初步实现了我们想要的效果。
但是,这种做法存在这样一个问题: 由于amount暴露在全局作用域下,是一个全局变量,所以任何人都可以随意修改我的精力值。如果我们不小心在另一个js文件里也声明了amount这样一个全局变量并给它赋值,那么我们这里的amount就可能被覆盖。
我们当前不希望amount被覆盖,我们希望amount能“私有”一些,因此,我们对amount提出了两点要求:
- 它不能被外部随意修改;
- 需要能通过指定的方式去操作它。
首先,我们把上述代码包在一个函数里:
function xxx(){
var amount=5;
function add(){
amount+=1;
return amount;
}
function minus(){
amount-=1;
return amount;
}
}
这样,amount的作用域就变为了函数xxx的内部,在函数外面无法访问,这样就实现了“不能被外部随意修改”第一个要求。
但是,在函数xxx的外部,我们完全操作不到amount。因此,我们还需要给函数xxx留一个接口,让我们能够在函数外部去操作它。这该怎么办呢?
经过一番苦思冥想,我们终于想到,是否可以利用函数的返回值呢?
function xxx(){
var amount=5;
function add(){
amount+=1;
console.log(amount); //实时打印amount的当前值
return amount;
}
function minus(){
amount-=1;
console.log(amount); //实时打印amount的当前值
return amount;
}
return add; //为该函数添加一个返回值,返回add函数
}
然后调用函数xxx:
var outer_add=xxx();
outer_add(); //结果为6
outer_add(); //结果为7
我们发现,amount的值改变了,我们实现了我们想要实现的效果。
当调用函数xxx时,我们将xxx内部的add函数作为操作amount的一个接口返回,outer_add函数和xxx内部的add函数实际上是“一模一样”的(因为它们都指向同一块内存),而函数add可以访问amount,因此我们可以通过outer_add来访问amount。
需要注意的是,由于内部函数add在被访问后一直处于被引用状态,不能够被垃圾回收。一旦我们不再使用outer_add函数,可以将其设置为null ,使其内存得到释放。
实际上,这就是闭包的典型用法。现在我们再回头看看开头提到的闭包的作用,是否理解一些了呢?
细心的朋友可能已经发现了,按这种做法,我们只能返回一个函数add,那函数minus怎么办呢?
我们这样修改:
function xxx(){
var amount=5;
function add(){
amount+=1;
console.log(amount);
return amount;
}
function minus(){
amount-=1;
console.log(amount);
return amount;
}
return {
outer_add:add,
outer_minus:minus
}
}
然后调用函数xxx:
var outer=xxx();
outer.outer_add(); //结果为6
outer.outer_add(); //结果为7
outer.outer_minus(); //结果为6
这样,我们虽然不能直接操作amount这个局部变量,但可以通过函数xxx暴露出的函数outer_add和outer_minus来间接操作amount。
我们也可以这样修改:
function xxx(){
var amount=5;
function add(){
amount+=1;
console.log(amount);
return amount;
}
function minus(){
amount-=1;
console.log(amount);
return amount;
}
window.outer_add=add; //暴露outer_add函数,可间接访问amount
window.outer_minus=minus; //暴露outer_minus函数,可间接访问amount
}
然后调用函数xxx:
xxx();
outer_add(); //结果为6
outer_add(); //结果为7
outer_minus(); //结果为6
结果相同。
二、立即执行函数
1、什么是立即执行函数
立即执行函数,顾名思义,声明一个函数,并立即执行它。在上面的代码中,我们定义了一个函数xxx,并调用了它。这个函数用于页面初始化,并且只会调用一次,因此我们可以使用立即执行函数来代替函数xxx。
改写代码如下:
function(){ //声明了一个匿名函数
var amount=5;
function add(){
amount+=1;
console.log(amount);
return amount;
}
function minus(){
amount-=1;
console.log(amount);
return amount;
}
window.outer_add=add; //暴露outer_add函数,可间接访问amount
window.outer_minus=minus; //暴露outer_minus函数,可间接访问amount
}(); //立即调用这个匿名函数
但是我们发现,浏览器竟然报错了。
首先我们区分一下函数声明和函数表达式:区分函数声明和函数表达式最简单的方法是看function关键字出现在声明中的位置。如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
所以,上述代码之所以报错,是因为浏览器认为函数声明后再加 “(” 是错误的。所以我们需要通过加()、+、-、~、!等运算符,使上面的代码变为函数表达式,这样就不会报错了,如:
(function(){
var amount=5;
function add(){
amount+=1;
console.log(amount);
return amount;
}
function minus(){
amount-=1;
console.log(amount);
return amount;
}
window.outer_add=add; //暴露outer_add函数,可间接访问amount
window.outer_minus=minus; //暴露outer_minus函数,可间接访问amount
}());
然后我们就可以操作outer_add和outer_minus了:
outer_add(); //结果为6
outer_add(); //结果为7
outer_minus(); //结果为6
这就是立即执行函数的作用,创建一个局部作用域,使不想暴露在全局的变量变为局部变量。
2、一个经典面试题
题目如下:
for(var i=0;i<5;i++){
setTimeout(function(){
console.log(i);
},1000);
}
结果:1s后,连续打印出5个5。
但是我们的本意是1s后分别打印出0、1、2、3、4,这时,我们就可以利用闭包和立即执行函数:
for(var i=0;i<5;i++){
!function(j){
setTimeout(function(){
console.log(j);
},1000);
}(i);
}
结果:1s后,连续打印出0、1、2、3、4。
3、es6中的let
因为let声明的变量是块级作用域变量(而var声明的变量为函数作用域变量),因此,我们可以利用let的特性来创建一个局部作用域,从而取代立即执行函数:
{ //用块级作用域{}代替立即执行函数
let amount=5;
function add(){
amount+=1;
console.log(amount);
return amount;
}
function minus(){
amount-=1;
console.log(amount);
return amount;
}
window.outer_add=add; //暴露outer_add函数,可间接访问amount
window.outer_minus=minus; //暴露outer_minus函数,可间接访问amount
}
然后我们就可以操作outer_add和outer_minus了:
outer_add(); //结果为6
outer_add(); //结果为7
outer_minus(); //结果为6
块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。
这篇博客就先整理到这里。由于个人水平有限,博客错误之处,烦请指正!