结合执行上下文理解js闭包、作用域及作用域链
一MDN
上闭包的定义
- 函数和其对周围状态(词法环境---作用域链)的引用捆绑在一起构成了闭包。
- 每当函数创建时,就会在函数生成时形成闭包。
- 内部函数可以访问外部函数作用域
二、js中的作用域及作用域链---静态作用域--词法作用域--词法环境
先来看一段几乎都知道的白话文:作用域是变量和函数的可访问范围及生命周期。在当前作用域访问变量,如果没找到,就会去外层作用域查询,一级级向上,直到最外层的window对象为止,这样的层级关系便是作用域链。
在执行上下文中的体现:访问变量、函数时,去当前执行上下文中的作用域链scope chain
中查询,scope chain
的顶端为当前执行上下文的变量对象————对应着当前作用域。若没有查询到,则去外层变量对象中查询,直到最外层的变量对象为止(即全局作用域)。(当前执行上下文中的作用域链属性保存了其能访问到的所有变量对象)
引出下一个问题:执行上下文属性中的scope chain
是怎么创建的?
- 在函数定义创建时,会把其能访问到的所有外层变量对象都存储到函数的内部属性
[[scope]]
中 - 创建执行上下文时,首先创建
scope chain
这个属性,再把函数的内部属性[[scope]]
复制到scope chain
中,但此时还并不是完整的作用域链,等到variable object
变量对象创建完后,添加到scope chain
的顶端,形成最终的作用域链。
得出重要结论:
- 在函数创建时,就已经保存好了其所能访问到的所有外层的变量集合————也就是白话文中的静态作用域,即函数的作用域在定义时就已经确定了。
MDN
文档中闭包官方文定义:函数和其对周围状态的引用捆绑在一起构成了闭包。
翻译成白话文:函数和其作用域链上的变量对象构成了闭包。作用域链上的变量对象————即外层作用域下的变量。
闭包就是函数,对变量、函数的访问,也一样是从其执行上下文中的作用域链上查询。
需要强调的是,作用域链的构成为当前执行上下文中的变量对象(即当前作用域下的变量及函数)和函数定义时所能访问到的变量对象集合(即函数定义时的位置的作用域链)。
function fun(n,o) {
console.log(o)
return {
fun:function(m){
return fun(m,n);
}
};
}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1); c.fun(2); c.fun(3);//undefined,?,?,?
几条准则:
- 函数调用时,在函数内部代码前,会先创建一个函数执行上下文,每次函数调用,都会创建一个执行上下文,互不影响。创建执行上下文的详细过程
- 进行变量、函数、形参的访问,都是通过执行上下文中的
scope chain
进行逐级查找 - 函数执行上下文的外层变量对象,在函数定义时已经确定。
解析:
- 开始js代码的运行,浏览器创建一个全局执行上下文,此为第零层执行上下文。初始化变量对象
{fun(){...}}
————保存了fun函数的声明 - 调用了
fun
函数,即语句var a = fun(0)
,在当前作用域链中查找该函数的声明,最终在全局执行上下文中找到。此为第一层执行上下文,在开始执行函数内部的代码前,浏览器解析器会创建一个函数执行上下文。初始化其变量对象{n:undefined,o:undefined}
,根据函数调用传入的实参,改变其变量对象{n:0,o:undefined}
a.fun(1);
创建函数执行上下文,并初始化其变量对象{m:undefined}
,而后根据函数调用,改变其变量对象{m:1}
fun(m,n)
,在全局执行上下文中的变量对象中找到了该函数声明,初始化并改变其变量对象{n:m,o:n},其内部代码又访问了变量
o`,便会沿着作用域链去找
实际上,闭包的最大特性应该是,使函数的所有外层变量对象常驻在内存,除非手动置空。其他的特点实际与普通函数没有差别,都是通过作用域链进行查找变量及函数,这里说的作用域链是执行上下文中的定义。