❝
环境对人的塑造更持久,更有决定性 --《习惯的力量》
❞
简明扼要
- 作用域(scope)控制着变量的可见性和生命周期
- 在JS中,生成作用域\
- 函数\
- 块级作用域
- 不同的作用域能够拥有同名的变量
- 外部作用域的变量可以在内部作用域中访问
- JS通过**「词法作用域」**(静态作用域)来实现变量查询机制
- 「闭包(closure)是一个函数」:其有权访问其词法作用域内部的变量即使该函数在词法作用域外部被调用
- 常规的闭包生成方式\
- Event handler(事件处理器)\
- callback(回调函数)\
- 函数式编程-柯里化
一图胜千言
文章概要
- 作用域
- 作用域嵌套
- 词法作用域(lexicsl scope)
- 闭包
- 闭包示例
在进行闭包讲解之前,我们需要对一些前置知识点,做一些简单的介绍:何为作用域 和 词法作用域。只有在了解了这些概念,我们才会对闭包的认识有的放矢。
1. 作用域
当你定义一个变量,你想要该变量在某些范围内是**「可访问的」**。例如:在fnnc()
内部定义一个变量 result
,该变量只能在函数内部可见;而在函数外部是不可访问的。
「作用域(scope)管理变量的是否被有权访问」
❝
作用域就是变量与函数的可访问范围,即作用域控制着变量的**「可见性」和「生命周期」**
❞
你可以在作用域**「内」访问在该作用域内定义的变量,而在作用域「外」**,无法访问该变量。
❝
在JS中,作用域由\
- 函数\
- 块级作用域生成
❞
function foo() { // 函数作用域 let count = 0; console.log(count); // 有权访问count变量 } foo(); // 函数作用域外访问作用域内的变量 console.log(count); // ReferenceError: count is not defined 复制代码
在foo()
函数作用域内,有权访问count
变量;而在foo()
函数作用域外,count
变量是不能够被访问的。
如果你在函数内部或者块级作用域内定义了一个变量,你只能在函数内部或块级作用域内部访问该变量。
一图胜千言
我们得出一个结论:「作用域是一个空间策略:控制的变量的可访问性」
针对不同的作用域,我们还可以得出如下结论:
❝
不同的作用域能够拥有同名的变量
❞
function foo() { // "foo" 函数作用域 let count = 0; console.log(count); // logs 0 } function bar() { // "bar" 函数作用域 let count = 1; console.log(count); // logs 1 } foo(); bar(); 复制代码
foo()
和bar()
函数作用域含有属于它们自己的变量(count
),虽然名称相同,但是它们直接互不影响。
2. 作用域嵌套
由上文的作用域介绍所知,在JS中每个函数或者块级作用域都会产生与其对应的作用域对象。而我们在平时开发中,经常会看到多个函数嵌套的现象。例如:函数innerFunc()
内嵌在函数outerFunc()
中。
function outerFunc() { // 外部作用域 let outerVar = 'I am outside!'; function innerFunc() { // 内部作用域 console.log(outerVar); // => 输出 "I am outside!" } innerFunc(); } outerFunc(); 复制代码
通过代码可知,outerVar
能够在内部作用域innerFunc()
中可见。
❝
外部作用域的变量可以在内部作用域中访问
❞
一图胜千言
从上面的示例中我们可以得出两个结论
- 作用域可以嵌套
- 外部作用域的变量可以在内部作用域中访问
3. 词法作用域(lexicsl scope)
❝
JS通过**「词法作用域」**(静态作用域)来实现作用域查询机制。
❞
词法作用域意味着变量的可访问性由变量在嵌套作用域中的位置决定。之所以叫词法(静态)作用域,是因为JS引擎(V8)在JS源代码**「编译阶段」**就将作用域之间的关系通过他们的位置关系确定下来了,而非执行阶段。
const myGlobal = 0; function func() { const myVar = 1; console.log(myGlobal); // 输出 "0" function innerOfFunc() { const myInnerVar = 2; console.log(myVar, myGlobal); // 输出 "1 0" function innerOfInnerOfFunc() { console.log(myInnerVar, myVar, myGlobal); // 输出 "2 1 0" } innerOfInnerOfFunc(); } innerOfFunc(); } func(); 复制代码
innerOfInnerOfFunc()
的词法作用域包含innerOfFunc()
,func()
和全局作用域(最外层的作用域)。所以,在innerOfInnerOfFunc()
中你有权访问myInnerVar
, myVar
和 myGlobal
。
4. 闭包
词法作用域允许**「静态地」**访问外部作用域的变量,这个定律仅差一步就能实现闭包。
function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // => 输出 "I am outside!" } innerFunc(); } outerFunc(); 复制代码
在innerFunc()
作用域中,有权访问外部作用域的变量(outerVar
)。
innerFunc()
函数调用发生在词法作用域内(outerFunc()
)。
将代码进行改动,将innerFunc()
的调用移动到词法作用域外部:在ecec()
中执行。
function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // => 输出 "I am outside!" } return innerFunc; } function exec() { const myInnerFunc = outerFunc(); myInnerFunc(); } exec(); 复制代码
innerFunc()
在它的词法作用域外部被执行,也就是在exec()
作用域内被执行。
innerFunc()
仍然有权访问存在其词法作用域内部的变量(outerVar
),甚至能够在其词法作用域外部被调用。
换句话说,innerFunc()
「记住了」(closes over)来自它词法作用域内的变量(outerVar
)。
innerFunc()
是一个闭包:它记住了来自它词法作用域内的变量(outerVar
)。
一图胜千言
我们可以得出如下结论
❝
「闭包(closure)是一个函数」:其有权访问其词法作用域内部的变量即使该函数在词法作用域外部被调用
❞
更简单的讲:闭包是一个函数,它会从定义它的地方记住变量,而不管它稍后在哪里执行。
有一个识别闭包的经验:如果函数内部存在外部变量,那么该函数就是一个闭包,因为外部变量已经被**「记住了」**
5. 闭包示例
5.1 Event handler
let countClicked = 0; myButton.addEventListener('click', function handleClick() { countClicked++; myText.innerText = `You clicked ${countClicked} times`; }); 复制代码
执行如上代码,myText
的文本显示的是按钮被点击的次数。
当按钮被点击,handleClick()
是在DOM节点的范围内被执行。「函数执行和函数定义的地方大相径庭」。
但是,由于handleClick()
是一个闭包,所以,它能够记住(捕获)对应词法作用域中的变量countClicked
,并且在点击按钮的时候,更新该变量的值。
5.2 Callbacks
在回调函数中,也存在变量捕获的情况。 例如:setTimeout
的回调函数
const message = 'Hello, World!'; setTimeout(function callback() { console.log(message); //输出 "Hello, World!" }, 1000); 复制代码
callback
是一个闭包,它捕获了message
外部变量。
例如:递归函数forEach()
let countEven = 0; const items = [1, 5, 100, 10]; items.forEach(function iterator(number) { if (number % 2 === 0) { countEven++; } }); countEven; // => 2 复制代码
5.3 函数式编程(柯里化)
柯里化技术,主要体现在函数里面返回函数。就是将多变量函数拆解为单变量(或部分变量)的多个函数并依次调用。
function multiply(a) { return function executeMultiply(b) { return a * b; } } const double = multiply(2); double(3); // => 6 double(5); // => 10 const triple = multiply(3); triple(4); // => 12 复制代码
利用闭包,可以形成一个不销毁的私有作用域,把预先处理的内容都存在这个不销毁的作用域里面,并且返回一个函数,以后要执行的就是这个函数。