变量提升带来的问题
1. 后面定义的变量 会覆盖 前面的同名变量
因为变量提升, 后定义的变量 会在不察觉的情况下 覆盖前面的 同名变量,导致 bug 而自己 没有察觉,尤其是在 非常长的代码块中。
2. 应该销毁的变量没有被销毁
1
for 循环中的 i , 结束后应该被销毁,结果却变成了全局变量, 在全局执行上下文中,污染变量。
ES6 带来的解决方案
块级作用域: ES6 引入了 let 和 const 关键字,使 JS 拥有了块级作用域。
块级作用域 就是使用⼀对大括号包裹的⼀段代码,比如函数、判断语句、循环语句,甚至单独的⼀个{ } 都可以被看作是 ⼀个块级作用域。
块级作用域
作用域
作⽤域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是 变量 与 函数 的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
在 ES6 之前, JS 只有两种作用域:全局作用域 和 函数作用域。
全局作用域中的 对象 在代码的 任何位置 都能访问到,它的声明周期 与 页面的声明周期相同。
函数作用域 就是定义在 函数内部 的 变量 或者函数,并且定义的变量或者函数只能在函数内部访问到。函数执行结束后,函数内部定义的变量会被销毁。
块级作用域的实现
示例代码
分析代码执行过程:
1 编译阶段,将 foo 放入全局执行上下文 变量环境中; 执行阶段,执行 foo:编译阶段:创建 foo 函数执行上下文,将用 var 声明的变量 a 和 c 放入变量环境 并赋值 undefined; 将用 let 声明的 b f放入词法环境 并赋值 undefined,此时, 函数内部的 块级作用域 中的 用 let 声明的变量 却 没有放入词法环境。
调用栈
2. 执行到函数内的块级作用域时, 此时 a 已经赋值为 1, b = 2; 将块级作用域中 用 let 声明的变量, 单独 放在词法环境的另一个区域,它与另外一个区域 相互独立,且不影响作用域外面的变量。
这个结构 与我们的调用栈一样,它也是一个栈结构,存放着用 let 和 const 声明的变量。进入一个块级作用域,就会将这些变量 压入栈中,当这个块级作用域执行完,就会弹出栈,这就是词法环境的结构。
调用栈
3 执行 块级作用域中的 console.log,,在 词法环境 和 变量环境 中查找变量 a ,顺序为:首先在词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JS 引擎,如果没有查找到,那么继续在 变量环境 中查找。当执行完 块级作用域中的代码,这些变量就会从词法环境的栈顶弹出。
出栈
总结:块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,两者结合,JS 引擎也就同时 支持了变量提升 和 块级作用域了。
作用域链
上面讲到在 词法环境 和 变量环境 找寻变量,这就 涉及到了作用域的另外一个概念,作用域链。
首先看一段代码
作用域链
如果按照上面的 模拟,应该是 1 ,那么这里为什么是 2 呢?
在每个执行上下文的变量环境中,都包含了⼀个 外部引用,用了来指向外部的执行上下文,我们把这个外部引用称为 outer。
outer
bar 函数 和 foo 函数的 outer 都指向 全局执行上下文,所以,如果 函数 内部使用了外部变量,那么 JS 引擎都会去 全局执行上下文中查找。 我们把这个查找的链条叫做 作用域链。
那么 为什么 bar 调用 foo 函数,foo 函数引用的是 全局的执行上下文, 而不是 bar 函数中的上下文呢,因为 词法作用域。
词法作用域 就是定义在词法阶段的作用域。简单来理解,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的(由代码中函数声明的位置来决定的, 跟在哪里调用这个函数没关系),词法作用域是静态的作用域。
所以 在定义 foo 函数 和 bar 函数的时候,已经决定了 它们的上级作用域是 全局作用域,词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系 。
闭包
首先看一段代码
输出结果 2
我们模拟下 它执行时候的 调用栈,当执行到 return bar 这行的时候
调用栈
foo 函数中定义的 bar 函数,根据 词法作用域,bar 函数中的 a 变量 是指向 foo 函数中的 a。当 return 的 bar 作为 baz 的 返回值的时候,函数 foo 已经执行完毕,但是 bar 函数 依然能使用 foo 函数中的 a 变量。
调用栈
foo 函数执行完毕, foo 函数执行上下文弹出调用栈, 但是 返回值 bar 内部依然使用了 foo 函数中的变量 a, 所以 a 变量 保存在 内存中。除了 bar 函数 ,其他地方无法访问到 这个内存。
在 JS 中,根据词法作用域的规则,内部函数 总是可以访问其外部函数中声明的变量,当通过调用⼀个外部函数返回⼀个内部函数后,即使该外部函数已 经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。 比如外部函数是foo,那么这些变量的集合就称为 foo 函数的闭包 。
那么 闭包如何使用
调用栈
查找 a 变量顺序:bar 函数执行上下文‒> foo函数闭包 ‒> 全局执行上下文, 也就是 内部函数一层层向外寻找,形成了一个作用域链。
闭包的回收
闭包如果使用不正确,很容易造成内存泄漏。
1. 如果引⽤闭包的函数是⼀个全局变量,那么闭包会⼀直存在直到页⾯关闭;但如果这个闭包以后不再 使用的话,就会造成内存泄漏。
2. 如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JS 引擎执行垃圾回收时,判断闭包内容,如果已经不再被使用了,那么 JS 引擎的垃圾回收器就会回收这块内存。
所以,如果该闭包会⼀直使用,那么它可以作为全局变量而存在;但如果使用频率不高,并且内存占用较大,那就尽量让它成为⼀个局部变量。












网友评论