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对象,这个对象在全局环境中不存在)作为变量对象。
作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样一直延续到全局执行环境。全局执行环境的变量对象时钟都是作用域链中的最后一个对象。
那么接下来,按照代码的执行顺序分析一下:
首先在进入全局执行环境时,就会创建全局执行环境下的变量对象的作用域链,会把环境中定义的变量和函数添加进去。而在浏览器环境下,全局变量对象就是window,所以这时的scope chain 为:
[[scope chain]] = [
{
window call object //包含变量girl以及函数scopeTest、printGirl
}
]
创建好作用域后,继续执行到对于scopeTest的调用,进入到scopeTest的函数作用域中,创建活动对象,虽然没有传入对象,但是其中还是有arguments这个变量,因此此时的scope chain为:
[[scope chain]] = [
{
arguments:[],
girl: "Alice"
},
{
window call object //包含变量girl以及函数scopeTest、printGirl
}
]
然后继续执行,到printGirl的调用,进入到printGirl的函数作用域中,创建活动对象,这个环境中没有定义变量,所以活动对象中只有一个arguments变量。需要注意的是,在printGirl的外部环境中,并不包含scopeTest中定义的变量。此时的scope chain为:
[[scope chain]] = [
{
arguments:[]
},
{
window call object //包含变量girl以及函数scopeTest、printGirl
}
]
继续执行,就到了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。
根据上面的描述,我们也发现,在函数调用时才会创建一个作用域,这也就说明在一个函数作用域中定义的变量都是同一级的。比如说:
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中的函数运行在它们定义的作用域,而不是它们运行的作用域下。