• JS闭包—你不知道的JavaScript上卷读书笔记(二)


    关于闭包,初学者会被绕的晕头转向,在学习的路上也付出了很多精力来理解。 让我们一起来揭开闭包神秘的面纱。

    闭包晦涩的定义

    看过很多关于闭包的定义,很多讲的云里雾里,晦涩难懂。让不少人以为闭包是多么玄乎的东西。在我看过的所有书籍中,我更喜欢《你不知道的javascript(上卷)》的定义:

    当函数可以记住并访问所在的词法作用域时,就产生了闭包,或者说函数在创建时的词法作域之外执行。

    通俗的来说(不严谨): 就是函数套函数,子函数可以有权访问父函数的变量、父函数的父函数的变量、一直到全局变量。子函数如果不被销毁,整条作用域链上的变量仍然保存在内存中。

    关于词法作用域,这里不做过多的解释,详情参考:http://www.cnblogs.com/ylweb/p/7531259.html.

    下面用一些代码来解释这个定义:

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

    严格来说这段代码并没有形成闭包,因为bar是在创建时所在的词法作用域执行。bar() 对a 的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。从学术的角度说:,在上面的代码片段中,函数bar() 具有一个涵盖foo() 作用域的闭包
    (事实上,涵盖了它能访问的所有作用域,比如全局作用域)。也可以认为bar() 被封闭在
    了foo() 的作用域中。为什么呢?原因简单明了,因为bar() 嵌套在foo() 内部。

    下面我们来看一段代码,清晰地展示了闭包:

    function foo() {
    	var a = 2;
    	function bar() {
    		console.log( a );
    	}
    	return bar;
    }
    
    var baz = foo();
    baz(); // 2 —— 朋友,这就是闭包的效果。
    

    函数bar() 的词法作用域能够访问foo() 的内部作用域。然后我们将bar() 函数本身当作一个值类型进行传递。在这个例子中,我们将bar 所引用的函数对象本身当作返回值。

    在foo() 执行后,其返回值(也就是内部的bar() 函数)赋值给变量baz 并调用baz(),实际上只是通过不同的标识符引用调用了内部的函数bar()。

    bar() 显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。

    在foo() 执行后,通常会期待foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去foo() 的内容不会再被使用,所以很自然地会考虑对其进行回收。

    而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是bar() 本身在使用。

    拜bar() 所声明的位置所赐,它拥有涵盖foo() 内部作用域的闭包,使得该作用域能够一直存活,以供bar() 在之后任何时间进行引用。

    bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。

    循环与闭包

    以下是一个最常见的for循环例子:

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

    正常情况下,我们对这段代码行为的预期是分别输出数字1~5,但实际上,这段代码在运行时输出五次6。 Why?

    首先解释6 是从哪里来的。这个循环的终止条件是i 不再<=5。条件首次成立时i 的值是6。因此,输出显示的是循环结束时i 的最终值。

    仔细想一下,这好像又是显而易见的,延迟函数的回调会在循环结束时才执行。事实上,当定时器运行时即使每个迭代中执行的是setTimeout(.., 0),所有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个6 出来。

    问题实质:

    我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个i 的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i

    改进方案:

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

    ES6改进:

    for (let i=1; i<=5; i++) {
    	setTimeout( function timer() {
    		console.log( i );
    	}, i*1000 );
    }
  • 相关阅读:
    手写spring事务框架, 揭秘AOP实现原理。
    centos7修改端口登陆
    数据库的锁机制
    linux安装mysql5.6
    SpringMVC数据格式化
    Java处理小数点后几位
    docker学习(七)常见仓库介绍
    docker学习(六) Docker命令查询
    docker学习(六)
    docker学习(五)
  • 原文地址:https://www.cnblogs.com/ylweb/p/7804309.html
Copyright © 2020-2023  润新知