作用域说明的是一个变量可以在什么地方被使用,什么地方不能使用。
我们要了解以下几个知识:
看例子:
for(var i=0; i<10;i++){
var num = i;
}
console.log(i); //10
console.log(num); //9
上面这段代码在JavaScript中是不会报错的,但是在其他的编程语言中(C#、C、JAVA)会报错。
这是因为,在JavaScript中没有块级作用域,使用{}标记出来的代码块中声明的变量num,是可以被{}外面访问到的。
但是在其他的编程语言中,有块级作用域,那么{}中声明的变量num,是不能在代码块外部访问的,所以报错。
说明:
JS没有块级作用域
词法作用域就是代码在编写过程中体现出来的作用范围。代码一旦写好,不用执行,其作用范围就已经确定好了。
和词法作用域相对的叫动态作用域。Js中词法作用域不是动态作用域。
看例子说明:
var num = 123;
function func1(){
console.log(num);
}
function func2(){
var num = 456;
func1();
}
f2();
你猜这个结果会输出什么?
正确答案是: 123
词法作用域的规则:
(1)函数允许访问函数外的数据.
(2)整个代码结构中只有函数可以限定作用域.
(3)作用域规则首先使用提升规则分析
(4)如果当前作用域中有了该变量, 就不考虑外面的同名变量
重点:.在JavaScript中唯一能产生作用域的东西是 函数!
上面的解释为:
看代码:
f();
function f(){
console.log("我就是能被执行!");
}
这段代码中,函数f的调用时在其声明之前。如果说JavaScript代码真的是从上到下依次解释执行的话,那么第一句调用f的时候会报错,然后事实并非如此。上面代码可以正常执行,并且console.log出来的是: 我就是能被执行
因此,尽管JavaScript是解释的语言,但javascript**并非**仅在运行时简单地从上到下逐句执行!
JavaScript代码的执行分为两个步骤:
第一步:预解析阶段
在预解析阶段,会将以var声明的变量名和function开头的语句块,进行提升操作。
第二步:执行
当变量和函数的声明处在被调用之后的位置,变量和函数的声明将会被提升到调用之前。
重新看我们的例子:
再来个变量声明提升的例子。
console.log(k);
var k = 2;
由于JavaScript的与解析机制,这段代码最后输出的是 : undefined。 (如果没有预解析,代码应该会报错: k is not defined,而不是输出值。)
那变量提升的话,不应该输出2吗,为什么是undefined呢。
这里有必要说明一下,声明、定义、初始化的区别。
行为 | 说明 |
---|---|
声明 | 告诉编译器/解析器有这个变量存在,这个行为是不分配内存空间的,在JavaScript中,声明一个变量的操作为:var a; |
定义 | 为变量分配内存空间,在C语言中,一般声明就包含了定义,比如:int a;,但是在JavaScript中,var a;这种形式就只是声明了。 |
初始化 | 在定义变量之后,系统为变量分配的空间内存储的值是不确定的,所以需要对这个空间进行初始化,以确保程序的安全性和确定性 |
赋值 | 赋值就是变量在分配空间之后的某个时间里,对变量的值进行的刷新操作(修改存储空间内的数据) |
JavaScript中说的提升,是声明的提升。
那么上段代码应该等效于这样
函数同名
现在我们已经对变量、函数声明的提升有了简单的理解。那么若是函数同名,该怎么办呢?
看例子:
f1();
function f1() {
console.log("我是f1函数");
}
f1();
function f1() {
console.log("我是最后声明的f1函数");
}
结果输出:
我是最后声明的f1函数
我是最后声明的f1函数
同名的函数,后面的函数会覆盖前面的,所以两次输出结果都是 我是最后声明的f1函数。
变量和函数同名
看例子:
console.log(f);
function f() {
console.log("整个函数被输出哦!")
}
var f = 3;
当出现变量和函数同名的时候,只会对函数声明进行提升,变量会被忽略。所以上面的输出结果是
function f() {
console.log("整个函数被输出哦!")
}
var s = 4;
function s() {
console.log(s);
}
s();
结果执行为:Uncaught TypeError: s is not a function
看是如何预解析的
现在明白了吧!
预解析是分作用域的
声明提升并不是将所有的声明都提升到window对象下面,提升原则是提升到变量运行的环境(作用域)中去。
var num = 456;
function f(){
console.log(num);
var num = 10;
}
f();
预解析是分段的。分段其实就是分
<script>
f();
function f(){
console.log("First");
}
function f() {
console.log("Second");
}
script>
<script>
function f(){
console.log("third");
}
script>
在上面代码中,第一个script标签中的两个f进行了提升,第二个f覆盖了第一个f,但是第二个script标签中的f并没有覆盖上面的第二个f。所以说预解析是分段的。
tip:但是要注意,分段只是单纯的针对函数,变量并不会分段预解析。
函数表达式并不会被提升
示例:
f();
var f = function (){
console.log("我就是个函数表达式!");
}
结果报错: f is not a function.
原因: 函数表达式并不会被提升,只是简单地当做变量声明进行处理。
console.log(typeof f);
if(true) {
function f(){
console.log("怎么办,答案不一致!");
}
}
console.log(typeof f);
这段代码在Gecko引擎中打印”undefined”、”function”;而在其他浏览器中则打印”function”、”function”。
条件式函数声明跟函数表达式的处理方式一样。因此,条件式函数声明丧失了函数声明提升的特性。
基于以上原因,请不要在你的代码里将函数声明嵌套在条件语句内。
只要是代码,就至少有一个作用域,那就是全局作用域。在JavaScript中,只要是函数就可以创造作用域。函数内部的作用域可以访问函数外部的作用域。
函数中又可以嵌套函数,那么这个作用域中又可以产生另外一个作用域。
将这样的所用的作用域列出来,就可以有这样一个结构:函数内指向函数外的链式结构,而这个链式结构就称为作用域链。
作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问。
例子:
//f1的作用域--->全局作用域
function f1(){
//f2的作用域--->f1的作用域--->全局作用域
function f2(){
//f3的作用域---->f2的作用域--->f1的作用域--->全局作用域
function f3(){
}
//f4的作用域--->f2的作用域--->f1的作用域---->全局作用域
function f4(){
}
}
//f5的作用域--->f1的作用域---->全局作用域
function f5(){
}
}
从上面这个例子可以看出:每个函数都可以访问自己内部的变量也可以访问包含它的函数内的变量,这样一直延续到全局执行环境。全局执行环境的变量对象始终都是作用域链中的最后一个对象。
举个例子:
var color ="blue";
function changeColor() {
var anotherColor = "red";
function swapColors(){
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
//这里可以访问 color,anotherColor和tempColor
}
//这里可以访问color和anotherColor,但不能访问tempColor
swapColors();
}
//这里只能访问color
changeColor();
以上代码共涉及3个执行环境:全局环境,changeColor()的局部环境和swapColors()的局部环境。
全局环境中有一个变量color和一个函数changeColor()。changeColor()的局部环境中有一个名为anotherColor的变量和一个名为swapColors()的函数,但它也可以访问全局环境中的变量color。swapColors()的局部环境中有一个变量tempColor,该变量只能在这个环境中访问到。
无论全局环境还是changeColor()的局部环境都无权访问tempColors。然而,在swapColors()内部则可以访问其他两个环境中的所有变量,因为那两个环境是它的父执行环境。
该例子的作用域链
该矩形表示特定的执行环境。其中,内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。这些环境之间的联系是线性、有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境。
这个是javascript高级程序设计中的例子。
其实还可以这样,举个例子
function f1() {
function f2() {
}
}
var num = 456;
function f3() {
function f4() {
}
}
注意,同级的链不可混合查找
补充
声明变量使用var
, 如果不使用var
声明的变量就是全局变量( 禁用 )
function foo () {
var i1 = 1 // 局部
i2 = 2, // 全局
i3 = 3; // 全局
}
这两个语句都会在作用域链的前端添加一个变量对象。对with语句来说,会将制定的对象添加到作用域链中。对catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。
person={name:"kong",age:22,height:175,friend:{name:"diligentkong",age:20}};
with(person.friend){
console.log(name);
}
with语句将person.friend添加到当前作用域链的头部,所以输出的就是:diligentkong