词法作用域是指作用域是由代码中函数声明的位置决定的。
和函数的调用位置没有关系。
一、作用域链
作用域有全局作用域、函数作用域、块级作用域三种。
每个执行上下文(除了全局执行上下文)都有一个outer,该值指向外部的执行上下文。
例子:
function foo() {
var myName = "极客时间"
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.getName()
console.log(bar.getName())
这段代码在编译和执行过程如下:
编译:
-
创建全局执行上下文,foo函数,bar=undefied放入变量环境,并压入调用栈。foo函数的outer属性指向全局执行上下文。
执行:
- 执行到var bar = foo()时,编译foo函数。
- 创建foo函数的执行上下文,myName、innerBar为undefied放入foo函数的变量环境中,foo函数的执行上下文入栈。
- test1、test2为undefied放入foo函数的词法环境中。
- 执行foo函数,myName、innerBar、test1、test2赋值,foo函数的执行上下文出栈。
- bar被赋值为innerBar的地址引用。
- 执行bar.setName("极客邦"),编译setName函数。
- 创建setName函数的执行上下文,outer指向foo函数(根据定义的位置)执行上下文入栈。
- 执行setName函数,查找myName,setName函数中没有,向outer所指向的foo函数查找,找到了赋值,setName函数的执行上下文出栈。
- 执行bar.getName(),编译getName函数。
- 创建getName的执行上下文,并入栈,outer指向foo函数(根据定义的位置)。
- 执行console.log(test1),getName中没有,向outer指向的foo函数中查找,找到赋值,getName的执行上下文出栈。
执行到return时的调用栈:
setName执行时的调用栈
setName的作用域链:setName --> foo ---> window对象。
getName的作用域链:getName --> foo ---> window对象。
二、闭包
上面的例子中,foo函数的执行完了,为什么在getName、setName中获取到foo函数中的变量?这就是我们要讲的闭包。
闭包,当一个外部函数调用返回一个内部函数时,我们能通过返回的这个内部函数访问外部函数中的变量,能访问的这些变量的集合就叫做闭包。比如上面例子中,test1、myName的集合就可以成为foo函数的闭包。
那么为什么foo函数已经执行完了,执行上下文已经出栈了,setName、getName中还能获取到foo函数中的变量?因为虽然foo函数已经执行完了,执行上下文出栈了,但是test1、myName函数因为在setName、getName中被使用了,所以这两个变量还在内存中,但是只能这两个函数访问得到。
function foo() {
var myName = "极客时间"
let test1 = 1
const test2 = 2
var innerBar = {
setName:function(newName){
myName = newName
},
getName:function(){
console.log(test1)
return myName
}
}
return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())
如上面这个例子:
当js引擎在编译foo函数时,快速的进行词法扫描,发现foo函数中的myName、test1变量被内部函数使用了,于是在堆空间中创建了一个对象closure(foo),并将这两个对象放入该对象中。
产生闭包的核心:
- 预扫描内部函数
- 将内部函数使用到的外部变量保存到堆中。
注意:
- 如果引用闭包函数的是一个局部变量,那么在这个变量所属的函数执行完之后,闭包就会被回收。
- 如果引用必报函数的是一个全局变量,那么只有当页面被关闭时才会被回收。
- 只有当函数里面定义内部函数,内部函数访问外部函数时,才会产生闭包。