JS 预编译、变量提升、作用域、闭包
一、预编译和变量提升
JavaScript是一门解释型语言,浏览器运行JS代码时,为逐行执行。而在运行代码之前会进行JS的运行三部曲:
①语法分析:扫描语法错误,如果有则无法运行直接报错。
②预编译:解析JS代码,变量提升
③解释代码并执行
那么预编译过程中的变量提升又是什么意思呢?
变量提升包含一般变量提升(声明提升、赋值提升)和函数整体提升。
1. 函数整体提升
看下面这个例子,我们在声明之前调用了 a()函数,仍然会输出 1,这就是由于预编译时造成的函数整体提升。
a()
function a() {
console.log(1) // 1
}
// 在预编译完成后,函数整体提升到调用之前,实际代码其实是下面这样
function a() {
console.log(1) // 1
}
a()
2. 变量提升
// 情况1
var a = 111
console.log(a) // 这里毫无疑问会输出 111
// 情况2
console.log(b) // 如果我们先打印,再声明,这里会输出什么呢? undefined
var b = 222
// 情况3
console.log(c) // 如果我们啥也不干,直接输出,这里又会输出什么呢? 其实这里会报错:c is not defined
上述案例中,情况2和情况3在打印之前,变量均未定义,为什么一个是 undefined 一个是报错呢?在预编译时,情况2的运行顺序实际上会改为以下这样:
var b;
console.log(b) // undefined
b = 222
这里变量b实际上进行了变量的声明提升,并未进行赋值提升。
3. 变量提升与函数提升的优先级
这里我们直接给出结论:函数先被提升,变量后被提升。
console.log(fn)
function fn () {
console.log(1)
}
var fn = 2
console.log(fn)
// 以上代码在预编译后,运行顺序实际如下:
function fn () {
console.log(1)
}
var fn
console.log(fn) // function fn () {console.log(1)}
fn = 2
console.log(fn) // 2
请注意,我们在第一次 console.log(fn) 之前,声明了两次 fn ,但 fn 输出的仍是 fn() ,而不是上面的 undefined,这就说明:函数提升优先级比变量提升要高,且不会被变量声明所覆盖,但是会被变量赋值所覆盖。
4. 例题练习
例题1:
function test() {
return a
a = 1
function a() {}
var a = 2
}
console.log(test()) // function a() {}
// 实际顺序
function a() {}
var a
return a
a = 1
a = 2
例题2:
console.log(test()) // 2
function test() {
a = 1
function a() {}
var a = 2
return a
}
// 实际顺序与例题1一致,只是return时,a的值不一样
function a() {}
var a
a = 1
a = 2
return a
二、作用域
作用域是可访问变量的集合。
// -全局作用域 window
let a1 = 1
function foo2() {
// --foo2函数作用域
let a2 = 2
console.log(a3) // 报错:a3 is not undefined
function foo3() {
// ---foo3函数作用域
let a3 = 3
console.log(a1) // 1
}
foo3
foo2()
在foo3函数中打印a1变量,这时foo3作用域中未定义a1,于是会往父级作用域foo2中查找a1,而父级foo2中也未定义a1,这时会往全局作用域window中查找,这就是所谓的作用域链。
由上例可知,子作用域可以沿着作用域链,访问父作用域中的变量,而父作用域无法访问子作用域中的变量。
在ES6中,新增的关键字 let、const 与传统的 var 就有作用域上的差异。
if (true) {
let a4 = 4
var a5 = 5 // 这里如果使用 var 关键字定义,便不具有块级作用域
}
console.log(a4) // 报错:a4 is not undefined
console.log(a5) // 5
变量无法跨作用域提升。
console.log(a) // 根据变量提升规则,这里打印a是没有问题的,那么我们在 a 中定义的 b函数能否也提升呢?
console.log(b) // 报错
function a() {
function b() {
}
}
console.log(b) // 报错
三、闭包
什么是闭包?
一个函数和它周围状态的引用捆绑在一起的组合。
当内部函数被返回到外部并保存时,一定会产生闭包,闭包会产生原来的作用域链不释放,过度的闭包可能会导致内存泄漏,或加载过慢。
1. 函数作为返回值
function foo() {
let a = 1
let test = function() {
console.log(a)
}
return test
}
let fn = foo()
let a = 2
fn() // 1
在以上案例中,fn() 在执行时并未在它所在的作用域寻找 a = 2,而是在 foo() 返回的函数做定义的位置所在的作用域中寻找 a。
2. 函数作为参数
function test(fn) {
let a = 1
fn()
}
let a = 2
function fn() {
console.log(a)
}
// 当 fn 作为 test 函数的参数被执行时
test(fn) // 2
在上述案例中,fn() 并未在test()函数执行fn的时候寻找a,而是在fn所定义的位置所在的所用域中寻找a。由此可见,当一个函数形成闭包时,它所定义的位置所在的作用域显得尤为重要。
3. 闭包典型案例
function test() {
var arr = []
// let i = 0
for(var i = 0; i < 5; i++) {
arr[i] = function() {
console.log(i)
}
}
return arr
}
var resArr = test()
resArr.forEach(n => {
n() // 5 5 5 5 5
})
// n() 在调用时,i 已经变成了5,因此打印出来的全是 5
// 解决方法1:使用ES6 let 关键字声明 i
// 解决方法2:使用立即执行函数
function test() {
var arr = []
for(var i = 0; i < 5; i++) {
// --------------
(function (j) {
arr[j] = function() {
console.log(j)
}
})(i)
// --------------
}
return arr
}
var resArr = test()
resArr.forEach(n => {
n() // 1 2 3 4 5
})