作用域
一个函数可以访问定义在其范围内的任何变量和函数
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Functions
注意是”定义”的地方
作用域,词法作用域 :《你不知道的 javascript 上卷》 1-2 章,编译和词法作用域;
执行上下文、活动对象 AO、变量对象 VO、作用域:《 javascript 高级程序设计》 第四章
变量对象 ,[[scope]]:《 javascript 高级程序设计》 7.2 闭包
[[Scope]]是一个包含了所有上层变量对象的分层链,它属于当前函数上下文,并在函数创建的时候,保存在函数中。
在源代码中当你定义(书写)一个函数的时候(并未调用),
js引擎根据你函数书写的位置,函数嵌套的位置,给你生成一个[[scope]], 保存所有父变量对象到其中,作为该函数的属性存在(这个属性属于函数的)
所以叫词法作用域(静态作用域)
执行环境EC (Execution Context) 或者叫执行上下文
EC建立分为两个阶段:进入执行上下文和执行阶段。
进入上下文阶段:发生在函数调用时,但是在执行具体代码之前(比如,对函数参数进行具体化之前)
执行代码阶段:变量赋值,函数引用,执行其他代码。
可以将EC看做是一个对象。
EC={
VO:{/ 函数中的arguments对象, 参数, 内部的变量以及函数声明 /},
this:{},
Scope:{ / VO以及所有父执行上下文中的VO /}
}
此时,执行上下文里的scope和之前属于函数的那个[[scope]]不是同一个,
执行上下文里的scope,是在之前函数的[[scope]]的基础上,又新增一个当前的AO对象构成的。
Scope = [AO].concat([[Scope]]);
Scope是EC的属性,而[[scope]]则是函数的静态属性
进入执行上下文时,VO的初始化过程具体如下, 注意:该过程是有先后顺序的。
函数的形参(当进入函数执行上下文时)
—— 变量对象的一个属性,其属性名就是形参的名字,其值就是实参的值;对于没有传递的参数,其值为undefined
函数声明(FunctionDeclaration, FD)
—— 变量对象的一个属性,其属性名和值都是函数对象创建出来的;如果变量对象已经包含了相同名字的属性,则替换它的值
变量声明(var,VariableDeclaration)
—— 变量对象的一个属性,其属性名即为变量名,其值为undefined; 如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性。
当EC环境为函数时,我们访问的是AO,而不是VO
AO是在进入函数的执行上下文时创建的,并为该对象初始化一个arguments属性
AO = {
arguments: {
callee:,
length:,
properties-indexes: //函数传参参数值
}
};
执行代码阶段时,VO中的一些属性undefined值将会确定
EC分为三种:
全局级别的代码 – 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。
函数级别的代码 – 当执行一个函数时,运行函数体中的代码。
Eval的代码 – 在Eval函数内运行的代码。代码eval的上下文与当前的调用上下文(calling context)拥有同样的作用域链.
VO(变量对象,Variable Object)
AO(活动对象,Active Object)
变量对象 :就是执行环境中包含了所有变量和函数的对象。解析器在后台使用它,保存在内存中,代码无法直接访问。后台的每个执行环境都有一个表示变量的对象—-变量对象。
活动对象: 当执行环境是函数时,将其活动对象作为变量对象。
当某个函数被调用时,会创建一个执行环境( execution context )及相应的作用域链。
然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象( activation object )
作用域链:本质上是一个指向 变量对象 的指针列表,只引用 变量对象 。
在ECMAScript 中,在代码执行阶段有两个声明能修改作用域链。这就是with声明和catch语句。
它们添加到作用域链的最前端,对象须在这些声明中出现的标识符中查找。
如果发生其中的一个,作用域链简要的作如下修改:
Scope = withObject|catchObject + AO|VO + [[Scope]]
ECS(执行环境栈Execution Context Stack)
ECStack管理EC的压栈和出栈
栈底总是全局上下文,栈顶是当前(活动的)执行上下文。
当在不同的执行上下文间切换(退出的而进入新的执行上下文)的时候,栈会被修改(通过压栈或者退栈的形式)
每次控制器进入一个函数(哪怕该函数被递归调用或者作为构造器),都会发生压栈的操作
参考链接
知识点整理 https://segmentfault.com/a/1190000000533094
一个完整过程的栗子 https://github.com/mqyqingfeng/Blog/issues/6
书籍中的知识点位置 https://www.v2ex.com/t/438395
with/catch/eval等细节 https://www.cnblogs.com/TomXu/archive/2012/01/18/2312463.html
复习题
function t(age) {
console.log(age); // [Function: age]
var age = 99;
console.log(age); // 99
function age() {
}
console.log(age); // 99
}
t(5);
原因见上面的VO初始化过程
a(); // undefined
b(); // undefined
function b() { var x = 5; a(); }
a(); // undefined
b(); // undefined
function a() { console.log(x); }
a(); // undefined
b(); // undefined
var x = 10;
a(); // 10
b(); // 10
闭包
闭包是可访问上一层函数作用域里变量的函数,即便上一层函数已经关闭。
闭包是阻止垃圾回收器将变量从内存中移除的方法,使得在创建变量的执行环境的外面能够访问到该变量。
<单页Web应用>page49
栗子
var add = (function () {
var counter = 0;
return function () {return counter += 1;}
})();
add();
add();
add();
// 计数器为 3
栗子 <单页Web应用>page51