一、作用域(Scope)
1. 作用域是什么
作用域是当前的执行上下文,值和表达式在其中“可见”或可被访问。如果一个变量或表达式不在当前的作用域中,那么它是不可用的。作用域也可以堆叠成层次结构,子作用域可以访问父作用域,反过来则不行。
2. 作用域分类
作用域有两种工作模型:动态作用域、静态作用域(又称词法作用域),JavaScript采用的是静态作用域。
在JavaScript中可以分为三种作用域,分别是全局作用域、函数作用域、块级作用域(ES6新增)。
-
全局作用域:是指在整个代码中定义的变量和函数,它们可以在整个代码中访问。
- 定义在最外层的变量或函数,或者说全局上下文中存储的变量或函数拥有全局作用域
- 所有未声明的变量自动声明为拥有全局作用域的变量
-
函数作用域:函数作用域也被称为局部作用域,每个函数都有自己的函数作用域,在函数内部定义的变量和函数只能在该函数内部访问。当函数执行完毕时,这些变量和函数的作用域将被销毁。使用
var关键字声明的变量具有函数作用域。 -
块级作用域:指代码中使用
{ }包裹起来的区域,如if、for、while、匿名块等,使用let和const关键字生命的变量具有块级作用域。
var a = 1;
function fn() {
b = 2;
var c = 3;
return function(){
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
}
}
{
var d = 4
const e = 5
}
var func = fn();
func();
console.log(a); // 1
console.log(b); // 2
console.log(c); // c is not defined
console.log(d); // 4
console.log(e); // e is not defined
- 全局作用域:变量
a、b、d,函数fn、func - 函数作用域:变量
c(函数外部访问不到,所以console.log(c)报错c is not defined) - 块级作用域:变量
e
3. 暂时性死区
暂时性死区的本质:只要进入当前作用域,使用let和const声明的变量就已经存在,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
另外只要作用域内存在let命令,它所声明的变量就“绑定”这个块级区域,不再受外部的影响。
ES6明确规定,如果区块中存在let和const命令,则这个区块对这些命令声明的变量从一开始就形成封闭作用域,只要在声明之前使用这些变量,就会报错。
在代码块内,使用let命令声明变量之前,该变量都是不可用的,这在语法上成为“暂时性死区”(temporal dead zone,简称TDZ)
var a = 1
if(a){
a = 2 // ReferenceError
let a
}
在if的块级作用域中,变量a使用let命令声明,所以在声明之前都属于a的“死区”。a = 2 这行代码则会报错Uncaught ReferenceError: Cannot access 'a' before initialization
二、作用域链
1. 作用域链是什么
创建执行上下文的同时也会创建作用域链,它是由当前上下文和所有父级上下文的变量对象共同形成的链表。
根据查找规则,JavaScript 执行查找变量会从作用域链顶端开始依次查找对应变量**,** 如果找不到就会沿着作用域链一层层向外查找。假定在一个函数内访问一个变量,如果在当前函数内没有找到该变量会去父级作用域查找,父级作用域没有再去父级作用域的父级作用域,一直找到全局作用域,如果依旧没有找到才会抛出x is undefind 错误。
在bar函数体中使用了变量a、b,但在bar函数作用域中并没有找到这些变量,于是它就会到其父级foo函数作用域中继续查找,找到了变量b,变量a还没有找到。继续查找,到foo函数作用域的父级作用域中查找,找到变量a,至此查找结束。
-
当代码开始执行时就会创建全局执行上下文环境,此时在作用域中的全局
(Global)中能找到全局对象window,在脚本(Script)中能还看到变量a和func,但是二者是用let声明的,也就是产生了上面提到的暂时性死区,此时若是获取和使用a或func则会报错。 -
代码在执行完
foo函数后,可以作用域中的本地(Local)中看到foo函数作用域内有变量b和函数bar。 -
bar函数中需要用到变量a和b,a是最外层变量,foo函数产生了闭包,所以foo函数里面的bar函数可以取到函数外层的b和模块最外层变量a,要注意而这里a并不是全局变量,是当前模块文件的最外层变量,和全局变量不同,它只能被模块文件内的代码访问。