虽然执行上下文主要有全局上下文和函数上下文两种(eval()调用内部存在第三种上下文),但有
其他方式来增强作用域链。某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执
行后会被删除。通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况时:
try/catch 语句的catch 块
with 语句
这两种情况下,都会在作用域链前端添加一个变量对象。对with 语句来说,会向作用域链前端添
加指定的对象;对catch 语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误
对象的声明。看下面的例子:
function buildUrl() {
let qs = “?debug=true”;
with(location){
let url = href + qs;
}
return url;
}
这里,with 语句将location 对象作为上下文,因此location 会被添加到作用域链前端。
buildUrl()函数中定义了一个变量qs。当with 语句中的代码引用变量href 时,实际上引用的是
location.href,也就是自己变量对象的属性。在引用qs 时,引用的则是定义在buildUrl()中的那
个变量,它定义在函数上下文的变量对象上。而在with 语句中使用var 声明的变量url 会成为函数
上下文的一部分,可以作为函数的值被返回;但像这里使用let 声明的变量url,因为被限制在块级作
用域(稍后介绍),所以在with 块之外没有定义。
变量声明
ES6 之后,JavaScript 的变量声明经历了翻天覆地的变化。直到ECMAScript 5.1,var 都是声明变量
的唯一关键字。ES6 不仅增加了let 和const 两个关键字,而且还让这两个关键字压倒性地超越var
成为首选。
- 使用var 的函数作用域声明
在使用var 声明变量时,变量会被自动添加到最接近的上下文。在函数中,最接近的上下文就是函
数的局部上下文。在with 语句中,最接近的上下文也是函数上下文。如果变量未经声明就被初始化了,
那么它就会自动被添加到全局上下文,如下面的例子所示:
function add(num1, num2) {
var sum = num1 + num2;
return sum;
}
let result = add(10, 20); // 30
console.log(sum); // 报错:sum 在这里不是有效变量
这里,函数add()定义了一个局部变量sum,保存加法操作的结果。这个值作为函数的值被返回,
但变量sum 在函数外部是访问不到的。如果省略上面例子中的关键字var,那么sum 在add()被调用
之后就变成可以访问的了,如下所示:
function add(num1, num2) {
sum = num1 + num2;
return sum;
}
let result = add(10, 20); // 30
console.log(sum); // 30
这一次,变量sum 被用加法操作的结果初始化时并没有使用var 声明。在调用add()之后,sum
被添加到了全局上下文,在函数退出之后依然存在,从而在后面可以访问到。
var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。这个现象叫作“提升”
(hoisting)。提升让同一作用域中的代码不必考虑变量是否已经声明就可以直接使用。可是在实践中,提
升也会导致合法却奇怪的现象,即在变量声明之前使用变量。下面的例子展示了在全局作用域中两段等
价的代码:
var name = “Jake”;
// 等价于:
name = ‘Jake’;
var name;
下面是两个等价的函数:
function fn1() {
var name = ‘Jake’;
}
// 等价于:
function fn2() {
var name;
name = ‘Jake’;
}
通过在声明之前打印变量,可以验证变量会被提升。声明的提升意味着会输出undefined 而不是
Reference Error:
console.log(name); // undefined
var name = ‘Jake’;
function() {
console.log(name); // undefined
var name = ‘Jake’;
}