在开始闭包之前,我们可以先来了解下相关知识
函数作用域和全局作用域
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,
打印结果为10。
let 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