函数作用域与闭包
函数作用域
要理解闭包,必须从理解函数被调用时都会发生什么入手。
我们知道,每个javascript函数都是一个对象,其中有一些属性我们可以访问到,有一些不可以访问,这些属性仅供JavaScript引擎存取,是隐式属性。[[scope]]就是其中一个。
[[scope]]就是我们所说的作用域,其中存储了执行期上下文的集合。由于这个集合呈链式链接,我们把这种链式链接叫做作用域链。
当函数被定义(创建)时有一个自己所在环境的作用域(GO全局作用域 ,若是在函数内部,就是引用别人的作用域),当函数被执行时,会将自己的独一无二的AO(活动对象,是使用arguments和该函数内部的变量值初始化的活动对象)执行上下文放在前端,形成一个作用域链;当该函数执行完,自己的AO会被干掉,回到被定义时的状态。
另外,变量的查找,就是找所在函数的作用域,首先从作用域的顶端开始查找,找不到的情况下,会查找外部函数的活动对象,依次向下查找,直到到达作为作用域链终点的全局执行环境。
下面看几个查找变量例子,深入理解函数作用域及作用域链。
function a(){
function b(){
var b=2223;
}
var a=78;
}
a()
b()
console.log(b)
输出结果: error: b is not defined
当函数a执行完毕后,该函数内部的活动对象AO就会被销毁。所以函数外部是访问不到函数内部的变量的。
function outer(){
function inner(){
var b=2223;
a=0
}
var a=78;
inner() //①
console.log(a)
console.log(b)
}
outer()
输出结果: 0 , error: b is not defined
当函数inner在被定义的阶段,就会拥有(引用)函数outer的作用域(包括函数outer自己局部的活动对象AO和全局作用域);当函数inner()被执行的时候,会再创建一个自己的活动对象AO并被推入执行环境作用域链的前端。
inner()函数在被执行的时候,由于变量a在outer()函数中已经存在并被inner()引用,所以inner()函数内部的变量a会修改掉外部函数变量a的值,并且可以不用声明。当inner()函数执行完毕后(执行到①处),inner()函数局部的AO会被销毁,下面就访问不到变量b了,而且这时候变量a的值将是被inner()函数修改过的值。
function a(){
function b(){
var b=2223;
a=0
}
var a=78;
b=1
b()
console.log(b)
}
a()
输出结果: 1
var x=10;
function a(){
console.log(x);
}
function b(){
var x=20;
a();
}
a();//10
b();//还是10;
总之:函数在被定义阶段,会引用着其所在环境的作用域;执行阶段,会创建一个自己独一无二的活动对象AO,并推入执行环境作用域链的前端;函数执行完毕之后,自己的执行上下文AO会被销毁,所以,这时候访问其内部的变量是访问不到的。但是,闭包的情况又有不同。
简述什么是闭包
闭包:有权访问另一个函数作用域中的变量的函数。
从理论的角度上,所有的JavaScript函数都是闭包,因为函数在被定义阶段就会存储一个自己所在环境的作用域,可以访问这个作用域中的所有变量。可以说,闭包是 JS 函数作用域的副产品。理解js作用域,自然就明白了闭包,即使你不知道那是闭包。
从技术实践的角度,以下函数才算闭包:
- 定义该函数的执行环境的作用域(执行上下文)即使被销毁 ,它的活动对象(AO)仍然会留在内存中,被该函数引用着。
- 引用了函数体外部的变量。
创建闭包常见方式,就是在一个函数A内部创建另一个函数B,然后通过return这个函数B以便在外部使用,这个函数B就是一个闭包。
举个例子:
//也算闭包
var a = 1;
function foo() {
console.log(a);
}
foo();
//函数内部定义函数
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()() //这里相当于:
//var foo = checkscope();
//foo();
输出结果:local scope
f()函数在被定义阶段就被保存到了外部,这个时候就相当于外部的函数可以访问另一个函数内部的变量,f()函数会形成一个闭包。
按照函数作用域的概念,当checkscope()执行完毕后,其局部的活动对象AO会被销毁;但是由于checkscope()函数执行完毕后返回一个函数,根据函数在被定义阶段会引用该函数所在执行环境的执行上下文,被返回的函数f()即使被保存到了外部依然引用着checkscope()函数的执行期上下文,直到函数f()执行完毕,checkscope()函数的执行上下文才会被销毁。
也就是说被嵌套的函数f()无论在什么地方执行,都会包含着外部函数(定义该函数)的活动对象。所以,即使f()被保存到外部,也可以访问到另一个函数checkscope()中定义的变量。
无论通过何种手段将内部函数传递到所在的词法作用域以外, 它都会持有对原始定义作用域的引用, 无论在何处执行这个函数都会使用闭包。
由此看来,闭包可能会导致一个问题:导致原有作用域链不释放,造成内存泄漏(内存空间越来越少)。可以通过手动将被引用的函数设为null,来解除对该函数的引用,以便释放内存。
闭包的作用
- 实现公有变量。
function a(){
var num=100;
function b(){
num++;
console.log(num)
}
return b;
}
var demo=a();
demo();//101
demo();//102
function test(){
var num=100;
function a(){
num++;
}
function b(){
num--;
}
return [a,b]
}
var demo=test()
demo[0]();//101
demo[1]();//100
//函数a和函数b引用的是同一个作用域。
- 实现私有变量。
闭包通常用来创建内部变量,使得这些变量不能被外部随意修改,同时又可以通过指定的函数接口来操作。
通过在立即执行函数中return 将方法保存到外部等待调用,内部的变量由于是私有的,外部访问不到,可防止污染全局变量,利于模块化开发。
var foo = ( function() {
var secret = 'secret';
// “闭包”内的函数可以访问 secret 变量,而secret变量对于外部却是隐藏的
return {
get_secret: function () {
// 通过定义的接口来访问 secret
return secret;
},
new_secret: function ( new_secret ) {
// 通过定义的接口来修改 secret
secret = new_secret;
}
};
} () );
foo.get_secret (); // 得到 'secret'
foo.secret; // undefined,访问不能
foo.new_secret ('a new secret'); // 通过函数接口,我们访问并修改了secret 变量
foo.get_secret (); // 得到 'a new secret'
var name='bcd';
var init=(function (){
var name='abc';
function callName(){
console.log(name);
}
//其他方法
return function () {
callName();
//其他方法
}
}())
init () //abc
闭包经典题
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
console.log(i);
};
}
return result;
}
var fun = createFunctions();
for(var i=0;i<10;i++){
fun[i]();
}
输出结果:打印十个10
数组每个值都是一个函数,每个函数对createFunctions()
形成一个闭包,此时i都是引用createFunctions()
中同一个i变量。
function test(){
var arr=[];
for(var i=0;i<10;i++){
(function(j){
arr[j]=function(){
console.log(j);
}
}(i))
}
console.log(i)//10 ,i还是10
return arr
}
var myArr=test();
for(var i=0;i<10;i++){
myArr[i]()
}
输出结果:从0到9
这次依然把数组每个值赋为函数,不同的是循环十次立即执行函数,并将当前循环的i作为参数传进立即执行函数,由于参数是按值传递的,这样就把当前循环的i保存下来了。
闭包中的this对象
在闭包中使用this对象可能会导致一些问题,结果往往不是预想的输出结果。
看个例子:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());
输出结果:The Window
this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。匿名函数往往具有全局性,这里可以这样理解,没有任何对象调用这个匿名函数,虽然这个匿名函数拥有getNameFunc()
的执行上下文。
因为这个匿名函数拥有getNameFunc()
的执行上下文,通过把外部函数getNameFunc()
作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问到该对象了。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()());
输出结果:My Object
理解到这里,基本上就搞定了闭包了。