前端面试基础-闭包

144 阅读4分钟

前言

闭包已经是老生常谈了,面试中也经常遇到,一个JS基础扎实的前端必须掌握闭包
记录前端闭包的学习
学习已完成

  • 1.作用域
  • 2.作用域本质
  • 3.闭包

1.作用域

作用域就是变量的可用范围,作用域的目的是防止不同范围的变量之间相互干扰

全局作用域与局部作用域

var a = 1; // 全局作用域 全局变量
function fn(){
    var b = 2; // 局部作用域 局部变量
}

1)全局作用域

全局作用域可反复使用保存在 window,但会全局污染

2)局部作用域

局部变量不会被污染,但却无法反复使用
函数作用域:保存在函数内的范围成为函数作用域,里面的变量称为局部变量
局部变量有两种

  • 直接在function{}内定义声明的
  • 形参变量
function fn(n) {
    n = 1 // 局部变量
    var a = 2; // 局部变量
}

只有function的{}形成作用域才有局部变量,对象的{}与对象中的属性 均不是局部变量,都阻挡不了内部的变量超出{}的范围

// 局部变量 a/n
function fn(n) {
    n = 1 // 局部变量
    var a = 1; // 局部变量
}
fn()
console.log('a: ', a); // a is not defined 变量c只存在于局部,所以error

// 非局部变量 b/c/d
{
    var b = 2
}
console.log("b", b); // b=2 说明{} 并不能锁住局部作用域

if (false) {
    var c = 3
}
console.log('c: ', c); // c=undefined 因为c被提到全局了,在全局没有赋值,所以c为undefined
var arr = [1]
for (var i = 0; i < arr.length; i++) {
    var d = 5
}
console.log("d", d) // d=5  说明for循环{} 并不能锁住局部作用域,其他循环亦是如此

2.作用域本质

全局作用域是一个名为window的对象,所有全局变量和全局函数都是window对象的成员

函数作用域其实是JS引擎在调用函数时才临时创建的一个作用域对象。其中保存函数的局部变量。当函数调用完,函数作用域对象就会被释放了。

JS中函数作用域对象,还有个别名——活动的对象(Actived Object)简称AO。所以,局部变量不可重用。

3.闭包

闭包一种既重用变量又保护变量不被污染的一种编程方法。
使用场景:希望保存一个可反复使用又不被外界污染的专属局部变量时,用闭包 闭包有3个特性:

①函数嵌套函数

②函数内部可以引用函数外部的参数和变量

③参数和变量不会被垃圾回收机制回收

1)使用闭包的步骤举例

// 写一个花钱的函数
// 第1步: 用外层函数包裹要保护的变量和内层函数
function bag() {
    var total = 1000;
    //第2步: 返回内层函数对象
    return function(money) {
        total -= money
        console.log(`花了${money}还剩${total}元`)
    }
}
//第3步: 调用外层函数,用变量接住内层函数对象
var pay = bag(); // pay接住的即是bag()返回出来的内层函数对象
pay(100); // 900
total = 0; // 无法修改局部变量
pay(100); // 800

2)本质

闭包的本质就是外层函数调用后,外层函数的作用域对象,被返回的内层函数的作用域链引用着无法释放就形成了闭包对象

作用域链:每个函数在定义时,就已经规划好了自己专属的一个查找变量的路线图,称为作用域链。函数会从自身的局部作用域一直往上走查找要用的变量,一直到全局作用域,一个函数可用的所有作用域串联起来,就行成了当前函数的作用域链。

步骤1)中的例子就形成了作用域链,分别为内层函数自己的作用域对象 -> 外层函数的作用域对象 -> 全局作用域对象

3)再举例定时器问题

for(var i = 0; i < 3; i++) {
    (function(a){
        setTimeout(() => {
            console.log(a)
        }, a * 1000)
    })(i)
}

使用 IIFE(立即执行函数)每一轮循环生成一个新的函数作用域,令setTimeout回调可以将新的作用域封闭在每一轮循环内部,每一轮循环内部都会保存一个外部的i变量进行访问。
整个立即执行函数就是闭包,立即执行函数执行结束时,变量a被定时器setTimeout一直引用着无法释放。

4)再举例过滤一个数组

// 过滤一个数组取得 5-10之间的数字
var arr = [1, 5, 10, 15, 20]
// 直接写法
var arrNew = arr.filter(function (z) {
    return 5 <= z && 10 >= z
})
console.log('arrNew: ', arrNew);

// 用闭包封装复用
function between(x, y) { // 外层函数的作用域对象里的x,y不断的被内层函数的作用域链引用着
    return function (z) { 
        return x <= z && y >= z // 内层函数作用域引用着外层作用域的值x,y
    }
}
console.log(arr.filter(between(5, 10))) // [5, 10]

5)闭包的缺点

无法回收闭包中引用变量,容易造成内存泄漏,所以要即时释放不用的闭包

总结

两句话概括闭包:

  • 有权访问另一个函数作用域中变量的函数。
  • 外层函数调用后,外层函数的作用域对象,被返回的内层函数的作用域链引用着无法释放就形成了闭包对象

最后

以上的方式总结只是自己学习总结,有其他方式欢迎各位大佬评论
渣渣一个,欢迎各路大神多多指正,不求赞,只求监督指正( ̄. ̄)
有关该文章经常被面试问到的可以帮忙留下言,小弟也能补充完善完善一起交流学习,感谢各位大佬(~ ̄▽ ̄)~