# 函数作用域和块作用域

作用域包含了一系列的'气泡',每一个都可以作为容器,其中包含了标识符(变量、函数)的定义。这些气泡互相嵌套并且整齐的排列成蜂窝型,排队的结构是在写代码的时候定义的。

# 函数中的作用域

function foo(a) {
  var b = 2
  // 一些代码
  function bar() {
    // ...
  }

  // ...

  var c = 3
}
1
2
3
4
5
6
7
8
9
10
11

在这个代码片段中,foo(..) 的作用域气泡中包含了标识符 a、b、c 和 bar。无论标识符声明出现在作用域中何处,这个标识符所代表的变量和函数都将附属于所处作用域的气泡。

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用以及复用(事实上在嵌套的作用域中也可以使用)。这种设计方案是非常有用的,能充分利用JavaScript变量可以根据需要改变值类型的【动态】特性。

# 隐藏内部实现

function doSomething(a) {
  b = a + doSomethingElse(a * 3)
  console.log(b * 3)
}
// 全局
function doSomethingElse (a) {
  return a - 1
}

var b

doSomething (2)
1
2
3
4
5
6
7
8
9
10
11
12

变量 b 和 doSomethingElse 应该是 doSomething 内部具体实现的私有内容。给予外部作用域对 b 和 doSomethingElse的访问权不仅没必要,而且可能是危险的。因为他们可能被有意或无意的以非预期的方式使用,从而导致超出了 doSomething 的适用条件。变更如下:

function doSomething (a) {
  // 私有
  function doSomethingElse (a) {
    return a - 1
  }

  var b
  b = a + doSomethingElse(a * 2)
  console.log(b * 3)
}
doSomething(2) // 15
1
2
3
4
5
6
7
8
9
10
11

# 规避冲突

'隐藏'作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突,两个标识符可能具有相同的名字但用途却不一样,无意间可能造成命名冲突。冲突会导致变量的值被意外覆盖。

function foo() {
  function bar(a) {
    i = 3; // 修改 for 循环所属作用域中的
    console.log( a + i)
  }

  for (var i = 0; i < 10; i++) {
    bar (i * 2) // 糟糕,无线循环了
  }
}
foo()
1
2
3
4
5
6
7
8
9
10
11

bar(...) 内部的赋值表达式 i = 3 意外的覆盖了声明在 foo(...) 内部 for 循环中的 i。在这个例子中将会导致无限循环,因为 i 被固定设置为 3,永远满足小于 10 这个条件。

function foo() {
  function bar(a) {
    var i = 3; // 遮蔽变量 或者换一个变量名字
    console.log( a + i)
  }

  for (var i = 0; i < 10; i++) {
    bar (i * 2) // 糟糕,无线循环了
  }
}
foo()
1
2
3
4
5
6
7
8
9
10
11

变量冲突的解决方式:命名空间模块管理

// 立即执行函数表达式
var a = 2
// 此处是一个函数表达式,不是函数声明
(function (){
  var a = 3
  console.log(a) // 3
})()
console.log(a) // 2
1
2
3
4
5
6
7
8

区分函数声明和函数表达式: 看 function 关键字出现在整个声明中的位置,如果 function 是声明中的第一个词,那就是一个函数声明,否则是一个函数表达式。

函数声明和函数表达式最重要的区别是他们的名称标识符将会绑定在何处

# 块作用域

for (var i = 0; i < 10; i++) {
  console.log(i) // 0 1 2 3 4 5 6 7 8 9
}
console.log(i) // 10
1
2
3
4
  • with
  • try catch
  • let

let 声明不会再块作用域中进行提升

{
  console.log(bar) // ReferenceError
  let bar = 2
}
1
2
3
4

let 循环

for (let i = 0; i < 10; i++) {
  console.log(i)
}

console.log(i) // ReferenceError

1
2
3
4
5
6

for 循环头部的 let 不仅将 i 绑定到了 for 循环的块中,事实上他将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。等同于下面

{
  let j 
  for (j = 0; j < 10; j++) {
    let i = j // 每个迭代重新绑定
    console.log(i)
  }
}
1
2
3
4
5
6
7

const 同样可以用来创建块作用于变量,但其值是固定的(常量)。任何试图修改值的操作都会引起错误

# 小结

函数是 JavaScript 中最常见的作用于单元,本质上,声明在一个函数内部的变量或函数会在所处的作用域中‘隐藏’起来。 函数不是唯一的作用域单元,块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指的是{...}内部)。即任何声明在某个作用域内的变量,都将附属于这个作用域