# 重学es6系列(1) --块级作用域绑定

JS中作用域有:全局作用域、函数作用域。没有块作用域的概念。ECMAScript 6(简称ES6)中新增了块级作用域。 块作用域由 { } 包括,if语句和for语句里面的{ }也属于块作用域。

# 1. es5中的var

在很久以前,大家都是使用var 来定义一个js变量。但是使用var会有一个特点, 无论你是在函数作用域中(在一个函数中)或者是在全局作用域中(一个js文件或者html文件中的script标签内)声明这个变量,都会当做在当前作用域的顶部声明的变量,这就是我们常说的提升机制。

console.log(cc)
var cc = 'cc'
// 输出 undefined

console.log(bb)
let bb = 'bb'
// Uncaught ReferenceError: Cannot access 'bb' before initialization
1
2
3
4
5
6
7

上面一个var是es5的语法, 一个let是es6的语法, 发现输出的结果完全不同,一个输出了undefined一个却报错了。

那是因为在js的预编译阶段,js引擎会将上面的var语法修改成下面这样:

var cc
console.log(cc)
cc = 'cc'
// 输出 undefined
1
2
3
4

为此

es6引入块级作用域来强化对变量生命周期的控制。let 和 const 都不会进行所谓的变量提升。

# 2. es6中的let和const

# 禁止重复声明

先看一段代码

var aa = 'cc'
var aa = 'aa'
console.log(aa)
// 输出 aa

let bb = 'cc'
let bb = 'bb'
//  Identifier 'bb' has already been declared
1
2
3
4
5
6
7
8

会发现var允许重复声明一个同样名字的变量,那在开发中就要注意了,小心自己定义的变量撞脸了别人的变量。而使用let声明同样名字的变量会报错。

# 块级作用域

块级作用域(亦被称为词法作用域)存在于:

  • 函数内部
  • 块中(字符{和}之间的区域)

es5之前没有let和const该怎么创建块级作用域呢?

(function(){
  // todo
}())
1
2
3

这样就可以保证不污染外面的变量。

# const 必须初始化

const aa
//  Missing initializer in const declaration
1
2

会报错, 告诉你常量必须初始化。

# const 来声明对象和数组

const obj = { name: 'jack' }
obj.name = 'lili' // ok
obj = { name: 'lili' }
//  Assignment to constant variable.

const arr = [1,2,3]
arr[0] = 4  // ok
console.log(arr) // [4,2,3]
arr = [5,2,3]
//  Assignment to constant variable.
1
2
3
4
5
6
7
8
9
10

const 是用来声明常量的, 也就是不可变数据。但是声明为对象和数组时, 却可以改变其中的属性。

# 循环中的块级作用域

# 以往使用var来循环

for (var i = 1; i <= 10; i++) {
  console.log(i) // 循环输出1~10
}
console.log(i) // 输出 11
1
2
3
4

会发现在 for 循环之外也可以访问到i。但是其实我们都是不希望在外面访问到i的,希望的是在for循环结束之后i就要销毁。

# 创建函数

var funs = []
for (var i = 1; i <= 10; i++) {
  funs.push(function() {
    console.log(i)
  })
}
funs.forEach(function (item) {
  item() // 输出10个 11
})
1
2
3
4
5
6
7
8
9

代码输出了10个11, 这不是我们想要的结果。我们希望的是输出每次循环到的值,也就是1~10。这是因为函数会保存对变量的引用, 即在函数执行时才会去取i最后的值。下面的代码输出的是10个99.

var funs = []
for (var i = 1; i <= 10; i++) {
  funs.push(function() {
    console.log(i)
  })
}
i = 99
funs.forEach(function (item) {
  item() // 输出10个 99
})
1
2
3
4
5
6
7
8
9
10

为了解决这个问题, 我们可以采用立即执行函数来处理。这样就是输出了1~10。将我们想写的代码放入todo里面,这里我们 返回一个函数给数组, 因为立即执行函数会直接执行我们的代码。

var funs = []
for (var i = 1; i <= 10; i++) {
  funs.push((function(val) {
    return function () {
      // todo
      return console.log(val)
    }
  })(i))
}
funs.forEach(function (item) {
  item() // 1~10
})
1
2
3
4
5
6
7
8
9
10
11
12

# 循环中的异步

有一道比较经典的面试题,如何输出1~11,下面的代码只能输出10个11。原因就是循环走完了, 但是异步还没走。等到循环结束了, 这时i = 11了,开始执行异步。

for (var i = 1; i <= 10; i++) {
  setTimeout(function() {
    console.log(i) // 10 个 11
  }, 0)
}
1
2
3
4
5

下面有几种方法解决这个问题:

  1. 采用 setTimeout 的第三个参数, 将外面的参数传入到函数里面。
for (var i = 1; i <= 10; i++) {
  setTimeout(function(val) {
    console.log(val)
  }, 0, i)
}
1
2
3
4
5
  1. 还有上面说的立即执行函数来处理
for (var i = 1; i <= 10; i++) {
  setTimeout((function(val) {
    console.log(val)
  }(i)), 0)
}
// 还可以这样
for (var i = 1; i <= 10; i++) {
  (function (val) {
    setTimeout(function() {
      console.log(val)
    }, 0)
  })(i)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. 使用 bind
for (var i = 1; i <= 10; i++) {
  setTimeout(function(val) {
    console.log(val)
  }.bind(null, i), 0)
}
1
2
3
4
5
  1. 最好的方法 ,使用es6中的let
for (let i = 1; i <= 10; i++) {
  setTimeout(function() {
    console.log(i)
  }, 0)
}
1
2
3
4
5

参考资料:

阮一峰的es6教程 (opens new window)

上次更新: 10/14/2022, 4:05:31 PM