为什么要了解闭包?
- 同样一直是面试的热点
- Javascript最基本最重要概念之一
- 非单一概念,涉及作用域,作用域链,执行上下文,内存管理等
1.函数作用域和全局作用域
作用域可以理解为某种规则下的限定范围,该规则用于指导开发者如何在特定场景下查找变量。
Javascript执行某个函数 时,如遇见变量且需要读取其值,就会"就近"现在函数内部查找该变量的声明和复赋值情况,若在函数无法找到该变量,就跳出函数作用域,再更上层作用域查找。
var a = 'bar'
function bar() {
function foo() {
console.log(b)
}
foo()
}
bar()
2. 块级作用域和暂时性的死区
- ES6新增了通过let和const声明变量的块级作用域
- 块级作用域:指作用域范围限制在代码块中 说起来暂时性的死区,还需要从”变量提升“说起来
var a = 'bar'
function bar() {
console.log(foo) //暂时性死区,这里会报错
let foo = 'foo'
console.log(foo) //这里可以正常访问,如果上面不报错的前提
}
bar()
还有一中种情况产生暂时性死区
function foo(arg1 = arg2, arg2) { //暂时性死区
console.log(`${arg1} ${arg2}`)
}
foo(undefined, 'arg2') //arg2 is not undefined
在上面foo函数中,如果没有传入第一个参数,则会使用第二个参数作为第一个实参,但是当第一个参数为默认值时,执行 arg1 = arg2 会被当成暂时性死区处理。
3. 执行上下文和调用栈
执行上下文:当前代码的执行环境/作用域。包含:变量对象,作用域链,this
代码执行的两个阶段
- 代码预编译阶段
- 代码执行阶段
与传统编译不同,这里是JS的独特概念,会由编译器将JS代码编译成可执行代码
需要注意:
- 在预编译阶段进行变量声明
- 在预编译阶段进行变量提升,但值为undefined
- 在预编译阶段对所有非表达式的函数声明进行提升
foo(10)
function foo(num) {
console.log(foo)
foo = num
console.log(foo)
var foo
}
console.log(foo)
foo = 1
console.log(foo)
输出结果如下:
undefined
10
function foo(num) {
console.log(foo)
foo = num
console.log(foo)
var foo
}
1
在执行foo(10)时,在函数体内进行变量提升,此时第一行输出undefined,执行函数体的第三行会输出foo
接着运行代码,运行到函数体外的console.log(foo)语句时,会输出foo函数的内容
代码执行的整个过程,就像一条流水线,
- 第一道工序在预编译阶段创建变量对象VO,但未赋值
- 第二道工序在代码执行阶段,变量对象会转为激活对象AO,此时作用域链也将被确定
- 作用域链由当前的执行环境的变量对象和外层所激活对象组成
调用栈
function foo1() {
foo2()
}
function foo2() {
foo3()
}
function foo3() {
foo4()
}
function foo4() {
console.log('foo4')
}
foo1()
以上代码执行顺序foo1->foo2->foo3->foo4
具体过程:foo1先入栈,紧接着foo1调用foo2,foo2再入栈,以此类推,直到foo4执行完,然后foo4先出栈,foo3在出栈,接着foo2出栈,最后foo1出栈。这个过程满足先进后出的规则,因此形成调用栈。
注意:正常情况下,在函数执行完毕并出栈时,函数内部变量,在下一个垃圾回节点会被回收,该函数对应的执行上下文将会被销毁,这也是外界无法访问函数内部定义的变量的原因。
闭包
介绍了这么多前置概念,总算到闭包环节了。 示例:
function numGenerator() {
let num = 1
num++
return function () {
console.log(num)
}
}
var getNum = numGenerator()
getNum()
闭包原理:
- 前面我们知道,正常情况下外界无法访问函数内部的变量,函数执行后,上下文被销毁
- 但是我们在函数中,如果返回了另外一个函数,并且这个返回函数使用了函数内的变量,那么外界就可以通过这个返回函数获取原函数的内部变量值
内存管理
var foo = 'bar' //分配内存
alert(foo) //读取内存
foo = null //释放内存
内存空间分为栈空间和堆空间
- 栈空间:由操作系统自动分配释放,存放函数的参数值,局部变量的值等
- 堆空间:一般由开发者分配释放,这部分要考虑垃圾回收的问题
JS中数据类型包括基本数据类型和引用类型(未包含es6)
- 基本数据类型:undefined,number,string,boolean,null
- 引用类型:object,array,function
一般情况,基本数据类型按照值大小保存在栈空间,占固定大小的内存空间; 引用类型保存在堆空间中,内存空间大小并不固定,按照引用情况访问。
内存泄漏是指内存空间明明已经不再被使用,但是由于种种原因没被释放
内存泄漏场景一
var element = document.getElementById('element')
element.mark = 'marked'
// 移除element
function remove(){
element.parentNode.removeChild(element)
}
remove()
把id为element的节点移除了,但是变量element依然存在
内存泄漏场景二
var element = document.getElementById('element')
element.innerHTML = '<button id="button"></button>'
var button = document.getElementById('button')
button.addEventListenter('click',function(){
//...
})
element.innerHTML = ''
button元素已经从DOM中移除了,但是由于事件处理句柄还在,所有该变量依然无法被回收,还需要添加removeEventListener函数,防止内存泄漏
内存泄漏场景三
function foo() {
var name = 'lucas'
window.setInterval(function () {
console.log(name)
},1000)
}
foo()
由于存在window.setInterval,所以name内存空间始终无法释放,合理时机使用clearInterval清理
内存泄漏场景四
function foo() {
var value = 123
function bar(){ alert(value) }
return bar
}
let bar = foo()
变量value将会被保存在内存中,如果加上bar = null ,随着bar不再被引用,value也会被清除