关于js作用域问题详解
执行上下文
函数表达式和函数声明
1. console.log(a); // ReferenceError: a is not defined // ReferenceError(引用错误)对象表明一个不存在的变量被引用。 2. console.log(a); // undefined var a; 3. console.log(a); // undefined var a = 10; 4. var a = 10; console.log(a); // 10
在一段js代码拿过来真正一句一句运行之前,浏览器已经做了一些“准备工作”,其中就包括对变量的声明,而不是赋值。变量赋值是在赋值语句执行的时候进行的。可用下图模拟:第一句报错,a未定义,很正常。第二句、第三句输出都是undefined,说明浏览器在执行console.log(a)时,已经知道了a是undefined,但却不知道a是10(第三句中)。
接下来的这段代码需要注意代码注释中的两个名词——“函数表达式”和“函数声明”。虽然两者都很常用,但是这两者在“准备工作”时,却是两种待遇。
“准备工作”
1. console.log(a); // ReferenceError: a is not defined // ReferenceError(引用错误)对象表明一个不存在的变量被引用。 2. console.log(a); // undefined var a; 3. console.log(a); // undefined var a = 10; 4. var a = 10; console.log(a); // 10
在“准备工作”中,对待函数表达式就像对待“ var a = 10 ”这样的变量一样,只是声明。看以上代码。“函数声明”时我们看到了第二种情况,而“函数表达式”时我们看到了第一种情况。
而对待函数声明时,却把函数整个赋值了。
总结一下,在“准备工作”中完成了哪些工作:
- 变量、函数表达式——变量声明,默认赋值为undefined;
- this——赋值;
- 函数声明——把函数整个赋值;
这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。
function fn(x) { console.log(arguments); //Arguments { 0: 10, 等 2 项… } console.log(x); //10 } fn(10);
以上代码展示了在函数体的语句执行之前,arguments变量和函数的参数都已经被赋值。从这里可以看出,函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。
另外一点不同在于,函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。用一个例子说明一下:
var a = 10; function fn() { console.log(a); //a是自由变量 //函数创建时,就确定了a要取值的作用域 } function bar() { var a = 20; fn(); //打印10不是20 } bar(fn); //10
执行上下文栈给执行上下文环境下一个通俗的定义——在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用undefined占个空。
执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。
其实这是一个压栈出栈的过程——执行上下文栈。
1 var a = 10, //1.进入全局上下文环境 2 fn, 3 bar = function(x) { 4 var b = 5; 5 fn(x + b); //3.进入fn函数上下文环境 6 }; 7 8 fn = function(y) { 9 var c = 5; 10 console.log(y + c); 11 } 12 13 bar(10); //2.进入bar函数上下文环境
在执行代码之前,首先将创建全局上下文环境。
全局 | 上下文环境 |
---|---|
a | undefined |
fn | undefined |
bar | undefined |
this | window |
然后是代码执行。代码执行到第12行之前,上下文环境中的变量都在执行过程中被赋值。
全局 | 上下文环境 |
---|---|
a | 10 |
fn | function |
bar | function |
this | window |
执行到第13行,调用bar函数。
跳转到bar函数内部,执行函数体语句之前,会创建一个新的执行上下文环境。
全局 | 上下文环境
—|—
b | undefined
x | 10
arguments | [10]
this | window
并将这个执行上下文环境压栈,设置为活动状态。
执行到第5行,又调用了fn函数。进入fn函数,在执行函数体语句之前,会创建fn函数的执行上下文环境,并压栈,设置为活动状态。
待第5行执行完毕,即fn函数执行完毕后,此次调用fn所生成的上下文环境出栈,并且被销毁(已经用完了,就要及时销毁,释放内存)。
同理,待第13行执行完毕,即bar函数执行完毕后,调用bar函数所生成的上下文环境出栈,并且被销毁(已经用完了,就要及时销毁,释放内存)。
好了,给大家介绍了一段简短代码的执行上下文环境的变化过程,一个完整的闭环。其中上下文环境的变量赋值过程我省略了许多,因为那些并不难,一看就知道。
作用域
基础认识
“javascript没有块级作用域”。所谓“块”,就是大括号“{}”中间的语句。
比如一个if语句
var i = 10; if (i > 1) { var name = "yzh"; } console.log(name); //yzh
for (var i = 0; i < 10; i++) { } console.log(i); //10
for语句
我们在编写代码的时候,不要在“块”里面声明变量,要在代码的一开始就声明好了。以避免发生歧义
var i; for (i = 0; i < 10; i++) { } console.log(i);
我们在声明变量时,全局代码要在代码前端声明,函数中要在函数体一开始就声明好。除了这两个地方,其他地方都不要出现变量声明。而且建议用“单var”形式你光知道“javascript没有块级作用域”是完全不够的,你需要知道的是——javascript除了全局作用域之外,只有函数可以创建的作用域。
概念
如上图,全局代码和fn、bar两个函数都会形成一个作用域。而且,作用域有上下级的关系,上下级关系的确定就看函数是在哪个作用域下创建的。例如,fn作用域下创建了bar函数,那么“fn作用域”就是“bar作用域”的上级。
作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突
例如以上代码中,三个作用域下都声明了“a”这个变量,但是他们不会有冲突。各自的作用域下,用各自的“a”。
作用域和上下文环境
如上图,我们在上文中已经介绍了,除了全局作用域之外
每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时确定。
下面我们将按照程序执行的顺序,一步一步把各个上下文环境加上
第一步,在加载程序时,已经确定了全局上下文环境,并随着程序的执行而对变量就行赋值。
第二步,程序执行到第27行,调用fn(10),此时生成此次调用fn函数时的上下文环境,压栈,并将此上下文环境设置为活动状态。
第三步,执行到第23行时,调用bar(100),生成此次调用的上下文环境,压栈,并设置为活动状态。
第四步,执行完第23行,bar(100)调用完成。则bar(100)上下文环境被销毁。接着执行第24行,调用bar(200),则又生成bar(200)的上下文环境,压栈,设置为活动状态。
第五步,执行完第24行,则bar(200)调用结束,其上下文环境被销毁。此时会回到fn(10)上下文环境,变为活动状态。
第六步,执行完第27行代码,fn(10)执行完成之后,fn(10)上下文环境被销毁,全局上下文环境又回到活动状态。
最后我们可以把以上这几个图片连接起来看看。
作用域只是一个“地盘”,一个抽象的概念,其中没有变量。要通过作用域对应的执行上下文环境来获取变量的值。
同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。
如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值。
自由变量
在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量。
例:
var x = 50; function fn() { var b = 20; console.log(x + b); } fn(); //70
var x = 50; function fn() { console.log(x); } function show(f) { var x = 20; (function() { f(); //50 不是20 })(); } show(fn); //50 不是20
在调用fn()函数时,函数体中第6行。取b的值就直接可以在fn作用域中取,因为b就是在这里定义的。而取x的值时,就需要到另一个作用域中取。到哪个作用域中取呢?
有人说过要到父作用域中取,其实有时候这种解释会产生歧义
例如:
不要在用以上说法了。相比而言,用这句话描述会更加贴切——要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”,切记切记——其实这就是所谓的“静态作用域”。
对于本文第一段代码,在fn函数中,取自由变量x的值时,要到哪个作用域中取?——要到创建fn函数的那个作用域中取——无论fn函数将在哪里调用。
上面描述的只是跨一步作用域去寻找。
如果跨了一步,还没找到呢?——接着跨!——一直跨到全局作用域为止。要是在全局作用域中都没有找到,那就是真的没有了。
这个一步一步“跨”的路线,我们称之为——作用域链。
全局环境 | changeColor()的局部环境 | swapColors()的局部环境 |
---|---|---|
变量color | 变量anotherColor | 变量tempColor |
函数changeColor() | 函数swapColors() |
以上代码共涉及3个执行环境:全局环境、changeColor()的局部环境和swapColors()的局部环境。
内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。
每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。
最后这个例子是从书上找到的,比较经典和简单。
var color = "blue"; function changeColor() { var anotherColor = "red"; function swapColors() { var tempColor = anotherColor; anotherColor = color; color = tempColor; //这里可以访问color,anotherColor和tempColor } //这里可以访问color和anotherColor,但不能访问tempColor swapColors(); } changeColor(); //注释后alert显示为blue //这里只能访问color alert("Color is now " + color); //red