什么是作用域
当前的执行上下文。
值和表达式在其中 "可见" 或可被访问到的上下文。
如果一个变量或者其他表达式不 "在当前的作用域中",那么它就是不可用的。
作用域也可以根据代码层次分层,以便子作用域可以访问父作用域,通常是指沿着链式的作用域链查找,而不能从父作用域引用子作用域中的变量和引用。
包含几层意思:
- 作用域是指当前代码执行的上下文;
- 作用域的特点是作用域内定义的值或表达式在作用域内有效可用;
- 不在当前作用域内的变量或表达式是不可用的;
- 作用域分层构成作用域链,作用域链上内层可以访问外层作用域,外层不能访问内层作用域,即子作用域可以访问父作用域,父作用域不能访问子作用域;
静态作用域与动态作用域
作用域又分为静态作用域和动态作用域。
- 静态作用域
静态作用域又叫词法作用域,是定义在词法阶段的作用域,也就是在词法定义阶段作用域就已经确定了。
- 动态作用域
与静态作用域相对的是动态作用域,动态作用域是指在词法定义阶段并不确定作用域,而是在调用的时候才确定作用域。
JavaScript就是静态作用域的。拿函数举例:
let str = 'global';
function fun() {
let str = 'partial';
function fun2() {
return str;
}
return fun2();
}
console.log(fun());// 'partial'
上面代码执行结果就是因为JavaScript采用的是静态作用域,在fun2创建定义的时候已经确定了作用域,当时的str是partial。
- 欺骗静态作用域
欺骗静态作用域是指修改了所在的作用域,可以使用eval()和with(),缺点是会导致性能下降。
eval()
var abc = 1;
var str = 'var abc = 2;'
function fun(s) {
eval(s);
console.log(abc + 1);
}
fun(str);
// 3
eval()函数接收一个字符串,并执行其中的JavaScript代码。
with()
function fun(obj) {
with (obj) {
a = 2;
b = 4;
}
}
var obj = {
b: 3
}
fun(obj);
console.log(obj.a);// undefined
console.log(obj.b);// 4
console.log(a);// 2
with()语句在执行时确定作用域,所以with执行时作用域为obj对象,obj对象中存在b,所以b被修改为4,obj对象中不存在a,所以被声明成了全局变量并赋值为2。
全局作用域与局部作用域
在JavaScript中有两种类型的作用域:
- 全局作用域
全局作用域就是在任何地方都可以访问到,在浏览器中也就是会挂载在window对象上,在node环境下会挂载在global对象上。
- 局部作用域
局部作用域就是在局部范围内可以访问,之外的地方无法直接访问,如函数作用域,函数内部的变量在函数外部就无法访问到。
可以通过闭包间接的在局部作用域之外访问局部作用域内部的变量。
全局变量与局部变量
-
全局作用域中声明的变量是全局变量,全局变量在全局范围内都可以访问;
-
局部作用域内部声明的变量是局部变量,局部变量只能在局部作用域内访问,如函数内部声明的变量,只能在函数内部访问;
-
如果赋值的变量尚未声明,则会自动声明为全局变量,如
global = '1'; -
在严格模式下不会自动创建全局变量;
变量提升与函数优先
先看结论:JavaScript中,函数和变量的声明总是会被悄悄地“提升”到各自作用域的最顶部。
- 变量提升
console.log(abc);// undefined
var abc = '1';
上面的代码在浏览器中不会报错,打印的是undefined。这就是因为JavaScript在ES6之前存在变量提升机制,允许变量先声明后赋值,或者说允许先使用后声明。
为什么会这样呢?
这是因为JavaScript代码运行存在两个阶段:第一个是编译阶段,第二个是执行阶段。上面的代码分阶段可以理解为:
// 编译阶段
var abc
// 执行阶段
console.log(abc) // undefined
abc = '1'
- 函数优先
函数的声明和变量的声明是类似的,都会被提升。看下面的代码执行结果:
fun();// ccc
// 第一次函数声明
function fun() {
console.log('aaa');
}
// 第一次函数表达式声明
var fun = function() {
console.log('bbb');
}
// 第二次函数声明
function fun() {
console.log('ccc');
}
fun();// bbb
- 第一行的
fun()没有报错,就是因为函数声明也被提升了; - 第一行的
fun()输出ccc是因为相同名称的函数都提升了,后面的覆盖了前面的函数; - 最后一行的
fun()输出bbb是因为函数提升后,在执行阶段同名函数被函数表达式修改了;
上面的代码可以理解为:
// 编译阶段
// 第一次函数声明
function fun() {
console.log('aaa');
}
// 第二次函数声明
function fun() {
console.log('ccc');
}
var fun
// 执行阶段
fun();// ccc
// 第一次函数表达式声明
fun = function() {
console.log('bbb');
}
fun();// bbb
块级作用域
由于变量提升和函数优先机制的存在,写出来的代码不易于理解,且可能会隐含BUG,所以ES6引入了块级作用域。
块级作用域就是在指定的代码块形成一块作用域。{}内部代码块中声明的变量在代码块{}内部可以访问,在外面无法访问。为了和var区分,块级作用域使用let和const(声明常量,声明后不可修改)关键字进行变量声明。
- 块级作用域解决变量提升;
块级作用域也存在变量提升,但是行为和var不一样,块级作用域中的变量提升或形成暂时性死区,在声明前访问会直接报错。
console.log(abc);// Uncaught ReferenceError: abc is not defined
let abc = '1';
- 不允许重复声明;
let abc = '1';
let abc = '2';// Uncaught SyntaxError: Identifier 'abc' has already been declared
- 块级作用域内有效,不影响全局;
let abc = 'global';
var cde = 'global';
if(true) {
let abc = 'partial';
var cde = 'partial';
}
console.log(abc);// global
console.log(cde);// partial
- 循环中的应用
for(var i = 0; i < 3; i ++) {
setTimeout(function() {
console.log(i);
}, 500);
}
// 3
// 3
// 3
for(let i = 0; i < 3; i ++) {
setTimeout(function() {
console.log(i);
}, 500);
}
// 0
// 1
// 2
作用域嵌套与作用域链
当一个作用域(代码块或函数)内部嵌套了另一个作用域(代码块或函数),就形成了作用域嵌套。作用域嵌套的查询规则是先当前作用域,再向外层作用域查找,直至全局作用域。这样就形成了一个查找链条,就是作用域链。
作用域链条主要由嵌套的层数决定长度,但是有些语句可以在作用链的前端临时增加一个变量对象,这个变量对象在代码执行完后移除,这样作用域链就延长了,如try...catch的catch和with。
try...catch
let abc = '1';
try {
console.log(abc + cde);
} catch (e) {
console.log(e);
}
// ReferenceError: cde is not defined
try...catch当捕获到异常执行到catch代码块时,或在作用域链最前面添加一个变量e,就是错误对象,可以通过e来访问到错误对象,这就相当于作用域链延长了。这个e在catch代码块执行完成后销毁。
with
let abc = '1';
let cde = {
abc: '2'
};
with (cde) {
console.log(abc);// 2
}
console.log(abc);// 1
with语句在上面的代码中修改了作用域链,将cde添加到作用域链的最前面了,查找变量时,优先在cde上查找,with执行执行完成后,作用域链会恢复正常状态。
需要注意with语句里面的作用域链要执行时才能确定,JavaScript引擎没办法优化,所以严格模式下禁止使用with。
总结
- 作用域是指当前代码执行的上下文,为变量和函数划定了一个有效范围;
JavaScript是静态作用域的,就是在代码定义时就确定了作用域,不同于动态作用域在调用时才确定作用域;eval()和with()可以欺骗静态作用域;JavaScript的全局作用域是全局范围有效的,在浏览器中会挂载在window对象上,在node环境下会挂载在global对象上;- 局部作用域在局部范围有效,外部无妨直接访问,可以通过闭包间接访问;
- 全局作用域定义的变量是全局变量,局部作用域定义的变量是局部变量,未声明就赋值的变量会被声明为全局变量,严格模式下不会创建全局变量;
JavaScript存在编译和执行两个阶段,变量和函数的声明都会被提升到编译阶段声明;所以可以先赋值后声明,或先使用后定义,但是这样不易于理解;ES6中引入了块级作用域,在{}代码块内有效;- 区别于
var,块级作用域使用let和const声明变量; - 块级作用域解决了变量提升问题,块级作用域内不能重复声明,块级作用域内声明的变量不会影响全局对象的属性,块级作用域解决了循环中异步调用参数不对的问题(以前使用立即执行函数解决);
- 作用域嵌套会形成作用域链,在变量查找时会先在当前作用域查找,找不到再沿着作用域链向外层查找,直到全局作用域;
try...catch的catch和with会延长作用域链,往最前面添加一个对象,执行完成后移除;
参考链接: