1. 作用域
作用域就是一个独立的区域,讲得具体点就是在我们的程序中定义变量的一个独立区域,它决定了当前执行代码对变量的访问权限。
在 JavaScript 中有两种作用域:
- 全局作用域
- 局部作用域
如果一个变量在函数外面,或者在代码块外也就是大括号{}
外声明,那么就定义了一个全局作用域,在 ES6 之前局部作用域只包含了函数作用域,ES6 为我们提供的块级作用域,也属于局部作用域。
function fun() {
//局部(函数)作用域
var innerVariable = "inner"
}
console.log(innerVariable)
// Uncaught ReferenceError: innerVariable is not defined
上面的例子中,变量innerVariable
是在函数中,也就是在局部作用域下声明的,而在全局作用域没有声明,所以在全局作用域下输出会报错。
也就是说,作用域就是一个让变量不会向外暴露出去的独立区域。作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
function fun1(){
var variable = 'abc'
}
function fun2(){
var variable = 'cba'
}
上面的例子中,有两个函数,分别都有同名的一个变量variable
,但它位于不同的函数内,也就是位于不同的作用域中,所以他们不会产生冲突。
ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数(局部)作用域。块语句({}
中间的语句),如 if
和 switch
条件语句或 for
和 while
循环语句,不像函数,它们不会创建一个新的作用域。
if(true) {
var a = 1
}
for(var i = 0; i < 10; i++) {
...
}
console.log(a) // 1
console.log(i) // 9
2. 全局作用域
在代码中任何地方都能访问到的对象拥有全局作用域,一般来说以下几种情形拥有全局作用域:
1、最外层函数和在最外层函数外面定义的变量拥有全局作用域
var outVariable = "我是最外层变量";//最外层变量
function outFun() { //最外层函数
var inVariable = "内层变量"
function innerFun() { //内层函数
console.log(inVariable)
}
innerFun()
}
console.log(outVariable) //我是最外层变量
outFun() //内层变量
console.log(inVariable) //inVariable is not defined
innerFun() //innerFun is not defined
上面例子中,调用outFun()
后,里面的 innerFun
函数执行,输出inVariable
变量,因为内层作用域可以访问外层作用域,所以能正常输出内层变量
。但是下一行输出 inVariable
是在全局作用域中,不能访问局部作用域的变量,所以该变量会访问不到。
2、所有未定义直接赋值的变量(也称为意外的全局变量),自动声明为拥有全局作用域
function outFun2() {
variable = "未定义直接赋值的变量";
var inVariable2 = "内层变量2";
}
outFun2(); //要先执行这个函数,否则根本不知道里面有什么
console.log(variable); //“未定义直接赋值的变量”
console.log(inVariable2); //inVariable2 is not defined
3、所有 window 对象的属性拥有全局作用域
一般情况下,window 对象的内置属性都拥有全局作用域,例如 window.name、window.location、window.document、window.history 等等。
全局作用域有个弊端:如果我们写了很多行 JS 代码,变量定义都没有用函数包括,那么它们就全部都在全局作用域中。这样就会污染全局命名空间, 容易引起命名冲突。
// A写的代码中
var data = {a: 1}
// B写的代码中
var data = {b: 2}
这就是为何 jQuery、Zepto 等库的源码,所有的代码都会放在(function(){....})()
(立即执行函数)中。因为放在里面的所有变量,都不会被外泄和暴露,不会污染到外面,不会对其他的库或者 JS 脚本造成影响。这是函数作用域的一个体现。
3. 局部作用域
和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到。局部作用域分为函数作用域和块级作用域。
3.1 函数作用域
函数作用域,是指声明在函数内部的变量或函数。
function doSomething(){
var name = "Rockky";
function innerSay(){
console.log(name);
}
innerSay();
}
console.log(name); //name is not defined
innerSay(); //innerSay is not defined
作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行。我们看个例子,用泡泡来比喻作用域可能好理解一点:
最后输出的结果为 2, 4, 12
- 泡泡 1 是全局作用域,有标识符 foo;
- 泡泡 2 是作用域 foo,有标识符 a,bar,b;
- 泡泡 3 是作用域 bar,仅有标识符 c。
3.2 块级作用域
ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
第一种场景,内层变量可能会覆盖外层变量。
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
上面代码的原意是,if
代码块的外部使用外层的tmp
变量,内部使用内层的tmp
变量。但是,函数f
执行后,输出结果为undefined
,原因在于变量提升,导致内层的tmp
变量覆盖了外层的tmp
变量,而temp
变量的初始化并不会提升,也就是变量声明了但未初始化,所以 temp 的值为undefined
。
注:变量提升的作用域是整个函数,var声明的函数会被提升到所在作用域的最顶端。意思就是说函数中的所有地方都是变量提升的范围,但是只会提升到所在作用域的顶端。
第二种场景,用来计数的循环变量泄露为全局变量。
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
上面代码中,变量i
只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
ES6 的块级作用域在一定程度上解决了这些问题。
块级作用域可通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:
- 在一个函数内部
- 在一个代码块(由一对花括号包裹)内部
let 声明的语法与 var 的语法一致。基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。块级作用域有以下几个特点:
- 声明变量不会提升到代码块顶部,即不存在变量提升
- 禁止重复声明同一变量
- 循环中的绑定块作用域的妙用
3.2.1 变量提升
var
命令会发生 “变量提升” 现象,即变量可以在声明之前使用,值为undefined
。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。为了纠正这种现象,let
命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
上面代码中,变量foo
用var
命令声明,会发生变量提升,即脚本开始运行时,变量foo
已经存在了,但是没有值,因为 JS 引擎只会将变量的声明进行提升,并不会将变量的初始化进行提升。等同于如下代码:
// var 的情况
var foo;
console.log(foo); // 输出undefined
foo = 2;
所以会输出undefined
。变量bar
用let
命令声明,不会发生变量提升。这表示在声明它之前,变量bar
是不存在的,这时如果用到它,就会抛出一个错误。
如果有函数和变量同时声明了,哪个才会进行变量提升呢?
console.log(foo);
var foo = 'abc';
function foo(){}
输出结果是function foo(){}
, 也就是函数内容。如果是另外一种形式呢?
console.log(foo);
var foo = 'abc';
var foo = function(){}
输出结果是undefined
对两种结果进行分析说明:
- 第一种:函数声明。就是上面第一种,
function foo(){}
这种形式 - 第二种:函数表达式。就是上面第二种,
var foo = function(){}
这种形式
第二种形式其实就是 var 变量的声明定义,因此上面的第二种输出结果为 undefined 应该就能理解了。 而第一种函数声明的形式,在提升的时候,会被整个提升上去,包括函数定义的部分!因此第一种形式跟下面的这种方式是等价的!
var foo = function(){}
console.log(foo);
var foo ='abc';
- 函数声明被提升到最顶上;
- 声明只进行一次,因此后面
var foo='abc'
的声明会被忽略。 - 函数声明的优先级优于变量声明,且函数声明会连带定义一起被提升(这里与变量不同)
只要块级作用域内存在let
命令,它所声明的变量就 “绑定”(binding)这个区域,不再受外部的影响。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
上面代码中,存在全局变量tmp
,但是块级作用域内let
又声明了一个局部变量tmp
,导致后者绑定这个块级作用域,所以在let
声明变量前,对tmp
赋值会报错。
ES6 明确规定,如果区块中存在let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用let
命令声明变量之前,该变量都是不可用的。这在语法上,称为 “暂时性死区”(temporal dead zone,简称 TDZ)。
if (true) {
// 暂时性死区开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // 暂时性死区结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
上面代码中,在let
命令声明变量tmp
之前,都属于变量tmp
的 “死区”。
“暂时性死区” 也意味着typeof
不再是一个百分之百安全的操作。
typeof x; // ReferenceError
let x;
上面代码中,变量x
使用let
命令声明,所以在声明之前,都属于x
的 “死区”,只要用到该变量就会报错。因此,typeof
运行时就会抛出一个ReferenceError
。
作为比较,如果一个变量根本没有被声明,使用typeof
反而不会报错。
typeof undeclared_variable // "undefined"
上面代码中,undeclared_variable
是一个不存在的变量名,结果返回 “undefined”。所以,在没有let
之前,typeof
运算符是百分之百安全的,永远不会报错。现在这一点不成立了。这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。
有些 “死区” 比较隐蔽,不太容易发现。
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
上面代码中,调用bar
函数之所以报错(某些实现可能不报错),是因为参数x
默认值等于另一个参数y
,而此时y
还没有声明,属于 “死区”。如果y
的默认值是x
,就不会报错,因为此时x
已经声明了。
function bar(x = 2, y = x) {
return [x, y];
}
bar(); // [2, 2]
另外,下面的代码也会报错,与var
的行为不同。
// 不报错
var x = x;
// 报错
let x = x; // ReferenceError: x is not defined
上面代码报错,也是因为暂时性死区。使用let
声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量x
的声明语句还没有执行完成前,就去取x
的值,导致报错”x 未定义 “。
ES6 规定暂时性死区和let
、const
语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。
总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
let/const
声明并不会被提升到当前代码块的顶部,因此你需要手动将 let/const
声明放置到顶部,以便让变量在整个代码块内部可用。
function getValue(condition) {
if (condition) {
let value = "blue";
return value;
} else {
// value 在此处不可用
return null;
}
// value 在此处不可用
}
块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。
// IIFE 写法
(function () {
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
3.2.2 重复声明
如果一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符进行 let 声明就会导致抛出错误。例如:
// 报错
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}
但如果在嵌套的作用域内使用 let
声明一个同名的新变量,则不会抛出错误。
var count = 30;
if (condition) {
let count = 40; // 不会抛出错误
}
此外,也不能在函数内部重新声明参数。
function func(arg) {
let arg;
}
func() // 报错
function func(arg) {
{
let arg;
}
}
func() // 不报错
另外,还有一个需要注意的地方。ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
// 第一种写法,报错
if (true) let x = 1;
// 第二种写法,不报错
if (true) {
let x = 1;
}
3.2.3 for 循环
开发者可能最希望实现for
循环的块级作用域了,因为可以把声明的计数器变量限制在循环内,例如:
for (let i = 0; i < 10; i++) {
// ...
}
console.log(i); // ReferenceError: i is not defined
上面代码中,计数器i
只在for
循环体内有效,在循环体外引用就会报错。
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
上面代码中,变量i
是 var 命令声明的,在全局范围内都有效,所以全局只有一个变量i
。每一次循环,变量i
的值都会发生改变,而循环内被赋给数组a
的函数内部的console.log(i)
,里面的 i 指向的就是全局的i
。也就是说,所有数组a
的成员里面的i
,指向的都是同一个i
,导致运行时输出的是最后一轮的i
的值,也就是 10。
如果使用let
,声明的变量仅在块级作用域内有效,最后输出的是 6。
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
上面代码中,变量i
是let
声明的,当前的i
在本轮循环有效,所以每一次循环的i
其实都是一个新的变量,所以最后输出的是6
。你可能会问,如果每一轮循环的变量i
都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i
时,就在上一轮循环的基础上进行计算。
再看下面这个例子:
<button>点我打印</button>
<button>点我打印</button>
<button>点我打印</button>
let el = document.querySelectorAll('button')
let i
for (i = 0; i < 3; i++) {
el[i].addEventListener('click', function (event) {
console.log(i)
})
}
依次点击 button 按钮,会打印出什么呢?
答案是:3,3,3
因为let i
是在全局下声明的,for 循环里每一次循环改变的都是全局的i
,类似于上面用var
声明的那个例子,所以最终输出都是 3.
另外,for
循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部又是一个单独的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
上面代码正确运行,输出了 3 次abc
。这表明函数内部的变量i
与循环变量i
不在同一个作用域,有各自单独的作用域(同一个作用域不可使用 let
重复声明同一个变量)。
4. 作用域链
如下代码中,console.log(a)
要得到变量 a,但是在当前的作用域中没有定义 a(可对比一下 b)。当前作用域没有定义的变量,会成为 自由变量
。自由变量的值如何得到呢?它会向父级作用域一层一层地向外查找,直到找到全局window
对象,也就是全局作用域,如果全局作用域里还没有,就返回undefined
。类似于顺着一条链条从里往外一层一层查找变量,这条链条,我们就称之为作用域链。内部环境可以通过作用域链访问所有外部环境,但外部环境不能访问内部环境的任何变量和函数。
var a = 100
function fn() {
var b = 200
console.log(a) // 这里的a在这里就是一个自由变量
console.log(b)
}
fn() // 100
下面再来看一个例子
var x = 10
function fn() {
console.log(x)
}
function show(f) {
var x = 20
(function() {
f() //10,而不是20
})()
}
show(fn)
在 fn 函数中,取自由变量 x 的值时,要到哪个作用域中取?——要到创建 fn 函数的那个作用域中取,无论 fn 函数将在哪里调用。
所以,用这句话描述自由变量的取值过程可能会更加贴切:要到创建这个函数的那个作用域中取值, 这里强调的是 “创建”,而不是 “调用”(有点类似于箭头函数的 this 指向) ,切记切记——其实这就是所谓的 "静态作用域"
var a = 10
function fn() {
var b = 20
function bar() {
console.log(a + b) //30
}
return bar
}
var x = fn(),
b = 200
x() //bar()
fn()
返回的是bar
函数,赋值给x
。执行x()
,即执行bar
函数代码。取b
的值时,直接在fn
作用域取出。取a
的值时,试图在fn
作用域取,但是取不到,只能转向创建fn
的那个作用域中去查找,结果找到了, 所以最后的结果是30
5、闭包
(1)闭包基本概念
MDN 中闭包的定义:
一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。
通俗来讲,闭包其实就是一个可以访问其他函数内部变量的函数。即一个定义在函数内部的函数,或者说闭包是个内嵌函数。
通常情况下,函数内部变量是无法在外部访问的(即全局变量和局部变量的区别),因此使用闭包的作用,就具备实现了能在外部访问某个函数内部变量的功能,让这些内部变量的值始终可以保存在内存中:
function fun1() {
var a = 1;
return function(){
console.log(a);
};
}
var result = fun1();
result(); // 1
这段代码在控制台中输出的结果是 1(即 a 的值)。可以发现,a 变量作为一个 fun1 函数的内部变量,正常情况下作为函数内的局部变量,是无法被外部访问到的。但是通过闭包,最后可以拿到 a 变量的值。
从直观上来看,闭包这个概念为 JavaScript 中访问函数内变量提供了途径和便利。这样做的好处很多,比如,可以利用闭包实现缓存等。
(2)闭包产生原因
前面说了作用域的概念,我们还需要知道作用域链的基本概念。当访问一个变量时,代码解释器会首先在当前的作用域查找,如果没找到,就去父级作用域去查找,直到找到该变量或者不存在父级作用域中,这样的链路就是作用域链。
需要注意,每一个子函数都会拷贝上级的作用域,形成一个作用域链:
var a = 1;
function fun1() {
var a = 2
function fun2() {
var a = 3;
console.log(a);//3
}
}
}
可以看到,fun1 函数的作用域指向全局作用域(window)和它自己本身;fun2 函数的作用域指向全局作用域 (window)、fun1 和它本身;而作用域是从最底层向上找,直到找到全局作用域 window 为止,如果全局还没有的话就会报错。这就很形象地说明了什么是作用域链,即当前函数一般都会存在上层函数的作用域的引用,那么他们就形成了一条作用域链。
由此可见,闭包产生的本质就是:当前环境中存在指向父级作用域的引用。
function fun1() {
var a = 2
function fun2() {
console.log(a); //2
}
return fun2;
}
var result = fun1();
result();
可以看到,这里 result 会拿到父级作用域中的变量,输出 2。因为在当前环境中,含有对 fun2 函数的引用,fun2 函数恰恰引用了 window、fun1 和 fun2 的作用域。因此 fun2 函数是可以访问到 fun1 函数的作用域的变量。
那是不是只有返回函数才算是产生了闭包呢?其实也不是,回到闭包的本质,只需要让父级作用域的引用存在即可,因此可以这样修改上面的代码:
var fun3;
function fun1() {
var a = 2
fun3 = function() {
console.log(a);
}
}
fun1();
fun3();
可以看到,其中实现的结果和前一段代码的效果其实是一样的,就是在给 fun3 函数赋值后,fun3 函数就拥有了 window、fun1 和 fun3 本身这几个作用域的访问权限;然后还是从下往上查找,直到找到 fun1 的作用域中存在 a 这个变量;因此输出的结果还是 2,最后产生了闭包,形式变了,本质没有改变。
(3)闭包应用场景
下面来看看闭包的表现形式及应用场景:
- 在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包:
// 定时器
setTimeout(function handler(){
console.log('1');
},1000);
// 事件监听
document.getElementById(app).addEventListener('click', () => {
console.log('Event Listener');
});
- 作为函数参数传递的形式:
var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 这是闭包
fn();
}
foo(); // 输出2,而不是1
- IIFE(立即执行函数),创建了闭包,保存了全局作用域(window)和当前函数的作用域,因此可以输出全局的变量:
var a = 2;
(function IIFE(){
console.log(a); // 输出2
})();
IIFE 是一种自执行匿名函数,这个匿名函数拥有独立的作用域。这不仅可以避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。
- 结果缓存(备忘模式)
备忘模式就是应用闭包的特点的一个典型应用。比如下面函数:
function add(a) {
return a + 1;
}
当多次执行 add() 时,每次得到的结果都是重新计算得到的,如果是开销很大的计算操作的话就比较消耗性能了,这里可以对已经计算过的输入做一个缓存。所以这里可以利用闭包的特点来实现一个简单的缓存,在函数内部用一个对象存储输入的参数,如果下次再输入相同的参数,那就比较一下对象的属性,如果有缓存,就直接把值从这个对象里面取出来。实现代码如下:
function memorize(fn) {
var cache = {}
return function() {
var args = Array.prototype.slice.call(arguments)
var key = JSON.stringify(args)
return cache[key] || (cache[key] = fn.apply(fn, args))
}
}
function add(a) {
return a + 1
}
var adder = memorize(add)
adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(2) // 输出: 3 当前: cache: { '[1]': 2, '[2]': 3 }
使用 ES6 的方式实现:
function memorize(fn) {
const cache = {}
return function(...args) {
const key = JSON.stringify(args)
return cache[key] || (cache[key] = fn.apply(fn, args))
}
}
function add(a) {
return a + 1
}
const adder = memorize(add)
adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(2) // 输出: 3 当前: cache: { '[1]': 2, '[2]': 3 }
备忘函数中用 JSON.stringify 把传给 adder 函数的参数序列化成字符串,把它当做 cache 的索引,将 add 函数运行的结果当做索引的值传递给 cache,这样 adder 运行的时候如果传递的参数之前传递过,那么就返回缓存好的计算结果,不用再计算了,如果传递的参数没计算过,则计算并缓存 fn.apply(fn, args),再返回计算的结果。
(4)循环输出问题
最后来看一个常见的和闭包相关的循环输出问题,代码如下:
for(var i = 1; i <= 5; i ++){
setTimeout(function() {
console.log(i)
}, 0)
}
这段代码输出的结果是 5 个 6,那为什么都是 6 呢?如何才能输出 1、2、3、4、5 呢?
可以结合以下两点来思考第一个问题:
- setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。
- 因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6。
那如何按顺序依次输出 1、2、3、4、5 呢?
1)利用 IIFE 可以利用 IIFE(立即执行函数),当每次 for 循环时,把此时的变量 i 传递到定时器中,然后执行,改造之后的代码如下。
for(var i = 1;i <= 5;i++){
(function(j){
setTimeout(function timer(){
console.log(j)
}, 0)
})(i)
}
可以看到,通过这样改造使用 IIFE(立即执行函数),可以实现序号的依次输出。利用立即执行函数的入参来缓存每一个循环中的 i 值。
2)使用 ES6 中的 let ES6 中新增的 let 定义变量的方式,使得 ES6 之后 JS 发生革命性的变化,让 JS 有了块级作用域,代码的作用域以块级为单位进行执行。
for(let i = 1; i <= 5; i++){
setTimeout(function() {
console.log(i);
},0)
}
可以看到,通过 let 定义变量的方式,重新定义 i 变量,则可以用最少的改动成本,解决该问题。
3)定时器第三个参数 setTimeout 作为经常使用的定时器,它是存在第三个参数的。我们经常使用前两个,一个是回调函数,另外一个是定时时间,setTimeout 从第三个入参位置开始往后,是可以传入无数个参数的。这些参数会作为回调函数的附加参数存在。那么结合第三个参数,调整完之后的代码如下:
for(var i=1;i<=5;i++){
setTimeout(function(j) {
console.log(j)
}, 0, i)
}
可以看到,第三个参数的传递,可以改变 setTimeout 的执行逻辑,从而实现想要的结果,这也是一种解决循环输出问题的途径。
参考
- 阮一峰的 ES6 教程:es6.ruanyifeng.com/#docs/let
- 深入理解 JavaScript 作用域和作用域链:juejin.cn/post/684490…
- 深入理解 JavaScript 从作用域与作用域链开始:juejin.cn/post/684490…
- 重学 JavaScript 作用域和闭包 - 掘金 (juejin.cn)