JS 预编译、变量提升、作用域、闭包

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
})

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值