学习闭包基础知识点


在开始闭包之前,我们可以先来了解下相关知识

无需了解,直接开始闭包


函数作用域和全局作用域

function fn(){
	var str = 'bar'
	console.log(str)   // bar
}
fn()    

以上代码等同于以下代码

var str = 'bar'
function fn(){
	console.log(str)   // bar
}
fn()

也可以

function fn(){
	var str = 'bar'
	function fn1(){
		console.log(str)   // bar
	}
	fn1()
}
fn()  

可以

var str = 'bar'       // 全局作用域
function fn(){        // fn作用域
	function fn1(){   // fn1作用域
		console.log(str)   // 输出bar
	}
	fn1()
}
fn()  

太过简单  不解释

块级作用域和暂时性死区

function fn(){
	console.log(str)  // undefined
	var str = 'bar'
}
fn()

以上代码会输出undefined

但是把 var 换成 let 就会输出 defined

function fn(){
	console.log(str)  // XXX is not defined
	let str = 'bar'
}
fn()

上面的代码就是 let 的暂时性死区。但是如下就可以正常运行

function fn(){
	let str = 'bar'
	console.log(str)  // bar
}
fn()let 声明的变量,会有块级作用域(独立作用域),外部无法访问

function fn(){
	function fn1(){
		let str = 'bar'
	}
	fn1()
	console.log(str)  // 无法访问 str
}
fn()

同样

function fn(){
	if(true){
		let str = 'bar'
	}
	function fn1(){
		console.log(str)   // 无法访问 str
	}
}
fn()


下面则是一种比较极端的 暂时性死区

function foo(arg1 = arg2, arg2){
	console.log(arg1, arg2)  // arg1 arg2
}
foo(arg1, arg2)

若没有传入第一个参数,那第二个参数就会变成第一个参数。
当第一个参数为默认值时,执行arg1 = arg2 就会被当做暂时性死区

实例如下:
function foo(arg1 = arg2, arg2){
	console.log(arg1, arg2)  // undefined arg2
}
foo(undefined, arg2)

同下

function foo(arg1 = arg2, arg2){
	console.log(arg1, arg2)  // null arg2
}
foo(null, arg2)

有点跑题了,不如看看这个
function fn(arg1){
	let arg1
}
fn('arg1')
以上代码会报错,因为第一函数参数已经声明了arg1,而函数体内再次声明arg1。就会报错

执行上下文和调用栈

略…


开始闭包

说到这里,我认为比较容易理解的闭包定义为:函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数在全局环境下可访问,进而形成闭包
例子

function numFn(){
	let num = 1
	num++
	return () =>{
		console.log(num)
	}
}
var getNum = numFn()
getNum()

numFn 创建 num 变量,接着返回打印 num 值的匿名函数,
   因为引用了 num 变量,所以外部可以通过调用 getNum方法访问 num 变量。
   在 numFn 执行完毕后,即相关调用栈出栈,变量 num 不会消失,仍可被外界访问。

在js引擎里可以看到 num 值被标记为 Closure ,即闭包变量,打印为 num: 2。

闭包原理:在函数外层中,如果返回了另一个函数,而且这个函数使用了函数外层内的变量,
    那外界就可以通过这个返回的函数获取原函数外层内部的变量值。

内存管理

let foo = 'bar'   分配内存
alert(foo)        读写内存
foo = null        释放内存

内存管理基本概念:
   栈空间:由操作系统自由分配释放,存放函数的参数值,局部变量等。操作类似于数据结构中的栈
   堆空间:一般由开发者分配释放,这部分空间要考虑垃圾回收机制问题

数据类型(未包含ES Next新数据类型):
   基本数据类型:nudefined,null,number,boolean,string等
   引用数据类型:object,array,function等

一般情况下:数据类型按照值大小保存在栈空间,占固定大小
          引用类型保存在堆空间,大小不固定,需按引用情况访问

let a = 11
let b = 10
let c = [1,2,3]
let d = {e: 20}
如下图:

在这里插入图片描述

对于分配内存与读写,所有语音较为一致,但释放内存的行为在不同语言间也都有差异。
js依赖宿主浏览器的垃圾回收机制,一般情况下不用程序员操心,但并不代表在释放内存方面就万事大吉了,
某些情况下依然会出现内存泄漏的情况。

内存泄漏场景举例:
内存泄漏是指内存空间明明不再被使用,但由于某些原因并没有被释放的现象。会导致运行缓,甚至崩溃

let element = dicument.getElementById('box')
element.mark = 'marked'
 
移除elemnet节点
function remove(){
	element.parentNode.removeChild(element)
}

上面代码中,我们只是把节点给移除了,但是变量 element 仍然存在,并没有被释放。
只需要在remove方法里写上 elemnt = null ,就可以了


let ele = document.getElementById('ele')
ele.innerHTML = "<button id='button'>点击</button>"

let btn = document.getElementVyId('button')

btn.addEventListener('click',function(){
	...
})
ele.innerHTML = ''

以上代码因为最后一句的 ele.innerHTML = '' ,button元素已经从DOM中移除了,
但由于视处理句柄还在,所以该节点变量依然无法被回收。需要添加removeEventListener函数,防止内存泄漏



function fn(){
	let name = 'lucas'
	window.setInterval(function(){
		console.log(name)
	},1000)
}
fn()

在这段代码中,由于存在window.setInterval,所以name内存空间无法被释放,
如果不是业务要求,一定记得在合适时机使用 clearInterval 对其清除



浏览器垃圾回收
对于浏览器垃圾回收,除了开发者主动保证回收外,大部分场景下浏览器都会依靠
标记清除和引用计数两种算法进行回收,这不不再拓展



内存泄漏和垃圾回收注意事项 

关于内存泄漏和垃圾回收,要在实战中分析,不能停留在理论层次,毕竟浏览器千变万化一直在演进

从以上事例,借助闭包来绑定数据变量,可以保护这些数据变量的内存块在闭包存活时,始终不被垃圾回收机制回收
正因为闭包使用不当极有可能引发内存泄漏,因此需格外注意

function foo(){
	let val = 123
	function bar(){
		 alert(val) 
	}
	return bar
}

lelt bar = foo()

变量 val 将会被保存在内存中,如果加上 bar = null ,则随着 bar 不再被调用, val 也会被清除

结合浏览器引擎的优化,对代码改动如下:
function foo(){
	let val = Math.random()
	function bar(){
		debugger
	}
	return bar
}
let bar = foo()
bar()

在 Chrome 浏览器 V8 最新引擎中执行代码,并在函数 bar 中设置断点,会发现 val 并没有被引用

在这里插入图片描述

下面在 bar 函数中 加入对 val 的引用
function foo(){
	let val = Math.random()
	function bar(){
		console.log(val)
		debugger
	}
	return bar
}
let bar = foo()
bar()
此时会发现引擎中存在闭包变量 val 的值

在这里插入图片描述

例题分析

猜一下以下代码输出结果

const foo = (function(){
	let v = 0
	return ()=>{
		return v++
	}
}())

for(let i = 0;i<10;i++){
	foo()
}
console.log(foo())    // ?

分析:
	foo是一个立即执行函数,当我们尝试打印foo时,要执行以下代码
	const foo = (function(){
		let v = 0
		return () =>{
			return v++
		}
	}())
	console.log(foo)

	输出结果
	() =>{
		return v++
	}
	当循环执行foo时,引用自由变量10次,v自增10,之后执行foo时,得到10。所以打印结果为10。
	这里的自由变量是没有在相关函数作用域中声明,但却被使用了的变量
	


执行以下代码,输出结果?
const foo = () => {
	let arr = []
	let i	
	for(i = 0;i<10;i++){
		arr[i] = function(){
			console.log(i)   // ?
		}
	}
	return arr[0]
}
foo()()

分析:
	在这里,自由变量为 i,执行foo返回的是 arr[0],arr[0]此时是函数,其中变量 i 的值为10,
	打印结果为10let fn = null
const foo = () => {
	let a = 2
	function innerFoo(){
		console.log(a)   // ?
	}
	fn = inerFoo
}

const bar = () => {
	fn()
}
foo()
bar()

分析:
	正常说,函数执行完,生命周期结束,内存释放,上下文消失。
	但通过innerFoo函数赋值给全局变量 fn,foo的变量对象 a 也被保留了下来。
		所以函数 fn 在函数 bar 内部执行时,依然可以访问这个被保留下来的变量对象。
		则输出的结果为 2



对上面代码进行修改

let fn = null
const foo = () => {
	var a = 2
	function innerFoo(){
		console.log(c)   // ?
		console.log(a)   // ?
	}
	fn = inerFoo
}

const bar = () => {
	var c = 100
	fn()
}
foo()
bar()

分析:
	在 bar 执行 fn 时,fn 已经被复制为 innerFoo,变量 c 并不在其作用域链上, 
		c 只是 bar 函数的内部变量,因此会报错 ReferenceError: c is not defind
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值