【js】作用域、作用域链和闭包

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. 全局作用域

在代码中任何地方都能访问到的对象拥有全局作用域,一般来说以下几种情形拥有全局作用域:

扫描二维码关注公众号,回复: 14956076 查看本文章

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 声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:

  1. 在一个函数内部
  2. 在一个代码块(由一对花括号包裹)内部

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;

上面代码中,变量foovar命令声明,会发生变量提升,即脚本开始运行时,变量foo已经存在了,但是没有值,因为 JS 引擎只会将变量的声明进行提升,并不会将变量的初始化进行提升。等同于如下代码:

// var 的情况
var foo;
console.log(foo); // 输出undefined
foo = 2;

所以会输出undefined。变量barlet命令声明,不会发生变量提升。这表示在声明它之前,变量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 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用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 规定暂时性死区和letconst语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 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

上面代码中,变量ilet声明的,当前的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 的执行逻辑,从而实现想要的结果,这也是一种解决循环输出问题的途径。

 参考

猜你喜欢

转载自blog.csdn.net/qq_37308779/article/details/125923427