【浏览器工作原理与实践】专栏学习笔记
先看一个例子
function bar() { console.log(myName) } function foo() { var myName = "极客邦" bar() } var myName = "极客时间" foo()
其调用栈的状态图如下所示:
全局执行上下文和 foo 函数的执行上下文中都包含变量 myName,那 bar 函数里面 myName 的值用哪个?
我们先去掉全局变量的一行,去控制台输出一下看看:
显然说明了 bar 函数里面 myName 的值用的全局变量的,原因是什么?
每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。
带有外部引用的调用栈示意图:
bar 函数和 foo 函数的 outer 都是指向全局上下文的,这也就意味着如果在 bar 函数或者 foo 函数中使用了外部变量,那么 JavaScript 引擎会去全局执行上下文中查找。我们把这个查找的链条就称为作用域链。
我当时看到这儿的时候也有一个问题:那就是那为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?
要解决这个问题,就需要了解词法作用域:因为作用域链是由词法作用域决定的。
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
用一个函数嵌套来表示一下:词法作用域
词法作用域链的顺序是:foo 函数作用域—>bar 函数作用域—>main 函数作用域—> 全局作用域。
词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。
例子:
function bar() { var myName = "极客世界" let test1 = 100 if (1) { let myName = "Chrome浏览器" console.log(test) } } function foo() { var myName = "极客邦" let test = 2 { let test = 3 bar() } } var myName = "极客时间" let myAge = 10 let test = 1 foo()
块级作用域中是如何查找变量的:
首先是在 bar 函数的执行上下文中查找,但因为 bar 函数的执行上下文中没有定义 test 变量,所以根据词法作用域的规则,下一步就在 bar 函数的外部作用域中查找,也就是全局作用域。
结合代码来理解什么是闭包:
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())
执行结果:
执行到 return innerBar 时候的调用栈:
innerBar 对象里包含了 getName 和 setName 的两个方法,方法内部使用了 myName 和 test1 两个变量
根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量。
foo 函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的 setName 和 getName 方法中使用了 foo 函数内部的变量 myName 和 test1,所以这两个变量依然保存在内存中。这些变量的集合就称为 foo 函数的闭包。
闭包的产生过程:
在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。
闭包是如何使用的呢?
上面代码执行到 bar.setName 里的 myName = "极客邦"
时,JavaScript 引擎会沿着“当前执行上下文–>foo 函数闭包–> 全局执行上下文”的顺序来查找 myName 变量:如图
我们通过开发者工具来看看:在代码出加一个 debugger
然后展开,开发者工具中的闭包展示如下:
当调用 bar.getName 的时候,右边 Scope 项就体现出了作用域链的情况:
从 Local–>Closure(foo)–>Global
就是一个完整的作用域链。
为什么?
因为如果闭包使用不正确,会很容易造成内存泄漏。
通常,如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。
如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。
使用闭包原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。
var bar = { myName:"time.geekbang.com", printName: function () { console.log(myName) } } function foo() { let myName = "极客时间" return bar.printName } let myName = "极客邦" let _printName = foo() _printName() bar.printName()
参考答案:来自网友–《蓝配鸡》
最后输出的都是 “极客邦”,这里不会产生函数闭包。
全局执行上下文: 变量环境: Bar=undefined Foo= function 词法环境: myname = undefined _printName = undefined 开始执行: bar ={myname: "time.geekbang.com", printName: function(){...}} myName = " 极客邦 " _printName = foo() 调用foo函数,压执行上下文入调用栈 foo函数执行上下文: 变量环境: 空 词法环境: myName=undefined 开始执行: myName = " 极客时间 " return bar.printName 开始查询变量bar, 查找当前词法环境(没有)->查找当前变量环境(没有) -> 查找outer词法环境(没有)-> 查找outer语法环境(找到了)并且返回找到的值 pop foo的执行上下文 _printName = bar.printName printName()压bar.printName方法的执行上下文入调用栈 bar.printName函数执行上下文: 变量环境: 空 词法环境: 空 开始执行: console.log(myName) 开始查询变量myName, 查找当前词法环境(没有)->查找当前变量环境(没有) -> 查找outer词法环境(找到了) 打印" 极客邦 " pop bar.printName的执行上下文 bar.printName() 压bar.printName方法的执行上下文入调用栈 bar.printName函数执行上下文: 变量环境: 空 词法环境: 空 开始执行: console.log(myName) 开始查询变量myName, 查找当前词法环境(没有)->查找当前变量环境(没有) -> 查找outer词法环境(找到了) 打印" 极客邦 " pop bar.printName的执行上下文