作用域与作用域链
作用域[[scope]]:每个javascript函数都是一个对象,对象中有些属性我们可以访问,但有些不能访问,不能访问的属性仅供javascript内部调用,[[scope]]就是其中的一个。
[[scope]]指的就是作用域,其中存储了运行期上下文的集合(之前介绍的AO活动对象)
作用域链:[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式链接,这种链式链接就是作用域链。
代码的执行分两个步骤:
1)预编译
2)函数执行
在预编译环节会将全局变量字段组成一个Global Object(GO),而这个Global Object 会首先存入代码a函数的作用域里面(此时作用域里面只有一个Global Object),当代码运行时a函数会产生一个活动对象也就是运行时期的上下文,里面存有的字段属性都是a函数运行时确定的,看如下简易图解:
a函数运行前(通过预编译)
a函数运行后(产生AO)
闭包是怎么产生的
当a函数运行产生AO后便形成了一个作用域链,a函数运行时如果找某个字段,首先会从自己的AO中查找,如果没有再到GO中查找,比如查找global字段
通过以上的讲解,想必明白一个函数的作用域是什么了吧,接下来要说的就是 ---闭包
闭包的产生
当a函数运行后,里面构成的作用域我们已经知道了,要明白AO的生命周期是在a函数运行完毕后自动销毁的,函数里面的return b;就宣布a函数运行完毕,而此时返回的是b函数的一个引用,这就等于又声明了一个b函数,注意b函数里面又用到了a函数里面的num字段,而a函数已经销毁了,这里就产生了闭包,我们做如下图解:
b函数声明:
b函数声明的数据正是a函数运行后的GO+AO,a函数运行结束后的销毁操作就是不再指向AO,回到最初声明的模样。
b函数运行:
从图中可以看到即使a函数销毁,b函数依然拿着a函数当时运行所生成的AO,当然包括里面的各种属性字段,此时已然形成了闭包。
闭包的定义
闭包就是能够读取其他函数内部变量的函数。
知道什么是闭包了,接下来看一个小例子加深闭包的概念
输出结果:
b函数运行时,num++修改的是a函数运行时产生的AO里面的字段num;
c函数运行时,num--修改的也是a函数运行时产生的AO里面的字段num;
我们发现a函数运行所产生的AO成了公共的了,谁都可以调用修改,如下图:
以上便是闭包产生的由来以及一个关于闭包的小例子。
1.闭包允许函数访问并操作函数外部的变量,只要变量或函数存在于声明函数时的作用域内,闭包就可以访问这些变量和函数
//全局闭包 不明显
var outerValue = "ninja";
function outerFunction(){
outerValue === "ninja"; //true
}
outerFunction();
//闭包例子
var outerValue = "samurai";
var later;
function outerFunction(){
var innerValue = "ninja";
function innerFunction(){
outerValue === 'samurai'; //true
innerValue === 'ninja'; //true
}
later = innerFunction();
}
outerFunction();
later();
当在外部函数中声明内部函数时,不仅定义了函数的声明,还创建了一个闭包
该闭包不仅包含了函数的声明,还包含了 函数声明时该作用域中的所有变量
闭包创建了被定义时的作用域内的变量和函数的安全气泡
每一个通过闭包访问变量的函数 都具有一个作用域链,作用域链包含闭包的全部信息。
:star: 闭包所有的信息都存储在内存中,直到 JavaScript 引擎确保这些信息不再使用或页面卸载时,才会清理。
2. 使用闭包
封装私有变量
私有变量 是对外部隐藏的对象属性
function Ninja(){
var feints = 0;
this.getFeints = function(){
return feints;
}
this.feint = function(){
feints++;
}
}
var ninja1 = new Ninja();
ninja1.feint();
ninja1.feints //无法直接从外部访问属性
ninja1.getFeints() == 1 //true 可以通过方法来访问
var ninja2 = new Ninja(); //创建一个新的对象实例,新对象实例作为上下文 this指向新的对象实例
ninja2.getFeints() == 0 //true 新实例有自己的私有变量
:zap: 在构造器中隐藏变量,使其在外部作用域中不可访问,但是可在闭包内部进行访问
通过变量 ninja , 对象实例是可见的
因为 feint 方法在闭包内部,因此可以访问变量 feints
在闭包外部,无法访问变量 feints
回调函数
//在 interval 的回调函数中使用闭包
//回调函数中 this 指向变了
function animateIt(elementId){
var elem = document.getElementById(elementId);
var tick = 0;
var timer = setInterval(function(){
if(tick < 100){
elem.style.left = elem.style.top = tick + 'px';
tick++;
}else{
clearInterval(timer);
tick === 100; //true
}
},10);
}
animateIt("box1");
//通过在函数内部定义变量,并基于闭包,使得在计时器的回调函数中可以访问这些变量,每个动画都能够获得属于自己的"气泡"中的私有变量
闭包内的函数 不仅可以在闭包创建时可以访问这些变量,而且可以在闭包函数执行时,更改这些变量的值。
闭包不是在创建的那一时刻的快照,而是一个真实的状态封装,只要闭包存在,就可以对变量进行修改。
3. 执行上下文跟踪代码
JavaScript 有两种类型的代码:一种是全局代码,一种是函数代码
上下文分为两种:全局执行上下文和函数执行上下文
全局执行上下文只有一个,当 JavaScript 程序开始执行时就已经创建了全局上下文;而函数执行上下文在每次 调用 函数时,就会创建一个新的。
JavaScript 是单线程的:一旦发生函数调用,当前的执行上下文必须停止执行,并创建新的函数执行上下文来执行函数。当函数执行完成后,将函数执行上下文销毁,并重新回到发生调用时的执行上下文中。
栈:syringe:的活塞
//创建执行上下文
function skulk(ninja){
report(ninja + " skulking");
}
function report(message){
console.log(message);
}
skulk("Kuma");
skulk("Yoshi");
可以通过 JavaScript 调试器中查看,在 JavaScript 调试器中可以看到对应的调用栈(call stack)。
4. 使用词法环境跟踪变量的作用域
词法环境 是 JavaScript 引擎内部用来跟踪标识符与特定变量之间的映射关系
词法环境 是 JavaScript 作用域的内部实现机制,称为 作用域 (scopes)
代码嵌套
词法环境主要基于代码嵌套,通过代码嵌套可以实现代码结构包含另一代码结构
在作用域范围内,每次执行代码时,代码结构都获得与之关联的词法环境。
内部代码结构可以访问外部代码结构中定义的变量。
代码嵌套与词法环境
每个执行上下文都有一个与之关联的词法环境,词法环境中包含了在上下文中定义的标识符映射表。=> 作用域链
在特定的执行上下文中,我们的程序不仅直接访问词法环境中定义的局部变量,而且还会访问外部环境中定义的变量。
无论何时创建函数,都会创建一个与之相关联的词法环境,并存储在名为 [[Environment]]的内部属性上。两个中括号表示是内部属性。
函数都有词法环境
var ninja = "Muneyoshi";
function skulk(){
var action = "Skulking";
function report(){
var intro = "Aha!";
assert(intro === "Aha!","Local");
assert(action === "Skulking","Outer");
assert(ninja === "Muneyoshi","Global");
}
report();
}
skulk();
:question: 为什么不直接跟踪整个执行上下文搜索标识符,而是通过词法环境来跟踪呢?
JavaScript 函数可以作为任意对象进行传递,定义函数时的环境与调用函数的环境往往是不同的(闭包)
:zap: 无论何时调用函数,都会创建一个新的执行环境,被推入执行上下文栈。此外,还会创建一个与之关联的词法环境。外部环境与新建的词法环境,JavaScript 引擎将调用函数的内置[[Environment]]属性与创建时的环境进行关联。
5. 理解 JavaScript 的变量类型
3个关键字定义变量:var let const
不同之处:可变性、词法环境
vs 不可变
const 不可变,let var 可变
声明时需要初始化,一旦声明完成之后,其值不可更改。 => 指向不可更改
const firstConst = "samurai";
firstConst = "ninja"; //报错
const secondConst = {};
secondConst.weapon = "wakizashi"; //不报错
const thirdConst = [];
thirdConst.push("Yoshi"); //不报错
如果 const 的值是 静态变量,则不允许重新赋值;如果 const 的值是对象或者是数组类型,则可以对其增加新元素,但是不能重写。其实不可变的是引用,而不是值。
vs 词法环境
var 一组,let 和 const 一组
关键字 var :变量是在距离最近的 函数内部 或是在 全局词法环境 中定义的。忽略块级作用域
var 声明的变量实际上总是在 距离最近的函数内 或 全局词法环境中 注册的, 不关注块级作用域
let 与 const 直接在最近的词法环境中定义变量(可以是 块级作用域内 、 循环内 、 函数内或全局环境内 )。我们可以用 let 和 const 定义 块级别、函数级别、全局级别的变量。
词法环境注册标识符
词法作用域又叫静态作用域,因为js的作用域在词法解析阶段就确定了
动态作用域:区别于静态作用域,即在函数调用时才决定作用域
JavaScript 代码的执行 分两个阶段:
一旦创建了新的词法环境,就会执行第一阶段。在第一阶段,没有执行代码,而是JavaScript引擎会访问并注册在当前词法环境中所声明的变量和函数。 变量和函数声明提升
第二阶段的执行取决于变量的类型(let var const 函数声明)以及环境类型(全局环境、函数环境或块级作用域)
1.如果是创建一个函数环境,那么创建形参及函数参数的默认值。如果是非函数环境,将跳过此步骤。
2.如果是创建全局或函数环境,就扫描当前代码进行函数声明(不会扫描其他函数的函数体),但是不会扫描函数表达式或箭头函数。对于所找到的函数声明,将创建函数,并绑定到当前环境与函数名相同的标识符上。若该标识符已经存在,那么该标识符的值将被重写。如果是块级作用域,将跳过此步骤。
3.扫描当前代码进行变量声明。在函数或全局环境中,找到所有当前函数以及其他函数之外通过 var 声明的变量,并找到所有在其他函数或代码块之外通过 let 或 const 定义的变量。在块级环境中,仅查找当前块中通过 let 或 const 定义的变量。对于所查找到的变量,若该标识符不存在,进行注册并将其初始化为 undefined。若该标识符已经存在,将保留其值。
若函数是函数声明进行定义的,则可以在函数声明之前访问函数。
若函数是函数表达式或箭头函数进行定义的,则不会在之前访问到函数
变量的声明会提升至函数顶部,函数的声明会提升至全局代码顶部。
其实,变量和函数的声明并没有实际发生移动,只是在代码执行之前,先在词法环境中进行注册。
6. 闭包的工作原理
闭包可以访问创建函数时所在作用域内的全部变量
//通过函数访问私有变量,而不通过对象访问
function Ninja(){
var feints = 0;
this.getFeints = function(){
return feints;
}
this.feint = function(){
feints++;
};
}
var ninja1 = new Ninja();
ninja1.feint();
var imposter = {};
imposter.getFeints = ninja1.getFeints;
imposter.getFeints() == 1 //true
JavaScript 中没有真正的私有对象属性,但是可以通过闭包实现一种可接受的“私有”变量的方案。
JavaScript