作用域
作用域其实可理解为执行上下文中声明的 变量和声明的作用范围
。可分为 块级作用域
和 函数作用域
特性
变量提升
: 一个声明在函数体内都是可见的, 函数优先于变量var
声明的变量会提升在执行上下文最顶层,存储为undefined
function
声明的函数提升到执行上下文最顶层,存储为函数本身
let|const
没有变量提升,必须先声明后使用- 非匿名自执行函数,函数变量为
只读
状态,无法修改(function foo() { // 此foo是非匿名自执行函数,只读状态 无法修改 foo = 10 console.log(foo) // function foo() })()
1
2
3
4
5
作用域链
我们在当前执行上下文中能访问到父级甚至是全局的变量,这主要是因为作用域链。
作用域链可以理解为一组对象列表,包含 父级及自身的变量对象
。
由两部分组成:
[[scope]]
属性: 指向父级变量对象和作用域链,也就是包含了父级的[[scope]]
和AO
AO: 自身活动对象
自由变量查找规则
函数在哪儿定义, 就从哪儿沿着作用域链一层层往上找,不管在哪儿调用函数。
let aa = 22
function a(){
console.log(aa)
}
function b(fn){
let aa = 11
fn()
}
b(a) //22
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
闭包
闭包是指有权访问另外一个函数作用域中的变量
的函数
闭包表现形式
函数体内返回函数
function test() {
let a = 1
return function() {
console.log(a)
}
}
// 此时返回的函数 引用了父执行上下文的变量a
const fn = test()
fn() // 1
// 当将函数体test销毁 变量a还是存在闭包之中不会被销毁
test = null
fn() // 1
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
作为函数参数传递
let a = 1
function test() {
let a = 2
function baz() {
console.log(a)
}
bar(baz)
}
function bar(fn) {
fn()
}
test() // 2
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
立即执行函数
let a = 1
(function () {
console.log(a) // 1
})()
1
2
3
4
2
3
4
TIP
在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包
闭包思考题
如何解决下面的循环输出问题?
for(var i = 1; i <= 5; i ++){
setTimeout(function timer(){
console.log(i)
}, 0)
}
1
2
3
4
5
2
3
4
5
详解
首先我们需要弄清楚上述,打印为什么都是6?
setTimeout是宏任务,这里涉及到事件队列,事件循环,详情见event-loop。 当循环体执行完毕,timer模块才会从异步队列中提取异步任务按照先进先出原则依次执行,此时i已经是6了。
解决方案:将 当时循环变量i闭包在函数timer体内。
// es6新的声明变量方式 具有块级作用域
for(let i = 1; i <= 5; i ++){
setTimeout(function timer(){
console.log(i)
}, 0)
}
1
2
3
4
5
6
2
3
4
5
6
// setTimeout 第三参数传递
for(var i = 1; i <= 5; i ++){
setTimeout(function timer(i){
console.log(i)
}, 0, i)
}
1
2
3
4
5
6
2
3
4
5
6
// 匿名函数自运行闭包
for(var i = 1; i <= 5; i ++){
(function(i) {
setTimeout(function timer(){
console.log(i)
}, 0)
})(i)
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8