JavaScript作用域学习笔记

JavaScript作用域学习笔记

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

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

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

JavaScript的作用域链

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

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

var girl = "Jane"
function scopeTest() {
    var girl = "Alice";
    printGirl();
}
function printGirl() {
    console.log(girl);
}
scopeTest();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

JavaScript的块级作用域

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

var isSingleDog = false;
if(true) {
    var girlFriend = "cuteGirl";
} else {
    var bromance = "awesomeBoy";
}
console.log(girlFriend);
console.log(bromance);

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

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

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

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

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

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

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

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

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

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

总结

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

你可能感兴趣的:(JavaScript)