执行上下文与作用域
简介:
执行上下文的概念在JS中是颇为重要的.变量或函数的上下文决定了它们可以访问那些数据,以及它们的行为.每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上.虽然无法通过代码访问变量的对象,但后台处理数据会用到它.
全局上下文:
全局上下文是最外层的上下文.根据ES实现的宿主环境,表示全局上下文的对象可能不一样.在浏览器中,全局上下文就是我们常说的window对象,因此所有通过var定义的全局变量和函数都会成为window对象的属性和方法.使用let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的.上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器).
函数上下文:
每个函数调用都有自己的上下文.当代码执行流进入函数时,函数的上下文被推到一个上下文栈上.在函数执行完之后,上下文栈会弹出该函数的上下文,将控制权返还给之前的执行上下文.ES程序的执行流就是通过这个上下文栈进行控制的.
作用域链:
上下文中的代码在执行的时候,会创建变量对象的一个作用域链.这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序.代码正在执行的上下文的变量对象始终位于作用域链的最前端.如果上下文是函数,则其活动对象用作变量对象.活动对象最初只有一个定义变量:arguments.(全局上下文中没有这个变量).作用域链中的下一个变量对象来自包含上下文,再下一个对象来自下一个包含上下文.以此类推直至全局上下文;全局上下文的变量对象始终是作用域链的最后一个变量对象.
代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的.搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符.
eg:
var color = 'blue';
function changeColor(){
if(color === 'blue'){
color = 'red';
}
else{
color = 'blue';
}
}
对这个例子而言,函数changeColor()的作用域链包含两个对象:一个是它自己的变量对象(就是定义arguments对象的那个),另一个是全局上下文的变量对象.这个函数内部之所以能够访问变量color,就是因为可以在作用域链中找到它.
此外,局部作用域中定义的变量可用于在局部上下文中替换全局变量.
var color = 'blue';
function changeColor(){
let anotherColor = 'red';
function swapColors(){
let tempColor = anotherColor;
anotherColor = color;
color = tempColor;
//这里可以访问color,anotherColor和tempCOlor,
}
//这里可以访问color和anotherColor 和 tempColor
swapColors();
}
//这里只能访问color
changeColor();
以上代码设计3个上下文:全局上下文,changeColor()的局部上下文和swapColor()的局部上下文.全局上下文中有一个color和一个函数changeColor(),但在这里访问全局上下文中的变量color. swapColor()的局部上下文都无法访问到tempColor.而在swapColor()中则可以访问另外两个上下文中的变量,因为它们都是父上下文.
由此可以总结: 内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文中的任何东西.上下文之间的连接是线性的,有序的.每个上下文都可以到上一级上下文中去搜索变量和函数,但任何上下文都不能到下一级上下文中去搜索.
Warning: 函数参数被认为是当前上下文的变量,因此也跟上下文中的其他变量遵循相同的访问规则.
作用域链增强:
虽然执行上下文主要有全局上下文和函数上下文两种,但有其他方式来增强作用域链.某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除.通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况时:
try/catch语块的catch块
with语句
这两种情况,都会在作用域链前端添加一个变量对象.对with语句来说,会向作用域链前端添加指定的对象;对catch语句而言,则会添加一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明.
eg:
function buildUrl(){
let qs = '?debug=true';
with(location){
let url = href + qs;
}
return url;
}
这里,with语句将locaton语句作为上下文,因此location会被添加到作用域最前端.buildUrl()函数中定义了一个变量qs.当with语句中的代码引用变量href时,实际上引用的是location.href,也就是自己变量对象的属性.在引用qs时,引用的规则则是定义在buildUrl()中的那个变量,它定义在函数上下文的变量对象.而在with语句中使用var声明的变量url会成为函数上下文的一部分,所以在with块之外没有定义.
变量声明
在使用var声明变量时,变量会被自动添加到最接近的上下文.在函数中,最接近的上下文就是函数的局部上下文.在with语句中,最接近的上下文也是函数上下文.如果变量未经声明就初始化了,那么它就会自动被添加到全局上下文.
function add(num1,num2){
var sum = num1 + num2;
return sum;
}
let res = add(10,20);//30
console.log(sum);//报错
如果省略上面例子中的关键字var,那么sum在add()被调用之后就变成可以访问的了.
function add(num1,num2){
sum = num1 + num2;
return sum;
}
let res = add(10,20);//30
console.log(sum);//30
变量提升
var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前,这个现象叫做"提升",提升让同一作用域中的代码不必考虑变量是否已近声明就可以直接使用.可是在实践中,提升也会导致合法却奇怪的现象,即在变量声明之前使用变量.
var name = 'Jake';
//等价于
name = 'Jake';
var name;
function fn1(){
var name = 'Jake';
}
//等价于
function fn2(){
name = 'Jake';
var name;
}
2.使用let的块级作用域声明
块级作用域: 块级作用域由最近的一对包含花括号{}界定. 换句话说,if块,while块,function块,甚至连单独的块也是let声明的作用域.
let 与 var 的另一个不同之处是在同一个作用域内不能被声明两次.
let 的行为非常适合在循环中声明迭代便量:
for(var i=0;i<10;++i){
}
console.log(i);//10
for(let j=0;j<10;++j){
}
console.log(j);//j未定义
3.使用const常量声明
除了let ,ES6还新增了const关键字.使用const声明的变量必须同时初始化为某个值.一经声明,在其声明周期的任何时候都不能再重新赋予新值.
const 声明只应用到顶级原语或者对象.换句话说,赋值为const变量不能再被重新赋值为其他引用值.但对象的键则不受限制.