作用域和闭包及预解析,IIFE,var,let,const区别

309 阅读18分钟

1.作用域

JavaScript是函数级作用域编程语言:变量只在其定义时所在的function内部有意义。

function fn(){
    let a = 10;  //变量a在fn函数内部定义  fn就是a的作用域  变量a被称为局部变量  
}

fn();

console.log(a); //报错

全局变量

如果不将变量定义在任何函数的内部,此时这个变量就是全局变量,它在任何函数内都可以被访问和

更改。

let a;

function fn(){
    a = 10;
    a++;
    console.log(a);
}

fn();
console.log(a);

遮蔽效应

如果函数中也定义了和全局同名的变量,则函数内的变量会将全局的变量“遮蔽”

//全局变量
let a = 10;

function fn(){
    //局部变量,会把全局变量a遮蔽
    let a = 666;
    a++;
    console.log(a);
}		

fn(); //667
console.log(a); //10

注意变量提升的情况

var a = 10;

function fn(){
    a++;  //局部变量a被自增 1  a此时是undefined 自增1结果是NaN
    var a = 20;
    console.log(a); //20
}

fn();  
console.log(a); // 10

形参也是局部变量

let a = 10;

function fn(a) {
    a++;
    console.log(a);
}

fn(699); //700
console.log(a); //10

作用域链

函数的嵌套

一个函数内部也可以定义一个函数。

和局部变量类似,定义在一个函数内部的函数是局部函数。

function fn(){
    function inner(){
        console.log("云牧DSB");
    }
    inner();  //调用内部函数
}

fn(); //调用外部函数
// inner(); 报错

在函数嵌套中,变量会从内到外逐层寻找它的定义。

let a = 10;
let b = 20;
function fn() {
    let c = 30;
    function inner() {
        let a = 40;
        let d = 50;
        console.log(a, b, c, d);  //使用变量时  Js会从当前层开始,逐层向上寻找定义
    }
    inner();
}

fn(); //40 20 30 50

//先从自身的AO局部作用域开始寻找  自身有用自己  没有的话一层层往上找  直到全局作用域GO  

函数的作用域 是定义函数的时候决定的

而不是执行函数时候决定的

let a = 10;

function f(){
    console.log(a);
}

function ff(){
    let a = 20;
    f();
}

ff();//10

不加let将定义全局变量

在初次给变量赋值时,如果没有加var,则将定义全局变量

function fn(){
    a = 666;
}
fn();
console.log(a);
let a = 1;
let b = 2;

function fn(){
    //字母c没有加关键字定义,所以它就变为了全局变量
    c = 3;
    c ++;
    let b = 4;
    b++;
    console.log(b);
}

fn();
console.log(b);
//函数外部可以访问变量C
console.log(c);

2.var let const

ES6之前的作用域

全局作用域

函数作用域

块级作用域

let const 有块级作用域

通俗的讲就是一对花括号中的区域{ ...}

{} for(){} while(){} do{}while() if(){} switch(){}

块级作用域可以嵌套

let变量

let 声明 变量 代替 var

1.let声明的变量只在当前(块级)作用域内有效

{
    let age = 18;
    console.log(age); //18
}

console.log(age); //报错
if(true){
    var a = 10;
    let b  = 20;
}
console.log(a);//10
console.log(b);//报错
let b = 666;
if(true){
    var a = 10;
    let b  = 20;
}
console.log(a);//10
console.log(b);//666
for(let i = 0 ; i<3; i++){
	let a = 20;
}

console.log(a);//报错
console.log(i);//报错

2.使用let const 声明的变量 不能重新被声明

var 允许重复声明 let const不允许

let不允许在相同作用域内,已经存在的变量和常量 又声明一遍 ;

var name1 = "我是云牧";
var name1 = "我是夕颜"
console.log(name);//我是夕颜
var name2 = "云牧";
let name2 = "夕颜";//报错
let name2 = "我也是云牧";u
let name2 = "我也是夕颜"; //报错
function fn(a){
    let a = 1;
}
fn(2); //报错

因此,不能在函数内部重新声明参数。

3.let不存在变量提升

var 会提升变量的声明到当前作用域的顶部

console.log(n);//undefined
var n = "云牧";
console.log(n);//报错

let n = "云牧";

4.暂时性死区(TDZ)

let const 定义存在 暂存死区 禁止在声明的位置前访问

只要作用域内有 let const 他们所声明的变量或者常量就会自动"绑定"在这个区域,不再受外部作用域的影响

let a = 2;
function fn(){
    console.log(a);
    let a = 1;
}
fn(); //报错 内部函数定义了a 绑定这个作用域 你只要找a变量只会在当前作用域去查找 

//养成良好的编程习惯,对于所有的变量或者常量,做到先声明,后使用

5.window上面的属性和方法

全局作用域中,var声明的变量,通过function声明的函数,会自动变成 window对象的属性或方法

let const不会

全局作用域的本质是全局对象的属性

览器中全局对象是window,我们申明的变量都相当于在全局对象window下添加属性

var age = 18;
console.log(window.age); //18


function fn(){}
console.log(window.fn); 
console.log(window.fn === fn);  //true

let b = 2
console.info(window.b)  // undefined

const 常量

常量必须在声明的时候赋值 目的就是为了那些一旦初始化就不希望重新赋值的情况设计的

注意

​ 使用const必须在声明的时候赋值 不能留到以后赋值 其他特性参考let

const a; //报错

const允许在不重新赋值的情况下修改它的值

主要针对引用数据类型

//基本数据类型
const name = "云牧";
name  = "夕颜"; //报错
//引用数据类型
const obj = {
    name: "云牧"
}
//obj = {}; 报错
obj.name = "夕颜";

console.log(obj);

3.预解析

console.log(a)  // undefined   (没有报错,而是输出了undefined)

var a = 10;

console.info(a)  // 10 

函数声明和函数表达式的区别

a();//函数声明允许提前调用
b();//通过 = 赋值定义的函数  不允许提前使用

function a(){
    console.log("a函数");
}

let b = function(){
    console.log("b函数");
}

解析顺序可以整体的分为两步,第一步定义,第二步执行

定义过程

找到当前作用域所有的var声明的变量名

找到当前作用域function定义的有名函数块

提升在最开头

注意

var声明的变量名提升此时仅仅只是变量名,

后面的 = 号不会在这一步执行,也就是说在这一步,所有var的变量都是初始值undefined。

执行过程

会从上到下的执行代码,也就是我们传统理解的那样了

变量提升只在当前作用域执行:

如果在执行过程中,执行了函数,

那么就会开辟一个新的子作用域,此时会进入新的作用域解析里面的代码,

同样的也遵循上述的两项解析步骤。

var x = 10;
fn();
console.log(x);//10

function fn(){
    var x = 20;
}

/*
以上代码的执行步骤为:
 首先进行变量提升
	var x;
	function fn(){ alert(1) }   

之后再进行赋值和函数执行
	x = 10;
	fn();
		fn ==> 产生新的作用域   ==>  1.定义  var x ;
								  2.执行  x = 20;(当前作用域的x)

	console.log(x)  // 10
*/

变量/函数 重复定义

var var重名只留一个

function function重名留后面的

var function重名留function ( 不管定义顺序如何 function 都会覆盖同名的变量 )

var x = 10;
var x = 20;
console.log(x)   // 20
console.log(y); // function y(){ alert(2) }
function y(){ alert(1) }
function y(){ alert(2) }
console.log(y)  // function y(){ alert(2) }
console.log(z); // function z(){ alert(1) }
function z() {
    alert(1);
}
var z = 10;

小练习

题目1

var x = 5;
a();
function a(){
    alert(x);//undefined
    var x = 10;
}
alert(x);//5

/*
1.定义
	var x;
	function a(){...}
 2.执行
 	x = 5;
    a()   ===> 新的作用域  ===>  1.定义
     								var x;
                 				2.执行
                                	  alert(x);//undefined
                 					 x = 10;(不影响全局)
     alert(x);//5
 */

题目2

a();
function a(){
    alert(x);//undefined
    var x = 10;
}
alert(x);//报错

/*
1.定义
	function a(){...}
2.执行
	a()  ===>  新的作用域  ===> 1.定义
    								var x;
                 			   2.执行
                               		alert(x)//undefined
     
    alert(x)//报错
*/

题目3

function a(){
    alert(1);
}
var a;
alert(a);//函数体

题目4

alert(a);//函数体
var a = 10;
alert(a);//10
function a(){alert(20)}
alert(a);//10
var a = 30;
alert(a);//30
function a(){alert(40)}
alert(a);//30

/*
1.定义
	function a(){alert(40)}
2.执行
	alert(a);函数体
    a = 10;(当前作用域有a 修改自己的a)
	alert(a);//10
	alert(a);//10
	a = 30;(当前作用域有a 修改自己的a)
	alert(a);//30
	alert(a);//30
*/

题目5

var a = 10;
alert(a);//10
a();//报错
function a(){
    alert(20);
}

/*
1.定义
	function a(){...}
2.执行
	a = 10;
    alert(a);//10
   	a();//报错
*/

题目6

a();//2
var a = function(){alert(1)};
a();//1
function a(){alert(2)};
a();//1
var a = function(){alert(3)};
a();//3

/*
1.定义
	function a(){alert(2)};
2.执行
	a();//2
	a = function(){alert(1)};
	a();//1
	a();//1
	a = function(){alert(3)};
	a();//3
*/

题目7

var a = 10;
function fn() {
    alert(a);//undefined
    var a = 1;
    alert(a);//1
}
fn();
alert(a);//10

/*
1.定义
	var a;
	function fn() {...}
2.执行
	a = 10;
    fn()  ==> 新的作用域  ===> 1.定义
    							 var a;
                   			  2.执行
                              		alert(a);//undefined
                   					a=1;
                   					alert(a);//1
     alert(a);//10  
*/

题目8

fn();
alert(a);//undefined
var a = 10;
alert(a);//10
function fn(){
    var a = 1;
}

/*
1.定义
	var a;
	function fn(){}
2.执行
	fn();  ==> 新的作用域  ===> 1.定义
    							var a;
                                2.执行
                                   a = 1;(当前作用域有a 修改自己的a)
	alert(a);//undefined
	a = 10;
	alert(a);//10
*/

4.垃圾回收机制

JavaScript自动回收不再使用的变量,释放其所占用的内存,开发者不需要手动做垃圾回收的处理。

Javascript 会找出不再使用的变量,不再使用意味着这个变量生命周期的结束。

Javascript 中存在两种变量——全局变量和局部变量,全部变量的声明周期会一直持续,直到页面卸载,所以当我们定义了一个全局的对象时,使用完毕之后,最好给它重新赋值为null,以便释放它所占的内存(这个变量没有被回收,只是改变了指向,减少内存占用。)

但是有一种情况的局部变量不会随着函数的结束而被回收,那就是局部变量被函数外部的变量所使用,其中一种情况就是闭包

垃圾回收的两种实现方式

垃圾回收有两种实现方式,分别是标记清除引用计数

标记清除:

当某个变量不再被使用时,该变量就会被回收。

当变量进入执行环境时标记为“进入环境”,当变量离开执行环境时则标记为“离开环境”,

被标记为“进入环境”的变量是不能被回收的,因为它们正在被使用,

而标记为“离开环境”的变量则可以被回收

function fn1 () {
      let a = 1;
      let b = 2;
      // 函数执行时,a b 分别被标记 进入环境
}

fn1(); // 函数执行结束,a b 被标记 离开环境,被回收

引用计数:

极少数浏览器(如 IE object-c)上针对引用类型的回收机制

统计引用类型变量声明后被引用的次数,当次数为 0 时,该变量将被回收

当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1

如果这个变量的值又被赋值给了另外一个变量(即两个变量的地址都指向同一个引用类型),则该值的引用次数+1。

相反,如果包含这个引用类型的变量又取了另外一个值,则引用次数 - 1。

当这个引用类型的引用次数变为 0 时,则说明无法再访问这个变量。

当垃圾收集器下次再运行时,它就会释放引用次数为 0 的值所占用的内存。

let xm = {
     name : 'xm',
     age:18
}//1

let xh  = xm ;//2
xh = {};//1
xm = {};//0

但是引用计数的方式,有一个相对明显的缺点——循环引用

function fn(){
    let xm = {};//1
    let xh = {};//1
}

fn();//fn里面的xm xh是局部变量使用完了之后会把xmxh默认设置为 null
xm = null;
xh = null;
function  fn(){
    let xm ={}; //1
    let xh = {}; //1
    xm.wife = xh;//2
    xh.husband = xm;//2
}

fn();
xm = null; //1
xm = null; //1
//此时这个变量不会被回收

//像上面这种情况就需要手动将变量的内存释放
xm.wife  = null
xh.husband = null
let x = 10;

function fn(){
    let xx = 100;
    console.log(xx);//xx(局部变量)被使用完了之后,就会被垃圾回收机制回收
}

fn();
//全局变量永远不回收(关闭当前网页回收)

在现代浏览器中,Javascript 使用的方式是标记清除,所以我们无需担心循环引用的问题

5.闭包

从一个题目开始

//创建一个函数
function fn(){
    //定义局部变量
    let name = "云牧";
	//定义一个局部函数
    function innerFun(){
        alert(name);
    }
	
    return innerFun;  //返回了内部函数
}

//定义一个全局变量
let name = "夕颜";

let inner = fn(); // 内部函数被移动到了外部执行
//执行inner函数,就相当于在fn函数的外部,执行了内部的函数
inner();

什么是闭包

JavaScript中函数会产生闭包(closure)。闭包是函数本身和该函数声明时所处的环境状态的组合。

**函数能够“记忆住”其定义时所处的环境,**即使函数不在其定义的环境中被调用,也能访问定义时所处环境的变量。

闭包: 一个函数中嵌套另一个函数,内部函数使用了外部函数的参数或变量,就构成了闭包(这个内部函数就叫做闭包)

或者 父级作用域 包裹了 子作用域 子作用域使用了父作用域的变量/参数

闭包使得局部的变量/参数 不会被回收

在JavaScript中,每次创建函数时都会创建闭包。

但是,闭包特性往往需要将函数“换一个地方”执行,能被观察出来。

function fn1(){
    //父作用域
    let x = 10;

    function fn2(){
        //子作用域
        x++;
        console.log(x);
    }
}
//父级作用域不能是全局 全局没有闭包的概念

//上述代码  子作用域使用了父作用域的变量 构成闭包
let x = 10;

function fn() {
    let a = 666;
    a++;
    console.log(a); //局部变量a使用完了之后就会被垃圾回收机制回收
}

fn();  //667
fn();  //667
let x = 20;
function fn(){
    x++;
}
fn();
//虽然全局变量不会被回收,但是会造成命名污染

JavaScript 有两种作用域:全局作用域和函数作用域。函数内部可以直接读取全局变量。

let n = 999;

function f1() {
  console.log(n);
}
f1() // 999

上面代码中,函数f1可以读取全局变量n

但是,正常情况下,函数外部无法读取函数内部声明的变量。

function f1() {
  let n = 999;
}

console.log(n) // Uncaught ReferenceError: n is not defined
// 函数f1内部声明的变量n,函数外是无法读取的。

如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。

function f1() {
  let n = 999;
  function f2() {
  console.log(n); // 999
  }
}

上面代码中,函数f2就在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是 JavaScript 语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

既然f2可以读取f1的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

function f1() {
    let n = 999;
    function f2() {
        console.log(n);
    }
    return f2;
}

let result = f1();
result(); // 999

上面代码中,函数f1的返回值就是函数f2,由于f2可以读取f1的内部变量,所以就可以在外部获得f1的内部变量了。

闭包就是函数f2,即能够读取其他函数内部变量的函数。由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

闭包的最大用处有两个,一个是可以读取外层函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。

function createIncrementor(start) {
  return function () {
    return start++;
  };
}

var inc = createIncrementor(5);

inc() // 5
inc() // 6
inc() // 7

上面代码中,start是函数createIncrementor的内部变量。通过闭包,start的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc使得函数createIncrementor的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。

为什么闭包能够返回外层函数的内部变量?原因是闭包(上例的inc)用到了外层变量(start),导致外层函数(createIncrementor)不能从内存释放。只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取。

闭包非常实用

闭包很有用,因为它允许我们将数据与操作该数据的函数关联起来。这与“面向对象编程”有少许相似之处。

闭包的功能:记忆性、模拟私有变量。

闭包用途1-记忆性

当闭包产生时,函数所处环境的状态会始终保持在内存中,不会在外层函数调用后被自动清除。这就是闭包的记忆性

闭包的记忆性举例

创建体温检测函数checkTemp(n),可以检查体温 n 是否正常,函数会返回对应提示。

但是,不同的小区有不同的体温检测标准,比如A小区体温合格线是37.1度,而B小区体温合格线是37.3度,应该如何编程?

<script>
    function createCheckTemp(standardTemp){
        function checkTemp(n){
            if(n >= standardTemp){
                alert("你的体温偏高");
            }else{
                alert("体温正常,可以通过");
            }
        }
        return checkTemp;
    }
	
    //创建一个checkTemp函数,它以37.1度为标准线
    let checkTemp_A = createCheckTemp(37.1);

    // checkTemp_A(37.2);

   //创建一个checkTemp函数,它以37.3度为标准线
    let checkTemp_B = createCheckTemp(37.3);

    checkTemp_B(37.2)
</script>

闭包用途2–模拟私有变量

题目:请定义一个变量a,要求是能保证这个a只能被进行指定操作(如加1、乘2),而不能进行其他操作,应该怎么编程呢?

在Java、C++等语言中,有私有属性的概念,但是JavaScript中只能用闭包来模拟。

<script>
    //封装一个函数 这个函数的功能就是私有化变量
    function fn() {
        //定义一个局部变量a
        let a = 1;

        return {
            getA: function () {
                return a;
            },
            addA: function () {
                a++;
            },
            subA: function () {
                a--;
            },
            pow: function () {
                a *= 2;
            }
        };
    }

    let obj = fn();
    //如果想在fn函数外面使用变量a,唯一的方法就是调用getA()方法
    console.log(obj.getA());
    
    obj.addA();
    obj.pow();
    obj.addA();
    console.log(obj.getA());
</script>

使用闭包的注意点

不能滥用闭包,否则会造成网页的性能问题,严重时可能导致内存泄露。

所谓内存泄漏是指程序中不再被需要的内存, 由于某种原因, 无法被释放

闭包一道面试题

<script>
    function addCount() {
        let count = 0;
        return function () {
            count = count + 1;
            console.log(count);
        };
    }

    let fn1 = addCount();
    let fn2 = addCount();
    fn1();
    fn2();
    fn2();
    fn1();
</script>

事件函数里面为什么建议使用this

<style>
    #wrap{
        width: 100px;
        height: 100px;
        background-color: pink;
    }
</style>
<body>
    <div id="wrap"></div>
    <script>
        (function(){
            let oWrap = document.getElementById("wrap");

            oWrap.onclick  = function(){
                this.innerHTML = "云牧丫";
            }
        })();

        //换成this 此时子作用域没有引用父作用域的变量 oWrap就可以被回收
    </script>
</body>

6.IIFE

llFE(Immediately Invoked Function Expression,立即调用函数表达式)是一种特殊的JavaScript函数

写法,一旦被定义,就立即被调用

(function () 
 	statements
})();
//外边括号会将函数变为表达式  后面直接加括号运行函数

这种类型的立即执行函数其实可以从计算公式去理解

function add(){
	return 3;
}

let a= 1 + add(); //a结果是4  add函数执行一次然后将返回值用来计算,

//那么我们去掉1
var b = +add();//b就等于3
//所以 +add()就变成+function(){return 3;}();
//这就是立执行函数的原理

作用

让每个模块独立出来 ,不必为函数命名,避免了污染全局变量

可以封装一些外部无法读取的私有变量。

有时,我们需要在定义函数之后,立即调用该函数。

这时,你不能在函数的定义之后加上圆括号,这会产生语法错误。

function f(){ /* code */ }();

//报错
//JavaScript 规定,如果function关键字出现在行首,一律解释成语句
//引擎看到行首是function关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错

解决方法就是不要让function出现在行首,让引擎将其理解成一个表达式

let b = function(){
	console.log("b函数");
}();

//函数表达式可以加括号自执行  这个写法很奇怪

还有哪些方式可以将函数变成函数表达式呢?

最简单的处理

就是将其放在一个圆括号里面。

(function () {
    console.log("111")
})();

//以圆括号开头,引擎就会认为后面跟的是一个表示式而不是函数定义语句,所以就避免了错误

//(Immediately-Invoked Function Expression),简称 IIFE。

其他写法

(function () {/* code */ } ());
!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();

注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能就会报错

// 报错
(function(){ /* code */ }())
(function(){ /* code */ }())
//上面代码的两行之间没有分号,JavaScript 会将它们连在一起解释,将第二行解释为第一行的参数。

IIFE的作用

1.为变量赋值

语法显得紧凑

let age = 6;
let sex = "男";
let title;
if (age < 18) {
    title = "小朋友";
} else {
    if (sex == "男") {
        title = "先生";
    } else {
        title = "女士";
    }
}
//代码不紧凑

let title = (function(){
    if(age < 18){
        return "小朋友";
    }else{
        if(sex == "男"){
            return "先生";
        }else{
            return "女士";
        }
    }
})();

alert(title);

2.将全局变量变为局部变量

IFE可以在一些场合(如for循环中)将全局变量变为局部变量,形成闭包

<script>
    let arr = [];
    for(var i = 0; i < 5; i++){
        arr.push(function(){
            console.log(i);  //变量i是全局变量 , 所有函数都共享内存中的同一个变量i
        });
    }

    arr[0]();
    arr[1]();
    arr[2]();
    arr[3]();//5
    arr[4]();
    
    //使用IIFE  或者把var改成let产生块级作用域
    let arr = [];
      for (var i = 0; i < 5; i++) {
        (function (index) {
          arr.push(function () {
            console.log(index);
          });
        })(i);
      }
</script>
<style>
    div {
        width: 300px;
        height: 300px;
        background-color: rgba(0, 0, 0, 0.4);
        margin: 100px auto 0;
        font-size: 50px;
        color: #fff;
        text-align: center;
        line-height: 300px;
        cursor: pointer;
    }
</style>
</head>
<body>
    <div class="one">one</div>
    <div class="two">two</div>

    <script>
        (function () {
            let one = document.querySelector(".one");

            let index = 0;

            one.onclick = function () {
                console.log(index++);
            };
        })();

        (function () {
            let two = document.querySelector(".two");

            let index = 0;

            two.onclick = function () {
                console.log(index++);
            };
        })();
    </script>

let const的应用

<style>
    * {
        margin: 0;
        padding: 0;
    }
    body {
        height: 100vh;
        display: flex;
        justify-content: space-evenly;
        align-items: center;
    }
    button {
        width: 200px;
        height: 200px;
        font-size: 100px;
        cursor: pointer;
    }
</style>
</head>
<body>
    <button>0</button>
    <button>1</button>
    <button>2</button>

    <script>
        let btn = document.querySelectorAll("button");

        for (var i = 0; i < btn.length; i++) {
            btn[i].onclick = function () {
                console.log(i); //每次点击都打印4
            };
        }
        //  for循环产生3个事件函数都使用了全局的同一个i(循环结束的i)
    </script>

0Przod.png

//使用IIFE形成闭包  保存变量
for (var i = 0; i < btn.length; i++) {
    (function (index) {
        btn[i].onclick = function () {
            console.log(index);
        };
    })(i);
}

0Ps2tA.png

//使用let 形成块级作用域
for (let i = 0; i < btn.length; i++) {
    btn[i].onclick = function () {
        console.log(i);
    };
}
//let直接产生一个作用域 和里面的事件函数  构成闭包  i不会被回收

0PsXpq.png