likes
comments
collection
share

前端之路-了解什么是闭包理解闭包就像LOL点加了一个技能,对我们帮助很大,理解闭包对于深入掌握 JavaScript 语

作者站长头像
站长
· 阅读数 49

在开始介绍闭包之前,先给大家看一段代码,现在看不明白不要紧,在看完我对闭包的介绍后,你肯定就能看懂他 var arr = [];

for (var i = 0; i < 10; i++) {
    arr[i] = function () {
        console.log(i);
    }
}
arr.forEach(function (item) {
    item()
})

在了解什么是闭包之前我们需要先了解这两个概念 作用域链 和 词法作用域

🐟作用域链

  1. 函数在执行前预编译,会创建执行上下文对象
  2. 变量环境中有一个内置的outer属性用于指明的外层作用域是谁
  3. outer的指向是根据词法作用域来的

js引擎查找函数变量先会在函数中查找,找不到就会根据outer的指向去到外层作用域去查找,层层往上,这种查找的关系链就称为作用域链

🐟 词法作用域

一个域所处的环境,是由函数声明的位置来决定的

我们来看一个例子

    var myname = 'tom'
    let test1 = 100
    if (1) {
        let myname = 'jerry'
        console.log(test, myname);
    }
}

function foo() {
    var myname = '彭于晏'
    let test = 2
    {
        let test = 3
        bar()
    }
}
var myname = '晟哥'
let test = 1;
foo()
前端之路-了解什么是闭包理解闭包就像LOL点加了一个技能,对我们帮助很大,理解闭包对于深入掌握 JavaScript 语

因为foo函数和bar函数都定义在全局变量,所以他们的outer都指向全局变量.foo() 函数执行,因为bar()foo()内,所以bar()也会执行,因为bar()内没有test这个变量,他就会去outer指向的外层作用域去查找,他声明在了全局中,所以他会去全局中去找test,最后找到了test=1,所以最后输出的是1,jerry

通过这个例子,我们现在对这两个概念应该有了初步的了解,outer并不是从上往下依次指向的,而是应该根据词法作用域的的位置来决定,词法作用域就是函数声明的地方(记住并不是调用的地方)。

接下来我们通过这两个概念去了解闭包到底是个啥?

前端之路-了解什么是闭包理解闭包就像LOL点加了一个技能,对我们帮助很大,理解闭包对于深入掌握 JavaScript 语

大家先看下面这段代码,分析一下他们会输出什么?

function foo() {
var name = '大仙'
function bar() {
    console.log(count, age);

}
var count = 1
var age = 18
return bar
}
var age = 20
const baz = foo()
baz()

首先我们需要去写一个调用栈去分析出他们的全局上下文

前端之路-了解什么是闭包理解闭包就像LOL点加了一个技能,对我们帮助很大,理解闭包对于深入掌握 JavaScript 语

全局中定义了age,foo()=fun,还有此法环境的baz=func,然后去执行foo(),写出foo()中声明的变量 他的outer指向全局,因为他声明在全局,foo()执行完会立即销毁。然后是全局调用baz()的执行,baz其实就是bar(),只是不同的名字,函数体一样。baz()指向的是foo(),但是foo()已经被销毁了 所以我们是不是无法输出count,age了,应该输出的是报错 ,但是结果却和我们分析的大相径庭

前端之路-了解什么是闭包理解闭包就像LOL点加了一个技能,对我们帮助很大,理解闭包对于深入掌握 JavaScript 语

为啥不仅没有报错,还输出了在已经销毁了的foo()中所声明的变量,首先我们需要知道一个函数执行完毕后,他的执行上下文一定会被销毁,我们可以想象成每当一个函数执行完了,就会有一个清洁工大爷去清理调用栈的执行上下文,以免占地方.可是当foo执行完时,大爷过去清洁它的时候,大爷问他你是不是已经执行完毕了,我要把你清理出调用栈了,但是这个时候foo却回答我也不知道我是否已经执行完了,因为foo说他还有个逆子,虽然是foo生的,但是确却家出走,不在foo体内调用,所以foo并不知道bar是否已经被调用了,而且儿子(bar)在走的时候还说了他会回来的,要用一个背包去留着他房间的床(count)和桌子(age),出于人道主义,清洁工大爷把其他的都销毁了,但是留下了床和桌子。

前端之路-了解什么是闭包理解闭包就像LOL点加了一个技能,对我们帮助很大,理解闭包对于深入掌握 JavaScript 语

红线是儿子(bar)的查找规则,先是此法环境再是调用环境然后是outer指向,指向了上文的背包,我们把这个背包就称为闭包,即函数销毁之后留下的小背包.

下面还有一个例子帮助你理解闭包

function foo() {
   var a = 1
   var b = 2
   function bar() {
       console.log(a);
   }
   bar()
   }
   foo()

前端之路-了解什么是闭包理解闭包就像LOL点加了一个技能,对我们帮助很大,理解闭包对于深入掌握 JavaScript 语

在这段代码中,bar仍然声明在foo中但是不同的是他没有在foo中返回即return,所以foo()要执行完要等bar()执行完才行所以在bar指向的时候,foo()并不会被销毁,bar可以去访问foo声明的变量 a 对比这两个例子我们可以了解闭包是怎么形成的,就是当一个函数定义在一个函数体内,又不在他的体内调用,在外部调用,这就是第一个例子.这是闭包的形成条件但是什么是闭包呢?

🐟闭包

根据js词法作用域的规则,内部函数总是能访问外部函数的变量,当通过调用一个外部函数返回的一个内部函数后,即使外部函数已经执行完了,但是内部函数引用了外部函数中的变量也依旧需要保存在内存中,我们把这些变量的集合就是闭包.

在这里有个坑,外部函数返回并不是说只有return才是,还有其他的方式,请看下述代码

function foo() {
    var a = 1
    var b = 2
    function bar() {
        console.log(a);
    }
    window.fn = bar
}
foo()
window.fn()

这样也会形成闭包,并不是只有return这个方法,只要你能拿的出去.

🐟闭包的作用

还是老规矩我们先看一段代码

     function add() {
        var count = 0
            count++
            return count
   
    }
    console.log(add());
    console.log(add());
    console.log(add());

你觉得会输出什么?是 1 2 3 吗,并不是,答案是1 1 1.这是为什么呢,因为每次调用bar都会把count初始化为0,我们再改良一下 将var count=0提出去

var count = 0
function add(){
            
             count++
             return count
        }
            console.log(add());
            console.log(add());
            console.log(add());

这样会输出 1 2 3,但是这样声明了一个全局变量,非常不利于开发,会让代码后期的维护很难.因为开发中不只是你一个人在开发,是大家在一起开发,全局变量很容易影响别人的代码.

再改良一下

function add() {
    var count = 0

    function foo() {
        count++
        return count
    }
    return foo
}
var bar = add()
console.log(bar());
console.log(bar());
console.log(bar());

在每次调用bar时,bar都会去找到bar下一级(foo)背包(闭包)中的count,这样每次foo中的count都会++ 这样输出的也是 1 2 3

这个时候我们就能初步明白闭包的作用

1.实现共有变量(企业的模块开发)

2.做缓存

3.封装模块,防止全局变量污染

虽然闭包好用。但是不能过度使用,他也有缺点,不能闭着眼睛乱用闭包.他是一把双刃剑

调用栈的可用空间会被闭包占用,可用栈空间变小 所以内存泄漏

现在我们在对开头的代码进行分析

var arr = [];

for (var i = 0; i < 10; i++) {
    arr[i] = function () {
        console.log(i);
    }
}
arr.forEach(function (item) {
    item()
})

运行代码 输出的全是10

前端之路-了解什么是闭包理解闭包就像LOL点加了一个技能,对我们帮助很大,理解闭包对于深入掌握 JavaScript 语

我们怎么改这段代码可以让他输出 0~9呢,我们可以使用闭包去实现

var arr = [];

for (var i = 0; i < 10; i++) {
function foo(){
    var j =i
    arr[j] = function () {
        console.log(j);
    }
    }
}
arr.forEach(function (item) {
    item()
})

foo每执行一次都会创建一次执行上下文,循环10次,会创建10个foo的执行上下文,他们在调用栈都不能被销毁,调用栈留下了10个闭包

前端之路-了解什么是闭包理解闭包就像LOL点加了一个技能,对我们帮助很大,理解闭包对于深入掌握 JavaScript 语 我们再让这段代码优雅一点,使用立即执行函数

var arr = [];
for (var i = 0; i < 10; i++) {
     (function foo() {
        var j = i
        arr[j] = function () {
            console.log(j);
        }
    })()
}

立即执行函数:(function() { /* 函数体 */ })();

在 JavaScript 中,立即执行函数是一种非常有用的技术,可以帮助你更好地组织和管理代码。

还有一种更优雅的写法

var arr = [];
for (var i = 0; i < 10; i++) {
    ! function foo(j) {
        // var j = i
        arr[j] = function () {
            console.log(j);

        }
    }(i)
}

i作为实参传进去,j作为形参接受数值.

Ending

看到这里相信你已经对闭包也算初步了解了,如有看不懂地方可以在评论区提问,大家一起相互交流👀

转载自:https://juejin.cn/post/7422154695743389708
评论
请登录