在 JavaScript 中,作用域是指变量在代码中可访问的范围。理解 JavaScript 的作用域和作用域链对于编写高质量的代码至关重要。本文将详细介绍 JavaScript 中的词法作用域、作用域链和闭包的概念,并探讨它们在实际开发中的应用场景。

一、闭包

1、什么是闭包?

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。— from MDN

闭包是指函数和其词法环境的组合。它可以访问其词法作用域中定义的变量,即使在函数外部也可以访问这些变量。闭包在 JavaScript 中常用于创建私有变量和实现模块化开发。

javascript 闭包的本质源自 2 点:词法作用域和函数当做值传递。

1
2
3
4
5
6
7
8
9
10
11
12
function createCounter() {
var count = 0

return function () {
count++
console.log(count)
}
}

var counter = createCounter()
counter() // 输出: 1
counter() // 输出: 2

闭包的应用场景:
私有变量:闭包提供了一种实现私有变量的机制,可以隐藏变量并提供访问控制。
模块化开发:通过创建闭包,可以实现模块化的代码组织,将变量和函数封装在私有作用域中,提供了良好的封装性和代码组织性。
延迟执行:通过使用闭包,可以延迟执行函数,实现异步操作和事件处理。

2、词法作用域

词法作用域是 JavaScript 中最常见的作用域类型。它是在代码编写阶段确定的,而不是在代码执行阶段确定的。在词法作用域中,变量的访问权限是由它们在代码中的位置决定的。

1
2
3
4
5
6
7
8
9
10
11
12
function init() {
// 局部变量
var outerName = 'jude'
// 内部函数,一个闭包
function displayName() {
var innerName = 'summer'
// 父函数中声明的变量
console.log(outerName + innerName)
}
displayName()
}
init() // jude summer

在上面的示例中,函数 displayName 内部可以访问外部函数 init 中定义的变量 outerName,这是因为它们处于词法作用域中。词法作用域确保了变量在代码编写阶段就能够正确地被访问。

name、displayName()是 init()函数的一个局部变量和一个内部函数,displayName()函数没有自己的局部变量,但是它可以访问到外部函数的变量,所以 displayName()可以使用父函数 init()中声明的变量 outerName。

词法作用域的应用场景:
1、变量访问控制:词法作用域使得我们可以控制变量的可见性和访问权限,避免命名冲突和变量污染。
2、模块化开发:通过使用函数和闭包,可以实现模块化的代码组织,将变量和函数封装在私有作用域中,提供了良好的封装性和代码组织性。
3、函数嵌套:函数嵌套是 JavaScript 中常见的编程模式,词法作用域确保了内部函数可以访问外部函数的变量,实现了信息的隐藏和封装。

3、函数当做值传递

即所谓的 first class 对象。就是可以把函数当作一个值来赋值,当作参数传给别的函数,也可以把函数当作一个值 return。一个函数被当作值返回时,也就相当于返回了一个通道,这个通道可以访问这个函数词法作用域中的变量,即函数所需要的数据结构保存了下来,数据结构中的值在外层函数执行时创建,外层函数执行完毕时理因销毁,但由于内部函数作为值返回出去,这些值得以保存下来。而且无法直接访问,必须通过返回的函数。这也就是私有性。

闭包的形成很简单,在执行过程完毕后,返回函数,或者将函数得以保留下来,即形成闭包。

4、闭包的缺点:造成内存泄漏

如果一个很大的对象被函数引用,本来函数调用结束就能销毁,但是现在引用却被通过闭包保存到了堆里,而且还一直用不到,那这块堆内存就一直没法使用,严重到一定程度就算是内存泄漏了。所以闭包不要乱用,少打包一点东西到堆内存。

二、作用域

1、作用域

作用域是指程序中定义变量的区域,该位置决定了变量的生命周期,也就是变量和函数的可访问范围。

作用域分为函数作用域、全局作用域

全局作用域:代码在程序任何地方都能访问,例如 window 对象
函数作用域: 固定代码片段中才能被访问

JavaScript 引擎会把内存分为函数调用栈、全局作用域和堆,其中堆用于放一些动态的对象,调用栈每一个栈帧放一个函数的执行上下文,里面有一个 local 变量环境用于放内部声明的一些变量,如果是对象,会在堆上分配空间,然后把引用保存在栈帧的 local 环境中。全局作用域也是一样,只不过一般用于放静态的一些东西,有时候也叫静态域。

每个栈帧的执行上下文包含函数执行需要访问的所有环境,包括 local 环境、作用域链、this 等。

作用域最大的用处:隔离变量,不同作用域下同名变量不会有冲突。

2、作用域链

作用域链是 JavaScript 中用于查找变量的一种机制。它由当前作用域和所有父级作用域的变量对象组成。当访问一个变量时,JavaScript 引擎会首先在当前作用域的变量对象中查找,如果找不到,则沿着作用域链向上查找,直到找到变量或者到达全局作用域。

在 JavaScript 里面,函数、块、模块都可以形成作用域(一个存放变量的独立空间),他们之间可以相互嵌套,作用域之间会形成引用关系,这条链叫做作用域链。

⼀般情况下,变量取值到创建这个变量的函数的作⽤域中取值,但是如果在当前作⽤域中没有查到值,就会向上级作⽤域去查,直到查到全局作⽤域,这么⼀个查找过程形成的链条就叫做作⽤域链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var globalVariable = 'Global'

function outer() {
var outerVariable = 'Hello'

function inner() {
var innerVariable = 'World'
console.log(globalVariable + ' ' + outerVariable + ' ' + innerVariable)
}

inner()
}

outer() // 输出: Global Hello World

在上面的示例中,函数 inner 内部可以访问全局作用域中定义的变量 globalVariable,以及外部函数 outer 中定义的变量 outerVariable,这是因为 JavaScript 引擎按照作用域链的顺序查找变量。

作用域链的应用场景

作用域链在 JavaScript 中有多种应用场景,包括:
1、变量查找:作用域链决定了变量的查找顺序,使得 JavaScript 可以正确地找到并访问变量。
2、闭包:通过创建闭包,内部函数可以访问外部函数的变量,实现了信息的保留和共享。
3、模块化开发:作用域链的特性使得我们可以实现模块化的代码组织,将变量和函数封装在私有作用域中,提供了良好的封装性和代码组织性。

三、原型链

当访问一个对象的某个属性时,会先在这个对象本身属性上查找, 如果没有找到,则会去它的proto隐式原型上查找,即它的构造函数的 prototype, 如果还没有找到就会再在构造函数的 prototype 的proto中查找, 这样一层一层向上查找就会形成一个链式结构,我们称为原型链。

四、总结

作用域、作用域链和闭包是 JavaScript 中重要的概念,它们相互关联,共同构建了 JavaScript 的变量访问和代码组织机制。理解这些概念的原理和应用场景对于编写高质量的 JavaScript 代码至关重要。

通过词法作用域,我们可以控制变量的可见性和访问权限,实现模块化的代码组织,避免命名冲突和变量污染。

作用域链决定了变量的查找顺序,使得 JavaScript 可以正确地找到并访问变量。同时,作用域链的特性也为闭包的创建提供了基础,通过闭包,我们可以创建私有变量,实现模块化的代码组织以及延迟执行函数等。

深入理解作用域、作用域链和闭包,能够帮助我们更好地编写可维护、高效的 JavaScript 代码。