【译】理解JavaScript:闭包

原文连接javascript

为何深度学习JavaScript?

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

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

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

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

在个人开发职业生涯中那次失败的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的部分。

相关文章
相关标签/搜索