“闭包是函数和声明该函数的词法环境的组合。”

这是MDN上对闭包的定义。

《JavaScript高级程序设计》中则是这样定义的:闭包是指有权访问另一个函数作用域中的变量的函数。

个人更倾向于MDN的闭包定义,原因有三:

其一,如果仅将闭包定义为可访问其父作用域(链)的局部变量的函数,那么就忽视了它持有外部环境(使外部作用域不被销毁)的意义。

其二,闭包有权访问的必然是其父作用域(链)中的局部变量,“另一个函数作用域”的说法不够明确清晰。

其三,就是本篇博文的主题了,闭包在ES6中,是不限于访问另一个函数的作用域的,还可以是块作用域。当然,《JavaScript高级程序设计》这本书出版时,还没有ES6,书里也明确说明JavaScript是没有块作用域的,因此这一点不能成为批评《JavaScript高级程序设计》的理由。

定义通常讲究严谨、言简意赅,也就意味着不太好理解。

换个通俗点的说法,闭包就是指在一个非全局作用域中声明的函数及其所在的这个作用域,这个函数在该作用域外被调用时,仍然能够访问到该作用域内的(局部)变量。如果不在声明函数的作用域外调用,或者该函数没有访问外部作用域的局部变量,闭包也就没有什么存在的意义了。

由于ES6出现以前,没有块作用域,这个非全局作用域就只能是一个函数了,那么闭包就是声明在另一个函数内部的函数(及其所在的函数)了。为了实现在声明它的作用域外也能调用该函数,就需要将该函数作为一个返回值,返回到父作用域(父级函数)之外了。

举例说明(例1):

var age = ;
var fn;
fn = (function () {
var age = ;
var name = "Tom";
return function () {
console.log("name is " + name + ".");
console.log("age is " + age + ".");
};
})();
fn();

运行结果:

name is Tom.
age is 20.

可以看到,age 获取的是匿名函数中声明的局部变量 age 的值 20,不是全局变量 age 的值 30。name 更是干脆没有同名全局变量,只有匿名函数中声明的局部变量。

对于ES6,因为块作用域的存在,闭包就有了另一种实现,举例如下(例2)

let age = 30;
let fn;
{
let age = 20;
let name = "Tom";
fn = function () {
console.log("name is " + name + ".");
console.log("age is " + age + ".");
};
}
fn();

运行结果与例1相同:

name is Tom.
age is 20.

可见,在ES6中,声明在块作用域内的函数,在离开块作用域后,优先访问的依然是声明它的块作用域的局部变量。

在《你不知道的JavaScript》中文版下卷中,曾经提到过块作用域函数,即声明在块作用域内的函数,在块外无法调用。

原文的例子如下(例3):

{
foo();
function foo() {
//...
}
}
foo();

书中认为,第一个foo()调用会正常返回结果,第二个foo()调用会报 ReferenceError 错误。经在 chrome(64.0) 和 firefox(58.0)版中测试,非严格模式下,两个调用均正常返回结果,不会出现 ReferenceError 错误。仅在严格模式下,与其预期结果相同。

也就是说,非严格模式下,声明在块作用域内的函数,是在块作用域外的父作用域中有效的。

这也导致了例2还有一个变体(例4):

let age = 30;
{
let age = 20;
let name = "Tom";
function fn() {
console.log("name is " + name + ".");
console.log("age is "+ age + ".");
}
}
fn();

其结果也是:

name is Tom.
age is 20.

究其原因,在于函数是在块作用域内声明的,因此它在被调用时,会优先访问块作用域内的局部变量。又因为它虽然是在块内声明,却被提升至其父作用域,所以可以在块作用域外被访问。

不过这种写法,意图不够清晰,且在多层作用域的情况下,容易产生混乱,严格模式下,还会导致错误。

现在再来看上一篇博文中的循环变量的例子(例17、例18和例19):

(例17)

var i;
var fn = [];
for (i = 0; i < 3; i++) {
fn.push(function () {
console.log(i);
});
}
fn[0]();
fn[1]();
fn[2]();

之所以会输出三个3,是因为函数在调用时才会尝试获取i值,而不是在定义时就获取了i的值,而调用是在循环之后发生的。调用时因为i是全局变量,其值已经在循环中自增到了3。因此3次调用均返回3。

(例19)

var i;
var fn = [];
for (i = 0; i < 3; i++) {
fn.push((function (i) {
return function () {
console.log(i);
}
})(i));
}
fn[0]();
fn[1]();
fn[2]();

实际是个障眼法,循环内部的函数定义中,形参使用了和全局变量 i 同名的变量,由于子作用域同名变量的遮蔽作用,函数内部的 i 实际已经不是全局变量 i 了,而是一个匿名函数内部的局部变量。调用匿名函数时,将全局变量 i 的值传递给了局部变量 i 。而返回的那个闭包函数,按照闭包的定义,无论在何处调用,都只会先访问其父作用域中的局部变量。

如果把匿名函数中的 i 换个名字,就更能清晰地看出闭包在这里的作用了:

var i;
var fn = [];
for (i = 0; i < 3; i++) {
fn.push((function (k) {
return function () {
console.log(k);
}
})(i));
}
fn[0]();
fn[1]();
fn[2]();

而(例18):

var fn = [];
for (let i = 0; i < 3; i++) {
fn.push(function () {
console.log(i);
});
}
fn[0]();
fn[1]();
fn[2]();

就刚好是本篇博文所说的块作用域闭包。每个循环都会产生一个块作用域;而 for 语句中的 let,会在每个循环产生的块作用域内生成一个局部变量 i;声明在每个循环内的匿名函数,都会优先访问声明自己的那个循环产生的块作用域中的 i 的值。

其实际意义与如下例子是一样的:

var fn = [];
for (let i = 0; i < 3; i++) {
let k = i;
fn.push(function () {
console.log(k);
});
}
fn[0]();
fn[1]();
fn[2]();

比较而言,用函数作为外部作用域的闭包,可以用返回闭包函数的方式将闭包函数传递到闭包作用域外。而块作用域闭包没办法使用return,就只能是直接为外部作为域的变量赋值的方式,将闭包函数传递出去。

不过,对于例19,可以改造成不使用返回值,直接在闭包函数内使用外部作用域变量的形式:

var i;
var fn = [];
for (i = 0; i < 3; i++) {
(function (k) {
fn.push(function () {
console.log(k);
});
})(i));
}
fn[0]();
fn[1]();
fn[2]();

由于这种匿名函数立即调用的方式构造的闭包只执行一次,要将闭包函数传递给哪个变量,也是coding时能够确定的,返回值传递,还是直接使用外部变量,都是一样的。而这种形式,在ES6中都可以用块作用域闭包代替。

就代码本身的理解难度而言,ES6的块级作用域更容易一些。

回到本文开头的闭包定义,广义的解读,由于任何一个函数必然有声明它的记法环境,所以所有的函数和声明它的记法环境都构成闭包。比如全局作用域内的函数,它和全局作用域就构成了闭包。这也是《ES6标准入门》(阮一峰)在「let 和 const」一章中解释例17时,会说fn[*]()的调用是通过闭包获取的全局变量 i 的原因吧。

PS:

顺便说一下块作用域函数,《你不知道的JavaScript》中,关于块作用域函数有两个示例,其一见上文例3。

另一个例子如下(例4):

if (something) {
function foo() {
console.log("1");
}
} else {
function foo() {
console.log("2");
}
}
foo();

原文说,在前ES6环境中(应该相当于非严格模式),不管something的值是什么,foo()都会打印出“2”,因为两个函数声明都被提升到了块外,第二个总会胜出。

经在chrome(64.0) 和 firefox(58.0)版中测试,实际运行结果是:something为真,foo()打印“1”,something为假,foo()打印“2”。

严格模式下,则与其预期相符,抛出一个ReferenceError。

其实这种全局定义函数,在ES6中,与函数变量方式相比,不能算是最佳实践了。

ES6 学习笔记之二 块作用域与闭包的更多相关文章

  1. ES6学习笔记(二十一)编程风格

    本章探讨如何将 ES6 的新语法,运用到编码实践之中,与传统的 JavaScript 语法结合在一起,写出合理的.易于阅读和维护的代码. 1.块级作用域 (1)let 取代 var ES6 提出了两个 ...

  2. ES6学习笔记(二十)Module 的加载实现

    上一章介绍了模块的语法,本章介绍如何在浏览器和 Node 之中加载 ES6 模块,以及实际开发中经常遇到的一些问题(比如循环加载). 1.浏览器加载 传统方法 HTML 网页中,浏览器通过<sc ...

  3. ES6学习笔记(二)

    1.数组的解构赋值 基本用法 ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring). 以前,为变量赋值,只能直接指定值. var a = 1; va ...

  4. ES6学习笔记(二)变量的解构与赋值

    1.数组的解构赋值 1.1基本用法 ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring). 以前,为变量赋值,只能直接指定值. let a = 1 ...

  5. ES6学习笔记(二):教你玩转类的继承和类的对象

    继承 程序中的继承: 子类可以继承父类的一些属性和方法 class Father { //父类 constructor () { } money () { console.log(100) } } c ...

  6. ES6 学习笔记(二)解构赋值

    一.数组的解构赋值 1.基本用法 ES6允许按照一定模式从数组和对象中提取值,然后对变量进行赋值,该操作即为解构 如: let [a,b,c]=[1,2,3]; console.log(a,b,c) ...

  7. ES6学习笔记(二)—— 通过ES6 Module看import和require区别

    前言 说到import和require,大家平时开发中一定不少见,尤其是需要前端工程化的项目现在都已经离不开node了,在node环境下这两者都是大量存在的,大体上来说他们都是为了实现JS代码的模块化 ...

  8. ES6学习笔记(二):引用数据类型

    Array 新增方法 1.Array.from() 将类数组(dom对象 或 arguments)或set\map对象转换为数组 2.Array.of() 将一组值转换为数组,例如Array.of(3 ...

  9. ES6学习笔记(二)-字符串的扩展

    一.字符的 Unicode 表示法 JavaScript 允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode 码点. 表示法只限于码点在\u0000~\uFFFF之间的字符, ...

随机推荐

  1. memcached经典问题和现象

    缓存刷新时间集中问题 某个缓存失效了,导致其他节点的缓存命中率下降, 缓存中缺失的数据 去数据库查询.短时间内,会造成数据库服务器崩溃 需要将缓存失效时间离散分布在访问量比较低的时间段 multige ...

  2. OKMX6Q LTIB编译

    因为在16.04上编译有许多解决不了的错误,最后还是在飞凌的12.04虚拟机上编译的. 按照手册<OKMX6X-S2-LTIB编译手册-V1.1-2016-08-18>进行到第8步时,出现 ...

  3. 【编程技巧】Ext.QuickTips.init();

    启动悬浮提示(在你验证非法时.和现实提示语句等) 默认情况下悬浮提示没有启动:所以必须加上这句代码

  4. vue中组件之间的相互调用,及通用后台管理系统左侧菜单树的迭代生成

    由于本人近期开始学习使用vue搭建一个后端管理系统的前端项目,在左侧生成菜单树的时候遇到了一些问题.在这里记录下 分析:由于本人设定的菜单可以使多级结构,直接使用vue的v-for 遍历并不是很方便. ...

  5. 爬虫利器BeautifulSoup模块使用

    一.简介 BeautifulSoup 是一个可以从HTML或XML文件中提取数据的Python库.它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式,同时应用场景也是非常丰富,你可以使用 ...

  6. spring MVC 运行过程

    以Tomcat为例,想在Web容器中使用Spirng MVC,必须进行四项的配置: 1.修改web.xml, 2.添加servlet定义.编写servletname-servlet.xml( serv ...

  7. CentOS 7安装Tomcat8

    一.安装环境 tomcat的安装依赖于Java JDK,需要先安装配置正确的JDK http://www.cnblogs.com/VoiceOfDreams/p/8376978.html 二.安装包准 ...

  8. diffMerge安装配置使用

    概述: 在用git进行源代码版本维护的时候,常常会进行各代码版本之前区别的查看,例如在每次提交改动前进行git diff 可以看到源文件代码相对相应版本或是远程仓库的改动情况,如果有冲突还需要进行me ...

  9. junit设计模式--组合模式

    Composite,英语翻译下,复合,组合. 组合模式有时候又叫做部分-整体模式,它使我们在树形结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以像处理简单元素一样来处理复杂元素,从而使得客户 ...

  10. ClearCase新增文件

    原文地址:http://blog.csdn.net/ace_fei/article/details/7531376 大家应该都知道在clearcase上新增文件是通过以下过程来生成的: clearto ...