JS中的闭包详解

概念

    闭包是指可以访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过内部函数访问外部函数的局部变量,利用闭包可以突破作用域链,将函数内部的变量和方法传递到外部。

原理

    借助函数的立即执行、参数以及函数的 return 返回值,多创建了一层作用域。从而实现外部函数持续性被引用而不能释放内存空间,将值存储下来。

特点

  • 作为一个函数变量的一个引用,当函数返回时,其处于激活状态。
  • 一个闭包就是当一个函数返回时,一个没有释放资源的栈区。

    简单的说,Javascript 允许使用内部函数 —— 即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。

作用

    闭包就是将函数内部和函数外部连接起来的一座桥梁。使得内部函数可以读取外部函数中的变量,这些变量的值将始终保持在内存中,而不会被垃圾回收器回收。

简单的闭包

function addNum(num1, num2) {
	var num = 10
	return function() {
		console.log(num1 + num2 + num)
    }
}

var fn = addNum(1, 2)
fn()  // 13
  • var fn = addNum():返回了一个匿名的内部函数保存在变量 fn 中,并且引用了外部函数的变量 num,由于垃圾回收机制,addNum 函数执行完毕后,变量 num 并没有被销毁。
  • fn():执行返回的内部函数,依然能访问变量 num,输出 13

复杂的闭包

(function (document) {
    var viewport
    var obj = {
        init: function(id) {
           viewport = document.querySelector('#' + id)
        },
        addChild: function(child) {
            viewport.appendChild(child)
        },
        removeChild: function(child) {
            viewport.removeChild(child)
        }
    }
    window.jView = obj    //  将 obj对象暴露给 window全局对象
})(document)

    obj 是在匿名函数中定义的一个对象,这个对象中定义了一系列方法, 执行 window.jView = obj 就是在 window 全局对象中定义了一个变量 jView,并将这个变量指向 obj 对象,即全局变量 jView 引用了 obj ,而 obj 对象中的函数又引用了匿名函数中的变量 viewport ,因此匿名函数中的 viewport 不会被 GC(垃圾回收机制) 回收,viewport 会一直保存到内存中,所以这种写法满足了闭包的条件。

闭包的应用

  1. 内存泄露问题
<body>
<div id="btn">点击</div>
<script>
    var btn = document.getElementById('btn')
    btn.onclick = function() { 
        alert(this.id)      // 这样会导致闭包引用外层的 id,当执行完点击事件后,btn无法释放
    }
</script>
</body>

// 执行这段代码的时候,将匿名函数对象赋值给 btn 的 onclick 属性,然后匿名函数内部又引用了 btn 对象,
// 存在循环引用,所以不能被回收。

// 解决办法
<script>
	var btn = document.getElementById('btn')
	var id = btn.id    // 解除循环引用
	btn.onclick = function() { 
		alert(id)    // 这样会导致闭包引用外层的 id,当执行完点击事件后,btn无法释放
	}
	btn = null    // 将闭包引用的活动对象清除
</script>
  1. 引用可能发生变化的变量
<body>
<ul>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
</ul>
<script>
var lis = document.getElementsByTagName('li')
for(var i = 0; i < lis.length; i++) {
    lis[i].onclick = function () {
        alert(i)     //  点击每个 li 标签都是弹出 4
    }
}
</script>
</body>


// 解决方法一、将 var 改成 let 
for(let i = 0; i < lis.length; i++) {
    lis[i].onclick = function () {
        alert(i)     //  点击 li 标签弹出值不一样
    }
}

// 解决方法二、使用闭包
for(var i = 0; i < lis.length; i++) {
    lis[i].onclick = function (num) {
    	return function() {
        	alert(num)   // 此时访问的 num,是上层函数执行环境的 num,每个执行环境下的 num 都不一样			
    	}                // 点击 li 标签弹出值也不一样
    }(i)
}
  1. 定时器与闭包(面试会用到)
for(var i = 0; i < 5; i++) {
	setTimeout(function () {
	    console.log(i)    //  输出了 5 个  5
	})
}

    为什么会输入 5个5 而不是如预想一样输入 0,1,2,3,4 呢?原因是 javascript 是单线程的,在执行 for 循环的时候定时器 setTimeout 被安排到任务队列中排队等待执行,而在等待过程中 for 循环就已经在执行,等到 setTimeout 可以执行的时候,for 循环已经结束,此时 i 的值也已经变成了 5,所以打印出来 5个5

    两种就解决办法

// 解决方法一、将 var 改成 let 
for(let i = 0; i < 5; i++) {
	setTimeout(function () {
	    console.log(i)         //  输出 0, 1, 2, 3, 4
	})
}

// 解决方法二、使用闭包
for(let i = 0; i < 5; i++) {
	(function (num) {
    	setTimeout(function () {
        	console.log(num)   //  输出 0, 1, 2, 3, 4
        })
	})(i)
}

优点

  • 私有化变量
  • 避免全局变量的污染
  • 在内存中维持一个变量,可以做缓存(但使用多了的话也是一项缺点,消耗内存较大)

缺点

  • 容易引起内存泄露
  • 闭包会携带包含它的函数的作用域,因为会比其他函数占用更多内存,过度使用闭包,会导致内存占用过多。

    解决方法是可以在使用完变量后手动为它赋值为 null

发布了67 篇原创文章 · 获赞 584 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/fu983531588/article/details/89678584