在《你不知道的JavaScript(上)》中提到:作用域可分为 词法作用域和 动态作用域,JavaScript在编译中使用的词法作用域。
词法作用域
在我的理解里:作用域就好比一块块区域,每个区域有着不同的事物和规则,区域里的事物需要遵守区域的规则,同时受到区域的保护,不会被区域外的事物污染。
JavaScript代码的执行分为两步:预编译和执行期,词法作用域是在预编译阶段产生的,ES5中,词法作用域分为全局作用域和函数作用域。
var a = 2
var b = 34
function foo() {
a = 3
console.log(a) //3
console.log(b) //34
}
console.log(a) //2
foo()
/* 执行过程
1. 变量声明和函数声明 => GO {
a : undefined,
b : undefined,
foo : undefined
}
2.执行 => GO{
a : 2,
b : 34,
foo : function (){...}
}
3.console.log(a) 执行,输出2
4.foo()执行,产生函数作用域AO => {
a : 3
}
5.执行console。输出 3 34
*/
- 预编译过程中,会先将变量声明和函数声明在作用域内生产并全部标志位undefined,如上代码,先预编译全局作用域GO,当在作用域GO生产所有变量名和函数名后,对变量和函数进行赋值undefined,最后GO中会有{a = undefined; b = undefined; foo = undefined…}
- 在预编译还没完成前,执行语句不会执行。预编译完成后,代码执行,同时对作用域中的变量赋值真实值。当执行console.log(a)时,是在全局中执行的,因此可以访问GO中的变量a,最后输出2
- 当foo()执行时,foo是一个函数,函数内部也是一个作用域,这时会再次生产作用域AO,并进行预编译。从函数内部可以看到 a = 3 前面并没有var关键字,但是引擎会默认帮a作变量声明,在AO中生成一个变量a并赋值,但是GO和AO作用域的变量a不是同一个变量,它们是互不干扰的
- 当console.log(a)和console.log(b)执行时,需要访问的a和b都是变量,引擎会采用就近原则,先访问最近的作用域,也就是AO,AO中有变量a,所以直接访问AO中的变量a,输出3;但是AO中没有变量b,就会逐层往外查找,若最后没找到,这时会报错;如上代码中,GO中有变量b,所以访问GO中的变量b,输出34;
块级作用域
在ES5中,只有全局作用域和函数作用域,上一点中的GO就表示全局作用域,AO就表示函数作用域;而在ES6发布后,新增了一个概念:块级作用域;块级作用域一般是在{…}内部,它规定变量只作用于{…}内部
{
let a = 8;
console.log(a) //8
}
//这是一个块级作用域
console.log(a) //报错:ReferenceError: a is not defined
其实在ES6之前,也有类似于块作用域的概念,with和try…catch能产生块级作用域,但不建议使用,会影响性能,ES6引入了块作用域,它们的使用就更不必要了
var、let和const
- ES6引入了let和const,let和const会自动生产块级作用域,并将声明的变量绑定在块级作用域中,一般在{…}内部
- let常被用于声明变量,const常备用于声明常量,当用const声明的常量在后面执行过程中值被修改时,会报错。(不过,当修改的常量的地址不变时,不会报错,比如:修改对象中的属性的属性值)
- var会造成变量提升,不会自动产生块级作用域
//代码一
if(true) {
var a = 12
}
console.log(a) //输出什么??
console.log(b) //输出什么??
var b = 34
//代码二
if(true) {
let a = 12
}
console.log(a) //输出什么??
console.log(b) //输出什么??
let b = 34
- 代码一输出:12; undefined,而代码二输出:ReferenceError: a is not defined,第二句console不执行,若注释第一句,第二句执行同样会报错:ReferenceError: b is not defined。
- 代码一:预编译时,b会变量提升至console前并赋值undefined,if语句中的a也会被声明在全局作用域中,所以console.log(b)时会输出:undefined;console.log(a)会输出:12
- 代码二:let声明的变量会被绑定在块级作用域中,也不会变量提升。if中的a用let声明,会将a变量绑定在if后面的{…}中,只作用于其中,外部不能访问;用let声明的b不会变量提升,先访问再声明时会报错
变量提升
- var声明的变量会提升至执行语句之前,如:
console.log(a) // 输出:undefined
var a = 89
/* 预编译时,引擎解析:
1.var a
2.console.log(a)
3.a = 89
*/
- 不声明的变量不会变量提升,如:
console.log(a) // 输出:ReferenceError: a is not defined
a = 89
/* 预编译时,引擎解析:
1.console.log(a)
2.a = 89 ==> var a; a = 89
*/
- 里作用域内变量不声明,变量会提升至全局
function fn() {
a = 78
}
fn()
console.log(a) // 输出:78
/* 预编译时,引擎解析:
1.fn()执行
2.检测到a没var且全局没声明a
3.在全局:var a; a = 78
4.console.log(a)执行
*/
- 函数声明也会提升,当函数声明和变量声明都存在时,函数声明优先提升,如:
foo() //输出:1
var foo
function foo() {
console.log(1)
}
foo = function (){
console.log(2)
}
/* 编译时,引擎解析:
1.function foo()
2.foo()
3.foo = function()
*/
作用域链
在嵌套作用域中,查找变量时,引擎会像冒泡事件那样,从自身作用域向外逐层逐层查找,直到查到找全局作用域中,好像一条链子般。
let a = 1;
//...
function fn1() {
let b =2;
//...
function fn2() {
let c = 3
//...
}
}
// GO -> AO1 -> AO2......
闭包
什么是闭包?当函数在声明的词法作用域之外被调用时,被称为发生了闭包。我们在日常的代码书写中,处处都是闭包,只要使用了callback函数,都是在使用了闭包:定时器、事件监听器、Ajax请求、异步等等。
//典型例子
for(var i = 0; i < 5; i ++) {
setTimeout(function timer() {
console.log(i)
}, 1000)
}
原本想逐个输出0,1,2,3,4的,结果输出5个5,为什么呢?setTimeout里的延迟函数会在循环结束时才执行,访问的是全局的变量i,可此时i已经变成了5,console.log(i)会输出5
解决(给每个迭代都绑定一个新的作用域):
//let声明变量i
for(let i = 0; i < 5; i ++) {
setTimeout(function timer() {
console.log(i)
}, 1000)
}
//立即执行函数传参
for(var i = 0; i < 5; i ++) {
(function foo(i){
setTimeout(function timer() {
console.log(i)
}, 1000)
})(i)
}
以上能正常逐个输出:0,1,2,3,4。原因是:let和立即执行函数使setTimeout被包裹在块级作用域中,每次循环都产生一个对应变量i的块级作用域,setTimeout执行时,里面的函数访问对应块级作用域中的变量i
立即执行函数(IIFE)不是闭包,原因:函数并不是在它本身的词法作用域以外执行的