JavaScript中的作用域和闭包

作用域和闭包在JavaScript中是两个很重要的概念。刚开始接触的时候,他们一直让我感到非常的困惑。下面是对作用域和闭名解释,以帮助你能更好的了解它们是什么?

先从作用域开始。

作用域

在JavaScript中,作用域定义了你可以访问的变量。作用域有两种类似 —— 全局作用域和局部作用域。

全局作用域

如果变量在所有函数或大括号({})之外声明,则是全局作用域。

这只适用于Web浏览器中的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!'

尽管你可以在全局作用域内声明变量,但不建议这样做。这是因为有可能会造成命名冲突,比如其中有两个或多个变量被命名为相同的变量名。如果你使用constlet关键词来声明变量,那么当你遇到命名冲突时,将会报错。这样做是不可取的。

// 不要这样做
let thing = 'something'
let thing = 'something else' // => Error, thing has already been declared

作用域和闭包

如果你使用的是var关键词来声明变量,那么第二个变量将会覆盖第一个变量(变量名相同的情况之下)。这样也是不可取的,因为这样做会加大你调试代码的难度。

// 不要这样做
var thing = 'something'
var thing = 'something else' // 也许在你的代码中是在不同的地方
console.log(thing)           // => 'something else'

因此,你应该始终声明局部变量,而不是全局变量。

局部作用域

仅在代码的特定部分可用的变量被认为是局部作用域。这里的变量也称为局部变量

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

让我们先来讨论函数作用域。

函数作用域

当你在函数中声明的变量,你只能在函数内访问此变量。一旦你脱离了这个函数访问这个变量,你就无法访问这个变量。

在下面的示例中,变量hello只作用于sayHello()函数作用域:

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

块作用域是函数作用域的子集。因为函数需要用大括号来声明(除了ES6中的箭头函数)。

函数提升和作用域

函数在声明为函数时,总是被提升到当前作用域的顶部。所以这两个是等价的。

// 这和下面的是一样的
sayHello() // => Hello CSS-Tricks Reader!
function sayHello () {
    console.log('Hello CSS-Tricks Reader!')
}

// 这和上面的是一样的
function sayHello () {
    console.log('Hello CSS-Tricks Reader!')
}
sayHello() // => Hello CSS-Tricks Reader!

当使用函数表达式声明函数时,函数不会被提升到当用作用域的顶部。

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

由于这两种变化,函数提升可能会令人困惑,不应该被使用。在使用函数之前,最好先声明你要用的函数。

函数不能访问彼此的作用域

当你单独定义函数时,函数不能访问彼此的作用域。即使一个函数可以在另一个函数中使用。

在下面的这个例子中,second()函数不能访问first()函数中的firstFunctionVariable变量。

function first () {
    const firstFunctionVariable = `I'm part of first`
}

function second () {
    first()
    console.log(firstFunctionVariable) // => Error, firstFunctionVariable is not defined
}

作用域嵌套

当函数在另一个函数中定义时,内部函数可以访问外部函数的变量。这种行为称为词法作用域(Lexical Scoping)

然而,外部函数无法访问内部函数的变量。

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
}

为了可视化作用域如何工作,你可以想象单面玻璃(one-way glass)。你可以看到外面,但外面的人看不到你。

如果你在作用域内有作用域,多个层次的单向玻璃看起来像这样:

只有在了解了关于作用域的所有内容之后,你才能更好的了解闭包是什么。

闭包

当你在另一个函数中创建函数时,你已经创建了一个闭包。内部函数就是一个闭包。这个闭包常会返回内部函数,这样你就可以使用外部函数的变量。

function outerFunction () {
    const outer = `I see the outer variable!`

    function innerFunction() {
        console.log(outer)
    }

    return innerFunction
}

outerFunction()() // => I see the outer variable!

由天返回的是内部函数,你也可以通过返回参数来声明函数,这样可以缩短代码。

function outerFunction () {
    const outer = `I see the outer variable!`

    return function innerFunction() {
        console.log(outer)
    }
}

outerFunction()() // => I see the outer variable!

由于闭包可以访问外部函数中的变量,所以它们通常用于两种情况:

  • 控制副作用(side effects)
  • 创建私有变量

使用闭包控制副作用

当你在不从函数返回值的情况下执行某些操作时,会发生副作用。很多东西都可能会产生副作用,比如Ajax请求,timeout,甚至是console.log语句。

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

当你使用闭包来控制副作用时,你通常会关心那些可能会打乱你的代码流,比如Ajax或timeout

让我们通过一个例子来让事情更清楚些。

假设你想为你朋友的生日做一个蛋糕。这个蛋糕需要一秒钟的时间,所以你写了一个函数,在一秒钟打印出made a cake

下面的示例使用ES6的箭头函数,使示例变得更短,更容易理解。

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

正如你所见,这个制作蛋糕的函数有一个副作用:超时。

让我们再深入一点,你想让你的朋友为蛋糕选择一种口味。为此,你可以为你的makeCake()函数添加一个味道(flavor):

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

当你运行这个函数时,注意,蛋糕在一秒钟后就会被制作出来。

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

这里的问题是,你不希望在知道味道后立即做蛋糕。你想在时机成熟的时候再做这个蛋糕。

为了解决这个问题,你可以编写一个prepareCake()的函数来存储你需要的蛋糕味道。然后在prepareCake()中返回makeCake(),形成一个闭包。

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

const makeCakeLater = prepareCake('banana')

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暂停执行,以便进行调试。

这有一个示例,写了一个prepareCake()函数:

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

const makeCakeLater = prepareCake('banana')

打开你的DevTools并选择Sources选项,你就会看到可用的变量。

你还可以将debugger放到闭包内。注意作用域中变量的变化:

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

const makeCakeLater = prepareCake('banana')

第二种方法通过打断点来调式。

总结

作用域和闭包并不难以理解。一旦你知道如何通过向单玻璃看到它们,它们就很简单了。

当你在函数中声明变量时,你只能在函数中访问它。这些称为函数作用域。

如果在一个函数内定义任何函数,这个内部函数称为闭包。它保留对在外部函数中创建的变量的访问权。

如果有任何问题,欢迎在下面的评论中一起讨论。

本文根据@ZELL LIEW的《JavaScript Scope and Closures》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://css-tricks.com/javascript-scope-closures/

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.w3cplus.com/javascript/javascript-scope-closures.htmlnike air max 1 zappos

如需转载,烦请注明出处:https://www.w3cplus.com/javascript/javascript-scope-closures.html

如果文章中有不对之处,烦请各位大神拍正。如果你觉得这篇文章对你有所帮助,打个赏,让我有更大的动力去创作。(^_^)。看完了?还不过瘾?点击向作者提问!

赏杯咖啡,鼓励他创作更多优质内容!
返回顶部