块级作用于的概念
由一对花括号{}
中的语句集都属于一个块,在这个{}
里面包含的块内定义的所有变量在代码块外都是不可见的,因此称为块级作用域。
作用域永远都是任何一门语言的重中之中,因为它控制着变量和参数的可见性和生命周期。讲到这里,首先要理解两个概念:块作用域和函数作用域。什么是块级作用域呢?
任何一对花括号({})中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。
函数作用域就更好理解了定义在函数中的参数和变量在函数外是不可见的。
作用域分析
function fn1(x) {
var a = 0;
let b = 0;
{
var c = 1; // 未用let或const,不形成c的块作用域,依然是bar的函数作用域
let d = 1; // d形成块作用域,只可在当前代码块中访问
}
function fn() {
var e = 2;
let g = 2;
console.log('a:', a)
console.log('b:', b)
console.log('c:', c) // 访问外部作用域中的a/b/c,形成闭包
// console.log('d:', d) // d is undefined
}
f()
}
fn1(5)
代码执行进入fn1函数时的作用域如下:
- 在fn1中用var定义的a
- 在fn1中用let定义的b
- 在代码块中用var定义的c
- 在fn1中定义的函数fn
- fn1形参x
当前执行上下文栈是 [全局执行上下文, fn1执行上下文]
代码进入代码块时的作用域
当前执行上下文栈是 [ 全局执行上下文,fn1执行上下文, 块作用域1]
代码进入fn函数时的作用域
在fn中用var声明的e
在fn中用let声明的f
在fn中有对fn1变量对象的引用,形成闭包
当前执行上下文栈是 [ 全局执行上下文,fn1执行上下文(闭包), fn执行上下文]
for遍历中的var与let
function bar() {
var fnArr1 = []
var fnArr2 = []
var fnArr3 = []
for(var i = 0; i < 5; i++) {
fnArr1.push(function() {
return i
})
}
for(var k = 0; k < 5; k++) {
(function(k) {
fnArr2.push(function() {
return k
})
})(k)
}
for(let j = 0; j < 5; j++) {
fnArr3.push(function() {
return j
})
}
console.log('i:', i) // 5
console.log('fnArr1:', fnArr1.map(x => x())) // [4, 4, 4, 4, 4]
console.log('--------')
console.log('k:', k) // 5
console.log('fnArr2:', fnArr1.map(x => x())) // [0, 1, 2, 3, 4]
console.log('--------')
console.log('j:', j)
console.log('fnArr3:', fnArr1.map(x => x()))
}
在for循环中使用var声明表达式变量
var声明变量不具有块作用域特性,它声明的变量作用域为当前作用域,在循环中i会被反复覆盖,所以当循环遍历结束后,i的值为最后一次遍历的值,即在这里为5。
var fnArr1 = []
for(var i = 0; i < 5; i++) {
fnArr1.push(function() {
return i
})
}
console.log('fnArr1:', fnArr1.map(x => x())) // [5, 5, 5, 5, 5]
在for循环中使用var声明表达式变量且用立即执行函数
通过传递参数到立即执行函数,传递的参数是非引用类型变量,所以已然切割了与外面变量k的联系,即第一次循环传递的是数字0, 第二次循环传递的是数字1 … 以此类推,所以遍历执行数组的函数会返回一个递增的数组。
var fnArr2 = []
for(var k = 0; k < 5; k++) {
(function(k) {
fnArr2.push(function() {
return k
})
})(k)
}
console.log('fnArr2:', fnArr1.map(x => x())) // [0, 1, 2, 3, 4]
在for循环中使用let声明表达式变量
let声明的变量会有块级作用域的特点,所以在for循环表达式中使用let其实就等价于在代码块中使用let,也就是说
for(let j = 0; j < 5; j++) 这句话的圆括号之间,有一个隐藏作用域
for(let j = 0; j < 5; j++) { 循环体 } 在每次执行循环体之前,js引擎会把j在循环体的上下文中重新声明及初始化一次
var fnArr3 = []
for(let j = 0; j < 5; j++) {
fnArr3.push(function() {
return j
})
}// js引擎会处理成
for(let j = 0; j < 5; j++) {
let t = j
fnArr3.push(function() {
return t
})
}
console.log('fnArr3:', fnArr1.map(x => x())) // [0, 1, 2, 3, 4]
拓展
function fn() {
var fnArr = []
for (let p = { i: 0 }; p.i < 5; p.i++) {
fnArr.push(function() {
return p.i
})
}
console.log(fnArr.map(x => x())) // [5, 5, 5, 5, 5] 为什么??
}
fn()
按照上面的理解打印出来的应该是[0, 1, 2, 3, 4]才对,但是为什么与期望不符呢?这与浅拷贝/深拷贝的问题有关了
js引擎会把上面的循环处理为以下代码:
for (let p = { i: 0 }; p.i < 5; p.i++) {
let k = p // 这里为引用类型变量的赋值
fnArr.push(function() {
return k.i
})
}
js引擎在循环体中用let声明了一个变量k=p,这里p为引用类型变量*{ i: 0 }*, 即这里声明的k是对p对象的引用。所以执行fnArr中的函数,最终返回的是p的i属性;而p.i在一次次循环后已经自增为5,所以最终打印结果都是5。
那如何改写上面代码来实现想要的结果呢?
function fn() {
var fnArr = []
var o = { i: 0 }
for (let p = o.i; p < 5; p++) {
fnArr.push(function() {
return p
})
}
console.log(fnArr.map(x => x())) // [0, 1, 2, 3, 4]
}
fn()