题解JavaScript作用域

1,160 阅读5分钟
原文链接: github.com

又到一年底,大家是不是又开始蠢蠢欲动,准备开始到处吹嘘了。

不过你确定你已经准备好了吗?

image

凡是参加过面试的朋友应该都有过一个相同的感受,就是面试官常常会抛出个很宏观的问题,让你有种 狗啃乌龟,不知从何下口 的赶脚。

image

现在让我们来一道最基本的题:能不能说说你对JavaScript作用域的理解?

image

我记得之前问过一个从事开发十来年的大哥,大哥贼猛,前后端通杀,运维也照样搞,撸码就是一把梭,抡起jQuery就是干。。。跑题了。。。

当时他给我的回答大体是:

  JavaScript是门动态语言,跟Java不一样,JavaScript可以随意定义全局变量和局部变量,变量会在该作用
  域下提升,而且JavaScript没有块级作用域。全局变量就是定义在全局的变量了,局部变量是定义在函数
  里的变量,每一个函数都是一个作用域,当函数执行时会优先查找当前作用域,然后逐级向上。定义在 
  if 和 for 语句里的变量,在大括号外面也能访问到,这就是没有块级作用域。

如果你正在阅读这篇文章,你或许不能从这篇文章学到什么东西,但起码说明你是个愿意学习的人,所以我觉得你可能会在上面回答的基础上加一句:JavaScript是静态作用域,在对变量进行查询时,变量值由函数定义时的位置决定,和执行时的所处的作用域无关

知道ES6的童鞋可能还会指出 JavaScript已经有块级作用域了,而且用 let 和 const 定义的变量不会提升

到此为止,似乎JavaScript的作用域和作用域链都简单概括完了,但是,你真的没有问题了吗?

image

// 第一题
var a = 1;
function fn() {
  console.log('1:' + a);

  var a = 2;
  bar()
  console.log('2:' + a)
}

function bar() {
  console.log('3:' + a)
}

fn()
// 第二题
var a = 1;
function fn() {
  console.log('1:' + a);
  a = 2
}

a = 3;
function bar() {
  console.log('2:' + a);
}

fn();
bar();

大家先猜猜,这两道题打印的值分别是多少。

把你自己分析出来的值记下来,再和正确答案对比下,看看对了多少。

// 第一题正确答案
1:undefined
3:1
2:2

// 第二题正确答案
1:3
2:2

我们先看 第一题

第一个 a 打印的值是 1:undefined 而不是 1。因为我们在 fn() 中定义了变量 a,用 var 定义的变量会在当前作用域提升,但是并不会携带赋给变量的值一起提升。

第二个 a 打印的值是 3:1 而不是 2。因为函数 bar 是定义在全局作用域中的,所以作用域链是 bar -> global,bar 里面没有定义a,所以就会顺着作用域链向上找,然后在 global 中找到了 a。

第三个 a 打印的值是 2:2。这句话所在的作用域链是 fn -> global,执行 console.log('2:' + a) 会首先在 fn 作用域里查找 a,找到有 a,并且值为2,所以结果就是2。

再来看看 第二题

第一个 a 打印的值是 1:3,既不是 undefined 也不是 1。首先, fn 中的 a = 2 是给变量 a 赋值,并没有定义变量。然后,执行函数 fn,在查找变量 a 时,此时查找的变量就是全局变量 a,不过此时 a 的值为3。

第二个 a 打印的值是 2:2。函数 bar 所能访问的作用域链为 bar->global,在执行函数 bar 时,a 的值已经被修改成了 2。

结论

前面的东西可能有点啰嗦冗余,我们稍稍提炼总结一下。

  1. 在JavaScript中,通过 letconst 定义的变量具有块级作用域的特性。

  2. 通过 var 定义的变量会在它自身的作用域中进行提升,而 letconst 定义的变量不会。

  3. 每个JavaScript程序都具有一个全局作用域,每创建一个函数都会创建一个作用域。

  4. 在创建函数时,将这些函数进行嵌套,它们的作用域也会嵌套,形成作用域链,子作用域可以访问父作用域,但是父作用域不能访问子作用域。

  5. 在执行一个函数时,如果我们需要查找某个变量值,那么会去这个函数被 定义 时所在的作用域链中查找,一旦找到需要的变量,就会停止向上查找。

  6. “变量的由函数定义时的位置决定”这句话有歧义,准确说是查找变量时,是去定义这个函数时所在的作用域链查找。

小练习

之前本来没想过写这篇文章,只是前段时间在翻译 什么是JavaScript闭包? 这篇文章时,做里面的一道题时竟然错了一个值,所以写了这篇文章,没有什么新东西。算是为了提醒像我一样还没有称谓老司机的童鞋们,我们可能看了很多文章很多书,知道了很多理论,但是在分析代码和问题时,一定要仔细全面。

最后给一些新入坑的童鞋留个小作业,判断打印出来的 a 的值是多少:

// 第一题
var a = 1;
function fn(f) {
  var a = 2;
  return f;
}
function bar() {
  console.log(a)
}
var f1 = fn(bar);
f1()
// 第二题
var a = 1;
function fn(f) {
  var a = 2;
  return function () {
    console.log(a)
  }
}

var f1 = fn();
f1()