js作用域链,执行环境(一)
1.作用域链
从字面上理解:多个作用域连接在一起.
官方给出的定义是:当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问
2.作用域又是什么呢?
从字面上理解:起作用的范围(村里村长说话好使,村就是这个村长的作用域).
3.变量作用域
作用域是给变量用的,又称之为变量作用域.
4.变量
变量又分全局变量和局部变量.
2个注意小点:
1.如果局部变量和全局变量重名了,局部变量会覆盖掉全局变量.
2.在函数内部声明变量不使用var
,那么这个变量是一个全局变量.
那么进入正题,详细的好好说说js这个难点抽象点作用域链.
我们码代码要有一个写代码的开发环境IDE,同样,我们的代码在执行过程中也需要一个执行环境.
什么是执行环境(Execution Context)?
每当程序的执行流进入到一个可执行的代码时,就进入到了一个执行环境中。
简称ec
或者上下文对象(更抽象).
在javascript中,可执行的JavaScript代码分三种类型:
- Global Code,即全局的、不在任何函数里面的代码,例如:一个js文件、嵌入在HTML页面中的js代码等。
- Eval Code,即使用eval()函数动态执行的JS代码。
- Function Code,即用户自定义函数中的函数体JS代码。
跳过Eval Code,只说全局执行环境和函数执行环境。
每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
总的来说可以将执行环境看作是一个对象
1 | EC = { |
1、全局环境:
全局环境是最外围的一个执行环境。全局执行环境被认为是window对象。因此所有全局变量和函数都是作为window对象的属性和方法创建的。代码载入浏览器时,全局执行环境被创建(当我们关闭网页或者浏览器时全局执行环境才被销毁)。比如在一个页面中,第一次载入JS代码时创建一个全局执行环境。
这也是为什么闭包有一个内存泄露的缺点。因为闭包中外部函数被当成了全局环境。所以不会被销毁,一直保存在内存中.
2、函数执行环境
每个函数都有自己的执行环境,当执行进入一个函数时,函数的执行环境就会被推入一个执行环境栈的顶部并获取执行权。当这个函数执行完毕,它的执行环境又从这个栈的顶部被删除,并把执行权并还给之前执行环境。这就是ECMAScript程序中的执行流。
也可以这样解读:当调用一个 JavaScript 函数时,该函数就会进入与该函数相对应的执行环境。如果又调用了另外一个函数,则又会创建一个新的执行环境,并且在函数调用期间执行过程都处于该环境中。当调用的函数返回后,执行过程会返回原始执行环境。因而,运行中的 JavaScript 代码就构成了一个执行环境栈。
接下来看执行环境是如何参与工作的
当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行后,栈将其环境弹出,把控制权返回给这个环境栈中之前的执行环境。(有点绕).
这样看图片能理解了吧,执行环境A被弹出,就把控制权给执行环境B.
执行环境的具体细节
执行环境的建立分为两个阶段:进入执行上下文(创建阶段)和执行阶段(激活/执行阶段)
(1)进入上下文阶段:发生在函数调用时,但在执行具体代码之前。具体完成创建作用域链;创建变量、函数和参数以及求this的值
(2)执行代码阶段:主要完成变量赋值、函数引用和解释/执行其他代码
总的来说可以将执行上下文看作是一个对象
用一个实际函数说明:
1 | (function foo(x,y,z){ |
函数调用后,相应的执行环境对象如下:
1 | executionContextObj = { |
第二阶段:
1 | executionContextObj = { |
在第二阶段,就会为局部变量 a 、b 赋值,注意到 d 并没有在变量对象中,是因为,函数表达式是不会影响变量对象的,所以在作用域中任何一个位置引用d都会出现“d is not defined”的错误。
环境栈
在上文中有提及了一个名词,环境栈(执行环境栈).
JavaScript解释器在浏览器中是单线程的,这意味着浏览器在同一时间内只执行一个事件,对于其他的事件我们把它们排队在一个称为 执行环境栈
的地方。
每次函数的调用都会创建一个执行环境压入栈中,无论是函数内部的函数、还是递归调用等。
我们已经知道,当浏览器第一次加载你的script,它默认的进了全局执行环境。如果在你的全局代码中你调用了一个函数,那么顺序流就会进入到你调用的函数当中,创建一个新的执行环境并且把这个环境添加到执行栈的顶部。
如果你在当前的函数中调用了其他函数,同样的事会再次发生。执行流进入内部函数,并且创建一个新的执行环境,把它添加到已经存在的执行栈的顶部。浏览器始终执行当前在栈顶部的执行环境。一旦函数完成了当前的执行环境,它就会被弹出栈的顶部, 把控制权返回给当前执行环境的下个执行环境。下面例子展示了一个递归函数和该程序的执行栈:
1 | (function foo(i) { |
这段代码简单地调用了自己三次,由1递增i的值。每次函数foo被调用,一个新的执行环境就会被调用。一旦一个环境完成了执行,它就会被弹出执行栈并且把控制权返回给当前执行环境的下个执行环境直到再次到达全局执行环境。
那么到底什么是作用域链
作用域链,其实就是环境栈中各个环境的变量对象组成的
1 | function func1(){ |
作用域链的作用
1.搜索标识符(寻找变量)
1 | var scope = "global"; |
当某个函数第一次被调用时,就会创建一个执行环境(execution context)以及相应的作用域链,并把作用域链赋值给一个特殊的内部属性([scope])。然后使用this,arguments(arguments在全局环境中不存在)和其他命名参数的值来初始化函数的活动对象(activation object)。当前执行环境的变量对象始终在作用域链的第0位。
以上面的代码为例,当第一次调用fn1()时的作用域链如下图所示:
可以看到fn1活动对象里并没有scope变量,于是沿着作用域链(scope chain)向后寻找,结果在全局变量对象里找到了scope,所以就返回全局变量对象里的scope值。
标识符解析是沿着作用域链一级一级地搜索标识符地过程。搜索过程始终从作用域链地前端开始,然后逐级向后回溯,直到找到标识符为止(如果找不到标识符,通常会导致错误发生)—-《JavaScript高级程序设计》
2.闭包
1 | function outer(){ |
outer()内部返回了一个inner函数,当调用outer时,inner函数的作用域链就已经被初始化了(复制父函数的作用域链,再在前端插入自己的活动对象),具体如下图:
一般来说,当某个环境中的所有代码执行完毕后,该环境被销毁(弹出环境栈),保存在其中的所有变量和函数也随之销毁(全局执行环境变量直到应用程序退出,如网页关闭才会被销毁)
但是像上面那种有内部函数的又有所不同,当outer()函数执行结束,执行环境被销毁,但是其关联的活动对象并没有随之销毁,而是一直存在于内存中,因为该活动对象被其内部函数的作用域链所引用。
具体如下图:
outer执行结束,内部函数开始被调用
outer执行环境等待被回收,outer的作用域链对全局变量对象和outer的活动对象引用都断了.
像上面这种内部函数的作用域链仍然保持着对父函数活动对象的引用,就是闭包(closure).
在谈闭包
闭包有两个作用:
第一个就是可以读取自身函数外部的变量(沿着作用域链寻找)
第二个就是让这些外部变量始终保存在内存中
关于第二点,来看一下以下的代码:
1 | function outer(){ |
返回结果很出乎意料吧,你肯定以为依次返回0,1,但事实并非如此
来看一下调用fn[0]()
的作用域链图:
可以看到result[0]函数的活动对象里并没有定义i这个变量,于是沿着作用域链去找i变量,结果在父函数outer的活动对象里找到变量i(值为2),而这个变量i是父函数执行结束后将最终值保存在内存里的结果。
由此也可以得出,js函数内的变量值不是在编译的时候就确定的,而是等在运行时期再去寻找的。
那怎么才能让result数组函数返回我们所期望的值呢?
看一下result的活动对象里有一个arguments,arguments对象是一个参数的集合,是用来保存对象的。
那么我们就可以把i当成参数传进去,这样一调用函数生成的活动对象内的arguments就有当前i的副本。
改进之后:
1 | function outer(){ |
虽然的到了期望的结果,但是又有人问这算闭包吗?调用内部函数的时候,父函数的环境变量还没被销毁呢,而且result返回的是一个整型数组,而不是一个函数数组!
确实如此,那就让arg(num)函数内部再定义一个内部函数就好了:
这样result返回的其实是innerarg()函数
1 | function outer(){ |
当调用outer,for循环内i=0时的作用域链图如下:
由上图可知,当调用innerarg()时,它会沿作用域链找到父函数arg()活动对象里的arguments参数num=0.
上面代码中,函数arg在outer函数内预先被调用执行了,对于这种方法,js有一种简洁的写法
1 | function outer(){ |
关于this对象
关于闭包经常会看到这么一道题:
1 | var name = "The Window"; |
《javascript高级程序设计》一书给出的解释是:
this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象调用时,this等于那个对象。不过,匿名函数具有全局性,因此this对象同常指向window