美文网首页前端开发那些事Web前端之路
你不可不知道的 JavaScript 作用域和闭包

你不可不知道的 JavaScript 作用域和闭包

作者: 白色鹈鹕鸟 | 来源:发表于2017-08-30 18:00 被阅读52次

    原文出处:JavaScript Scope and Closures

    作用域和闭包是 JavaScript 中重要的部分,但是当我开始学习时遇到了很多的困惑。这里就是一篇关于作用域和闭包的文章,能够帮助你理解它们。

    让我们先从作用域开始

    作用域

    JavaScript 作用域指定了哪些变量你能够访问。有两种作用域 —— 全局作用域和局部作用域

    全局作用域

    如果一个变量在函数外面或者大括号({})外申明,那么就是定义了一个全局作用域的变量。

    这个只是对于浏览器中的 JavaScript 来说,你在 Node.js 中申明的全局变量是不同的,但是我们在这片文章中不涉及 Node.js。

    const globalVariable = 'some value'

    一旦你申明了全局变量,那么你可以在任何地方使用它,甚至在函数中也行。

    const hello = 'Hello CSS-Tricks Reader!'
    
    function sayHello () {
      console.log(hello)
    }
    
    console.log(hello) // 'Hello CSS-Tricks Reader!'
    sayHello() // 'Hello CSS-Tricks Reader!'
    

    虽然你能够在全局作用域中申明函数,但是不建议这么做。因为这可能会和其他的的变量名冲突。如果你使用 const 或者 let 申明变量,你将在命名冲突时收到一个错误的信息,这是不值得的。

    // Don't do this!
    let thing = 'something'
    let thing = 'something else' // Error, thing has already been declared
    

    如果你使用 var 申明变量,你的第二个申明的同样的变量将覆盖前面的。这样会使你的代码很难调试。

    // Don't do this!
    var thing = 'something'
    var thing = 'something else' // perhaps somewhere totally different in your code
    console.log(thing) // 'something else'
    

    所以,你应该使用局部变量,而不是全局变量。

    局部作用域

    在你代码特定范围之内申明的变量可以称为处于局部作用域中,这些变量也被称为局部作用域。

    在 JavaScript 中,有两种局部作用于:函数作用域和块作用域。

    让我们先说说函数作用域

    函数作用域

    当你在函数中申明一个变量,你就只能够在这个函数范围内使用它。在范围之外你不能使用。

    在这个例子中,变量 hellosayHello 作用域中。

    function sayHello () {
      const hello = 'Hello CSS-Tricks Reader!'
      console.log(hello)
    }
    
    sayHello() // 'Hello CSS-Tricks Reader!'
    console.log(hello) // Error, hello is not defined
    

    块作用域

    当你在一个大括号中({})使用 const 或者 let 申明变量,那么这个变量只能够在这个大括号范围内使用。

    在这个例子中,变量 hello 就在大括号范围中。

    {
      const hello = 'Hello CSS-Tricks Reader!'
      console.log(hello) // 'Hello CSS-Tricks Reader!'
    }
    
    console.log(hello) // Error, hello is not defined
    

    块作用域是函数作用域的一个子集,因为函数需要用花括号声明。

    函数提升和作用域

    当你申明一个函数时,它总是会提升到作用域顶部。这两种写法是相等的。

    // This is the same as the one below
    sayHello()
    function sayHello () {
      console.log('Hello CSS-Tricks Reader!')
    }
    
    // This is the same as the code above
    function sayHello () {
      console.log('Hello CSS-Tricks Reader!')
    }
    sayHello()
    

    当申明一个函数表达式时,函数不会提升到作用域顶部。

    sayHello() // Error, sayHello is not defined
    const sayHello = function () {
      console.log(aFunction)
    }
    

    函数不能相互调用各自的作用域

    当你定义函数时,他们不能够相互使用各自的作用域,虽然它们可以互相调用。

    在这个例子中,second 不能够使用 firstFunctionVariable 变量。

    function first () {
      const firstFunctionVariable = `I'm part of first`
    }
    
    function second () {
      first()
      console.log(firstFunctionVariable) // Error, firstFunctionVariable is not defined
    }
    

    嵌套作用域

    当一个函数在另一个函数内定义,内部的函数能够访问外部函数的变量。我们称之为词法作用域

    然而,外部的函数不能够访问内部函数的变量。

    function outerFunction () {
      const outer = `I'm the outer function!`
    
      function innerFunction() {
        const inner = `I'm the inner function!`
        console.log(outer) // I'm the outer function!
      }
    
      console.log(inner) // Error, inner is not defined
    }
    

    这个图片介绍了它是如何工作的,你能够想象一面单面镜。你能够看见外面的人,外面的人却无法看见你。

    嵌套作用域01

    如果你遇见了嵌套作用域,可以理解成多层单面玻璃。

    嵌套作用域01

    当你彻底理解作用域之后,你才能够进一步理解闭包的原理。

    闭包

    当你在一个函数内部创建一个函数时,你就创建了一个闭包。内部函数就是闭包。这个闭包总是会 return 出来,所以你能够稍后使用外部函数中的变量。

    function outerFunction () {
      const outer = `I see the outer variable!`
    
      function innerFunction() {
        console.log(outer)
      }
    
      return innerFunction
    }
    
    outerFunction()() // I see the outer variable!
    

    当内部函数需要 return 时,你可以直接 reutrn 函数声明,这样的代码更加的精练。

    function outerFunction () {
      const outer = `I see the outer variable!`
    
      return function innerFunction() {
        console.log(outer)
      }
    }
    
    outerFunction()() // I see the outer variable!
    

    因为闭包允许变量来自外部的函数,他们通常被用来

    1. 控制副作用
    2. 创建私有变量

    用闭包控制副作用

    当你从一个函数中返回一个值时会产生副作用。很多事情都会有副作用,比如 Ajax 请求,timeout 或者一个 console.log。

    function (x) {
      console.log('A console.log is a side effect!')
    }
    

    当你使用闭包来解决副作用时,你通常会关心这样弄乱你代码,像 Ajax 或者 timeouts。

    让我们通过一个例子来理清这些。

    比如你想要为你给你朋友的生日制作一个蛋糕。这个蛋糕需要一秒钟制作完成,所以你写了一个函数,在一秒后打印出 made a cake

    我使用 ES6 的箭头函数来使得例子更加的简短和容易理解

    function makeCake() {
      setTimeout(_ => console.log(`Made a cake`), 1000)
      )
    }
    

    正如你所看见的,这个“制作蛋糕”的函数有一个副作用:延迟。

    更进一步,你想要你的朋友选择一个蛋糕口味,你能加一个口味到你的 makeCake 函数。

    function makeCake(flavor) {
      setTimeout(_ => console.log(`Made a ${flavor} cake!`), 1000))
    }
    

    当你运行这个函数时,提示蛋糕在一秒钟后立即制成。

    makeCake('banana')
    // Made a banana cake!
    

    这个问题是你不想要在知道口味之后立即制作蛋糕,而是在正确的时间之后再制作。

    为了解决这个额问题,你能写一个 prepareCake 函数存储你的口味。然后,在 prepareCake 中 return makeCake 函数。

    使用这个方法,你能够在任何时候调用 return 的函数,蛋糕会在一秒钟之后制作。

    function prepareCake (flavor) {
      return function () {
        setTimeout(_ => console.log(`Made a ${flavor} cake!`, 1000))
      }
    }
    
    const makeCakeLater = prepareCake('banana')
    
    // And later in your code...
    makeCakeLater()
    // Made a banana cake!
    

    这就是闭包被用来减少副作用的 —— 在你想要的时候通过创建一个函数来激活内部的闭包。

    闭包中的私有变量

    正如你现在所知道的,在函数内部创建的变量不能够被外部的函数访问。正因为他们不能够被外部函数访问,所以称之为私有变量。

    然后,有时候你需要在函数外部访问私有变量,你能够使用闭包来实现。

    function secret (secretCode) {
      return {
        saySecretCode () {
          console.log(secretCode)
        }
      }
    }
    
    const theSecret = secret('CSS Tricks is amazing')
    theSecret.saySecretCode()
    // 'CSS Tricks is amazing'
    

    saySecretCode 在这个例子中是将 secretCode 暴露给外层的 secret 的唯一函数(闭包)。像这样也被称之为特权函数

    使用 DevTools 调试作用域

    Chrome 和 Firefox 的 DevTools 使得调试当前作用域中的变量变得简单。这里有两种方式使用这个功能。

    第一种方式是在代码中添加 debugger,JavaScript 解释器遇见 debugger 会在浏览器中暂停,这样你就能够调试。

    这里是一个 prepareCake 的例子:

    function prepareCake (flavor) {
      // Adding debugger
      debugger
      return function () {
        setTimeout(_ => console.log(`Made a ${flavor} cake!`, 1000))
      }
    }
    
    const makeCakeLater = prepareCake('banana')
    

    如果你在 Chrome 中打开 DevTools 然后找到 Sources 选项(或者 Firefox 中的 Debugger 选项),你就能看到可用的变量。

    使用 DevTools 调试作用域01

    你也能把 debugger 放在闭包中,注意这个时候局部变量是怎么变化的。

    function prepareCake (flavor) {
      return function () {
        // Adding debugger
        debugger
        setTimeout(_ => console.log(`Made a ${flavor} cake!`, 1000))
      }
    }
    
    const makeCakeLater = prepareCake('banana')
    
    使用 DevTools 调试作用域02

    第二种方式是在 sources (或者 debugger) 选项中点击行数使用 debugging 功能来直接在你的代码中添加断点。

    使用 DevTools 调试作用域03

    结束语

    作用域和闭包并不是非常的难理解。一旦你真正理解了其中的原理,他们就变得非常的简单了。

    当你在函数中声明一个变量,你只能够在该函数中使用它。这些变量就被限制在了函数范围之内。

    如果你在其他函数中定义一个内部函数,这个内部函数被称之为闭包。它保留了在外部函数中声明的变量。

    期待你提出问题,我将尽我所能的回复你。

    如果你喜欢这篇文章,你也许也喜欢我写的其他文章,可以访问我的博客newsletter。我也一个免费的课程:JavaScript Roadmap

    相关文章

      网友评论

        本文标题:你不可不知道的 JavaScript 作用域和闭包

        本文链接:https://www.haomeiwen.com/subject/xasxjxtx.html