12、闭包的实战应用

139 阅读2分钟

需求:点击按钮,分别输出对应的数字1-5

<body>
    <button index="0">按钮1</button>
    <button index="1">按钮2</button>
    <button index="2">按钮3</button>
    <button index="3">按钮4</button>
    <button index="4">按钮5</button>    
</body>

一、常规思路

var buttonList = doucument.querySelectorAll('button')
for (var i = 0; i <buttonList.length; i++) {
	buttonList[i].onclick = function() {
    	console.log(`我是第${i+1}个按钮`)
    }
}

但是这种方式是实现不了的,点击按钮,输出结果都是“我是第6个按钮”,这是为什么呢?
因为:

  1. 循环中的i是全局的,每一轮循环给对应元素的click绑定方法(创建函数【存储的是代码字符串】,此时函数没有执行)
  2. 循环结束的时候,全局的i=5
  3. 点击某个按钮,执行之前绑定的函数,此时形成一个全新的私有上下文,它的上级上下文是全局上下文,函数代码执行过程中,遇到变量i,但i不是私有的,要去全局找,全局i是5,如下图

二、解决方法

方案一:闭包

解决问题的思路:当点击事件触发,执行对应的函数,用到的i不要在想全局查找了;相当于 自己形成一个上下文,而自己的上下文中,存储了你需要的i,存储的值事指定的索引即可 =>闭包的保存机制
闭包的弊端:循环多少次,就会产生多少个闭包,非常消耗内存 1.

for (var i = 0; i < buttonList.length; i++){
  // 每一轮循环都会把自执行函数执行,形成一个全新的私有上下文(一共形成了5个)
  // + 把当前这一轮全局i的值作为实参,传递给当前形成的私有上下文中的形参n[私有变量]
  // + 第一个私有上下文中的n=0,第二个私有上下文中的n=1, 第三个私有上下文中的n=2 ....
  // 每一个形成的上下文中,创建的函数都被外部的元素对象的onclick占用了,所以形成了5个闭包
  // 当点击按钮执行函数的时候,遇到一个变量n,不是自己私有的,则找上级上下文(闭包)中的n,而n存储的值就是它的索引
  (function (n) {
    buttonList[n].onclick = function () {
      console.log(n+1)
    }
  })(i)
}

2 . 和第一个思路一样,就是写法不同

for (var i = 0; i < buttonList.length; i++) {
  // 每一次循环,自执行函数都会执行,形成5个私有上下文,把每一次全局i的值作为实参传给自执行函数形参n[私有变量]
  buttonList[i].onclick = (function (i) {
    // i是每一轮形成的闭包中的私有变量,五个闭包中存储的值分别是0~4[索引]
    // 每一次自执行函数都会返回一个小函数,赋值给元素的点击事件,当点击元素的时候,执行返回的小函数
    return function () {
      console.log(i+1)
    }
  })(i)
}
  1. 还是基于“闭包的机制”,但是不是自己去执行函数构建,而是利用ES6中let产生的私有上下文实现

首先通过一个例子来了解下let

for(let i = 0; i < 3; i ++) {
	console.log(i)
}
console.log(i) // Uncaught ReferenceError: i is not defined

方案(和前两个一样都是闭包,但由于是浏览器底层内部处理形成的闭包,要比自己执行的函数性能好一点)

for (let i = 0; i < buttonList.length; i++) {
  // 第一轮循环 私有块1
  // + 私有变量 i = 0
  // + 当前私有上下文中的创建的一个函数被全局的元素对象的onclick占用了(闭包)【父级块级作用域没有东西被外部占用,循环执行完毕会被销毁】
  // ...
  buttonList[i].onclick = function () {
    console.log(i+1)
  }
}

私有块中的 (在while循环里用let,也会形成一个父级块级作用域)
问: 循环事件绑定,你是怎么处理索引的?或者笔试题,问你最终答案。这种题怎么答?
答:用let或者闭包。其实这种回答不准确,let也是闭包机制。 以上这几种方案都是闭包机制。只是代码不同,实现方案不同。 除了用闭包机制,还有其他解决方案

方案二:自定义属性

事先把一些信息存储到元素的身上,后期在一些其他操作中,想要获取这些信息,直接基于元素的属性访问就可以拿到这些值 => 操作DOM的时代,这种方案非常常用

var buttonList = document.querySelectorAll('button')
for(var i = 0; i < buttonList.length; i++) {
	// 把当前按钮的索引储存在它的自定义属性上(每个按钮都是一个元素对象)
	buttonList[i].myIndex = i
    buttonList[i].onclick = function(){
    	// 给当前元素的某个事件绑定方法,当事件触发,方法执行的时候,方法中的this是当前点击的按钮
    	console.log(this.myIndex+1)
    }
}

方案三:利用事件代理的机制(性能提高 >=40%)

(tips:
1.数字字符串前加+ ,会先默认转成数字再相加
2. 这种写法不用获取元素也不用创建5个堆内存,性能好很多)

<button index="0">按钮1</button>
<button index="1">按钮2</button>
<button index="2">按钮3</button>
<button index="3">按钮4</button>
<button index="4">按钮5</button>
document.body.onclick = function(){
	let target = ev.target
    if(target.tagName === 'BUTTON') {
    	let index  = target.getAttribute('index')
        console.log(+index+1)
    }
}