共计 3067 个字符,预计需要花费 8 分钟才能阅读完成。
执行环境、变量对象 / 活动对象、作用域链
执行环境(executioncontext,为简单起见,有时也称为“环境”)是 JavaScript 中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象(variableobject),环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。全局执行环境是最外围的一个执行环境。根据 ECMAScript 实现所在的宿主环境不同,表示执行环境的对象也不一样。在 Web 浏览器中,全局执行环境被认为是 window 对象,因此所有全局变量和函数都是作为 window 对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出——例如关闭网页或浏览器——时才会被销毁)。
每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回返回给之前的执行环境。ECMAScript 程序中的执行流正是由这个方便的机制控制着。当代码在一个环境中执行时,会创建变量对象的一个作用域链(scopechain)。
作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activationobject)作为变量对象。
活动对象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。
标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。
—- 摘自 JavaScript 高级程序设计
理论说完,直接上代码。
function Fn() {
var count = 0
function innerFn() {
count ++
console.log(‘inner’, count)
}
return innerFn
}
var fn = Fn()
document.querySelector(‘#btn’).addEventListener(‘click’, ()=> {
fn()
Fn()()
})
1、浏览器打开,进入全局执行环境,也就是 window 对象,对应的变量对象就是全局变量对象。
在全局变量对象里定义了两个变量:Fn 和 fn。
2、当代码执行到 fn 的赋值时,执行流进入 Fn 函数,Fn 的执行环境被创建并推入环境栈,与之对应的变量对象也被创建,当 Fn 的代码在执行环境中执行时,会创建变量对象的一个作用域链,这个作用域链首先可以访问本地的变量对象(当前执行的代码所在环境的变量对象),往上可以访问来自包含环境的变量对象,如此一层层往上直到全局环境。
Fn 的变量对象里有两个变量:count 和 innerFn,其实还有 arguments 和 this,这里先忽略。然后函数返回了 innerFn 函数出去赋给了 fn。
3、手动执行点击事件。
首先,执行流进入了 fn 函数,实际上是进入了 innerFn 函数,innerFn 的执行环境被创建并推入环境栈,执行 innerFn 代码,通过作用域链对 Fn 的活动对象中的 count 进行了 +1,并且打印。执行完毕,环境出栈。
然后,执行流进入了 Fn 函数,Fn 的执行跟第 2 步的一样,返回了 innerFn。接着执行了 innerFn 函数,innerFn 的执行跟前面的一样。
每一次点击都执行了 fn,Fn,innerFn,而 fn 和 innerFn 其实是一样逻辑的函数,但控制台打印出来的结果却有所不同。
点击了 3 次的结果,接下来进入闭包环节。
闭包
垃圾回收机制
先介绍下垃圾回收机制。
离开作用域的值将被自动标记为可以回收,因此将在垃圾收集期间被删除。“标记清除”是目前主流的垃圾收集算法,这种算法的思想是给当前不使用的值加上标记,然后再回收其内存。
—- 摘自 JavaScript 高级程序设计
通俗点说就是:1、函数执行完了,其执行环境会出栈,其变量对象自然就离开了作用域,面临着被销毁的命运。但是如果其中的某个变量被其他作用域引用着,那么这个变量将继续保持在内存当中。2、全局变量对象在浏览器关闭时才会被销毁。
接下来看看上面的代码。对了先画张图。
现在就解释下为什么会有不同的结果。
Fn()() — 执行 Fn 函数,return 了 innerFn 函数并立即执行了 innerFn 函数,因为 innerFn 函数引用了 Fn 变量对象中的 count 变量,所以即使 Fn 函数执行完了,count 变量还是保留在内存中。等 innerFn 执行完了,引用也随之消失,此时 count 变量被回收。所以每次运行 Fn()(),count 变量的值都是 1。
fn() — 从 fn 的赋值开始说起,Fn 函数执行后 return 了 innerFn 函数赋值给了 fn。从这个时候开始 Fn 的变量对象中的 count 变量就被 innerFn 引用着,而 innerFn 被 fn 引用着,被引用的都存在于内存中。然后执行了 fn 函数,实际上执行了存在于内存中的 innerFn 函数,存在于内存中的 count++。执行完成后,innerFn 还是被 fn 引用着,由于 fn 是全局变量除了浏览器关闭外不会被销毁,以至于这个 innerFn 函数没有被销毁,再延申就是 innerFn 引用的 count 变量也不会被销毁。所以每次运行 fn 函数实际上执行的还是那个存在于内存中的 innerFn 函数,自然引用的也是那个存在于内存中的 count 变量。不像 Fn()(),每次的执行实际上都开辟了一个新的内存空间,执行的也是新的 Fn 函数和 innerFn 函数。
闭包的用途
1、通过作用域访问外层函数的私有变量 / 方法,并且使这些私有变量 / 方法保留再内存中 2、避免全局变量的污染 3、代码模块化 / 面向对象编程 oop
举个例子
function Animal() {
var hobbies = []
return {
addHobby: name => {hobbies.push(name)},
showHobbies: () => {console.log(hobbies)}
}
}
var dog = Animal()
dog.addHobby(‘eat’)
dog.addHobby(‘sleep’)
dog.showHobbies()
定义了一个 Animal 的方法,里面有一个私有变量 hobbies,这个私有变量外部无法访问。全局定义了 dog 的变量,并且把 Animal 执行后的对象赋值给了 dog(其实 dog 就是 Animal 的实例化对象),通过 dog 对象里的方法就可以访问 Animal 中的私有属性 hobbies。这么做可以保证私有属性只能被其实例化对象访问,并且一直保留在内存中。当然还可以实例化多个对象,每个实例对象所引用的私有属性也互不相干。
当然还可以写成构造函数(类)的方式
function Animal() {
var hobbies = []
this.addHobby = name => {hobbies.push(name)},
this.showHobbies = () => {console.log(hobbies)}
}
var dog = new Animal()