Skip to content
On this page

作用域和作用域链

作用域

什么是作用域

作用域可以理解成一个独立地盘,让变量不会泄露,暴露出去。 也就是说,作用域最大的作用就是隔离变量,不同作用域下的同样名称变量不会有冲突。

ES6 之前 JS 是没有块级作用域的,只有全局作用域和函数作用域。 ES6 的到来为我们提供了 快级作用域,可以通过 letconst 来体现

全局作用域

最外层的函数和在外层外面定义的变量拥有全局作用域

javascript
var outVar = '最外层变量'

// 最外层函数
function outFun() {
  var inVar = '内层变量'
  
  // 内层函数 innerFun
  function innerFun() {
    console.log(inVar)
  }
  
  innerFun()
}


console.log(outVar)   // ==> 最外层变量

outFun()              // ==> 内层变量

console.log(inVar)    // ==> inVar is not defined

innerFun()            // ==> innerFun is not defined

所有未定义的直接赋值的变量,会自动声明为拥有全局作用域

javascript
function fun2() {
  var1 = '未定义直接赋值的变量'

  var var2 = '内部变量'
}

fun2() // 要执行这个函数,否则不知道里面是啥

console.log(var1) // 未定义直接赋值的变量 (全局变量,在函数 fun2执行后,会挂载到 window 下) 

console.log(var2) // 报错(函数内部变量,无法访问)

所有 window 对象的属性拥有全局作用域

全局作用域的弊端:污染全局命名空间,容易引起命名冲突

函数作用域

函数作用域是指 函数内部声明 的变量,和全局作用域相反

局部作用域一般只是在固定的代码片段内可以访问到,常见是函数内部

jQuery、Zepto 等库的源码,代码放在 (function(){....})() 中,变量不会被外泄和暴露, 不会污染到外面,不会对其他js库造成影响

javascript
function fun3() {
  var var1 = '内部变量'

  function innerSay() {
    alert(var1)
  }

  innerSay()  
}

alert(var1) // 报错 (var1 是函数内部的变量,访问不到)

innerSay() // 报错 (innerSay 是函数内部的函数,访问不到)

作用域分层

作用域是分层的,内层作用域可以访问外层作用域的变量,反之不行

如图

作用域分层

块语句(大括号“{}”中间的语句if、switch、for、while,不像函数,它们不会创建一个新的作用域

块级作用域

块级作用域可以用 letconst 声明,所声明的变量只能在指定块的作用域中访问

  • 在一个函数内部

  • 在一个代码块(花括号{})内部

  • 声明的变量不会提升到代码块顶部

  • 同一个作用域禁止重复声明

  • 循环中绑定块作用域

var 声明 for 循环

变量是全局变量,每次循环变量的i的值都会发生改变。

let 声明 for 循环:

只存在于块作用域内有效,每次循环都是一个新的变量, js 引擎内部会记住上一轮循环的值,在初始化本轮的变量 i 时,就在上一轮的基础值进行计算

TIP

for 循环还有一个特别之处,设置循环变量的那部分是一个父作用域,而循环体内部是一个单独子作用域

作用域链

什么是自由变量

没有在当前作用域定义的变量,可以通过向父级作用域寻找(不严谨)的变量——自由变量

javascript
var a = 10

function fn() {
  var  b = 20

  console.log(a) // 10 (a 在这里就是自由变量)

  console.log(b)
}

fu()

作用域链

如果父级没有呢?再一层一层的向上找,直到找到全局作用域还是没有找到,就宣布放弃, 这一层关系,就是作用域链

javascript
var a = 10

function f1() {
  var b = 20
  
  function f2() {
    var c = 30
    
    console.log(a)  // 自由变量,顺着作用域链找
    console.log(b)  // 自由变量,顺着作用域链找
    console.log(c)  // 当前作用域变量
  }
  
  f2()
}

f1()

自由变量的取值

要到创建这函数的那个作用域中取值,而不是调用, 这里强调是创建 ,无论函数将在哪里调用,这也就是所谓的 静态作用域

例子1:

javascript
var x = 10

function fn() {
  console.log(x) // 10
}

function show(f) {
  var x = 20
  
  (function () {
    f() // 10, 而不是20
  })()
}

show(fn)

简单过程

fn 函数在取值变量 x 时,要到自己定义的作用域中去寻找 x,x = 10

例子2:

javascript
var a = 10

function fn() {
  var b = 20
  
  function bar() {
    console.log(a + b)  // 30
  }
  
  return bar
}

var x = fn() // x = bar

b = 200

x() // bar() ==> 30

简单过程

fn() 返回 bar 函数,赋值x,执行 x(),执行bar

bar 作用域没有b,去向创建 bar 的作用域的 fn 作用域取找到 b = 20

fn作用域中没有 a,去创建 fn 的作用域中查处,找到 a = 10

作用域与执行上下文

js 属于解释型语言。解释和执行两个阶段

解析阶段

  • 词法分析

  • 语法分析

  • 作用域规则确定

执行阶段

  • 创建执行上下文(this 指向是执行的时候确定的)

  • 执行函数代码

  • 垃圾回收

作用域与 this 关系

  • 执行上下问是在运行的时候确定,随时可以改变

  • 作用域在定义的时候确定,并且不会改变

  • 同一个作用域下,不同的调用,会产生不同的执行上下文环境,继而产生不同的变量的值

  • 一个作用域下包括若干个上下文环境,也可能没有上下文环境(函数未调用),有可能同时存在一个或多个(闭包)

变量提升 和 函数提升

为什么会有 变量提升 和 函数提升

  • js 在读取代码过程分为两步
  • 第一步解析 js 代码,第二步执行 js 代码
  • 浏览器在解析遇到 var 和 function 整个函数时,会提升到当前作用域的最前面

变量提升

  • 在 es6 之前,js 没有块级作用域这一说,只有全局作用域和函数作用域。 var声明的变量提升到他所在的作用域的最顶端。

函数提升

  • 函数声明 可被提升: function foo() {}
  • 函数表达式 不可提升:var foo = function () {}

例子1:函数声明可被提升

javascript
console.log(square(5)); // 25

function square(n) {
  return n * n;
}

过程相当于

javascript
function square(n) {
  return n * n
}

console.log(square(5)) // 25

例子2:函数表达式不可被提升

javascript
console.log(square) // undefined

console.log(square(5)) // square is not a function =》 初始化并未提升,此时 square 值为 undefined

var square = function (n) {
  return n * n
}

过程:

javascript
var square
console.log(square) // undefined =》赋值没有被提升

console.log(square(5)) // square is not a function =》 square 值为 undefined 故报错

square = function (n) {
  return n * n
}

参考

深入理解JavaScript作用域和作用域链