Javascript 变量作用域,闭包
JavaScript 是一种非常面向函数的语言。它给了我们很大的自由度。在 JavaScript 中,我们可以随时创建函数,可以将函数作为参数传递给另一个函数,并在完全不同的代码位置进行调用。
我们已经知道函数可以访问其外部的变量。
但是,如果在函数被创建之后,外部变量发生了变化会怎样?函数会获得新值还是旧值?
如果将函数作为参数(argument)传递并在代码中的另一个位置调用它,该函数将访问的是新位置的外部变量吗?
让我们通过本文来学习这些相关知识,以了解在这些场景以及更复杂的场景下到底会发生什么。
我们将在这探讨一下
let/const
在 JavaScript 中,有三种声明变量的方式:
let
,const
(现代方式),var
(过去留下来的方式)。
- 在本文的示例中,我们将使用
let
声明变量。- 用
const
声明的变量的行为也相同(译注:与 let
在作用域等特性上是相同的),因此,本文也涉及用 const
进行变量声明。- 旧的
var
与上面两个有着明显的区别,我们将在 老旧的 "var" 中详细介绍。
代码块
如果在代码块 {...}
内声明了一个变量,那么这个变量只在该代码块内可见。
例如:
{
// 使用在代码块外不可见的局部变量做一些工作
let message = "Hello"; // 只在此代码块内可见
alert(message); // Hello
}
alert(message); // Error: message is not defined
我们可以使用它来隔离一段代码,该段代码执行自己的任务,并使用仅属于自己的变量:
{
// 显示 message
let message = "Hello";
alert(message);
}
{
// 显示另一个 message
let message = "Goodbye";
alert(message);
}
这里如果没有代码块则会报错
请注意,如果我们使用
let
对已存在的变量进行重复声明,如果对应的变量没有单独的代码块,则会出现错误:
// 显示 message let message = "Hello"; alert(message); // 显示另一个 message let message = "Goodbye"; // Error: variable already declared alert(message);
对于 if
,for
和 while
等,在 {...}
中声明的变量也仅在内部可见:
if (true) {
let phrase = "Hello!";
alert(phrase); // Hello!
}
alert(phrase); // Error, no such variable!
在这儿,当 if
执行完毕,则下面的 alert
将看不到 phrase
,因此会出现错误。(译注:就算下面的 alert
想在 if
没执行完成时去取 phrase
(虽然这种情况不可能发生)也是取不到的,因为 let
声明的变量在代码块外不可见。)
太好了,因为这就允许我们创建特定于 if
分支的块级局部变量。
对于 for
和 while
循环也是如此:
for (let i = 0; i < 3; i++) {
// 变量 i 仅在这个 for 循环的内部可见
alert(i); // 0,然后是 1,然后是 2
}
alert(i); // Error, no such variable
从视觉上看,let i
位于 {...}
之外。但是 for
构造在这里很特殊:在其中声明的变量被视为块的一部分。
嵌套函数
如果一个函数是在另一个函数中创建的,该函数就被称为“嵌套”函数。
在 JavaScript 中很容易实现这一点。
我们可以使用嵌套来组织代码,比如这样:
function sayHiBye(firstName, lastName) {
// 辅助嵌套函数使用如下
function getFullName() {
return firstName + " " + lastName;
}
alert( "Hello, " + getFullName() );
alert( "Bye, " + getFullName() );
}
这里创建的 嵌套 函数 getFullName()
是为了更加方便。它可以访问外部变量,因此可以返回全名。嵌套函数在 JavaScript 中很常见。
更有意思的是,可以返回一个嵌套函数:作为一个新对象的属性或作为结果返回。之后可以在其他地方使用。不论在哪里调用,它仍然可以访问相同的外部变量。
下面的 makeCounter
创建了一个 “counter” 函数,该函数在每次调用时返回下一个数字:
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2
尽管很简单,但稍加变型就具有很强的实际用途,比如,用作 随机数生成器 以生成用于自动化测试的随机数值。
这是如何运作的呢?如果我们创建多个计数器,它们会是独立的吗?这里的变量是怎么回事?
理解这些内容对于掌握 JavaScript 的整体知识很有帮助,并且对于应对更复杂的场景也很有益处。因此,让我们继续深入探究。
词法环境
前方高能!
一大波深入的技术讲解即将到来。
尽管我很想避免编程语言的一些底层细节,但是如果没有它们,我们就无法完整地理解词法作用域,所以我们这就开始吧!
为了使内容更清晰,这里将分步骤进行讲解。
Step 1. 变量
在 JavaScript 中,每个运行的函数,代码块 {...}
以及整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏)的关联对象。
词法环境对象由两部分组成:
- 环境记录(Environment Record) —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如
this
的值)的对象。 - 对 外部词法环境 的引用,与外部代码相关联。
一个“变量”只是 环境记录 这个特殊的内部对象的一个属性。“获取或修改变量”意味着“获取或修改词法环境的一个属性”。
举个例子,这段没有函数的简单的代码中只有一个词法环境:
这就是所谓的与整个脚本相关联的 全局 词法环境。
在上面的图片中,矩形表示环境记录(变量存储),箭头表示外部引用。全局词法环境没有外部引用,所以箭头指向了 null
。
随着代码开始并继续运行,词法环境发生了变化。
这是更长的代码:
右侧的矩形演示了执行过程中全局词法环境的变化:
- 当脚本开始运行,词法环境预先填充了所有声明的变量。
- 最初,它们处于“未初始化(Uninitialized)”状态。这是一种特殊的内部状态,这意味着引擎知道变量,但是在用
let
声明前,不能引用它。几乎就像变量不存在一样。 - 然后
let phrase
定义出现了。它尚未被赋值,因此它的值为 undefined
。从这一刻起,我们就可以使用变量了。 -
phrase
被赋予了一个值。 -
phrase
的值被修改。
现在看起来都挺简单的,是吧?
- 变量是特殊内部对象的属性,与当前正在执行的(代码)块/函数/脚本有关。
- 操作变量实际上是操作该对象的属性。
词法环境是一个规范对象
“词法环境”是一个规范对象(specification object):它只存在于 语言规范 的“理论”层面,用于描述事物是如何工作的。我们无法在代码中获取该对象并直接对其进行操作。
但 JavaScript 引擎同样可以优化它,比如清除未被使用的变量以节省内存和执行其他内部技巧等,但显性行为应该是和上述的无差。
Step 2. 函数声明
一个函数其实也是一个值,就像变量一样。
不同之处在于函数声明的初始化会被立即完成。
当创建了一个词法环境(Lexical Environment)时,函数声明会立即变为即用型函数(不像 let
那样直到声明处才可用)。
这就是为什么我们甚至可以在声明自身之前调用一个以函数声明(Function Declaration)的方式声明的函数。
例如,这是添加一个函数时全局词法环境的初始状态:
正常来说,这种行为仅适用于函数声明,而不适用于我们将函数分配给变量的函数表达式,例如 let say = function(name)...
。
Step 3. 内部和外部的词法环境
在一个函数运行时,在调用刚开始时,会自动创建一个新的词法环境以存储这个调用的局部变量和参数。
例如,对于 say("John")
,它看起来像这样(当前执行位置在箭头标记的那一行上):
在这个函数调用期间,我们有两个词法环境:内部一个(用于函数调用)和外部一个(全局):
- 内部词法环境与
say
的当前执行相对应。它具有一个单独的属性:name
,函数的参数。我们调用的是 say("John")
,所以 name
的值为 "John"
。 - 外部词法环境是全局词法环境。它具有
phrase
变量和函数本身。
内部词法环境引用了 outer
。
当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。
如果在任何地方都找不到这个变量,那么在严格模式下就会报错(在非严格模式下,为了向下兼容,给未定义的变量赋值会创建一个全局变量)。
在这个示例中,搜索过程如下:
- 对于
name
变量,当 say
中的 alert
试图访问 name
时,会立即在内部词法环境中找到它。 - 当它试图访问
phrase
时,然而内部没有 phrase
,所以它顺着对外部词法环境的引用找到了它。
Step 4. 返回函数
让我们回到 makeCounter
这个例子。
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
不同的是,在执行 makeCounter()
的过程中创建了一个仅占一行的嵌套函数:return count++
。我们尚未运行它,仅创建了它。
所有的函数在“诞生”时都会记住创建它们的词法环境。从技术上讲,这里没有什么魔法:所有函数都有名为 [[Environment]]
的隐藏属性,该属性保存了对创建该函数的词法环境的引用。
不同的是,在执行 makeCounter()
的过程中创建了一个仅占一行的嵌套函数:return count++
。我们尚未运行它,仅创建了它。
所有的函数在“诞生”时都会记住创建它们的词法环境。从技术上讲,这里没有什么魔法:所有函数都有名为 [[Environment]]
的隐藏属性,该属性保存了对创建该函数的词法环境的引用。
因此,counter.[[Environment]]
有对 {count: 0}
词法环境的引用。这就是函数记住它创建于何处的方式,与函数被在哪儿调用无关。[[Environment]]
引用在函数创建时被设置并永久保存。
稍后,当调用 counter()
时,会为该调用创建一个新的词法环境,并且其外部词法环境引用获取于 counter.[[Environment]]
:
现在,当 counter()
中的代码查找 count
变量时,它首先搜索自己的词法环境(为空,因为那里没有局部变量),然后是外部 makeCounter()
的词法环境,并且在哪里找到就在哪里修改。
在变量所在的词法环境中更新变量。
这是执行后的状态:
如果我们调用 counter()
多次,count
变量将在同一位置增加到 2
,3
等。
闭包
开发者通常应该都知道“闭包”这个通用的编程术语。
闭包 是指一个函数可以记住其外部变量并可以访问这些变量。在某些编程语言中,这是不可能的,或者应该以一种特殊的方式编写函数来实现。但如上所述,在 JavaScript 中,所有函数都是天生闭包的(只有一个例外,将在 "new Function" 语法 中讲到)。
也就是说:JavaScript 中的函数会自动通过隐藏的
[[Environment]]
属性记住创建它们的位置,所以它们都可以访问外部变量。
在面试时,前端开发者通常会被问到“什么是闭包?”,正确的回答应该是闭包的定义,并解释清楚为什么 JavaScript 中的所有函数都是闭包的,以及可能的关于
[[Environment]]
属性和词法环境原理的技术细节。
垃圾收集
通常,函数调用完成后,会将词法环境和其中的所有变量从内存中删除。因为现在没有任何对它们的引用了。与 JavaScript 中的任何其他对象一样,词法环境仅在可达时才会被保留在内存中。
但是,如果有一个嵌套的函数在函数结束后仍可达,则它将具有引用词法环境的 [[Environment]]
属性。
在下面这个例子中,即使在(外部)函数执行完成后,它的词法环境仍然可达。因此,此词法环境仍然有效。
例如:
function f() {
let value = 123;
return function() {
alert(value);
}
}
let g = f(); // g.[[Environment]] 存储了对相应 f() 调用的词法环境的引用
请注意,如果多次调用 f()
,并且返回的函数被保存,那么所有相应的词法环境对象也会保留在内存中。下面代码中有三个这样的函数:
function f() {
let value = Math.random();
return function() { alert(value); };
}
// 数组中的 3 个函数,每个都与来自对应的 f() 的词法环境相关联
let arr = [f(), f(), f()];
当词法环境对象变得不可达时,它就会死去(就像其他任何对象一样)。换句话说,它仅在至少有一个嵌套函数引用它时才存在。
在下面的代码中,嵌套函数被删除后,其封闭的词法环境(以及其中的 value
)也会被从内存中删除:
function f() {
let value = 123;
return function() {
alert(value);
}
}
let g = f(); // 当 g 函数存在时,该值会被保留在内存中
g = null; // ……现在内存被清理了
实际开发中的优化
正如我们所看到的,理论上当函数可达时,它外部的所有变量也都将存在。
但在实际中,JavaScript 引擎会试图优化它。它们会分析变量的使用情况,如果从代码中可以明显看出有未使用的外部变量,那么就会将其删除。
在 V8(Chrome,Edge,Opera)中的一个重要的副作用是,此类变量在调试中将不可用。
打开 Chrome 浏览器的开发者工具,并尝试运行下面的代码。
当代码执行暂停时,在控制台中输入 alert(value)
。
function f() {
let value = Math.random();
function g() {
debugger; // 在 Console 中:输入 alert(value); No such variable!
}
return g;
}
let g = f();
g();
正如你所见的 —— No such variable! 理论上,它应该是可以访问的,但引擎把它优化掉了。
这可能会导致有趣的(如果不是那么耗时的)调试问题。其中之一 —— 我们可以看到的是一个同名的外部变量,而不是预期的变量:
let value = "Surprise!";
function f() {
let value = "the closest value";
function g() {
debugger; // 在 console 中:输入 alert(value); Surprise!
}
return g;
}
let g = f();
g();
V8 引擎的这个特性你真的应该知道。如果你要使用 Chrome/Edge/Opera 进行代码调试,迟早会遇到这样的问题。
这不是调试器的 bug,而是 V8 的一个特别的特性。也许以后会被修改。你始终可以通过运行本文中的示例来进行检查。
任务
函数会选择最新的内容吗?
函数 sayHi 使用外部变量。当函数运行时,将使用哪个值?
let name = "John";
function sayHi() {
alert("Hi, " + name);
}
name = "Pete";
sayHi(); // 会显示什么:"John" 还是 "Pete"?
这种情况在浏览器和服务器端开发中都很常见。一个函数可能被计划在创建之后一段时间后才执行,例如在用户行为或网络请求之后。
因此,问题是:它会接收最新的修改吗?
解决方案
答案:Pete。
函数将从内到外依次在对应的词法环境中寻找目标变量,它使用最新的值。
旧变量值不会保存在任何地方。当一个函数想要一个变量时,它会从自己的词法环境或外部词法环境中获取当前值。
哪些变量可用呢?
下面的 makeWorker
函数创建了另一个函数并返回该函数。可以在其他地方调用这个新函数。
它是否可以从它被创建的位置或调用位置(或两者)访问外部变量?
function makeWorker() {
let name = "Pete";
return function() {
alert(name);
};
}
let name = "John";
// 创建一个函数
let work = makeWorker();
// 调用它
work(); // 会显示什么?
会显示哪个值?“Pete” 还是 “John”?
解决方案
答案:Pete.
下方代码中的函数 work()
在其被创建的位置通过外部词法环境引用获取 name
:
所以这里的结果是 "Pete"
。
但如果在 makeWorker()
中没有 let name
,那么将继续向外搜索并最终找到全局变量,正如我们可以从上图中看到的那样。在这种情况下,结果将是 "John"
。
Counter 是独立的吗?
在这儿我们用相同的 makeCounter
函数创建了两个计数器(counters):counter
和 counter2
。
它们是独立的吗?第二个 counter 会显示什么?0,1
或 2,3
还是其他?
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
let counter2 = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter2() ); // ?
alert( counter2() ); // ?
解决方案
答案是:0,1。
函数 counter
和 counter2
是通过 makeCounter
的不同调用创建的。
因此,它们具有独立的外部词法环境,每一个都有自己的 count
。
Counter 对象
这里通过构造函数创建了一个 counter 对象。
它能正常工作吗?它会显示什么呢?
function Counter() {
let count = 0;
this.up = function() {
return ++count;
};
this.down = function() {
return --count;
};
}
let counter = new Counter();
alert( counter.up() ); // ?
alert( counter.up() ); // ?
alert( counter.down() ); // ?
解决方案
当然行得通。
这两个嵌套函数都是在同一个词法环境中创建的,所以它们可以共享对同一个 count 变量的访问:
function Counter() {
let count = 0;
this.up = function() {
return ++count;
};
this.down = function() {
return --count;
};
}
let counter = new Counter();
alert( counter.up() ); // 1
alert( counter.up() ); // 2
alert( counter.down() ); // 1
if 内的函数
看看下面这个代码。最后一行代码的执行结果是什么?
let phrase = "Hello";
if (true) {
let user = "John";
function sayHi() {
alert(`${phrase}, ${user}`);
}
}
sayHi();
解决方案
答案:error。
函数 sayHi
是在 if
内声明的,所以它只存在于 if
中。外部是没有 sayHi
的。
闭包 sum
编写一个像 sum(a)(b) = a+b
这样工作的 sum
函数。
是的,就是这种通过双括号的方式(并不是错误)。
举个例子:
sum(1)(2) = 3
sum(5)(-1) = 4
解决方案
为了使第二个括号有效,第一个(括号)必须返回一个函数。
就像这样:
function sum(a) {
return function(b) {
return a + b; // 从外部词法环境获得 "a"
};
}
alert( sum(1)(2) ); // 3
alert( sum(5)(-1) ); // 4
变量可见吗?
重要程度: 4
下面这段代码的结果会是什么?
let x = 1;
function func() {
console.log(x); // ?
let x = 2;
}
func();
P.S. 这个任务有一个陷阱。解决方案并不明显。
解决方案
答案:error。
你运行一下试试:
let x = 1;
function func() {
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 2;
}
func();
在这个例子中,我们可以观察到“不存在”的变量和“未初始化”的变量之间的特殊差异。
你可能已经在 变量作用域,闭包 中学过了,从程序执行进入代码块(或函数)的那一刻起,变量就开始进入“未初始化”状态。它一直保持未初始化状态,直至程序执行到相应的 let
语句。
换句话说,一个变量从技术的角度来讲是存在的,但是在 let
之前还不能使用。
下面的这段代码证实了这一点。
function func() {
// 引擎从函数开始就知道局部变量 x,
// 但是变量 x 一直处于“未初始化”(无法使用)的状态,直到结束 let(“死区”)
// 因此答案是 error
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 2;
}
变量暂时无法使用的区域(从代码块的开始到 let
)有时被称为“死区”。
通过函数筛选
重要程度: 5
我们有一个内建的数组方法 arr.filter(f)
。它通过函数 f
过滤元素。如果它返回 true
,那么该元素会被返回到结果数组中。
制造一系列“即用型”过滤器:
-
inBetween(a, b)
—— 在 a
和 b
之间或与它们相等(包括)。 -
inArray([...])
—— 包含在给定的数组中。
用法如下所示:
-
arr.filter(inBetween(3,6))
—— 只挑选范围在 3 到 6 的值。 -
arr.filter(inArray([1,2,3]))
—— 只挑选与 [1,2,3]
中的元素匹配的元素。
例如:
/* .. inBetween 和 inArray 的代码 */
let arr = [1, 2, 3, 4, 5, 6, 7];
alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6
alert( arr.filter(inArray([1, 2, 10])) ); // 1,2
解决方案
inBetween 筛选器
function inBetween(a, b) { return function(x) { return x >= a && x <= b; }; } let arr = [1, 2, 3, 4, 5, 6, 7]; alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6
inArray 筛选器
function inArray(arr) { return function(x) { return arr.includes(x); }; } let arr = [1, 2, 3, 4, 5, 6, 7]; alert( arr.filter(inArray([1, 2, 10])) ); // 1,2
按字段排序
我们有一组要排序的对象:
let users = [
{ name: "John", age: 20, surname: "Johnson" },
{ name: "Pete", age: 18, surname: "Peterson" },
{ name: "Ann", age: 19, surname: "Hathaway" }
];
通常的做法应该是这样的:
// 通过 name (Ann, John, Pete)
users.sort((a, b) => a.name > b.name ? 1 : -1);
// 通过 age (Pete, Ann, John)
users.sort((a, b) => a.age > b.age ? 1 : -1);
我们可以让它更加简洁吗,比如这样?
users.sort(byField('name'));
users.sort(byField('age'));
这样我们就只需要写 byField(fieldName)
,而不是写一个函数。
编写函数 byField
来实现这个需求。
解决方案
function byField(fieldName){
return (a, b) => a[fieldName] > b[fieldName] ? 1 : -1;
}
函数大军
重要程度: 5
下列的代码创建了一个 shooters
数组。
每个函数都应该输出其编号。但好像出了点问题……
function makeArmy() {
let shooters = [];
let i = 0;
while (i < 10) {
let shooter = function() { // 创建一个 shooter 函数,
alert( i ); // 应该显示其编号
};
shooters.push(shooter); // 将此 shooter 函数添加到数组中
i++;
}
// ……返回 shooters 数组
return shooters;
}
let army = makeArmy();
// ……所有的 shooter 显示的都是 10,而不是它们的编号 0, 1, 2, 3...
army[0](); // 编号为 0 的 shooter 显示的是 10
army[1](); // 编号为 1 的 shooter 显示的是 10
army[2](); // 10,其他的也是这样。
为什么所有的 shooter 显示的都是同样的值?
修改代码以使得代码能够按照我们预期的那样工作。
解决方案
让我们检查一下 makeArmy
内部到底发生了什么,那样答案就显而易见了。
- 它创建了一个空数组
shooters
: - 在循环中,通过
shooters.push(function)
用函数填充它。 - 该数组返回自函数。
let shooters = [];
每个元素都是函数,所以数组看起来是这样的:
shooters = [
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); },
function () { alert(i); }
];
然后,对数组中的任意数组项的调用,例如调用 army[5]()
(它是一个函数),将首先从数组中获取元素 army[5]()
并调用它。
那么,为什么所有此类函数都显示的是相同的值,10
呢?
这是因为 shooter
函数内没有局部变量 i
。当一个这样的函数被调用时,i
是来自于外部词法环境的。
那么,i
的值是什么呢?
如果我们看一下源代码:
function makeArmy() {
...
let i = 0;
while (i < 10) {
let shooter = function() { // shooter 函数
alert( i ); // 应该显示它自己的编号
};
shooters.push(shooter); // 将 shooter 函数添加到该数组中
i++;
}
...
}
……我们可以看到,所有的 shooter
函数都是在 makeArmy()
的词法环境中被创建的。但当 army[5]()
被调用时,makeArmy
已经运行完了,最后 i
的值为 10
(while
循环在 i=10
时停止)。
因此,所有的 shooter
函数获得的都是外部词法环境中的同一个值,即最后的 i=10
。
正如你在上边所看到的那样,在 while {...}
块的每次迭代中,都会创建一个新的词法环境。因此,要解决此问题,我们可以将 i
的值复制到 while {...}
块内的变量中,如下所示:
function makeArmy() {
let shooters = [];
let i = 0;
while (i < 10) {
let j = i;
let shooter = function() { // shooter 函数
alert( j ); // 应该显示它自己的编号
};
shooters.push(shooter);
i++;
}
return shooters;
}
let army = makeArmy();
// 现在代码正确运行了
army[0](); // 0
army[5](); // 5
在这里,let j = i
声明了一个“局部迭代”变量 j
,并将 i
复制到其中。原始类型是“按值”复制的,因此实际上我们得到的是属于当前循环迭代的独立的 i
的副本。
shooter 函数正确运行了,因为 i
值的位置更近了(译注:指转到了更内部的词法环境)。不是在 makeArmy()
的词法环境中,而是在与当前循环迭代相对应的词法环境中:
如果我们一开始使用 for
循环,也可以避免这样的问题,像这样:
function makeArmy() {
let shooters = [];
for(let i = 0; i < 10; i++) {
let shooter = function() { // shooter 函数
alert( i ); // 应该显示它自己的编号
};
shooters.push(shooter);
}
return shooters;
}
let army = makeArmy();
army[0](); // 0
army[5](); // 5
这本质上是一样的,因为 for
循环在每次迭代中,都会生成一个带有自己的变量 i
的新词法环境。因此,在每次迭代中生成的 shooter
函数引用的都是自己的 i
。
至此,你已经花了很长时间来阅读本文,发现最终的解决方案就这么简单 — 使用 for
循环,你可能会疑问 —— 我花了这么长时间读这篇文章,值得吗?
其实,如果你可以轻松地明白并答对本题目,你应该就不会阅读它的答案。所以,希望这个题目可以帮助你更好地理解闭包。
此外,确实存在有些人相较于 for
更喜欢 while
,以及其他情况。
更多建议: