最近在看JavaScript高程和一些博客的时候对闭包有了更深的认识,关于闭包更像是JavaScript词法作用域,变量对象和作用域链体现
在JavaScript中每执行一段可执行代码时,都会创建对应的执行上下文(execution content)
对于每个执行上下文都有三个重要属性
- 变量对象(Variable object, VO)
- 作用域链(scope chain)
- this
词法作用域
JavaScript采用的是词法作用域,函数的作用域在函数定义的时候就决定了。而于词法作用域相对的是静态作用域,函数的作用域是在函数调用时才决定。
变量对象
变量对象是执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明
1.在函数上下文中的变量对象
在函数上下文中用AO(activation object)来表示变量对象。
只有在进入执行上下文时,变量对象会被激活成为AO
只有AO上的各种属性才能被访问,进行赋值等操作
2.执行过程
两个阶段
- 创建阶段,进入执行上下文
这个阶段,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。
- 代码执行
变量创建完成后,就会开始执行代码,进行变量赋值,函数应用,以及执行其它代码
2.1进入执行上下文
这是还没有执行代码
变量对象包括:先后顺序如下
- 函数的所有型参
- 名称和对应值组成的一个变量对象的属性被创建
- 没事实参,属性值为undefined
- 函数声明
- 名称和函数对象组成一个变量对像的属性被创建
- 如果变量对象已经存在相同名称的属性,则完全替换这个属性
- 变量声明
- 由名称和undefined组成一个变量对象的属性被创建
如果变量名称跟已经声明的参数或函数相同,则变量声明不会干扰已经存在的这类属性
举个例子
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
//在创建阶段进入执行上下文后,这时的AO
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
2.2 代码执行
代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值
//代码执行后的AO为
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}
总结
- 全局上下文的变量对象初始化是全局对象
- 函数上下文的变量对象初始化只包括 Arguments 对象
- 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
- 在代码执行阶段,会再次修改变量对象的属性值
作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表,就叫作用域链。
关于闭包
JavaScript是采用词法作用域的,这就意味着函数的执行依赖于函数定义的时候所产生(而不是函数调用的时候产生的)的变量作用域。为了去实现这种词法作用域,JavaScript函数对象的内部状态不仅包含函数逻辑的代码,除此之外还包含当前作用域链的引用。函数对象可以通过这个作用域链相互关联起来,如此,函数体内部的变量都可以保存在函数的作用域内,这在计算机的文献中被称之为闭包。
从技术的角度去将,所有的JavaScript函数都是闭包:他们都是对象,他们都有一个关联到他们的作用域链。绝大多数函数在调用的时候使用的作用域链和他们在定义的时候的作用域链是相同的,但是这并不影响闭包。当调用函数的时候闭包所指向的作用域链和定义函数时的作用域链不是同一个作用域链的时候,闭包become interesting。这种interesting的事情往往发生在这样的情况下: 当一个函数嵌套了另外的一个函数,外部的函数将内部嵌套的这个函数作为对象返回。一大批强大的编程技术都利用了这类嵌套的函数闭包,当然,javascript也是这样。可能你第一次碰见闭包觉得比较难以理解,但是去明白闭包然后去非常自如的使用它是非常重要的。
通俗点说,在程序语言范畴内的闭包是指函数把其的变量作用域也包含在这个函数的作用域内,形成一个所谓的“闭包”,这样的话外部的函数就无法去访问内部变量。所以按照第二段所说的,严格意义上所有的函数都是闭包。
需要注意的是:我们常常所说的闭包指的是让外部函数访问到内部的变量,也就是说,按照一般的做法,是使内部函数返回一个函数,然后操作其中的变量。这样做的话一是可以读取函数内部的变量,二是可以让这些变量的值始终保存在内存中。
闭包的作用
下面就是采用一个函数嵌套另一个函数并将内部的函数作为一个对象返回出去。
闭包 + 立即执行函数
- 可以用来做访问控制
- 可以用来隐藏数据细节
立即执行函数
隔离作用域,使得内部数据无法被外部访问。
隔离作用域后使里面的数据不能被随意访问,使用闭包来隐藏数据细节和实现访问控制,将数据以自己想要的形式返回出去。
!function() {
var person = {
name:'frank',
age: 18
}
windwo.frankGrowUp = function(){ //将栈内存写在全局变量
person.age += 1
return preson.age
}.call()
var accessor = function(){
var person = {
name: 'frank',
age: 18
}
return function(){
person.age += 1
return preson.age
}
}
var growUp = accessor().call()
growUp()
- 立即执行函数使得person无法被外部访问
- 闭包使得匿名函数可以操作person
- window.frankGrowUp保存匿名函数的地址
- 任何地方都可以使用window.frankGrowUp
=> 任何地方都可以使用window.frankGrowUp 操作 person,但不能直接访问person
匿名函数 + 闭包 + 全局变量 = 对象细节封装方法
关于理解闭包还有重要一点,只用函数才能有自己独立的作用域
下面是关于闭包的经典面试题
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0](); //3
data[1](); //3
data[2](); //3
data[0]Context = {
Scope: [AO, globalContext.VO]
}
globalContext = {
VO: {
data: [...],
i: 3
}
}
改为闭包
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0](); //0
data[1](); //1
data[2](); //2
data[0]Context = {
Scope: [AO, 匿名函数Context.AO globalContext.VO]
}
网友评论