闭包没有想象的那么简单

闭包的概念在JavaScript中占据了十分重要的地位,有不少开发者分不清匿名函数和闭包的概念,把它们混为一谈,我希望借这篇文章能够让大家对闭包有一个清晰的认识。

大家都知道变量的作用域有两种:全局变量和局部变量。在JavaScript中函数内部可以访问外部全局变量,而函数外部无法访问函数的内部局部变量。

上边这一小段话,看似简单,其实它是我们理解闭包最基础的东西。在下边的内容中,我们会对这一现象做出解释。我们先来看一个很简单的例子:

  1. const a = 100;
  2. function f1() {
  3. console.log(a); // => 100
  4. }
  5. f1();

上边的代码中的函数f1打印出了全局变量a的值,这说明函数内部可以访问外部全局变量。出于某种目的,或者是为了安全,或者是想使用私有变量,我们现在需要访问函数内部的一个局部变量。我们先看看下边的代码:

  1. function f1() {
  2. const a = 100;
  3. console.log(a); // => 100
  4. }
  5. console.log(a);

上边的代码会产生一个错误,说明我们无法在函数外部访问函数内部的局部变量。为了解决这个问题,我们就引出了闭包,看一个使用闭包解决上述问题的例子:

  1. function f1() {
  2. const a = 100;
  3. return function () {
  4. console.log(a);
  5. }
  6. }
  7. f1()();

上边的代码是一个很简答的例子,使用闭包后我们打印出的结果是100.这正好验证了上边说的,使用闭包的目的就是解决函数外部无法访问函数内部局部变量这一问题。

要彻底搞清楚其中的细节,必须从理解函数第一次被调用的时候都会发生什么入手。

当某个函数第一次被调用时,会创建一个执行环境(execution context)和相应的作用域链,并把作用域链赋值给一个特殊的内部属性(Scope),然后使用this,arguments和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,直到作为作用域链重点的全局执行环境。

上边的这一段话非常重要,它解释了函数执行的基本原理,闭包也是函数。大家有可能对上边的话不太理解。我们通过一个例子来解释一下:

  1. function compare(value1, value2) {
  2. if (value1 < value2) {
  3. return -1;
  4. } else if (value1 > value2) {
  5. return 1;
  6. } else {
  7. return 0;
  8. }
  9. }
  10. var result = compare(5, 10);

上边的代码中,我们首先定义了一个compare()函数,然后又在全局作用域中调用了它。当第一次调用它的时候,一共创建了以下几个对象:

  • 创建函数的执行环境,当然该函数的执行环境是全局环境
  • 创建函数的作用域链
  • 创建一个包含this,arguments,value1,value2的活动对象

我们看一个图:

后台的每个执行环境都有一个表示变量的对象---变量对象,全局环境的变量对象始终存在,而想compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。

变量对象本质上是一个对象,他存储了某些变量值。

其实,在compare()函数创建的时候,就已经创建了一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的Scope属性中。

这句话说明函数在创建后,其内部就有了一个属性保存着当前的作用域链,上边说compare()函数的作用域链指向全局变量对象,这说明作用域链中的每一项指向的都是一个变量对象。

当调用compare()函数时,会为函数创建一个执行环境,然后通过复制函数的Scope属性中的对象构建起执行环境的作用域链。

这句话说明函数在执行环境中,会新创建一个作用域链,这个新建的作用域链会把函数创建时的作用域链复制过来。

此后,又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前段。

这句话说明,函数执行后,会他this,arguments,函数的参数,函数内部的局部变量这四个作为属性,保存到一个对象中,然后把该对象放到作用域链的前段,因此我们就能够通过作用域链访问到我们需要的数据。

大家可以再次回到上边看看那个图,由于compare()函数是在全局环境中创建的,因此在执行的时候,它的作用域链只有两个对象,最前端的0指向了执行时的活动对象,1指向了全局的变量对象。

显然,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

这句话非常重要,这使我们理解函数调用过程最基本的原理。无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(也就是全局执行环境的变量对象)。但是闭包的情况又有所不同。

我们再看一个带有闭包的例子:

  1. function createCompareFunction(propertyName) {
  2. return function (object1, object2) {
  3. const value1 = object1[propertyName];
  4. const value2 = object2[propertyName];
  5. if (value1 < value2) {
  6. return -1;
  7. } else if (value1 > value2) {
  8. return 1;
  9. } else {
  10. return 0;
  11. }
  12. }
  13. }
  14. const compare = createCompareFunction("name");
  15. const result = compare({name: "James"}, {name: "Bond"});

上边的代码实现了按照对象的属性排序的功能。当我们在一个函数的内部定义了另一个函数,那么在该函数执行时,就会把该函数的活动对象添加到它内部的函数的作用域链之中。这也就是为什么compare()函数为什么能访问createCompareFunction内部参数的原因。

更为重要的是,createCompareFunction()函数在执行完毕后,器活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当createCompareFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象人让亲会留在内存中,直到匿名函数被销毁之后,createCompareFunction()函数的活动对象才会被销毁。

  1. const compare = createCompareFunction("name");
  2. const result = compare({name: "James"}, {name: "Bond"});
  3. compare = null;

这其中关于闭包最终要的问题就是,他内部的作用域链中会有一个外部函数的活动对象的引用。

我们看看上边代码执行过程中发生了什么:

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,建议大家只在绝对必要时再考虑使用闭包。

但是闭包在使用不当的情况下会产生一定的副作用,上文中,我们反复提到,闭包只能取得包含函数中任何变量的最后一个值,因为闭包保存的是整个变量对象,而是不是某个特殊的变量。

  1. function createFunctions() {
  2. var result = new Array();
  3. for (var i = 0; i < 10; i++) {
  4. result[i] = function () {
  5. return i;
  6. }
  7. }
  8. return result;
  9. }
  10. const funcs = createFunctions();
  11. console.log(funcs[2]());

上边的代码中,看似createFunctions()函数应该返回一个函数数组,数组中的每个函数都应该返回自己的索引值,但实际上,每个函数都返回10。createFunctions()函数返回的闭包中保存的是createFunctions()函数的活动对象,这个活动对象中的其中一个属性就是i。createFunctions()函数执行完毕后,i变成了10,因此当我们调用闭包函数的时候,他其实是去访问了活动对象中的i。基于这个原理,我们可以使用这种方式:

  1. function createFunctions() {
  2. var result = new Array();
  3. for (var i = 0; i < 10; i++) {
  4. result[i] = function (num) {
  5. return function () {
  6. return num;
  7. };
  8. }(i);
  9. }
  10. return result;
  11. }
  12. const funcs = createFunctions();
  13. console.log(funcs[2]());

杀精编的例子中,用了两层闭包,最内层的闭包访问num,外层的闭包访问i并且立即执行。

在闭包中使用this对象也可能会导致一些问题。我们知道,this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。

匿名函数的执行环境具有全局性,在没有指定调用对象的前提下,this对象通常指向window》

  1. const name = "The window";
  2. const object = {
  3. name: "My object",
  4. getNameFunction: function () {
  5. return function () {
  6. return this.name;
  7. }
  8. }
  9. };
  10. console.log(object.getNameFunction()());

上边的代码在调用了object.getNameFunction()返回了一个函数,然后在调用这个返回的函数,就返回了“The window”。

这里唯一的问题是,为什么匿名函数没有取得其包含作用域(或外部作用域)的this对象呢?

前面我们曾经提到过,每个函数在被调用时,其活动对象都会自动获取两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。

这说明访问内部函数的参数时,得到的就是内部函数的参数,而不是其他值,否则就乱套了。

把外部作用域中的this对象保存在一个闭包能够访问到的变量中,就可以让闭包访问该对象了:

  1. const name = "The window";
  2. const object = {
  3. name: "My object",
  4. getNameFunction: function () {
  5. const that = this;
  6. return function () {
  7. return that.name;
  8. }
  9. }
  10. };
  11. console.log(object.getNameFunction()());

记住,this和arguments这两个比较特殊,只能访问自身的活动对象。

在几种特殊的情况下,this的值可能会意外的改变:

  1. const name = "The window";
  2. const object = {
  3. name: "My object",
  4. getNameFunction: function () {
  5. return this.name;
  6. }
  7. };
  8. console.log(object.getNameFunction()); // => "My object"
  9. console.log((object.getNameFunction)()); // => "My object"
  10. console.log((object.getNameFunction = object.getNameFunction)()); // => "The window"

最后一行代码比较有意思,赋值表达式的结果就是函数,然后调用函数之后就打印出了"The window"。

本篇大部分内容来源于<<JavaScript高级程序设计>>

深入理解JavaScript中的闭包的更多相关文章

  1. 【原】理解javascript中的闭包

    闭包在javascript来说是比较重要的概念,平时工作中也是用的比较多的一项技术.下来对其进行一个小小的总结 什么是闭包? 官方说法: 闭包是指有权访问另一个函数作用域中的变量的函数.创建闭包的常见 ...

  2. 深入理解javascript中的闭包!(转)

    1.闭包的经典错误 假如页面上有若干个div,我们想给它每个绑定一个onclick方法,于是有了下面的代码. function A(){ var divs=document.getElementsBy ...

  3. 【原】理解javascript中的闭包(***********************************************)

    阅读目录 什么是闭包? 闭包的特性 闭包的作用: 闭包的代码示例 注意事项 总结 闭包在javascript来说是比较重要的概念,平时工作中也是用的比较多的一项技术.下来对其进行一个小小的总结 回到顶 ...

  4. 全面理解JavaScript中的闭包的含义及用法

    1.什么是闭包 闭包:闭包就是能够读取其他函数内部变量的函数;闭包简单理解成“定义在一个函数内部的函数”. 闭包的形式:即内部函数能够使用它所在级别的外部函数的参数,属性或者内部函数等,并且能在包含它 ...

  5. 理解JavaScript中的闭包

    (这篇文章后面关于onclick事件的解释是错误的,请不要被误导了2016.6.16) 闭包这个概念给JavaScript初学者心中留下了巨大的阴影,网络上关于闭包的文章不可谓不多,但是能让初学者看懂 ...

  6. 理解 JavaScript 中的 this

    前言 理解this是我们要深入理解 JavaScript 中必不可少的一个步骤,同时只有理解了 this,你才能更加清晰地写出与自己预期一致的 JavaScript 代码. 本文是这系列的第三篇,往期 ...

  7. [译]Javascript中的闭包(closures)

    本文翻译youtube上的up主kudvenkat的javascript tutorial播放单 源地址在此: https://www.youtube.com/watch?v=PMsVM7rjupU& ...

  8. JavaScript中的闭包理解

    原创文章,转载请注明:JavaScript中的闭包理解  By Lucio.Yang 1.JavaScript闭包 在小学期开发项目的时候,用node.js开发了服务器,过程中遇到了node.js的第 ...

  9. 深入理解javascript原型和闭包 (转)

    该教程绕开了javascript的一些基本的语法知识,直接讲解javascript中最难理解的两个部分,也是和其他主流面向对象语言区别最大的两个部分--原型和闭包,当然,肯定少不了原型链和作用域链.帮 ...

随机推荐

  1. CSS预处理语言——less与sass的使用

    我们一般所使用的Less跟Sass一般是将其编译成我们所熟悉的CSS再导入使用,当然不经编译,直接在浏览器使用 我是习惯用Koala来进行编译,简单智能方便,Hbuilder也自带编译功能,不过要手动 ...

  2. 使用fontawesome图标

     我每次找图标时都是在阿里的开源图标库中找的,但是使用起来不是很方便.而我发现了fontawesome之后,觉得实在不错,所以分享给大家.  这是一些参考的文档. fontawesome下载与使用介绍 ...

  3. Excel 数据导入(OleDb)

    @using (Html.BeginForm("Student", "Excel", FormMethod.Post, new { enctype = &quo ...

  4. JavaScript ,Python,java,Go系列算法之选择排序

    常见的内部排序算法有:插入排序.希尔排序.选择排序.冒泡排序.归并排序.快速排序.堆排序.基数排序等. 用一张图概括:   选择排序 选择排序是一种简单直观的排序算法,无论什么数据进去都是O(n2) ...

  5. Web测试到底是在测什么(资料合集)

    开始今晚的主题之前 先来看一张图, 这是老徐16年10月份,线上Web主题分享时整理的大纲 图片略模糊 看得清就好 Web测试, 进行抽离拆分,基本上就如上一些内容. 不管是测什么系统,什么功能,基本 ...

  6. iOS 发布证书提示 此证书的签发者无效 解决办法

    1. 打开钥匙串  查看发布证书 都是提示 此证书的签发者无效   解决办法 : 2. 到了 第 4 步骤 再去 查看 发布证书 就会 显示  此证书有效 3.  如果还不可以 就 把 Apple W ...

  7. 使用Tomcat-redis-session-manager来实现Tomcat集群部署中的Session共享

    一.工作中因为要使用到Tomcat集群部署,此时就涉及到了Session共享问题,主要有三种解决方案: 1.使用数据库来存储Session 2.使用Cookie来存储Session 3.使用Redis ...

  8. 读《Java并发编程的艺术》(二)

    上篇博客开始,我们接触了一些有关Java多线程的基本概念.这篇博客开始,我们就正式的进入了Java多线程的实战演练了.实战演练不仅仅是贴代码,也会涉及到相关概念和术语的讲解. 线程的状态 程的状态分为 ...

  9. 基于腾讯云的Centos6.2系统搭建Apache+Mysql+PHP开发环境

    搭建环境,我肯定需要先购买腾讯云服务器的哦! 然后,我们打开SecureCRT 7.3,这是一款可以连接Linux系统的客户端工具,使用的很方便快捷,要注意的是,若你是Linux系统的就要用22端口, ...

  10. SPRING AOP ....0 can't find referenced pointcut

    下载最新的aspectjweaver就可以了,因为JDK的版本的问题不兼容. //织入点语法 @Pointcut("execution(public * com.frank.dao..*.* ...