JavaScript作用域学习笔记

r囧r小猫 2022-06-03 09:56 360阅读 0赞

JavaScript作用域学习笔记

JavaScript中的作用域,是要单独拿出来学习的,因为它和别的语言不太一样。

不一样的地方主要有两点:

  1. JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。–JS权威指南
  2. 在ES6之前,JavaScript中是没有块级作用域的。

JavaScript的作用域链

首先,最重要的还是这句话,JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。

我们先来体会一下这句话:

  1. var girl = "Jane"
  2. function scopeTest() {
  3. var girl = "Alice";
  4. printGirl();
  5. }
  6. function printGirl() {
  7. console.log(girl);
  8. }
  9. scopeTest();

这里的输出是Jane,而不是Alice。

这就是因为printGirl这个函数的作用域下girl的值为“Jane”。

这下我们知其然了,下面就来知其所以然。

先来了解一下有关作用域链的基本知识

当代码在一个环境中执行时,会创建变量对象 (每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中,我们的代码中无法访问,但是解析器在处理数据时会在后台使用它)的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。

作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象 (活动对象在最开始时,只包含一个变量,就是arguments对象,这个对象在全局环境中不存在)作为变量对象。

作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样一直延续到全局执行环境。全局执行环境的变量对象时钟都是作用域链中的最后一个对象。

那么接下来,按照代码的执行顺序分析一下:

  1. 首先在进入全局执行环境时,就会创建全局执行环境下的变量对象的作用域链,会把环境中定义的变量和函数添加进去。而在浏览器环境下,全局变量对象就是window,所以这时的scope chain 为:

    1. [[scope chain]] = [
    2. {
    3. window call object //包含变量girl以及函数scopeTest、printGirl
    4. }
    5. ]
  2. 创建好作用域后,继续执行到对于scopeTest的调用,进入到scopeTest的函数作用域中,创建活动对象,虽然没有传入对象,但是其中还是有arguments这个变量,因此此时的scope chain为:

    1. [[scope chain]] = [
    2. {
    3. arguments:[],
    4. girl: "Alice"
    5. },
    6. {
    7. window call object //包含变量girl以及函数scopeTest、printGirl
    8. }
    9. ]
  3. 然后继续执行,到printGirl的调用,进入到printGirl的函数作用域中,创建活动对象,这个环境中没有定义变量,所以活动对象中只有一个arguments变量。需要注意的是,在printGirl的外部环境中,并不包含scopeTest中定义的变量。此时的scope chain为:

    1. [[scope chain]] = [
    2. {
    3. arguments:[]
    4. },
    5. {
    6. window call object //包含变量girl以及函数scopeTest、printGirl
    7. }
    8. ]
  4. 继续执行,就到了console.log(girl),需要对girl变量进行读操作,所以顺着scope chain往上找,由于外面只有一层,所以当找到全局变量时,就会找到在全局环境中定义的girl=”Jane”。

这里容易错的地方,就是误以为scopeTest中定义的变量作为printGirl的外部环境,会添加到作用域链中。

啰啰嗦嗦说了一大串,找个girl真的不容易(笑),其实最精华的还是那句话,JavaScript中的函数运行在它们定义的作用域,而不是在它们执行的作用域。

在以上的学习过程中,也用到了其他的知识点:

JavaScript的预编译,解释器在执行每一段代码之前,都会先处理var关键字和function定义式(函数定义式)。不过对于这两者的处理还是有所不同的:

  1. console.log(typeof fn1);//function
  2. console.log(typeof fn2);//undefined
  3. console.log(typeof str);//undefined
  4. function fn1() { }//函数定义式
  5. var fn2 = function() { };//函数表达式
  6. var str = "scopeTest";

对于函数定义式,会把函数的定义提前,而函数表达式,会在执行过程中才计算。

其实函数表达式是和变量的声明一样的。在预编译时只是声明了这个变量,但是还没有对齐赋值,所以是undefined。

JavaScript的块级作用域

根据上面的描述,我们也发现,在函数调用时才会创建一个作用域,这也就说明在一个函数作用域中定义的变量都是同一级的。比如说:

  1. var isSingleDog = false;
  2. if(true) {
  3. var girlFriend = "cuteGirl";
  4. } else {
  5. var bromance = "awesomeBoy";
  6. }
  7. console.log(girlFriend);
  8. console.log(bromance);

别着急看输出结果,猜一下是输出了awesomeBoy还是输出了cuteGirl呢?

两个都要,想得挺美,这两个当然是不可兼得的了。

如果isSingleDog=false,那么输出的将是cuteGirl和undefined。

如果isSingleDog=true,那么输出的将是undefined和awesomeBoy。

为什么明明只定义了一个,却没有报错呢。这就和前面讲到的预编译有关了。在进入这个作用域时,会先把所有的var声明的变量进行声明,因此不会报错。但是却并不会赋值,只会在后面的执行过程中对变量进行赋值。所以最终只能有一个被赋值,而另一个则是undefined了。

这看起来似乎并不算问题,对我们的编码似乎并不造成影响。其实不是的,看下面的代码:

  1. for(var i = 0; i < 10; i++) {
  2. ...
  3. }
  4. console.log(i);//10

在这里,我们显然只是想让i作为一个循环控制字,但是它却跑到了循环外面去。

虽然这个问题也不会造成很大的影响。但是在ES6中,还是引入了let关键字来创建一个块级作用域来避免这种问题的出现。

当然了,let的作用远不这这一点,在后面的学习笔记还会继续学习。

总结

由于JavaScript和其他语言对于作用域的处理方式不同而造成的一些现象。理解的关键点还是对于变量对象、活动对象和作用域链的概念,以及那一句话:JavaScript中的函数运行在它们定义的作用域,而不是它们运行的作用域下。

发表评论

表情:
评论列表 (有 0 条评论,360人围观)

还没有评论,来说两句吧...

相关阅读

    相关 JavaScript笔记作用闭包

    启示 闭包并不是一个需要学习新的语法或模式才能使用的工具,它也不是一件必须接受像 Luke 一样的原力训练才能使用和掌握的武器。 闭包是基于词法作用域书写代码时所产生的

    相关 JavaScript笔记:词法作用

    1、词法阶段 简单地说,词法作用域就是定义在词法阶段的作用域。 后面会介绍一些欺骗词法作用域的方法,这些方法在词法分析器处理过后依然可以修改作用域,但是这种机制

    相关 JavaScript笔记作用

    1、 编译原理 尽管通常将 JavaScript 归类为“动态”或“解释执行”语言,但事实上它是一门编译语言。 在传统编译语言的流程中,程序中的一段源代码在执行之前会经

    相关 JavaScript作用

    ES5中的作用域 一、全局变量 > 全局变量有 全局作用域: 网页中所有脚本和函数均可使用 > 如果变量在函数内没有声明(没有使用 var 关键字),该变量为全