闭包的概念其实和执行上下文,作用域紧密相关的。单独理解闭包往往是困难的,无从下手。所以我们先从执行上下文着手。
参考:javascript执行上下文、作用域与闭包系列
一. 执行上下文
执行上下文可以理解为代码的执行环境。分为三类:1.全局执行上下文(window),2.函数上下文,3.eval上下文。其实Js中的this
即当前执行上下文的引用。
上图中,紫色边框内全局上下文,绿色边框代表person函数上下文,蓝色边框括起来的部分代表person函数内的firstname函数的上下文;橙色边框括起来的部分代表person函数内的lastname函数的上下文。
二. 执行上下文与函数调用
当代码执行到当前上下文中的调用函数的代码的时候即在执行被调用的函数体中的代码以前,开始创建函数执行上下文。可将执行上下文看作一个对象
(executionContextObj = {
variableObject: { /* 函数中的参数对象并给参数赋值, 内部的变量以及函数声明 */ },
scopeChain: { /* variableObject 以及所有父执行上下文中的variableObject */ },
this: { }
}
函数调用的过程如下:
- 执行上下文第一个阶段-建立阶段
A 建立VariableObject对象(简称VO)
(建立arguments对象,建立一个函数声明属性指向函数,建立变量声明属性值为undfined)
B 初始化作用域链
C 确定上下文中this的指向对象 - 执行上下文第二个阶段-代码执行阶段
执行函数体中的代码,一行一行地运行代码,给VariableObject中的变量属性赋值。
示例:
function foo(i) {
var a = 'hello';
var b = function B() {
};
function c() {
}
}
foo(22);
在调用foo(22)时,执行上下文的建立阶段如下:
fooExecutionContext = {
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
scopeChain: { ... },
this: { ... }
}
在建立阶段,除了arguments,函数的声明,以及参数被赋予了具体的属性值,其它的变量值默认的都是undefined。一旦上述建立阶段结束,引擎就会进入代码执行阶段,这个阶段完成后,上述执行上下文对象如下:
fooExecutionContext = {
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function B()
},
scopeChain: { ... },
this: { ... }
}
三.作用域
ES6之前 javascript 中没有块级作用域,只有函数作用域和全局作用域。
ES6加入了块级作用的概念,即定义在{}之内的范围,如if,for。
简单来说,作用域相当于一个区域,就是为了说明这个区域有多大,而不包括这个区域的里有什么东西。这个区域里有什么东西恰恰就是这个作用域所对应的执行上下文所要说明的东西。
- 作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
- 作用域是在函数创建的时候就一键确定了,而不是函数调用的时候。
var aa = 22;
function a(){
alert(aa);
}
function b(fn){
var aa = 11;
fn();
}
b(a); //22
上述b(a)的执行结果为什么不是11,不是找离aa最近的赋值么?
但还有一个前提:作用域是在函数创建的时候就已经确定了,所以alert(aa)应该去找a()d的上一级作用域即全局作用域,而不是调用它的b()作用域
四.作用域与执行上下文
直接上例子
function a(){
var age=21;
var height=178;
var weight=70;
function b(){
alert(age);// 由于变量声明提升的关系,b函数第一行就已经有age的声明了,不过没有赋值,所以age = undefined
alert(height);// 由作用域链可知,前面最近的赋值在 height=178
var age=25;
height=180; //不带var的声明,不管这个变量在哪个位置,都相当于是在全局作用域里声明了height变量。
alert(age);//25
alert(height); //180
}
b();
}
a();
五.闭包
最基本的闭包是:
1.定义普通函数A
2.在A中定义普通函数B
3.在A中返回B
4.执行A,并把A的结果赋值给C
5.执行C
当一个内部函数被其外部函数之外的变量引用时,就形成了一个闭包
function fn(){
var max=10;
return function bar(x){
if(x>max)
{
alert(x);
}
}
}
var f1=fn();
f1(15);//15
为什么在执行完f1=fn()后max的值还在呢?(并没有销毁fn的上下文)。由于return的bar()存在对fn()上下文的引用,(因为存在对变量max的需要)所以不能将其销毁。
可以看到,使用闭包会使变量保存在内存中,但是缺点就是会增加内存开销
使外部变量访问到一个函数的内部变量是闭包的形式,
使变量保存在内存中是闭包的工作原理。