Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

闭包 #5

Closed
func-star opened this issue Jun 26, 2018 · 0 comments
Closed

闭包 #5

func-star opened this issue Jun 26, 2018 · 0 comments

Comments

@func-star
Copy link
Owner

func-star commented Jun 26, 2018

闭包一直以一种神秘的姿态出现在前端领域中。大家都知道这东西在代码中无处不在,可又很难表达出这东西是什么干嘛用的。接下来我来简单聊一下我对闭包的理解,篇幅不会太长,意在让大家理解。

对闭包的使用释义,网上一搜一大堆,主要可以概括为以下几种:

  • 内部作用域访问到了上级作用域的变量
  • 内部函数被传递到所在词法作用域以外来调用
  • LIFE模式(立即执行函数)
  • 模块模式

这些都是闭包的使用场景,由此可见闭包的功能真的很强,而且细微实用。

在正式开始讲闭包之前,我先来回顾以下引擎这个东西,在作用域是干什么用的中介绍过引擎负责执行经由编译器解析后生成的可执行的代码段。
这里再补充一点,引擎还拥有一垃圾回收机制,当作用域为标识符分配的内存空间不再被使用的时候,就会由垃圾回收器负责将其回收并释放该内存。

下面来看一段代码来给出我对闭包的理解定义:

function test() {
    var a = 1;
    function inner() {
        console.log(a);
    }
    return inner;
}
var fn = test();
fn(); // 1

test()执行结束之后,我们可能会以为整个test()的内部作用域都被销毁掉,因为引擎的垃圾回收器会负责释放不再被使用的内存。然后实际上inner被正常执行了,并且访问到了a

这个现象的背后就是因为闭包在阻止着这次垃圾回收。我们观察代码可以看出inner函数在使用着a标识符(变量),而a变量却被定义在了其上级作用域中,因此在inner函数执行之前,引擎识别到a留着还有用,所以不能清理a所在的作用域,这就是闭包的一种现象。

现在我们来归纳以下,内部作用域保持着对外部作用域的引用,这个引用其实就是闭包。我个人理解闭包其实就是一个作用域的集合,它包含自身的词法作用域和对执行过程中用得到的作用域。在上面的代码中,inner()一直保持有对test()整个作用域的引用。

希望上面的解释可以为你解惑,下面来看一个用烂了的例子:

for (var i=1; i<=5; i++) {
    setTimeout( function test() {
        console.log(i);
    }, i*1000 );
}

这个demo很多人肯定已经看很多遍了, 知道会输出5个6。这里我结合上面的理论来阐述一遍。

这里先重温一下两个点:

  1. 词法作用域,引擎会先向当前作用域询问变量是否存在,如果找不到就往上级找。
  2. setTimeout是一个异步方法,他的回掉函数并不会立马被添加到主进程中来执行。

下面我来翻译一下上面的代码:

var i = 1;
setTimeout( function test() {
    console.log(i);
}, 1000 );

i = 2;
setTimeout( function test() {
    console.log(i);
}, 2000 );

i = 3;
setTimeout( function test() {
    console.log(i);
}, 3000 );

i = 4;
setTimeout( function test() {
    console.log(i);
}, 4000 );

i = 5;
setTimeout( function test() {
    console.log(i);
}, 5000 );

i = 6

实际上,setTimeout的回调函数test访问的都是同一个共享的作用域(这里其实就是全局作用域)。而且setTimeout是一个异步的执行过程,因此等test执行到的时候i早已经被赋值为6。所以会输出5个6

其实想要让它输出12345,大家肯定也都会,我们只需要用LIFE(立即执行函数)包一下这个for循环中的执行体,并将即时的状态i记录在内部作用域中,那当test执行的时候就能访问到不同的i了,如下代码:

for (var i=1; i<=5; i++) {
    (function(j){
        setTimeout( function test() {
            console.log(j);
        }, j*1000 );
    }(i))
}

下面我们也用代码来解释这个过程:

var i = 1;
(function(){
    var j = i
    setTimeout( function test() {
        console.log(j);
    }, 1000 );
}())

i = 2;
(function(){
    var j = i
    setTimeout( function test() {
        console.log(j);
    }, 2000 );
}())

i = 3;
(function(){
    var j = i
    setTimeout( function test() {
        console.log(j);
    }, 3000 );
}())

i = 4;
(function(){
    var j = i
    setTimeout( function test() {
        console.log(j);
    }, 4000 );
}())

i = 5;
(function(){
    var j = i
    setTimeout( function test() {
        console.log(j);
    }, 5000 );
}())

i = 6

从上述代码可以看到实际上test()在执行的时候访问的标识符j已经是当前作用域的上级作用域私有的,不再是共享同一个作用域。

另一种方法当然是用let来生成块级作用域,道理都差不多,不再介绍。

@func-star func-star added the js label Jun 26, 2018
@func-star func-star removed the js label Oct 8, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant