作用域

作用域其实可理解为执行上下文中声明的 变量和声明的作用范围。可分为 块级作用域函数作用域

特性

  • 变量提升: 一个声明在函数体内都是可见的, 函数优先于变量

    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

闭包

闭包是指有权访问另外一个函数作用域中的变量的函数

闭包表现形式

函数体内返回函数

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

作为函数参数传递

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

立即执行函数

let a = 1
(function () {
  console.log(a) // 1
})()
1
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
详解

首先我们需要弄清楚上述,打印为什么都是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
// setTimeout 第三参数传递
for(var i = 1; i <= 5; i ++){
  setTimeout(function timer(i){
    console.log(i)
  }, 0, i)
}
1
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
上次更新: 2021/10/20 下午4:40:42
贡献者: 陈书进