资讯专栏INFORMATION COLUMN

【译】理解JavaScript:闭包

岳光 / 2194人阅读

摘要:当面试中让我解释一下闭包时我懵逼了。这个解释开始可能有点晦涩,让我们抽丝剥茧摘下闭包的真面目。此文不详述作用域有专门的主题阐述,不过作用域是理解闭包原理的基础。这才是闭包的真正便利之处。闭包使用不当就会很坑。

原文链接

为什么深度学习JavaScript?

JavaScript如今是最流行的编程语言之一。它运行在浏览器、服务器、移动设备、桌面应用,也可能包括冰箱。无需我举其他再多不相干的例子,只要你正从事web开发,你就不可避免地要写JavaScript。

很多web开发者仅仅因为能写可以运行的代码就声称了解JavaScript。对于JavaScript,你可以用一个月就能写代码,掌握它之后终生收益。(If there are no errors and nobody’s complaining why should you need to learn more?)(译者注:不知所云)

好吧,我就是曾经声称很了解此语言的一员。几年前我用AngularJS和Node写应用,当时对自己的能力非常自信。抛开功能,我坚信我已经征服了JavaScript。

当面试中让我解释一下闭包时我懵逼了。我感觉自己知道一点,和回调有关,我当时一直用回调(当时还不知道Promise),但就是不知道怎么描述其原理。

在我的开发职业生涯中那次失败的JavaScript面试是最耻辱和最具教育意义的经历。从那时起我历时一年半致力于JavaScript的高价段位,并决定分享于世人。先从一个最常见的JavaScript面试题开始:

什么是闭包?

毫无疑问你已经在各种应用中使用过闭包。你每次为事件处理器添加回调时你都在用闭包的神奇属性。

我遇到过很多关于此概念的解释,但我最信服是Kyle Simpson下的定义:

当一个方法执行完脱离了自己的词法作用域,但仍然能够记住并访问其词法作用域,这就是闭包。

这个解释开始可能有点晦涩,让我们抽丝剥茧摘下闭包的真面目。

此文不详述作用域(有专门的主题阐述),不过作用域是理解闭包原理的基础。作用域就是包含某些属性和方法的区域。每个JavaScript方法都会创建一个新的作用域,它内部的变量和入参都只能在其内部访问。

如果你在函数内声明一个变量,函数外是访问不到的。不过,我们可以在函数内部定义拥有作用域的内部函数。这些内嵌函数的特别之处在于它们可以访问父作用域的变量。

坦白说这也算不上什么特别之处,因为每一个在全局作用域中定义的函数都能访问全局变量。虽然我们提到的这些内嵌函数可以访问父函数的作用域,但它们不能在父函数之外被调用。除非我们将其暴露出来。

我们将内部函数暴露出来就可以在全局作用域中使用。牛逼!现在我们就可以随心所欲了。不过,暴露出来的内部函数实际上引用了它父作用域的变量,会不会有问题?不会!绝对不会,这就是闭包!

闭包是暴露出来的内嵌方法

我不确定这是否是给闭包下的最好的定义,但这确实能够很好地抓住此术语的本质。闭包就是我们在函数外部就能访问其父作用域的内部函数。你能否通过我们之前提到的词法作用域理解此解释呢?

function person(name) {
  return {
    greet: function() {
      console.log("hello from " + name)
    }
  }
}

let alex = person("alex");
alex.greet(); // hello from alex
console.log(alex.name); // undefined
console.log(name); // will throw ReferenceError

我们在此定义了只有一个参数nameperson函数。它返回一个以greet为属性的对象。现在我们知道,暴露出的greet函数可以访问父函数参数。尽管name变量并没有定义在greet的作用域中,因为它是闭包,所以greet可以从其父作用域中获取。

并不是特别难理解,你可能都用了很多次了。我学闭包前从没把它想象的多难,理解了其背后的原理,我就明白了封装并使用模块。

哇唔,哇唔...模块?封装?出乎意料。

模块和用闭包封装

我深陷JavaScript漩涡之前首先了解到其中很多高深词汇都有实践解释。模块和封装就是这类术语很完美的例子。我先从封装开始,用相同的策略各个击破去理解它们。

封装是基本的编程原则之一。学过OOP(面向对象编程)的人对此概念非常熟悉,但对于没学过的人来说---封装就是允许我们保持数据私有的基本隐藏机制。我们不想把方法的所有内容暴露给全局作用域,我们想让大多数内容保持私有且不可访问。

这才是闭包的真正便利之处。我们可以利用闭包访问父作用域,甚至在外部访问的时候获得适当地封装。在父函数中可能有很多方法和变量,通过利用闭包我们可以将其暴露给我们需要的函数。

我们可以用闭包为我们的方法定义一个公共API,并保持方法中所有东西私有。

我们现在已经掌握了封装,只需实践即可。在JavaScript中对此概念的实践就是使用模块。

模块

在ES6中可以使用importexport关键字产生以文件为基础的模块,但要注意这些只是语法糖而已。

function Person(firstName, lastName, age) {
  var private = "this is a private member";

  return {
    getName: function() {
      console.log("My name is " + firstName + " " + lastName);
    },
    getAge: function() {
      console.log("I am " + age + " years old")
    }
  }
}

let person = new Person("Alex", "Kondov", 22);
person.getName();
person.getAge();
console.log(person.private); //undefined

这是一个我们可以保持一些数据私有的简单例子。我们可以有其他内嵌方法,尽管导出后可以使用,但并没有都暴露出来。

function Order (items) {
  const total = items => {
    return items.reduce((acc, curr) => {
      return acc + curr.price
    }, 0)
  }
  
  const addTaxToPrice = price => price + (price * 0.2)
  
  return {
    calculateTotal: () => {
      return addTaxToPrice(total(items)).toFixed(2)
    }
  }
}

const items = [
  { name: "Toy", price: 14.99 },
  { name: "Candy", price: 7.99 }
]

const order = Order(items)
console.log(order.total) // undefined
console.log(order.addTaxToPrice) // undefined
console.log(order.calculateTotal()) // 27.58

在这个更接近真实的例子中方法返回了一个order对象,唯一暴露出来的方法是calculateTotalOrder函数有一个闭包,允许此闭包使用它的变量和入参。在你计算订单总价时隐藏了内部逻辑,也方便以后扩展。

怪异之处

JavaScript也有其怪异之处。实际上有些怪异之处让人非常蛋疼。闭包使用不当就会很坑。

下面的代码经常出现在JavaScript面试中让猜它的输出。

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

从1循环到5并在一段时间后打印出当前的数字。正常感觉会输出1,2,3,4,5,对吗?

让我惊奇的是上面的代码会在输出台上连续5次打印出6。如果循环之中没有setTimeout不会有任何问题,因为日志输出会被立即执行。很明显,排队操作引发了这个问题。

我们期望每次调用setTimeout都会获取i变量自身的拷贝,但实际情况却是它访问的是它的父作用域。又因为都在排队,第一个日志会在它排队1秒后发生。当1000毫秒过去的时候,循环早已结束,i变量也早已被赋值为6。

我明白了这个问题但如何修复呢?setTimeout会在全局作用域寻找i变量,无法打印出我们想要的数字。我们可以把setTimeout包裹到一个方法中并将我们想要输出的变量传进去。这样setTimeout会从它的父作用域而不是全局作用域进行访问。

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

我们使用IIFE(立即执行函数,Immediately Invoked Function Expression)并把想输出的数字传进去。IIFE是一种定义后立即调用的函数,它常用于这种情况---我们想要创建作用域。这种方式每次函数调用都用它们自己的变量拷贝,这也意味着setTimeout运行时会访问对应的数字。所以上面的例子我们会达到期待的结果:1,2,3,4,5

结束语

此文介绍了闭包的本质,但还有很多需要学习和更多的边际情况需要考虑。如果你想更进一步了解闭包,我强烈推荐Kyle Simpson的书中Scope & Closures的部分。

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

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

相关文章

  • [] 你想知道的关于 JavaScript 作用域的一切

    摘要:原文链接原文作者你想知道的关于作用域的一切译中有许多章节是关于的但是对于初学者来说甚至是一些有经验的开发者这些有关作用域的章节既不直接也不容易理解这篇文章的目的就是为了帮助那些想更深一步学习了解作用域的开发者尤其是当他们听到一些关于作用域的 原文链接: Everything you wanted to know about JavaScript scope原文作者: Todd Mott...

    Flands 评论0 收藏0
  • JavasScript重难点知识

    摘要:忍者级别的函数操作对于什么是匿名函数,这里就不做过多介绍了。我们需要知道的是,对于而言,匿名函数是一个很重要且具有逻辑性的特性。通常,匿名函数的使用情况是创建一个供以后使用的函数。 JS 中的递归 递归, 递归基础, 斐波那契数列, 使用递归方式深拷贝, 自定义事件添加 这一次,彻底弄懂 JavaScript 执行机制 本文的目的就是要保证你彻底弄懂javascript的执行机制,如果...

    forsigner 评论0 收藏0
  • [] addEventListener 与 onclick,孰优孰劣?

    摘要:上面的例子应用了匿名函数这个特性,还可以使用构造函数或者闭包来添加事件监听器另一个重要特性,则是上面这段代码中最后一行的最后一个参数,用来控制监听器对于冒泡事件的响应。在这里你不能使用闭包或者匿名函数,并且控制域也是有限的。 原文出处:addEventListener vs onclick 之所以会想到这个话题,是因为在回顾自己之前写的为 button 动态绑定事件的函数时,脑海里忽...

    wums 评论0 收藏0
  • 2017-08-23 前端日报

    摘要:前端日报精选免费的计算机编程类中文书籍英文技术文档看不懂看印记中文就够了的内部工作原理美团点评点餐前后端分离实践让你的动画坐上时光机中文译有多棒简书译别再使用图片轮播了掘金译如何在中使用掘金个让增长成亿美元公司的独特方法众成翻 2017-08-23 前端日报 精选 FPB 2.0:免费的计算机编程类中文书籍 2.0英文技术文档看不懂?看印记中文就够了!Virtual DOM 的内部工作...

    lordharrd 评论0 收藏0
  • 】stackoverflow 关于JavsScript的热门问答

    摘要:例如,考虑比较由字符串构造函数创建的字符串对象和字符串字面量这里的操作符正在检查这两个对象的值并返回,但是鉴于它们不是相同类型并且返回。我的建议是完全绕过这个问题,只是不使用字符串构造函数创建字符串对象。 Q1:javascript的闭包是如何工作的? 正如爱因斯坦所说的: 如果你不能把它解释给一个六岁的小孩,说明你对它还不够了解。 我曾尝试向一个27岁的朋友解释js闭包并且完全失败了...

    赵春朋 评论0 收藏0

发表评论

0条评论

岳光

|高级讲师

TA的文章

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