前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >什么是JavaScript 的闭包???

什么是JavaScript 的闭包???

作者头像
AlbertYang
发布2020-09-08 15:38:08
1.1K0
发布2020-09-08 15:38:08
举报

Javascript的闭包是指一个函数与周围状态(词法环境)的引用捆绑在一起(封闭)的组合,在JavaScript中,每次创建函数时,都会同时创建闭包。闭包是一种保护私有变量的机制,在函数执行时形成私有的作用域,保护里面的私有变量不受外界干扰,即形成一个不销毁的栈环境。

阅读本文前需要了解JS局部变量和全局变量。

不清楚的可以看我的上篇文章:

搞懂JavaScript全局变量与局部变量,看这篇文章就够了

1 计数器问题

如果你想要统计一个数值,你需要定义一个变量counter,这时你可以使用全局变量。实现代码如下:

代码语言:javascript
复制
var counter = 0;
    
function add() {
  counter += 1;
}
    
add();
add();
add();
console.log(counter); // 计数器现在为 3

上边的代码虽然可以实现计数, 但有一个大问题,那就是页面上的任何脚本都能改变计数器,这显然不是我们想要的,我们只想让特定的函数(例如:add)去改变counter ,所以我们要把counter变为局部变量,改变代码如下:

代码语言:javascript
复制
function myCounter() {
  var counter = 0;
}

function add() {
  counter += 1;
}

add();//报错,不能访问局部变量 counter
add();
add();

但是这样就不能实现计数功能了,因为counter 现在为局部变量,在函数add中不能访问。那么如何让add读取到局部变量?

2 JS闭包

要想让add读取到局部变量counter ,可以把add函数放到,myCounter函数里面,所以改变代码如下:

代码语言:javascript
复制
function myCounter() {
  var counter = 0;
       
  function add() {
    counter += 1;
    console.log(counter);
  }
    return add;
}

var myAdd = myCounter();
myAdd();
myAdd();
myAdd();// 计数器现在为 3

在上面的代码中,函数add被包括在函数myCounter内部,这时myCounter内部的局部变量counter ,对add是可见的。这是Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。我们把add作为返回值,实现了在myCounter外部改变它的内部变量counter的值。

在Java或一些其他编程语言中,一个函数中的局部变量仅存在于此函数的执行期间。也就是说一旦 myCounter() 执行完毕, counter 变量将不能再被访问。但是在 JavaScript 中显然不是这样的。这是因为JavaScript中的函数会形成闭包。 闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。在本例子中,myAdd是执行 myCounter时创建的 add()函数实例的引用。add的实例维持了一个对它的词法环境(变量 counter 存在于其中)的引用。因此,当 myAdd被调用时,变量 counter 仍然可用,此处的myAdd可以理解为Java中的一个对象引用,我们可以通过创建多个实例,创建多个计数器。代码如下:

代码语言:javascript
复制
function myCounter() {
  var counter = 0;
       
  function add() {
    counter += 1;
    console.log(counter);
  }
   return add;
}
    
var myAdd1 = myCounter();
myAdd1();
myAdd1();
myAdd1();// 计数器1现在为 3
    
var myAdd2 = myCounter();
myAdd2();
myAdd2();// 计数器2现在为 2

3 闭包的用处

闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。

在一些编程语言中,比如 Java,是支持将方法声明为私有的(private),即它们只能被同一个类中的其它方法所调用。而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。下面我们继续以上边的计时器为例,修改代码如下:

代码语言:javascript
复制
var myCounter = function() {
  var privateCounter = 0;

  function changeBy(val) {
    privateCounter += val;
  }

  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);

    },
    value: function() {
      return privateCounter;
    }
  }
};

var Counter1 = myCounter();
var Counter2 = myCounter();
console.log(Counter1.value()); /* 计数器1现在为 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* 计数器1现在为 2 */
Counter1.decrement();
console.log(Counter1.value()); /* 计数器1现在为 1 */

console.log(Counter2.value()); /* 计数器2现在为 0 */
Counter2.increment();
console.log(Counter2.value()); /* 计数器2现在为 1 */

我们创建了一个匿名函数含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问,Counter.increment,Counter.decrement 和 Counter.value,这三个公共函数共享同一个环境的闭包,多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。我们把匿名函数储存在一个变量myCounter 中,并用它来创建多个计数器,每次创建都会同时创建闭包,因为每个闭包都有它自己的词法环境,每个闭包都是引用自己词法作用域内的变量 privateCounter ,所以两个计数器 Counter1 和 Counter2 是各自独立的。

以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装

4 闭包导致的一些问题

在 ECMAScript 2015 引入let 关键字之前,在循环中有一个常见的闭包创建问题。请看以下代码:

代码语言:javascript
复制
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    <p id="help">提示信息</p>
    <p>E-mail: <input type="text" id="email" name="email"></p>
    <p>Name: <input type="text" id="name" name="name"></p>
    <p>Age: <input type="text" id="age" name="age"></p>

  </body>
  <script>
    function showHelp(help) {
      document.getElementById('help').innerHTML = help;
    }

    function setupHelp() {
      var helpText = [{
          'id': 'email',
          'help': '你的邮件地址'
        },
        {
          'id': 'name',
          'help': '你的名字'
        },
        {
          'id': 'age',
          'help': '你的年龄'
        }
      ];

      for (var i = 0; i < helpText.length; i++) {
        var item = helpText[i];
        document.getElementById(item.id).onfocus = function() {
          showHelp(item.help);
        }
      }
    }
    setupHelp();
</script>
</html>

数组 helpText 中定义了三个提示信息,每一个都关联于对应的文档中的input 的 ID。通过循环依次为相应input添加了一个 onfocus 事件处理函数,以便显示帮助信息。运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个input上,显示的都是关于年龄的信息。

演示地址:https://www.albertyy.com/2020/7/closure1.html

我们想要的正确效果:https://www.albertyy.com/2020/7/closure2.html

这是因为赋值给 onfocus 的是闭包。这些闭包是由他们的函数定义和在 setupHelp 作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item。这里因为变量item使用var进行声明,由于变量提升(item可以在函数setupHelp的任何地方使用),所以item具有函数作用域。当onfocus的回调执行时,item.help的值被决定。由于循环在onfocus 事件触发之前早已执行完毕,变量对象item(被三个闭包所共享)已经指向了helpText的最后一项。要解决这个问题,有以下几个方法。

4.1 第一:使用更多的闭包

代码语言:javascript
复制
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function makeHelpCallback(help) {
  return function() {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [{
      'id': 'email',
      'help': '你的邮件地址'
    },
    {
      'id': 'name',
      'help': '你的名字'
    },
    {
      'id': 'age',
      'help': '你的年龄'
    }
  ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}
setupHelp();

这段代码可以正常的执行了。这是因为所有的回调不再共享同一个环境, makeHelpCallback 函数为每一个回调创建一个新的词法环境。在这些环境中,help 指向 helpText 数组中对应的字符串。

4.2 第二种方法:使用了匿名闭包

代码语言:javascript
复制
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [{
      'id': 'email',
      'help': '你的邮件地址'
    },
    {
      'id': 'name',
      'help': '你的名字'
    },
    {
      'id': 'age',
      'help': '你的年龄'
    }
  ];

  for (var i = 0; i < helpText.length; i++) {
    (function() {
      var item = helpText[i];
      document.getElementById(item.id).onfocus = function() {
        showHelp(item.help);
      }
    })(); // 马上把当前循环项的item与事件回调相关联起来
  }
}
setupHelp();

4.3 第三种方法:使用用ES2015引入的let关键词

代码语言:javascript
复制
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [{
      'id': 'email',
      'help': '你的邮件地址'
    },
    {
      'id': 'name',
      'help': '你的名字'
    },
    {
      'id': 'age',
      'help': '你的年龄'
    }
  ];

  for (var i = 0; i < helpText.length; i++) {
    let item = helpText[i]; //使用let代替var
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}
setupHelp();

这个里使用let而不是var,因为let是具有块作用域的变量,即它所声明的变量只在所在的代码块({})内有效,因此每个闭包都绑定了块作用域的变量,这意味着不再需要额外的闭包。

4.4 第四种方法:使用 forEach()来遍历

代码语言:javascript
复制
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [{
      'id': 'email',
      'help': '你的邮件地址'
    },
    {
      'id': 'name',
      'help': '你的名字'
    },
    {
      'id': 'age',
      'help': '你的年龄'
    }
  ];

  helpText.forEach(function(text) {
    document.getElementById(text.id).onfocus = function() {
      showHelp(text.help);
    }
  });
}
setupHelp();

5 性能

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题。如果不是某些特定任务需要使用闭包,最好不要使用闭包。

例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。

请看以下代码:

代码语言:javascript
复制
function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();

  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

在上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改如下:

代码语言:javascript
复制
function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};

但我们一般不应该重新定义原型,所以可修改如下:

代码语言:javascript
复制
function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}

MyObject.prototype.getName = function() {
  return this.name;
};

MyObject.prototype.getMessage = function() {
  return this.message;
};

思考:

请看下面两段代码,请思考它们的运行结果是什么?

代码一:

代码语言:javascript
复制
var name = "Window";
var object = {
  name: "Object",

  getNameFunc: function() {
    return function() {
      return this.name;
    };
  }
};
alert(object.getNameFunc()());

代码片段二:

代码语言:javascript
复制
var name = "Window";
var object = {
  name: "Object",
  getNameFunc: function() {
    var that = this;
    return function() {
      return that.name;
    };
  }
};
alert(object.getNameFunc()());

好了,关于JavaScript 的闭包就先说到这里,由于本人能力和知识有限,如果有写的不对的地方,还请各位大佬批评指正。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 AlbertYang 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 计数器问题
  • 2 JS闭包
  • 3 闭包的用处
  • 4 闭包导致的一些问题
    • 4.1 第一:使用更多的闭包
      • 4.2 第二种方法:使用了匿名闭包
        • 4.3 第三种方法:使用用ES2015引入的let关键词
          • 4.4 第四种方法:使用 forEach()来遍历
            • 5 性能
              • 思考:
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档