一,执行上下文:
变量或函数的上下文决定了它们可以访问那些数据,以及他们的行为,每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象中。虽然无法通过代码访问变量对象,但后台处理数据会用到它;
在浏览器中,全局上下文是我们常说的window对象,因此所有通过var定义的变量和函数都会成为window对象的属性和方法,使用let和const的顶级声明,不会定义在全局上下文中,但在作用域链解析上效果是一样的。上下文在其所有代码执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或者退出浏览器);
每个函数调用都有自己的上下文,当代码的执行流进入函数时,函数的上下文被推到一个上下文栈上,在函数执行完后,上下文栈会弹出函数上下文,将控制权返还给之前的执行上下文;
上下文中的代码的执行的时候,会创建变量对象的一个作用域链,这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。如果上下文是函数,则其活动对象用作变量对象。活动对象最终只有一个定义变量:arguments(全局上下文中无此变量)。作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文,以此类推直至全局上下文;全局上下文的变量对象始终是作用域链的最后一个对象。
代码执行的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符(如果没有找到标识符,最终会报错);
示例如下:
var color = 'red';
function changeCol(){
if(color === 'red'){
color = 'yellow';
}else{
color = 'red';
}
}
changeCol();
函数changeCol()的作用域包含来两个对象,一个是它自己的变量对象,另一个是全局上下文的变量对象,这个函数内部之所以能够访问变量color,就是因为可以在作用域链中找到它;
局部作用域中定义的变量可用于在局部上下文中替换全局变量:示例如下:
var color = 'red';
function changeCol(){
let otherColor = 'blue';
function swapColor(){
let tempColor = otherColor;
otherColor = color;
color = tempColor;
console.log(color,otherColor,tempColor)// 'blue','red','blue'
}
console.log(color,otherColor)// 'red','blue'
console.log(tempColor)//报错
//只能访问color,otherColor,不能访问 tempColor
swapColor();
}
console.log(color)//'red'
changeCol();
以上代码涉及3个上下文,全局上下文,changeCol()的局部上下文,swapColor()的局部上下文。全局上下文中有一个变量color和一个函数changeCol()。
changeCol()的局部上下文有一个变量othercolor和一个函数swapColor(),这里可以访问全局上下文中的变量color。
swapColor()的局部上下文中有一个变量tempColor,只能在这个上下文中访问到,全局上下文和changeColor()的局部上下文都无法访问到tempColor.而在swapColor()中则可访问另外两个上下文中的变量,因为他们都是父上下文。上面代码作用域链如下图:(图片来自 JavaScript高级程序设计)
内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文中的任何东西,上下文中的链接是线性的,有序的。每个上下文都可以到上一级上下文中无搜索变量和函数,但任何上下文都不能到下一级上下文中去搜索。
swapColors()局部上下文有3个变量对象:swapColors()变量对象,changeColor()变量对象,全局变量对象。swapColors()上下文首先从自己的变量对象开始搜索变量和函数,搜不到就去搜索上一级变量对象。changeColor()上下文的作用域链中只有2个对象:它自己的变量对象和全局变量对象,因此不能访问swapColors()的上下文。
(函数参数被认为是当前上下文中的变量,因此也跟上下文中的其他变量遵循相同的访问规则)
二,作用域链增强:
某些语句会导致在作用域链前端临时添加一个上下文(一个变量对象),这个上下文在代码执行后会被删除,通常在两种情况下会出现这种现象:
try/catch语句的catch块
with语句
对with语句来说,会向作用域链前端添加指定的对象,对catch语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。示例如下:
function eventUrl(){
let qs = '?debug=true';
with(location){
let url = href + qs;
}
return url;
}
eventUrl();
with语句将location对象作为上下文,因此location会被添加到作用域链前端,eventUrl()函数中定义了一个变量qs,当with语句中的代码引用变量href时,实际上引用的是location.href。也就是自己变量对象的属性。在引用qs时,引用的则是定义在eventUrl()中的那个变量,它定义在函数上下文的变量对象上,这里使用let声明的变量url,因为被限制在块级作用域,在with块之外无定义。
三,标识符查找:
在特定上下文中为读取或写入而引用一个标识符时,必须通过搜索确定这个标识符表示什么,搜索开始于作用域链前端,以给定的名称搜索对应的标识符,如果在局部上下文中找到该标识符,则搜索停止,变量确定;如果没有找到变量名,则继续沿作用域链搜索。(作用域链中的对象也有一个原型链,因此搜索可能涉及每个对象的原型链)这个过程一直持续到搜索至全局上下文的变量对象。如果仍然未找到标识符,则说明其未声明。
示例如下:
var color = 'red';
function getColor(){
return color;
}
console.log(getColor())//'red'
调用函数getColor()时会引用变量color,为确定color的值会进行两步搜索,第一步,搜索getColor()的变量对象,查找名为color的标识符。没找到,继续搜索下一个变量对象(来自全局上下文),然后就找到了名为color的标识符。因为全局变量对象上有color的定义,所以搜索结束。
如果上下文中有一个同名的标识符,就不能在该上下文中引用父上下文中的同名标识符,如下示例:
var color = 'red';
function getColor(){
let color = 'blue';
return color;
}
console.log(getColor())//'blue'
使用块级作用域声明并不会改变搜索流程,但可以给词法层级添加额外的层次,示例如下:
var color = 'red';
function getColor(){
let color = 'blue';
{
let color = 'green';
return color;
}
}
console.log(getColor())//'green'
getColor()内部声明了一个名为color的局部变量。在调用这个函数时,变量会被声明,在之子那个到函数返回语句时,代码引用了变量color,于是开始在局部上下文中搜索这个标识符,结果找了值为‘green’的变量color。变量找到,搜索停止。在局部变量color声明之后的任何代码都无法访问全局变量color,除非使用完全限定的写法window.color。