问个闭包中局部变量的问题

curryhh
  • 155
function say() {
 // Local variable that ends up within closure
 var num = 888;
 var sayAlert = function() { alert(num); }
 num++;
 return sayAlert;
}
var sayAlert = say();
sayAlert();//889

为什么最后的结果的889求解答

回复
阅读 2.7k
6 个回答

以下用比较容易理解的思路来说明。

JS中的函数会有个名称,另外还有自己的主体,这是我们拿来作函数定义时使用的,分别说明如下:

  • 函数名称: 是拿来调用(执行、呼叫)的,这样才知道现在你是要调用哪个函数

  • 函数主体: 这中是个块级(区块),用花括号({})括来起来,里面有一些语句,就是在调用了这个函数时要执行的代码

所以函数一经调用时,就会执行自己的主体中的代码,这应该都很好理解。

不过函数在"定义"与"调用",这是两码子事情,这要先在心里有个底,很多情况都会看到有不同之处。

接著,在一个函数主体中也可以定义其它新的函数,我们称它是"内部函数"或"巢状函数",例如"

function funcA() {
  return function funcB() {
    console.log('B');
  }
}

var a = funcA(); //这一行a变量赋给到什么?
a(); //结果是B

上面的a变量,在调用了funcA函数后,它获得的是什么,是funcB的函数主体,也就是被赋值为funcB函数的执行代码内容,而且a变量成了一个函数类型。实验一下,你如果在浏览器主控台直接打印a变量,会得到下面的结果:

function funcB() {
    console.log('B');
}

所以a变量也像函数一样可以调用,调用之后的结果就是打印出B。到这里也都很容易理解了。

接著讲闭包了,下面这个例子会比较简单些:

var a = 1;

function funcA() {
 var b = 2;
 a++;
 return function funcB() {
    console.log(a);
    console.log(b);
 }
}
var funcC = funcA();
funcC(); //2 2

这个示例中的结果,打印出的a变量与b变量都是2,结果一样。闭包要理解得清楚,需要理解好函数的调用情况,以及当下的变量环境,这是什么意思呢?

刚前面已经有很重点的提醒,函数在"定义"与"调用"的情况是不一样的,所谓的不一样指的就是(变量)环境的改变。函数在定义情况的环境是这样,到了要被调用时,有可能被调用当下的变量有所不同,这样会造成函数调用的结果完全不同,尤其是函数主体中有用到外部的变量,外部变量指的是不在这个函数主体中所定义的变量。

以很简单的例子来说明,相信你一定看得懂:

var a = 1;

function funcA() {
  console.log(a)
}

funcA(); // 1

a = 2;
funcA(); // 2

上面的示例因为a变量改变了,而对funcA函数来说它在调用时会用到外部变量a,在调用后的结果要视变量a当下的值来决定结果,所以调用两次的结果是不同的。

这个设计是合理的,函数定义原本就只是集合多个执行语句的区块(块级)定义,调用时视调用当下的变量环境来决定这里面的执行语句该怎么执行,所以函数定义可以在代码文档中后面定义前面调用,这是JS语言与很多编程语言中都有的特色,这种特性称为"提升",不过这是另一回事了。

理解了这种函数"定义"与"调用"的概念,再回头来看原例子就不难了,但还有一个概念没清楚,就是这种变量环境会怎么运作,先来看原例子。

function say() {
 var num = 888;
 var sayAlert = function() { alert(num); }
 num++;
 return sayAlert;
}
var sayAlert = say(); //说明1
sayAlert(); //说明2

先看"说明1"的部份,在这个阶段,如同之前的示例一样,sayAlert变量给定的是由say函数所回传的sayAlert的函数主体,也就是得到function() { alert(num); },并且sayAlert也是成了函数类型。

在"说明2"调用了sayAlert变量所转变的函数主体,在这个情况下,它可能只有两种结果,一种就是888,另一个是有经过++的889。最终的结果是889而不是888,这是题主想要理解的部份。

为何是经过加一的889?

原因首先是之前说的函数定义与调用是两个不同的期间,另一个重点是在于变量值被"记忆"了,这是一个很重要的关键,主要是刚说的内部函数(巢状函数),它本身有一种很特别的设计,它在返回时会"记忆"住在"创建"时的变量环境。

刚只有谈到函数在"定义"与"调用"的两个时期,这时又多了一个"创建"的时期,"创建"时期是一个特别的期间,最常见到的是在内部函数或IIFE样式时,会出现这种期间。实际上你直接在JS的全局中定义出函数,也算被创建了,只是内部函数会比较特别被指出来这个期间。例如下面的示例:

function funcA() {
 var a = 1;
 return function(){
   a++;
   console.log(a);
 }
}

var b = funcA(); //这时间点b函数被创建出来

b(); //2
b(); //3

b = funcA()这行语句执行时,返回的这个内部函数,这就"创建"出b函数,所以返回时除了它的函数主体外,额外也会"记忆"住返回时的变量环境,在这里指的就是在funcA函数主体中的a变量值,也就是a=1这个变量值,所以每次当调用b函数(此时b变量已经变成一个函数,可被调用)时,就会将a变量作加一的处理,然后打印出来。

这种设计称之为"闭包"(closure),比较好的理解是代表这些变量值被封闭到这个函数主体的环境中,当然这个函数仍然需要经过调用,才会执行其中的代码,不过这种函数中会带有在"创建"当下所"记忆"住的变量值。

所以,原题中的例子,在内部函数返回那个当下时(创建时),num变量会被"记忆"起来,也就是经过加一的889,所以最后在调用时,当然就是889了。

注: 由于"闭包"这个字词有多层意义,你可以说它是一种技术,或是一种数据结构,或是这种有记忆环境值的函数。

注: 闭包所记忆的环境值,是"参照"而不是"复制"这些值,这一点要特别注意

注: 上面所说的"变量环境"改用"词法环境"会更接近标准中的定义

1.为什么不是888
var sayAlert = function() { alert(num); } 这句的意思是,创建一个函数,函数体是xx,赋值给sayAlert,所以alert并没有执行。所以其在num++之后执行。

2. 为什么可以访问 num
因为变量的访问看函数定义所在的位置,而不是调用的位置,详见闭包。

var sayAlert = say();

的时候num已经++了,变成了889

var sayAlert = function() { alert(num); }

这里的say()的内部变量num被外部引用了,所以常驻内存,没有被回收掉

这是js中的闭包,这方面知识还是有必要回去多看看理解理解的。还有就是js和其他语言一样,是在堆栈中运行的,say()函数执行的时候先压栈,因为num++;所以num的值变为889,之后sayAlert();执行,再压栈,打印输出
这是网上的闭包讲解可以看看
闭包

sayAlert 没有自己的局部变量,却可以访问 say 中的变量,这就是闭包在起作用,它是 JavaScript 中最基本的概念,但若要搞清它,得先深刻理解执行上下文,词法环境,作用域链这些概念,建议先查 MDN

因为sayAlert中的num是对外部num的引用

宣传栏