详解执行上下文,变量对象与活动对象,作用域与作用域链,闭包
一、执行上下文
执行上下文(Execution Context)是ECMAScript规范中用来描述 JavaScript 代码执行的抽象概念,规定了当前代码执行的环境(当前执行代码片段中的变量、函数、作用域链等),所有执行上下文又叫执行环境。
- 全局上下文(整个js文件被加载执行建立的上下文,必有且唯一)
- 函数上下文(函数被调用执行时建立的上下文,每次某个函数被调用,就会有个新的执行上下文为其创建,即使是调用的自身函数,也是如此。)
代码执行过程:执行上下文的创建、进栈、出栈、等待回收的过程;
js的运行采用栈的方式对执行上下文进行管理,栈底始终是全局上下文,栈顶始终是正在被调用执行的函数的执行上下文。
执行上下文生命周期
参考来源:https://www.cnblogs.com/ivehd/p/executionContext.html
二、变量对象与活动对象
变量对象是与执行上下文对应的概念,定义着执行上下文下的所有变量、函数以及当前执行上下文函数的参数列表。也就是说变量对象定义着一个函数内定义的参数列表、内部变量和内部函数。
数据结构来说:
变量对象的创建过程:
1 function outerFun (arg1, arg2) { 2 var outerV1 = 1 3 var outerV2 = 2 4 function innerFun1 () { 5 var innerV1 = 3; 6 var innerV2 = 4; 7 console.log('i am innerFun1...') 8 } 9 function innerFun2 () { 10 console.log('i am innerFun2...') 11 } 12 function outerV2 () { 13 return 'i am outerV2' 14 } 15 } 16 outerFun()
变量对象是在函数被调用,但是函数尚未执行的时刻被创建的,这个创建变量对象的过程实际就是函数内数据(函数参数、内部变量、内部函数)初始化的过程。
活动对象
未进入执行阶段之前,变量对象中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。所以活动对象实际就是变量对象在真正执行时的另一种形式。
全局变量对象
我们上面说的都是函数上下文中的变量对象,是根据执行上下文中的数据(参数、变量、函数)确定其内容的,全局上下文中的变量对象则有所不同。以浏览器为例,全局变量对象是window对象,全局上下文在执行前的初始化阶段,全局变量、函数都是被挂载倒window上的。
参考来源:https://www.cnblogs.com/ivehd/p/vo_ao.html
三、作用域与作用域链
作用域是一种规则,在代码编译阶段就确定了,规定了变量与函数的可被访问的范围;
作用域链是作用域规则的实现,通过作用域链的实现,变量在它的作用域内可被访问,函数在它的作用域内可被调用。
作用域链是一个只能单向访问的链表,这个链表上的每个节点就是执行上下文的变量对象(代码执行时就是活动对象),单向链表的头部(可被第一个访问的节点)始终都是当前正在被调用执行的函数的变量对象(活动对象),尾部始终是全局活动对象
function fun01 () { 2 console.log('i am fun01...'); 3 fun02(); 4 } 5 6 function fun02 () { 7 console.log('i am fun02...'); 8 } 9 10 fun01();
如上图,当程序访问一个变量时,按照作用域链的单向访问特性,首先在头节点的AO中查找,没有则到下一节点的AO查找,最多查找到尾节点(global AO)。在这个过程中找到了就找到了,没找到就报错undefined。
延长作用域链
1.with语句
1 function fun01 () { 2 with (document) { 3 console.log('I am fun01 and I am in document scope...') 4 } 5 } 6 7 fun01();
2.try-catch语句的catch块
四、关于闭包
1.什么是闭包?
函数对象可以通过作用域链相互关联起来,函数体内的数据(变量和函数声明)都可以保存在函数作用域内,这种特性在计算机科学文献中被称为“闭包”。既函数体内的数据被隐藏于作用于链内,看起来像是函数将数据“包裹”了起来。从技术角度来说,js的函数都是闭包:函数都是对象,都关联到作用域链,函数内数据都被保存在函数作用域内。
2.闭包的几种实现方式
实现方式就是函数A在函数B的内部进行定义了,并且当函数A在执行时,访问了函数B内部的变量对象,那么B就是一个闭包。如下:
如上两图所示,是在chrome浏览器下查看闭包的方法。两种方式的共同点是都有一个外部函数outerFun(),都在外部函数内定义了内部函数innerFun(),内部函数都访问了外部函数的数据。不同的是,第一种方式的innerFun()是在outerFun()内被调用的,既声明和被调用均在同一个执行上下文内。而第二种方式的innerFun()则是在outerFun()外被调用的,既声明和被调用不在同一个执行上下文。第二种方式恰好是js使用闭包常用的特性所在:通过闭包的这种特性,可以在其他执行上下文内访问函数内部数据。
我们更常用的一种方式则是这样的:
1 //闭包实例 2 function outerFun () { 3 var outerV1 = 10 4 function outerF1 () { 5 console.log('I am outerF1...') 6 } 7 8 function innerFun () { 9 var innerV1 = outerV1 10 outerF1() 11 } 12 return innerFun //return回innerFun()内部函数 13 } 14 var fn = outerFun() //接到return回的innerFun()函数 15 fn() //执行接到的内部函数innerFun()
此时它的作用域链是这样的:
3.闭包的好处及使用场景
js的垃圾回收机制可以粗略的概括为:如果当前执行上下文执行完毕,且上下文内的数据没有其他引用,则执行上下文pop出call stack,其内数据等待被垃圾回收。而当我们在其他执行上下文通过闭包对执行完的上下文内数据仍然进行引用时,那么被引用的数据则不会被垃圾回收。就像上面代码中的outerV1,放我们在全局上下文通过调用innerFun()仍然访问引用outerV1时,那么outerFun执行完毕后,outerV1也不会被垃圾回收,而是保存在内存中。另外,outerV1看起来像不像一个outerFun的私有内部变量呢?除了innerFun()外,我们无法随意访问outerV1。所以,综上所述,这样闭包的使用情景可以总结为:
(1)进行变量持久化。
(2)使函数对象内有更好的封装性,内部数据私有化。
进行变量持久化方面举个栗子:
我们假设一个需求时写一个函数进行类似id自增或者计算函数被调用的功能,普通青年这样写:
1 var count = 0 2 function countFun () { 3 return count++ 4 }
这样写固然实现了功能,但是count被暴露在外,可能被其他代码篡改。这个时候闭包青年就会这样写:
1 function countFun () { 2 var count = 0 3 return function(){ 4 return count++ 5 } 6 } 7 8 var a = countFun() 9 a()
这样count就不会被不小心篡改了,函数调用一次就count加一次1。而如果结合“函数每次被调用都会创建一个新的执行上下文”,这种count的安全性还有如下体现:
1 function countFun () { 2 var count = 0 3 return { 4 count: function () { 5 count++ 6 }, 7 reset: function () { 8 count = 0 9 }, 10 printCount: function () { 11 console.log(count) 12 } 13 } 14 } 15 16 var a = countFun() 17 var b = countFun() 18 a.count() 19 a.count() 20 21 b.count() 22 b.reset() 23 24 a.printCount() //打印:2 因为a.count()被调用了两次 25 b.printCount() //打印出:0 因为调用了b.reset()
以上便是闭包提供的变量持久化和封装性的体现。
4.闭包的注意事项
由于闭包中的变量不会像其他正常变量那种被垃圾回收,而是一直存在内存中,所以大量使用闭包可能会造成性能问题。
参考来源:https://www.cnblogs.com/ivehd/p/scopechain.html