资讯专栏INFORMATION COLUMN

理解 JavaScript 闭包

宠来也 / 691人阅读

摘要:如何在初学就理解闭包你需要接着读下去。这样定义闭包是函数和声明该函数的词法环境的组合。小结闭包在中随处可见。闭包是中的精华部分,理解它需要具备一定的作用域执行栈的知识。

这是本系列的第 4 篇文章。

作为 JS 初学者,第一次接触闭包的概念是因为写出了类似下面的代码:

for (var i = 0; i < helpText.length; i++) {
  var item = helpText[i];
  document.getElementById(item.id).click = function() {
    showHelp(item.help);
  }
}

给列表项循环添加事件处理程序。当你点击列表项时不会有任何反应。如何在初学就理解闭包?你需要接着读下去。

§ 什么是闭包

说闭包前,你还记得词法作用域吗?

var num = 0;
function foo() {
  var num = 1;
  function bar() {
    console.log(num);
  }
  bar();
}
foo(); // 1

执行上面的代码打印出 1。

bar 函数是 foo 函数的内部函数,JS 的词法作用域允许内部函数访问外部函数的变量。那我们可不可以在外部访问内部函数的变量呢?理论上不允许。

但是我们可以通过某种方式实现,即将内部函数返回。

function increase() {
  let count = 0;
  function add () {
    count += 1;
    return count;
  }
  return add;
}

const addOne = increase();

addOne(); // 1
addOne(); // 2
addOne(); // 3

内部函数允许访问其父函数的内部变量,那么将内部函数返回到出来,它依旧引用着其父函数的内部变量。

这里就产生了闭包。

简单来说,可以把闭包理解为函数返回函数

上面的代码中,当 increase 函数执行,压入执行栈,执行完毕返回一个 add 函数的引用,所以 increase 函数内部的变量对象依旧保存在内存中,不会被销毁。

调用 addOne 函数,相当于执行内部函数 add,它可以访问其父函数的内部变量,从而修改变量 count。而调用 addOne 函数所在的环境为全局作用域,不是定义 add 函数时的函数作用域。

所以,我理解的闭包是一个函数,它在执行时与其定义时所处的词法作用域不一致,并且具有能够访问定义时词法作用域的能力。MDN 这样定义:闭包是函数和声明该函数的词法环境的组合

§ 闭包的利与弊 ◆ 利

第一,闭包可以在函数外部读取函数内部的变量。

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }
})();

Counter.value(); // 0
Counter.increment();
Counter.increment();
Counter.value(); // 2
Counter.decrement();
Counter.value(); / 1

上面这种模式称为模块模式。我们使用立即执行函数 IIFE 将代码私有化但是提供了可访问的接口,通过公共接口来访问函数私有的函数和变量。

第二,闭包将内部变量始终保存在内存中。

function type(tag) {
  return function (data) {
    return Object.prototype.toString.call(data).toLowerCase() === "[object " + tag + "]";
  }
}

var isNum = type("number");
var isString = type("string");

isNum(1); // true
isString("abc"); // true

利用闭包将内部变量(参数)tag 保存在内存中,来封装自己的类型判断函数。

◆ 弊

第一,既然闭包会将内部变量一直保存在内存中,如果在程序中大量使用闭包,势必造成内存的泄漏。

$(document).ready(function() {
  var button = document.getElementById("button-1");
  button.onclick = function() {
    console.log("hello");
    return false;
  };
});

在这个例子中,click 事件处理程序就是一个闭包(在这里是个匿名函数),它将引用着 button 变量;而 button 在这里本身依旧引用着这个匿名函数。从而产生循环引用,造成网页的性能问题,在 IE 中可能会内存泄漏。

解决办法就是手动解除引用。

$(document).ready(function() {
  var button = document.getElementById("button-1");
  button.onclick = function() {
    console.log("hello");
    return false;
  };
  button = null; // 添加这一行代码来手动解除引用
});

第二,如果你将函数作为对象使用,将闭包作为它的方法,应该特别注意不要随意改动函数的私有属性。

§ 闭包的经典问题 ◆ 循环

现在我们来解决一下文章开头出现的问题。

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

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

额外声明一个 makeHelpCallBack 的函数,将循环每次的上下文环境通过闭包保存起来。

◆ setTimeout
for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
};

结果为 1 秒后,打印 5 个 5。

我们可以利用闭包保留词法作用域的特点,来修改代码达到目的。

for (var i = 0; i < 5; i++) {
  setTimeout((function(i) {
    return function () {
      console.log(i);
    }
  }(i)), 1000);
};

结果为 1 秒后,依次打印 0 1 2 3 4。

§ 小结

闭包在 JS 中随处可见。

闭包是 JS 中的精华部分,理解它需要具备一定的作用域、执行栈的知识。理解它你将收获巨大,你会在 JS 学习的道路上走得更远,比如会在后面的文章来讨论高阶函数和柯里化的问题。

◆ 文章参考

闭包 | MDN

学习 JavaScript 闭包 | 阮一峰

Understanding JavaScript Closures: A practical Approach | Paul Upendo

闭包造成问题泄漏的解决办法 | CSDN

§ JavaScript 系列文章

理解 JavaScript 执行栈

理解 JavaScript 作用域

理解 JavaScript 数据类型与变量

欢迎关注我的公众号 cameraee

前端技术 | 个人成长

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/100131.html

相关文章

  • 理解Javascript闭包

    摘要:但是闭包也不是什么复杂到不可理解的东西,简而言之,闭包就是闭包就是函数的局部变量集合,只是这些局部变量在函数返回后会继续存在。可惜的是,并没有提供相关的成员和方法来访问闭包中的局部变量。 (收藏自 技术狂) 前言:还是一篇入门文章。Javascript中有几个非常重要的语言特性——对象、原型继承、闭包。其中闭包 对于那些使用传统静态语言C/C++的程序员来说是一个新的语言特性。本文将...

    dayday_up 评论0 收藏0
  • Javascript闭包入门(译文)

    摘要:也许最好的理解是闭包总是在进入某个函数的时候被创建,而局部变量是被加入到这个闭包中。在函数内部的函数的内部声明函数是可以的可以获得不止一个层级的闭包。 前言 总括 :这篇文章使用有效的javascript代码向程序员们解释了闭包,大牛和功能型程序员请自行忽略。 译者 :文章写在2006年,可直到翻译的21小时之前作者还在完善这篇文章,在Stackoverflow的How do Java...

    Fourierr 评论0 收藏0
  • 【译】理解JavaScript闭包

    摘要:当面试中让我解释一下闭包时我懵逼了。这个解释开始可能有点晦涩,让我们抽丝剥茧摘下闭包的真面目。此文不详述作用域有专门的主题阐述,不过作用域是理解闭包原理的基础。这才是闭包的真正便利之处。闭包使用不当就会很坑。 原文链接 为什么深度学习JavaScript? JavaScript如今是最流行的编程语言之一。它运行在浏览器、服务器、移动设备、桌面应用,也可能包括冰箱。无需我举其他再多不相干...

    岳光 评论0 收藏0
  • 通过示例学习JavaScript闭包

    摘要:译者按在上一篇博客,我们通过实现一个计数器,了解了如何使用闭包,这篇博客将提供一些代码示例,帮助大家理解闭包。然而,如果通过代码示例去理解闭包,则简单很多。不过,将闭包简单地看做局部变量,理解起来会更加简单。 - 译者按: 在上一篇博客,我们通过实现一个计数器,了解了如何使用闭包(Closure),这篇博客将提供一些代码示例,帮助大家理解闭包。 原文: JavaScript Clos...

    xingpingz 评论0 收藏0
  • JavaScript中的闭包

    摘要:闭包引起的内存泄漏总结从理论的角度将由于作用域链的特性中所有函数都是闭包但是从应用的角度来说只有当函数以返回值返回或者当函数以参数形式使用或者当函数中自由变量在函数外被引用时才能成为明确意义上的闭包。 文章同步到github js的闭包概念几乎是任何面试官都会问的问题,最近把闭包这块的概念梳理了一下,记录成以下文章。 什么是闭包 我先列出一些官方及经典书籍等书中给出的概念,这些概念虽然...

    HmyBmny 评论0 收藏0

发表评论

0条评论

宠来也

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<