03 JS的进阶知识点
作用域链
一个例子
function bar() {
console.log(myName)
}
function foo() {
var myName = "foo"
bar()
}
var myName = "global"
foo()
调用栈分析
在执行代码之前,JavaScript引擎会先编译,会为上面这段代码创建全局执行上下文,包含了声明的函数bar(),foo()和变量myName,都保存在全局上下文的变量环境中
全局执行上下文准备好后,便开始执行全局代码,当执行到foo()这里,JavaScript 判断这是一个函数调用,那么将执行以下操作:
- 首先从全局执行上下文中,取出foo函数代码
- 其次,对foo函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码
- 当执行到bar()这里,JavaScript 判断这是一个函数调用,又对foo函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码
问题:此时bar中console.log(myName)的值应该使用全局执行上下文的还是foo函数执行上下文的?
按照第一反应是按照调用栈的顺序来查找变量,查找方式如下:
- 先查找栈顶是否存在 myName 变量,但是栈顶的bar中没有,所以接着往下查找foo函数中的变量
- foo函数中查找到了myName变量,这时候就使用foo函数中的myName = "foo"
但是实际结果是“global”,也就是说越过了foo函数的调用栈,直接到了全局执行上下文
这个地方就涉及到了作用域链
其实在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。
当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量
比如上面的例子,bar的执行上下文中不存在myName变量,所以JavaScript 引擎会继续在outer所指向的执行上下文中查找。
而bar和foo的outer都是指向全局上下文的,所以bar是用的全局的myName
outer的指向,用于查找当前执行上下文中没有的变量就是作用域链
所以查找顺序是按照outer的指向,而不是调用栈的顺序
问题2:但为什么bar和foo的outer都是指向全局上下文的呢,为什么bar的outer不是顺着调用栈指向foo呢?
这就涉及到词法作用域,JavaScript的作用域链是由词法作用域决定的
词法作用域
词法作用域:词法作用域就是根据代码的位置来决定的
foo 和 bar 的上级作用域都是全局作用域,所以如果 foo 或者 bar 函数使用了一个它们没有定义的变量,那么它们会到全局作用域去查找。也就是说,词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。
因为词法作用域是用于放let和const这类变量,所以用一个有let的例子来分析作用域链,这段代码开始有块级作用域了,还是来分析执行上下文
function bar() {
var myName = "bar"
let test1 = 100
if (1) {
let myName = "Chrome浏览器"
console.log(test)
}
}
function foo() {
var myName = "foo"
let test = 2
{
let test = 3
bar()
}
}
var myName = "global"
let myAge = 10
let test = 1
foo()
- 首先创建全局执行上下文放入调用栈
- 全局执行上下文变量环境存放myName、bar()、foo(),词法环境存放myAge,test
- 开始执行全局代码,将myName赋值为"global",myAge赋值为10,test赋值为1
- 当执行到foo(),创建foo函数的执行上下文,放入调用栈。
- foo函数的执行上下文变量环境存放myName,词法环境存放test
- 此时在foo函数的作用域块内部,通过 let 声明的变量并没有被存放到词法环境中
- 开始执行foo函数代码
- 当执行到代码块里面时,foo函数的执行上下文的变量环境中myName的值已经被设置成了“foo”,词法环境中 test的值已经被设置成了2
- 当进入函数的作用域块时,作用域块中通过 let 声明的变量test = 3,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量let test = 2,比如在作用域外面声明了变量test,在该作用域块内部也声明了变量test,当执行到作用域内部时,它们都是独立的存在
- 当在foo()中执行到bar(),创建bar函数的执行上下文,放入调用栈,同理如下图所示
- 当执行到bar的if块作用域中的 console.log(test)
- 而当前bar函数执行上下文的词法环境和变量环境都没有test,顺着outer找到全局执行上下文
- 在全局执行上下文的词法环境中先找到了test,所以打印 1
闭包
简单角度看闭包
闭包是JS的一种语法特性
闭包 = 函数 + 自由变量
对于一个函数来说,变量分为:全局变量,函数变量,自由变量
例子
//例子1
var count = 0;
function add(){ //访问了外部的变量,但这样不是闭包,因为函数本来就可以访问var全局变量
count += 1;
}
//怎么样才能形成闭包呢
//把上面代码放在【非全局环境】里,改为立即执行函数
//例子2
{
let count = 0; //现在count既不是上面代码的全局变量,也不是下面代码add的局部变量,属于自由变量
function add(){
//let count = 0; 此时count是add的局部变量,本来就可以访问,也不属于闭包
count += 1;
}
}
//完整的来说闭包就是 count和add组成的整体
//声明一个变量 等于一个立即执行函数
//立即执行函数里面有一个变量count
//再声明一个函数对变量count 进行操作
const add2 = function(){
let count = 0; //自由变量
return function add(){ //现在count不是全局变量了
//let count = 0; 此时count是add的局部变量,本来就可以访问,也不属于闭包
count += 1;
}
}()
词法作用域角度看闭包
方方说不推荐这个角度去看,因为用了一个更复杂的词汇来解释一个词汇,但结合到上篇分析的流程,这里也分析了
例子
了解了作用域链之后再来了解闭包,还是直接用代码分析
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())
建立调用栈如下图所示:
根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量
所以getName方法的console.log(test1)能输出1
且当innerBar对象作为函数foo的返回值返回给全局变量bar时
虽然foo函数已经执行结束,弹出调用栈,但是getName和setName函数依然可以使用foo函数中的变量myName 和 test1,这两个变量依然保存在内存中
闭包的概念就可以总结为:在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量
当通过调用外部函数foo返回一个内部函数(return innerBar)后,即使该外部函数foo已经执行结束了,但是内部函数引用外部函数的变量(test1 myName)依然保存在内存中,我们就把这些变量的集合称为闭包。
比如外部函数是 foo,那么这些变量(test1 myName)的集合就称为 foo 函数的闭包。
简单一点来说:闭包 = 函数 + 自由变量(与自由变量相对应的是全局变量)
闭包解决的问题
1 避免污染全局环境 (因为使用的是局部变量)
2 提供对局部变量的间接访问
3 维持变量,使其不被垃圾回收
闭包是怎么回收的
在理解了闭包和闭包的使用场景之后,我们来了解一下闭包是如何回收的,闭包不会造成内存泄露,而是闭包使用不当造成内存泄漏,并且这是在IE上的一个bug
function test(){
var x = {name:'x'};
var y = {name:'y',content:".....很长的一块儿内容"}
return function fn(){
return x;
}
}
const myFn = test(); //myFn就是fn了
const myX = myFn();//myx就是x了
//请问y会消失吗?
对于一个正常的浏览器来说,y是会在一段时间后消失的,但是IE仿佛把x y 绑定了一样,即使没有用到,也依旧存在
参考文章: