JavaScript 作用域链
一个函数就像一个屋子一样,这个屋子好像形成了一个单独的"域"和外界有一些阻隔,屋子里面能看到外面,外面看不到到屋子里面,而且屋子与屋子之间是彼此独立的。
可以把函数所生成的空间,管它叫做一种"作用域",但是不精准,下面就真正的探索一下作用域是什么?
作用域确实是因为函数的产生而产生的独特东西,作用域属于一个函数,一个函数产生了一样的作用域,这两个是相绑定的,作用域到底是什么呢?首先了解一下[[scope]]
一、作用域 [[scope]]
首先我们知道,每一个对象都有两个基本特性,有属性、有方法。对象可以有属性,一切为对象的东西就都可以有属性(古希腊哲学的三段论)。
函数也是一种特殊对象,因为函数也有属性比如name属性,函数是特殊类对象叫函数类对象,
函数类对象有一些我们可以用的属性,比如 test.name 包括后期学的 test.prototype ,这些都是我们可以访问的属性。
function test(){ } console.log(test.name); // name属性返回函数名test
还有一些属性是我们访问不了的,比如 test.[[scope]] ,scope翻译过来有区域的意思,计算机术语叫"域"的意思。
[[scope]]是什么的呢?
test.[[scope]]里面存的就是由这个test函数产生而产生的的作用域,[[scope]]里面存的就是一个作用域,里面很复杂的结构下面会详细学,现在就知道[[scope]]里面存的是一个作用域就OK了。
为什么[[scope]]属性我们能不能用呢?
因为这是隐式的属性,隐式的属性就是说,他是属性但是我们用不了。
这个属性是干什么的呢?
系统会通过它内部的一些原理,会定期的调用[[scope]]但不会让我们用的。下面是[[scope]]属性的官方解释。
[[scope]]属性的官方解释:
[[scope]]:每个javascript函数都是一个对象,对象中有些属性我们可以访问,但有些不可以,这些属性仅供javascript引擎存取,[[scope]]就是其中一个。
[[scope]] 指的就是我们所说的作用域,作用域中存储了运行期上下文的集合。
作用域链:[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。
[[scope]] 指的就是我们所说的作用域,作用域中存储了运行期上下文的集合。
执行期上下文预编译学了一些,这两节课特别有意思,
学闭包时必须要知道执行期上下文,但执行期上下文的精解又属于预编译的环节(闭包和执行期上下文有不挂钩),上次学的AO、GO就是行期上下文。下面看执行期上下文的官方解释:
执行期上下文官方解释:
当函数执行时,会创建一个称为 执行期上下文 的内部对象。
一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,执行上下文被销毁。
查找变量:从作用域链的顶端依次向下查找。
当函数执行时,会创建一个称为 执行期上下文 的内部对象。
注解:这句话精准的理解,是当函数执行的前一刻,因为预编译发生在执行的前一刻。执行期上下文简称AO
一个执行期上下文定义了一个函数执行时的环境
注解:泛泛的理解一下,什么是函数执行的环境,由于这个函数而产生的一些变量提升、函数提升
一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,执行上下文被销毁。
现在执行test()函数产生一个AO,执行完了再执行下一次,下一次新建了另一个AO和上一个的AO就没关系了,因为这是两次执行。两次执行就有两次环境,所以每次执行完之后,都会产一个生独一无二的AO执行期上下文。
function test(){ } // test() --> 第一次执行产生的AO{}; // test() --> 第二次执行产生的AO{};
第一次执行完后AO上哪去了?
AO只作为即时的存储空间用完就销毁了。就是一个函数执行的时候产生的AO,执行完AO就销毁掉。
看完执行期上下文的概念,回来再看作用域[[scope]]属性:
[[scope]] 指的就是我们所说的作用域,其中存储了运行期上下文的集合。
一个函数里面有一个[[scope]]属性里面存的是作用域,一个函数执行的时候还产生一个唯一的AO,为什么里面放的是一个集合呢?别着急下面继续学。
成哥在这块解释一下:[[scope]]里面存的其实叫"作用域链",这个作用域链才是执行期上下文的集合。
作用域链:[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。
也就是说[[scope]]里面存的是一个作用域链,这个作用域链是由多个执行期上下文链接起来的,像一个链条一样链接的结构。
链是那来的,除了函数自己产生的执行期上下文,那些不是自己产生的执行期上下文是哪来的?
二、作用域链精解
举例说明 [[scope]]
function a(){ function b(){ var b = 123; } var a = 123; b(); } var glob = 100; a(); /**** 第一步:a函数被定义 a defined a.[[scope]] --> 0 : GO{} 第二步:a函数执行 a doing a.[[scope]] --> 0 : AO{} 1 : GO{} ****/
第一步:a函数定义:
在函数a,刚刚出生的时候(函数的定义环节)就有属性和方法了,比如 a.name 属性还有 a.[[scope]] 属性。
a函数刚刚被定义的时候,a.[[scope]]里面就存了东西了,存的是全局的 GO。
a.[[scope]] 里面存的是作用域链(看下面图),作用域链里面装的是执行期上下文集合,这个时候还没有达到集合的程度,作用域链里面只存了一个第0位,第0位指向的 Global Object 就是GO,GO是全局的执行期上下文。
GO里面有一堆东西,有a函数、global变量等,都是GO里面应该有的东西。现在作用域链只有一个GO,还没有形成链结构。
a函数被定义完,接着走的环节是a函数执行。
第二步:a()函数执行
a()函数执行的时候发生了一个改变,一个函数执行的时候要产生一个执行期上下文(AO),a函数产生的AO放在哪里呢?
先想一个问题,为什么在一个函数里面去访问变量值的时候,是先找函数自己的AO,然后再往上面去找?
比如下面test函数里面访问变量a,函数执行a()打印结果是123
var a = 234; function test(){ var a = 123; console.log(a); } test();// 打印的是123
test函数自己的AO里面有变量a,全局GO里面也有变量a,函数先找自己AO里面的。也就是说先找函数自己AO再找GO的,肯定是函数自己的AO和GO有一点什么联系,这种联系就叫作用域链。
回到a函数:
当函数a()被执行的时候,产生自己的AO(执行期上下文)放到哪里了呢?放到作用域链的顶端,然后把GO往下放一位,形成了a函数自己的作用域链。
函数a()执行的时候里面有个 a.[[scope]] 属性,里面存的是作用域链叫 scope chain 有两位,第0位指向的是a函数自己新产生的AO,第1位指向的是GO。
所以这时候要在函数a里访问一个变量,系统到 a.[[scope]] 里去找这个变量,a.[[seope]]里面放的是一个链(有顶端、有底端),系统会到作用域链的顶端去找,然后依次下向的去找。
上一节课说要找一个变量到AO里找或GO里去找,其实标准说法是到函数的作用域链里面找,然后顺着作用域链从顶端找到作用域链的底端,这是整个查找的过程。
再整理一下
查找变量:从作用域链的顶端依次向下查找。
上面这句再补充一下,在哪个函数里面查找变量,就到哪个函数的作用域链顶端依次向下查找。
比如在a函数里面找一个变量,我们应该找a.[[scope]],因为a.[[scope]]已经是一个作用域链了,a函数怎么构成的作用域链?
a函数刚出生(定义)的时候,a.[[scope]]就已经存了GO,再形象点说a函数刚刚出生的时候,a.[[scope]]存的是他所在环境的执行期上下文,a函数所在环境是全局的所以他存的是GO。
第三步:b函数的定义
由于a函数的执行,产生一个过程叫 b函数的定义,b函数作为一个函数,他也有自己的b.[[scope]]。
当b函数刚出生的时候,他的环境不但在GO里面还在AO里面,b函数的环境是a()函数执行形成的环境。
所以b函数定义时候的b.[[scope]拿的现成的,就是a()函数执行时候一模一样的劳动成果a.[[scope]。
然后这个阶段没什么用,b函数定义的时候也访问不了,关键在b函数执行时候发生的事。
第四步:b函数执行
b函数在执行的时候发了什么事?b函数也得生成了一个自己的AO(执行期上下文),放到了b函数自己作用域链的最顶端。
如果在b函数里面访问一个变量,访问顺序是自顶部向下的去访问,自b.[[scope]]第0位向第2位去找。
重新整理一遍:
第一步:
a函数定义在全局的作用域里面,a函数定义有一个a.[[scope]],a.[[scope]]里面存的是全局的执行期上下文。然后紧接着a函数被执行了。
第二步:
a函数被执行产生里一个AO,a函数把自己的AO放到作用域链的最顶端,形成一个新的作用域链。
也就是说现在a.[[scope]] 和 刚才定义时的a.[[scope]]存的值不一样了,a.[[scope]]还是原来那个。
这个时候如果访问a函数里面的变量,访问的是a.[[scope]],遵循自顶向下的规则去查找。
第三步:
接着,a()函数的执行产生了b函数的定义,因为b函数在a函数体内,a函数执行了b函数才能被定义。
b函数被定义的时候,b函数有自己的[[scope]],b.[scope]]里面存里b函数的作用域链。
但是b函数的作用域链和a函数的作用域链不一样,a函数在全局的环境下出生,能看到的范围就是全局的。但是b函数出生的时候,可是站在a函数的肩膀上看世界的。所以a函数里面能看的东西,b函数都能看到。
所以b函数刚刚创建(定义)的时候,b.[[scope]]拿到的是a函数的劳动成果。
然后b函数被执行
第四步:
然后b()函数被执行,b被执行的时候,b函数自己产生了一个执行期上下文。
b函数产生的执行期上下文后,放到自己作用域链的最顶端形成一个作用域链。
然后在b函数里访问变量,会依照b函数的执行期上下文的顺序 0 -> 1 -> 2 访问。
三、作用域链精解中的问题
1、b函数作用域链的第1位的AO,是不是a函数的AO?
先看一个小例子:
1). a函数里面有变量aa,在b函数里面能访问变量aa
2). 在b函数里把变量aa的值变成0了
3). 然后b()函数的执行把变量aa变成0了
然后在b函数执行完之后访问变量aa,变量aa的值输出什么?
function a(){ function b(){ var bb = 234; aa = 0; // 1. 在b函数里面,是可以访问到变量aa,把变量aa的值变成0 } var aa = 123; b(); console.log(aa); // 2. b函数执行完,访问变量aa } var glob = 100; a(); // 0
现在讨论的是,b函数执行期上下文中第1位的AO,和a函数执行产生的AO,是不是同一人?
a()函数执行输出0,b函数作用域链里面的第1位AO,就是函数a产生的那个AO,就是同一个人。再换者说全局的GO总是一个人吧,但凡一个作用域链,必须存的都是同一个GO。
每一个函数自己都有一个执行期上下文的集合叫作用域链,我们真正在一个函数里面去访问一个变量,要遵循这个函数所产生的作用域链去访问。
2、研究一下函数执行完的一个顺序
a函数定义a()函数执行,b函数定义b()函数执行,b函数执行完之后呢?
一个函数执行完要销毁他的执行期上下文。
看图:
b函数执行完之后,把他自己执行期上下文的线去掉了(不指向那个空间了),相当于销毁了自己的执行期上下文。
销毁了之后,b函数又回归到它被定义的状态,等待下一次被执行。
b函数执行是最后一句,b函数执行完后相当于a函数也执行完了。
a函数的作用域链就两位,第0位存的是a函数自己的AO,第1位存的是GO。
a函数执行完a.AO也销毁了,a函数回到定义状态,等待下一次被执行。
但是a函数里面有一个b函数,a函数把线剪断后,b函数不用等待下一次被执行了。
a的执行期上下文里存的是b函数,a函数指向的a.AO的线剪断了,b函数不用等待下一次执行,因为b函数不复存在了。
不用管b函数了,刚才的b函数永远没了,a函数又回回归到被定义状态,等待下一次被执行。
当a函数下一次被执行的时候,又产生一个独一无二新执行期上下文,这个新执行期上下文,再放到a函数自己作用域链的最顶端。刚才产生的执行期上下文已经没有了。
然后由于产生了一个新的执行期上下文,又产生了一次b函数的定义,又一个全新的b函数呈现出来了,这个全新的b函数又保存了a函数的劳动成果,再一次等待被执行。
b函数被执行的时候,产生了一个全新的执行期上下文,到自己作用域链的最顶端,然后形成一个作用域链,等待被查找变量。
四、写一个小例子练一下
function a(){ function b(){ function c(){ } c(); } b(); } a(); /*** a defined a.[[scope]] --> 0 : GO a函数定义 a doing a.[[scope]] --> 0 : a.AO a()函数执行 1 : GO b defined b.[[scope]] --> 0 : a.AO a函数执行完产生了一个b函数定义,b.[[scope]]拿的是a.[[scope]]的劳动成果 1 : GO b doing b.[[scope]] --> 0 : b.AO 紧接着b()执行,第0位是b产生执行期上下文,放到作b函数做用域链的最顶端 1 : a.AO 2 : GO C defined c.[[scope]] --> 0 : b.AO 函数b()的执行产生了函数c的定义,c.[[scope]]存的是b函数的劳动成果 1 : a.AO 2 : GO c doing c[[scope]] --> 0 : c.AO 函数c()执行,产生自己的AO 1 : b.AO 2 : a.AO 3 : GO ***/
1、在任何一个函数里,想访问一个变量就找他的作用域链。
找变量肯定是函数执行时候产生的作用域链,执行时候生成的作用域链才是真正的作用域链。
记住一个事,这里面所有的a.AO都是一个人,b.AO也是一个人,只不过互相借用了一下而已。
2、c函数执行完会做什么?
c函数执行完会扔掉自己的执行期上下文,把c.AO干掉,c函数回归到被定义状态,如果在这个状态上,在执行一次c()函数呢?
会生成一个同样类似于c.[[scope]]的环节,类似就是除了b.AO、a.AO和GO一样,c.AO就不一样了,这次的是一个new_c.AO。剩下的都一样,因为基于的基础都是c defined(c函数定义)
a defined a.[[scope]] --> 0 : GO
a doing a.[[scope]] --> 0 : a.AO
1 : GO
b defined b.[[scope]] --> 0 : a.AO
1 : GO
b doing b.[[scope]] --> 0 : b.AO
1 : a.AO
2 : GO
C defined c.[[scope]] --> 0 : b.AO
1 : a.AO
2 : GO
c doing c[[scope]] --> 0 : c.AO
1 : b.AO
2 : a.AO
3 : GO
c doing c[[scope]] --> 0 : new_c.AO
1 : b.AO
2 : a.AO
3 : GO
3、问题维密天使:函数什么时候执行完,什么时候销毁?
执行:
1. c()执行这条语句要等,c函数体里面的语句执行完了,c()执行才算执行完了。
c()执行语句是b函数的最后一条语句,b的最后一条语句执行完了,说明b函数被执行完了。
2. b函数被执行完了,说明b()执行这条语句执行完,也就是a函数的最后一条语句执行完了
3. a函数的最后一条语句被执行完了,就宣告着a函数被执行完了
销毁:
每一个函数执行完,意味着这个函数产生的执行期上下文被销毁
c()这句非常有意思,叫c函数的执行(调用)语句,调用语句分两个环节,调用的时候是这个函数的执行,调用语句执行完了,这个函数就执行完了。
1. 所以调用的时候,首先c形成自己的执行期上下文,放到自己作用域链的最顶端。
2. 然后当c函数执行完,c函数先自己销毁自己的执行期上下文,说明c函数执行完了,也就宣告b函数执行完了
3. b函数执行完,b函数消耗自己的执行期上下文,然后宣告a函数执行完了
4. 然后a函数执行完,a函数消耗自己的执行期上下文
c函数先销毁,哪个函数先执行完,哪个函数先销毁,销毁和执行完是一个概念。