五分钟learn变量提升
五分钟learn调用栈
五分钟系列:块级作用域
作用域链
简单例子
function bar() {
console.log(myName)
}
function foo() {
var myName = "inner"
bar()
}
var myName = "outer"
foo()
执行foo,最终需要打印“myName”。按照调用栈内容,从上往下依次是bar执行上下文、foo执行上下文、全局执行上下文。
- 先查找栈顶是否存在myName变量,这里没有,接着往下查找foo函数执行上下文。
- 在foo中查找到了myName变量,这时候就使用foo函数中的myName,也就是“inner”
但实际上,打印的却是“outer”,这是为什么呢
定义
其实在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文。
当一段代码使用了一个变量时,JavaScript引擎首先会在“当前的执行上下文”中查找该变量,如果在当前的变量环境中没有查找到,那么JavaScript引擎会继续在所指向的外部执行上下文中查找,而不是根据调用栈往下查找。我们把这个查找的链条就称为作用域链。
在上面示例中,bar函数和foo函数的外部执行上下文都是指向全局执行上下文。
词法作用域
词法作用域就是指作用域是由代码中函数声明的位置来决定的,它是静态的作用域,代码编译阶段就决定好的,和函数是怎么调用的没有关系
从图中可以看出,词法作用域就是根据代码的位置来决定的,其中main函数包含了bar函数,bar函数中包含了foo函数,因为JavaScript作用域链是由词法作用域决定的,所以整个词法作用域链的顺序是:foo函数作用域—>bar函数作用域—>main函数作用域—>全局作用域。
再看之前的用例,foo和bar都是在外层定义的,所以它们的上级作用域都是全局作用域,所以如果foo或者bar函数使用了一个它们没有定义的变量,那么它们会到全局作用域去查找。
块级作用域中的变量查找
function bar() {
var myName = "bar"
let test1 = 100
if (1) {
let myName = "bxy"
console.log(test)
}
}
function foo() {
var myName = "foo"
let test = 2
{
let test = 3
bar()
}
}
var myName = "window"
let myAge = 10
let test = 1
foo()
当执行到打印test的情况时,其调用栈及查找路线如下图所示:
闭包
定义
在JavaScript中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。
用例
function foo() {
var myName = "foo"
let test1 = 1
const test2 = 2
var innerBar = {
getName:function(){
console.log(test1)
return myName
},
setName:function(newName){
myName = newName
}
}
return innerBar
}
var bar = foo()
bar.setName("bar")
bar.getName()
console.log(bar.getName())
当执行到foo函数内部的return innerBar这行代码时调用栈的情况:
从上面的代码可以看出,innerBar是一个函数,包含了getName和setName的两个方法。这两个方法都是在foo函数内部定义且内部都使用了myName和test1。
根据词法作用域的规则,内部函数getName和setName总是可以访问它们的外部函数foo中的变量,所以当innerBar对象返回给全局变量bar时,虽然foo函数已经执行结束,但是getName和setName函数依然可以使用foo函数中的变量myName和test1。所以当foo函数执行完成之后,其整个调用栈的状态如下图所示:
可以看出,foo函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的setName和getName方法中使用了foo函数内部的myName和test1,所以这两个变量依然保存在内存中。而且这两个变量只能被setName和getName访问到,其他任何地方都无法访问到,这就是所谓的闭包
堆栈
首先我们需要了解堆栈概念。前面我们一直在说“调用栈”,这其实就是栈空间,用来存储执行上下文的。
JS数据分为原始数据和引用数据,原始类型的数据值都是直接保存在“栈”中的,引用类型的值是存放在“堆”中的。然后,存放在堆中的位置,会被放到执行上下文中:
function foo(){
var a = 1;
var b = {name:"foo"};
var c = b
}
foo()
所有数据直接存放在“栈”中不可以吗?
不可以。因为JavaScript引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。
通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。
闭包数据放在哪儿
- 当JavaScript引擎执行到foo函数时,首先会编译,并创建一个空执行上下文。
- 在编译过程中,遇到内部函数setName,JavaScript引擎还要对内部函数做一次快速的词法扫描,发现该内部函数引用了foo函数中的myName变量,由于是内部函数引用了外部函数的变量,所以JavaScript引擎判断这是一个闭包,于是在堆空间创建换一个“closure(foo)”的对象(这是一个内部对象,JavaScript是无法访问的),用来保存myName变量。
- 接着继续扫描到getName方法时,发现该函数内部还引用变量test1,于是JavaScript引擎又将test1添加到“closure(foo)”对象中。这时候堆中的“closure(foo)”对象中就包含了myName和test1两个变量了。
- 由于test2并没有被内部函数引用,所以test2依然保存在调用栈中。
当执行到foo函数时,闭包就产生了;当foo函数执行结束之后,返回的getName和setName方法都引用“closure(foo)”对象,所以即使foo函数退出了,“ closure(foo)”依然被其内部的getName和setName方法引用。
产生闭包的核心有两步:
- 需要预扫描内部函数
- 把内部函数引用的外部变量保存到堆中
回收
如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭。 如果引用闭包的函数是个局部变量,等函数销毁后,在下次JavaScript引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么JavaScript引擎的垃圾回收器就会回收这块内存。