JS之作用域与闭包
作用域在JS中同样也是一个重要的概念。它不复杂,因为ES5中只有全局作用域和函数作用域,我们都知道他没有块级作用域。但在ES6中多了一个let,他可以保证外层块不受内层块的影响。即内层块形成了一个块级作用域,这是let的一个特点。它不简单,因为在许多的函数嵌套的情景下,只有对它理解深刻,才能更好的去分析。今天我们着重讲的是函数作用域与全局作用域。
同样在分析之前,我们来看一段代码。
var a=1; function f1(){ var b=2; function f2(){ var c=b; b=a; a=c; console.log(a,b,c); } f2(); } f1();//2,1,2
上面的代码,有三个执行上下文环境(EC),全局EC,f1EC,f2EC。全局环境下有一个变量a和一个函数f1(),在f1环境中,有一个变量b和一个函数f2(),在f2环境中有一个变量c。但在f2中,可以访问到f1环境中的b,也可以访问到全局环境中的a,在f1中,可以访问到全局环境下的a,但不可以访问f2中的c,在全局中,不可以访问f1中的b也不可以访问f2中的c.。这就是一个作用域链。
于是,我们可以知道,函数的内部环境可以通过作用域链访问到所有的外部环境,但是外部环境却不可以访问外部环境,这就是作用域的关键。但是我们要知道,作用域是在一个函数创建时就已经形成的,而不是调用时。所以有些人可能会认为按着作用域链向上查找是查找它的父作用域,就像上面的那个例子。但是这个例子只是一种特殊情况。我们要认识到并不是查找它的父作用域,而是查找创建该函数的那个作用域。看下面的这段代码。
var a=10; function fn(){ var a=20; return function b(){ console.log(a); }; } var g=fn(); g();//20
这里我们在调用g函数,发现他的值是20,。而它所谓的父作用域应该是全局作用域,但它却不是10.所以这就说明了作用域链向上查找是寻找创建它的那个作用域。
上面的这个例子同时引出了我下面要说明的一个问题,闭包。
闭包,是函数中一个核心的概念。它的文字说明多种多样,我看过很多人对它的文字描述,虽然说得不全一样,但是中心观点都差不太多。我个人认为我最喜欢的一个文字描述就是《锋利的jquery》关于插件描述的那个章节中的一段说明。
闭包,允许使用内部函数(即函数定义和函数表达式位于另一个函数的函数体内),而且,这些内部函数可以访问他们所在的外部函数中的声明的所有局部变量丶参数和声明的其他内部函数,当其中一个这样的内部函数在包含他们的外部函数之外被调用时,就会形成闭包。即内部函数会在外部函数返回后被执行。而当这个内部函数执行时,它仍然必须访问其外部函数的局部变量丶参数以及其他内部函数。这些局部变量丶参数和函数声明(最初时)的值是外部函数返回时的值,但也会受到内部函数的影响。
上面这段话就是《锋利的jquery》中关于闭包的一段描述。说的很长很详细,简单来说,就是在一个函数a内部定义的另一个函数b,当b在a之外被执行时,就会形成闭包。同时b函数仍然可以访问到a函数中的局部变量与函数。
我们在开始了解闭包时,有一个特别经典的题目。看下面代码
function fn(){ var array=[]; for(var i=0;i<10;i++){ array[i]=function(){ return i; } } return array; } fn();//[ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ]
我们的本意是得到这个数组中每个函数都能返回自己的索引值,可是得到的是每个函数却都返回了10.如上面的文字说明中所讲的那样,闭包保存的是定义它的那个函数内部的局部变量丶参数和其他内部函数,也就是说保存的是这个函数执行上下文中的整个VO,而不是一个变量。上面代码中的函数作用域链中都保存着fn的活动对象,他们引用的都是一个i,当fn返回时,i的值是10,所以每个函数都引用保存i那个变量的同一个变量。我们如果想得到原先想得到的那个结果,可以加上另一个匿名函数改变他的父作用域(其实应该是创建它的作用域),将它包裹起来。
function fn(){ var array=[]; for(var i=0;i<10;i++){ array[i]=function(num){ return function(){ return num; }; }(i); } return array; }
这个匿名函数有一个参数num,同时是返回值。在调用每个匿名函数时,传入了变量i。由于参数是按值传递的,所以i就会复制给num,而这个匿名函数的内部又创建了一个访问num的闭包,返回后能够访问到该匿名函数中的VO(包括参数),于是每个函数返回的都是num的一个副本,所以可以得到不同的值。
其实,说了这么多,我们只要熟悉闭包的两个应用场景,就能比较好的理解闭包的意义。
一.作为函数的返回值.。作为函数返回值被执行后仍然可以访问定义它的那个函数环境的VO。
function f(){ var a=1; return function(){ console.log(a); } } var g=f(); g();//1;
二.作为一个函数的参数。作为函数返回值被当做另一个函数的参数传入时,仍然是访问定义它的那个函数环境的VO
function f(){ var a=1; return function(){ console.log(a); } } var g=f(); g();//1; function F(fn){ var a=2; fn(); } F(g);//1
上面两个小例子也正好说明了闭包可以访问定义它的那个函数作用域下的内部变量和内部函数。其实是整个VO,所以还包含参数。
闭包的理解差不多就是这样,遇到比较复杂的情况我们只要按着定义慢慢的一步步的寻找,一切问题都能迎刃而解。