今天看笔记发现本身以前记了一个关于同名标识符优先级的内容,具体是下面这样的:javascript
arguments
而后我就想,为何会有这样的优先级呢,规定的?可是好像没有这个规定,因而开始查阅资料,就有了下文html
Execution Context
Execution Context
是Javascript
中一个抽象概念,它定义了变量或函数有权访问的其余数据,决定了它们各自的行为。为了便于理解,咱们能够近似将其等同于执行当前代码的环境,JavaScript
的可执行代码包括前端
eval()
代码每当执行流转到这些可执行代码时,就会“新建”一个Execution Context
并进入该Execution Context
java
在上图中,共有4个Execution Context
,其中有一个是Global Execution Context
(有且仅有一个),还有三个Function Execution Context
segmentfault
Execution Context Stack
浏览器中的JavaScript
解释器是单线程的,每次建立并进入一个新的Execution Context
时,这个Execution Context
就会被推(push
)进一个环境栈中,这个栈称为Execution Context Stack
,当当前Execution Context
的代码执行完以后,栈又会将其弹(pop
)出,并销毁这个Execution Context
,保存在其中的变量及函数定义也随之被销毁,而后把控制权返回给以前的Execution Context
(Global Execution Context
例外,它要等到应用程序退出后 —— 如关闭网页或浏览器 —— 才会被销毁)浏览器
JavaScript
的执行流就是由这个机制控制的,如下面的代码为例说明:闭包
var sayHello = 'Hello'; function name(){ var fisrtName = 'Cao', lastName = 'Cshine'; function getFirstName(){ return fisrtName; } function getLatName(){ return lastName; } console.log(sayHello + getFirstName() + ' ' + getLastName()); } name();
script
的时候,默认会进入Global Execution Context
,因此Global Execution Context
永远是在栈的最下面。name()
,此时新建并进入Function Execution Context name
,Function Execution Context name
入栈;getFirstName()
,因而新建并进入Function Execution Context getFirstName
,Function Execution Context getFirstName
入栈,因为该函数内部不会再新建其余Execution Context
,因此直接执行完毕,而后出栈,控制权交给Function Execution Context name
;getLastName()
,因而新建并进入Function Execution Context getLastName
,Function Execution Context getLastName
入栈,因为该函数内部不会再新建其余Execution Context
,因此直接执行完毕,而后出栈,控制权交给Function Execution Context name
;console
后,函数name
也执行完毕,因而出栈,控制权交给Function Execution Context name
,至此栈中又只有Global Execution Context
了关于Execution Context Stack
有5个关键点:异步
Global Execution Context
Function Execution Context
每一个函数调用都会建立新的Execution Context
,即便是本身调用本身,以下面的代码:函数
(function foo(i) { if (i === 3) { return; } else { foo(++i); } }(0));
Execution Context Stack
的状况以下图所示:ui
Execution Context
每一个Execution Context
在概念上能够当作由下面三者组成:
Variable object
,简称VO
)Scope Chain
)this
Variable object
)该对象与Execution Context
相关联,保存着Execution Context
中定义的全部变量、函数声明以及函数形参,这个对象咱们没法访问,可是解析器在后台处理数据是用到它(注意函数表达式以及没用var/let/const
声明的变量不在VO
中)
Global Execution Context
中的变量对象VO
根据宿主环境的不一样而不一样,在浏览器中为window
对象,所以全部的全局变量和函数都是做为window
对象的属性和方法建立的。
对于Function Execution Context
,变量对象VO
为函数的活动对象,活动对象是在进入Function Execution Context
时建立的,它经过函数的arguments
属性初始化,也就是最初只包含arguments
这一个属性。
在JavaScript
解释器内部,每次调用Execution Context
都会经历下面两个阶段:
建立阶段(发生在函数调用时,可是内部代码执行前,这将解释声明提高现象)
VO
this
的值激活/代码执行阶段
其中建立阶段的第二步建立变量对象VO
的过程能够理解成下面这样:
Global Execution Context
中没有这一步) 建立arguments
对象,扫描函数的全部形参,并将形参名称 和对应值组成的键值对做为变量对象VO
的属性。若是没有传递对应的实参,将undefined
做为对应值。若是形参名为arguments
,将覆盖arguments
对象扫描Execution Context
中全部的函数声明(注意是函数声明,函数表达式不算)
VO
的属性VO
已经存在同名的属性,则覆盖这个属性扫描Execution Context
中全部的变量声明
undefined
) 组成,做为变量对象的属性好~~如今咱们来看代码捋一遍:
function foo(num) { console.log(num);// 66 console.log(a);// undefined console.log(b);// undefined console.log(fc);// f function fc() {} var a = 'hello'; var b = function fb() {}; function fc() {} } foo(66);
当调用foo(66)时,建立阶段时,Execution Context
能够理解成下面这个样子
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 66, length: 1 }, num: 66, fc: pointer to function fc() a: undefined, b: undefined }, this: { ... } }
当建立阶段完成之后,执行流进入函数内部,激活执行阶段,而后代码完成执行,Execution Context
能够理解成下面这个样子:
fooExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 66, length: 1 }, num: 66, fc: pointer to function fc() a: 'hello', b: pointer to function fb() }, this: { ... } }
Scope Chain
)当代码在一个Execution Context
中执行时,就会建立变量对象的一个做用域链,做用域链的用途是保证对执行环境有权访问的全部变量和函数的有序访问
Global Execution Context
中的做用域链只有Global Execution Context
的变量对象(也就是window
对象),而Function Execution Context
中的做用域链还会有“父”Execution Context
的变量对象,这里就会要牵扯到[[Scopes]]
属性,能够将函数做用域链理解为---- 当前Function Execution Context
的变量对象VO
(也就是该函数的活动对象AO
) + [[Scopes]]
,怎么理解呢,咱们继续往下看
[[Scopes]]
属性[[Scopes]]
这个属性与函数的做用域链有着密不可分的关系,JavaScript
中每一个函数都表示为一个函数对象,[[Scopes]]
是函数对象的一个内部属性,只有JavaScript
引擎能够访问。
结合函数的生命周期:
函数定义
[[Scopes]]
属性在函数定义时被存储,保持不变,直至函数被销毁[[Scopes]]
属性连接到定义该函数的做用域链上,因此他保存的是全部包含该函数的 “父/祖父/曾祖父...” Execution Context
的变量对象(OV
),咱们将其称为全部父变量对象(All POV
)[[Scopes]]
是在定义一个函数的时候决定的函数调用
Function Execution Context
,根据前面讨论过的调用Function Execution Context
的两个阶段可知:先建立做用域链,这个建立过程会将该函数对象的[[Scopes]]
属性加入到其中AO
(做为该Function Execution Context
的变量对象VO
),并将建立的这个活动对象AO
加到做用域链的最前端this
的值经过上面的过程咱们大概能够理解:做用域链 = 当前Function Execution Context
的变量对象VO
(也就是该函数的活动对象AO
) + [[Scopes]]
,有了这个做用域链, 在发生标识符解析的时候, 就会沿着做用域链一级一级地搜索标识符,最开始是搜索当前Function Execution Context
的变量对象VO
,若是没有找到,就会根据[[Scopes]]
找到父变量对象,而后继续搜索该父变量对象中是否有该标识符;若是仍没有找到,便会找到祖父变量对象并搜索其中是否有该标识符;如此一级级的搜索,直至找到标识符为止(若是直到最后也找不到,通常会报未定义的错误);注意:对于this
与arguments
,只会搜到其自己的变量(活动)对象为止,而不会继续按着做用域链搜素。
如今再结合例子来捋一遍:
var a = 10; function foo(d) { var b = 20; function bar() { var c = 30; console.log(a + b + c + d); // 110 //这里能够访问a,b,c,d } //这里能够访问a,b,d 可是不能访问c bar(); } //这里只能访问a foo(50);
当浏览器第一次加载script的时候,默认会进入Global Execution Context
的建立阶段
Scope Chain
(做用域链)window
对象。而后会扫描全部的全局函数声明,再扫描全局变量声明。以后该变量对象会加到Scope Chain
中this
的值此时Global Execution Context
能够表示为:
globalEC = { scopeChain: { pointer to globalEC.VO }, VO: { a: undefined, foo: pointer to function foo(), (其余window属性) }, this: { ... } }
接着进入Global Execution Context
的执行阶段
遇到赋值语句var a = 10
,因而globalEC.VO.a = 10
;
globalEC = { scopeChain: { pointer to globalEC.VO }, VO: { a: 10, foo: pointer to function foo(), (其余window属性) }, this: { ... } }
遇到foo
函数定义语句,进入foo
函数的定义阶段,foo
的[[Scopes]]
属性被肯定
foo.[[Scopes]] = { pointer to globalEC.VO }
遇到foo(50)
调用语句,进入foo
函数调用阶段,此时进入Function Execution Context foo
的建立阶段
Scope Chain
(做用域链)foo
的活动对象。先建立arguments
对象,而后扫描函数的全部形参,以后会扫描foo
函数内全部的函数声明,再扫描foo
函数内的变量声明。以后该变量对象会加到Scope Chain
中this
的值此时Function Execution Context foo
能够表示为
fooEC = { scopeChain: { pointer to fooEC.VO, foo.[[Scopes]] }, VO: { arguments: { 0: 66, length: 1 }, b: undefined, d: 50, bar: pointer to function bar(), }, this: { ... } }
接着进入Function Execution Context foo
的执行阶段
遇到赋值语句var b = 20;
,因而fooEC .VO.b = 20
fooEC = { scopeChain: { pointer to fooEC.VO, foo.[[Scopes]] }, VO: { arguments: { 0: 66, length: 1 }, b: 20, d: 50, bar: pointer to function bar(), }, this: { ... } }
遇到bar
函数定义语句,进入bar
函数的定义阶段,bar
的[[Scopes]]
`属性被肯定
bar.[[Scopes]] = { pointer to fooEC.VO, pointer to globalEC.VO }
遇到bar()
调用语句,进入bar
函数调用阶段,此时进入Function Execution Context bar
的建立阶段
Scope Chain
(做用域链)bar
的活动对象。先建立arguments
对象,而后扫描函数的全部形参,以后会扫描foo
函数内全部的函数声明,再扫描bar
函数内的变量声明。以后该变量对象会加到Scope Chain
中this
的值此时Function Execution Context bar
能够表示为
barEC = { scopeChain: { pointer to barEC.VO, bar.[[Scopes]] }, VO: { arguments: { length: 0 }, c: undefined }, this: { ... } }
接着进入Function Execution Context bar
的执行阶段
遇到赋值语句var c = 30
,因而barEC.VO.c = 30
;
barEC = { scopeChain: { pointer to barEC.VO, bar.[[Scopes]] }, VO: { arguments: { length: 0 }, c: 30 }, this: { ... } }
遇到打印语句console.log(a + b + c + d);
,须要访问变量a,b,c,d
bar.[[Scopes]].globalEC.VO.a
访问获得a=10
bar.[[Scopes]].fooEC.VO.b,bar.[[Scopes]].fooEC.VO.d
访问获得b=20,d=50
barEC.VO.c
访问获得c=30
110
bar
函数执行完毕,Function Execution Context bar
销毁,变量c
也随之销毁foo
函数执行完毕,Function Execution Context foo
销毁,b,d,bar
也随之销毁Global Execution Context
才销毁,a,foo
才会销毁经过上面的例子,相信对Execution Context
和做用域链的理解也更清楚了,下面简单总结一下做用域链:
Execution Context
的变量对象;Execution Context
,以此类推;Global Execution Context
的变量对象;Execution Context
可经过做用域链访问外部Execution Context
;反之不能够;下面两种语句能够在做用域链的前端临时增长一个变量对象以延长做用域链,该变量对象会在代码执行后被移除
try-catch
语句的catch
块with
语句
将指定的对象添加到做用域链中
function buildUrl(){ var qs = "?debug=true"; with(location){ var url = href + qs; } //console.log(href) 将会报href is not defined的错误,由于with语句执行完with建立的变量对象就被移除了 return url; }
with
语句接收window.location
对象,所以其变量对象就包含了window.location
对象的全部属性,而这个变量对象被添加到做用域链的前端。因此在with
语句里面使用href
至关于window.location.href
。
如今咱们来解答最开始的优先级问题
形参优先级高于当前函数名,低于内部函数名
function fn(fn){ console.log(fn);// cc } fn('cc');
函数fn
属于Global Execution Context
,而形参fn
属于Function Execution Context fn
,此时做用域的前端是Function Execution Context fn
的变量对象,因此console.log(fn)
为形参的值
function fa(fb){ console.log(fb);// ƒ fb(){} function fb(){} console.log(fb);// ƒ fb(){} } fa('aaa');
调用fa
函数时,进入Function Execution Context fa
的建立阶段,根据前面所说的变量对象建立过程:
先建立arguments对象,而后扫描函数的全部形参,以后会扫描函数内全部的函数声明,再扫描函数内的变量声明;
扫描函数声明时,若是变量对象VO
中已经存在同名的属性,则覆盖这个属性
咱们能够获得fa
的变量对象表示为:
fa.VO = { arguments: { 0:'aaa', length: 1 }, fb: pointer to function fb(), }
因此console.log(fb)
获得的是fa.VO.fb
的值ƒ fb(){}
形参优先级高于arguments
function fn(aa){ console.log(arguments);// Arguments ["hello world"] } fn('hello world'); function fn(arguments){ console.log(arguments);// hello world } fn('hello world');
调用fn
函数时,进入Function Execution Context fn
的建立阶段,根据前面所说的变量对象建立过程:
先建立arguments对象,而后扫描函数的全部形参,以后会扫描函数内全部的函数声明,再扫描函数内的变量声明;
先建立arguments对象,后扫描函数形参,若是形参名为arguments,将会覆盖arguments对象
因此当形参名为arguments
时,console.log(arguments)
为形参的值hello world
。
形参优先级高于只声明却未赋值的局部变量,可是低于声明且赋值的局部变量
function fa(aa){ console.log(aa);//aaaaa var aa; console.log(aa);//aaaaa } fa('aaaaa');
调用fa
函数时,进入Function Execution Context fa
的建立阶段,根据前面所说的变量对象建立过程:
先建立arguments对象,而后扫描函数的全部形参,以后会扫描函数内全部的函数声明,再扫描函数内的变量声明;
扫描函数内的变量声明时,若是变量名与已经声明的形参或函数相同,此时什么都不会发生,变量声明不会干扰已经存在的这个同名属性
因此建立阶段以后Function Execution Context fa
的变量对象表示为:
fa.VO = { arguments: { 0:'aaaaa', length: 1 }, aa:'aaaaa', }
以后进入Function Execution Context fa
的执行阶段:console.log(aa);
打印出fa.VO.aa
(形参aa
)的值aaaaa
;因为var aa;
仅声明而未赋值,因此不会改变fa.VO.aa
的值,因此下一个console.log(aa);
打印出的仍然是fa.VO.aa
(形参aa
)的值aaaaa
。
function fb(bb){ console.log(bb);//bbbbb var bb = 'BBBBB'; console.log(bb);//BBBBB } fb('bbbbb');
调用fb
函数时,进入Function Execution Context fb
的建立阶段,根据前面所说的变量对象建立过程:
先建立arguments对象,而后扫描函数的全部形参,以后会扫描函数内全部的函数声明,再扫描函数内的变量声明;
扫描函数内的变量声明时,若是变量名与已经声明的形参或函数相同,此时什么都不会发生,变量声明不会干扰已经存在的这个同名属性
因此建立阶段以后Function Execution Context fb
的变量对象表示为:
fb.VO = { arguments: { 0:'bbbbb', length: 1 }, bb:'bbbbb', }
以后进入Function Execution Context fb
的执行阶段:console.log(bb);
打印出fb.VO.bb
(形参bb
)的值'bbbbb';遇到var bb = 'BBBBB';
,fb.VO.bb
的值将被赋为BBBBB
,因此下一个console.log(bb);
打印出fb.VO.bb
(局部变量bb
)的值BBBBB
。
函数和变量都会声明提高,函数名和变量名同名时,函数名的优先级要高。
console.log(cc);//ƒ cc(){} var cc = 1; function cc(){}
根据Global Execution Context
的建立阶段中建立变量对象的过程:是先扫描函数声明,再扫描变量声明,且变量声明不会影响已存在的同名属性。因此在遇到var cc = 1;
这个声明语句以前,global.VO.cc
为ƒ cc(){}
。
执行代码时,同名函数会覆盖只声明却未赋值的变量,可是它不能覆盖声明且赋值的变量
var cc = 1; var dd; function cc(){} function dd(){} console.log(cc);//1 console.log(dd);//ƒ dd(){}
Global Execution Context
的建立阶段以后,Global Execution Context
的变量对象能够表示为:
global.VO = { cc:pointer to function cc(), dd:pointer to function dd() }
而后进入Global Execution Context
的执行阶段,遇到var cc = 1;
这个声明赋值语句后, global.VO.cc
将被赋值为1
;而后再遇到var dd
这个声明语句,因为仅声明未赋值,因此不改变global.VO.dd
的值;因此console.log(cc);
打印出1
,console.log(dd);
打印出ƒ dd(){}
每一个Execution Context
都会有变量建立这个过程,因此会有声明提高;根据做用域链,若是局部变量与外部变量同名,那么最早找到的是局部变量,影响不到外部同名变量
JavaScript基础系列---变量及其值类型
Understanding Scope in JavaScript
What is the Execution Context & Stack in JavaScript?
深刻探讨JavaScript的执行环境和栈
做用域原理
JavaScript执行环境 + 变量对象 + 做用域链 + 闭包