JS作用域:全局作用域,函数作用域,块级作用域
- 背景
- 作用域
- 全局作用域
- 函数作用域
- 块级作用域
- 通过调用栈分析块级作用域
- 开发者工具查看作用域
- 选项卡示例
背景
由于 JavaScript 存在变量提升这种特性,从而导致很多与直觉不符的代码,这也是 JavaScript 的一个重要设计缺陷,这种设计缺陷带来的问题可以去看看JS变量和函数提升。
所以 ES6 通过引入块级作用域并配合 let
、const
关键字来避开了这种设计缺陷,但是由于 JavaScript 需要向下兼容,所以变量提升在相当长一段时间内还会继续存在。
作用域
作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。即,作用域是变量和函数的可访问范围,控制着变量和函数的可见性和生命周期。
ES6(2015年6月) 之前,ES 的作用域只有两种:全局作用域和函数作用域。相较而言,其他语言则都普遍支持块级作用域。
全局作用域
全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
函数作用域
函数作用域就是在函数内部定义的变量或函数,并且定义的变量或函数只能在函数内部被访问,函数执行结束后,函数内部定义的变量就会被销毁。
块级作用域
ES6 中给 JavaScript 新增了块级作用域
- 块级作用域由
{}
包括,if
语句和for
语句里面的{}
都属于块级作用域 var
定义的变量没有块级作用域概念,可以跨块级作用域访问let
和const
定义的变量只能在块级作用域里访问
下面几段示例代码可以帮助你对块级作用域的理解:
{
var a = 1
let b = 2
const c = 3
}
console.log(a) // 1
console.log(b) // 报错(let和const只能在块级作用域中访问)
console.log(c) // 报错
for(var a = 0; a < 3; a++) {
var d = 5
}
console.log(a) // 3
console.log(d) // 5
for(let a = 0; a < 3; a++) {
var d = 5
}
console.log(a) // 报错(for语句属于块级作用域)
console.log(d) // 5
if(true) {
var a = 5
}
console.log(a) // 5
if(true) {
let a = 5
}
console.log(a) // 报错(if语句属于块级作用域)
通过调用栈分析块级作用域
不了解调用栈的可以先去看JS调用栈
function foo() {
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
console.log(d)
}
foo()
-
第一步编译并创建执行上下文。函数内部通过
var
声明的变量在编译阶段全都被存到变量环境里
-
第二步继续执行代码。执行到函数内部代码块,在块级作用域之前,变量环境中的
a
已经被设置成了1
,词法环境中的b
已经被设置成了2
。
-
进入函数内的块级作用域,块级作用域中通过
let
声明的变量会被存放在词法环境一个单独的区域中,这个区域中的变量并不影响块级作用域块外面的变量,比如它们都有一个变量b
。其实在词法环境中维护了一个小型的栈结构,栈底时函数最外层的变量,进入一个块级作用域后,就会把该作用域内的变量压入栈中,当该作用域执行完成后就从栈中弹出。当然,这里的变量是指通过let
或const
声明的变量。
-
当执行到块级作用域中的
console.log(a)
这行代码时,需要在词法环境和变量环境中查找a
的值,具体查找方向是:沿着词法环境的栈顶向下查询,如果有直接返回给 JavaScript 引擎,如果都没有就去变量环境中查找。 -
块级作用域执行完后返回内容,弹出栈中,执行结果为
所以,块级作用域是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现的,通过两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了。
开发者工具查看作用域
var a = 'hello'
let b = 18
const c = 'female'
{
var aa = 'hi'
let bb = 188
const cc = 'male'
debugger
}
function foo() {
var aaa = 'go'
let bbb = 10
const ccc = 'man'
{
var aaaa = 'ok'
let bbbb = 8
const dddd = 'girl'
debugger
}
debugger
}
foo()
打开Chrome开发者工具查看这段代断点处作用域下变量值
选项卡示例
<style>
button.active{
color: red;
}
p{
display: none;
}
p.active{
display: block;
background-color: red;
}
</style>
<body>
<button>选项一</button>
<button>选项二</button>
<button>选项三</button>
<p>内容一</p>
<p>内容二</p>
<p>内容三</p>
</body>
// 用var
<script>
let buttons = document.querySelectorAll('button')
let ps = document.querySelectorAll('p')
for(var i = 0; i < buttons.length; i++) {
buttons[i].index = i
buttons[i].onclick = function() {
for(var j = 0; j < buttons.length; j++) {
buttons[j].className = ''
ps[j].className = ''
}
this.className = 'active'
ps[this.index].className = 'active'
}
}
</script>
</body>
// 使用let块级作用域
let buttons = document.querySelectorAll('button')
let ps = document.querySelectorAll('p')
for(let i = 0; i < buttons.length; i++) {
buttons[i].onclick = function() {
for(var j = 0; j < buttons.length; j++) {
buttons[j].className = ''
ps[j].className = ''
}
this.className = 'active'
ps[i].className = 'active' // i的for循环每一次都会生成一个块级作用域所以每个i都在指定的作用域中工作
}
}
总结:选项卡实例中,使用var
没有块级作用域所以for
循环结束,i
就变成了最后的值,所以必须要将索引值赋值给每一个元素,然后点击的时候根据索引去判断显影性。
使用let
创造了块级作用域,每次循环都会创造一个块级作用域的i
,所以可以直接使用ps[i]
去对对应的元素设置类名。