一、作用域
定义
作用域,即产生作用的特定区域。
javaScript的作用域,即js的变量或者函数产生作用的对应区域。也就是说区域内的可以访问区域外的变量和函数,但是区域外的则不能访问区域内的变量和函数。
分类
在ES5中,作用域分为两种:全局变量和局部变量。
(1)全局变量:所有地方均可访问(在函数外部声明的变量);
(2)局部变量:只能在函数内部访问(在函数内部用var关键字声明的变量以及函数的形参)。
在ES6中,新增了块级作用域。
(3)块级作用域:只能在距离最近的大括号的作用范围内访问(仅限于let声明的变量)。
异同点
{
document.write("----chaopy----in---00-----b="+b+"<br><br>");//a&c is not defined on line 3
let a = 5;
var b = 3;
const c = 4;
document.write("----chaopy----in---01-----a="+a+"-----b="+b+"-----c="+c+"<br><br>");
{
let a = 6;
var b = 8;
const c = 2;
//let b = 5;// Identifier 'b' has already been declared on line 8
document.write("--------chaopy----in---10-----a="+a+"-----b="+b+"-----c="+c+"<br><br>");
//var a = 9;// Identifier 'a' has already been declared on line 11
//let a = 10;//Identifier 'a' has already been declared on line 15
//const c = 1;// Identifier 'c' has already been declared on line 15
var b = 10;
document.write("--------chaopy----in---11-----a="+a+"-----b="+b+"-----c="+c+"<br><br>");
for(let i = 0; i<5;i++){
document.write("------------chaopy----in---12-----i="+i+"<br>");
}
document.write("<br>");//访问不了i,会报i is not defined
for(var j = 0; j<5;j++){
document.write("------------chaopy----in---13-----j="+j+"<br>");
}
document.write("<br>--------chaopy----in---14-----j="+j+"<br><br>");
}
document.write("----chaopy----in---02-----a="+a+"-----b="+b+"-----c="+c+"<br><br>");
}
//const d;//Uncaught SyntaxError: Missing initializer in const declaration on line 29
document.write("chaopy----out---00-----b="+b+"<br><br>");//访问不了a&c,会报a&c is not defined
const arr = [1,2,3];
document.write("chaopy----out---01-----arr="+arr+"<br><br>");//访问不了a&c,会报a&c is not defined
//arr = [1,2,3,4,5];//Uncaught TypeError: Assignment to constant variable. on line 32
arr[3] = 4;
document.write("chaopy----out---02-----arr="+arr+"<br><br>");//访问不了a&c,会报a&c is not defined
arr[2] = 6;
document.write("chaopy----out---03-----arr="+arr+"<br>");//访问不了a&c,会报a&c is not defined
结果如下:
分析:
(1)用let在块中重新声明的变量不会影响块外声明的变量(参考line13和line28的打印结果);
(2)用var在块内声明的变量会直接影响块外的同名变量(参考line13和line28的打印结果);
(3)在循环中,let和var声明的变量的作用域不同,其中let是块级作用域,var是函数作用域(参考line22和line26的打印结果);
(4)作为html中的全局变量,var定义的全局变量属于window对象,let定义的全局变量不属于window对象;
作为全局变量,var和let声明变量的作用相似;作为函数内的局部变量,var和let的作用相似。
(5)在同一个作用域(块)中:
a、通过let重新声明一个var变量是不允许的(参考line12的注释);
b、通过var重新声明一个let变量是不允许的(参考line15的注释);
c、为已有的const/let变量重新声明或赋值是不允许的(参考line14、15的注释);
d、允许在程序的任何位置使用var重新声明javaScript变量(参考line18的打印结果);
(6)通过var声明的全局变量会提升到顶端,在声明之前就可以访问和使用(参考line3的打印结果);
通过let定义的全局变量不会被提升到顶端,在声明之前使用会报错,变量从块的开头资质处于“暂时死区”直到声明为止;
const定义的变量与let类似,但是不能重新赋值,且在声明时必须赋值(参考line30的注释);
(7)const没有定义常量,只是定义了对值的常量引用。用const声明的常量对象,可以更改常量对象的属性,但无法为常量对象重新赋值。常量数组可以添加新数组元素或者为已有数组元素重新赋值,但不能为常量数组重新赋值(参考line35、37、39的注释和打印)。
(8)在另外一个作用域中,重新声明const是允许的。
应用
// 全局变量
var i = 0 ;
// 定义外部函数
function outer(){
// 访问全局变量
console.log(i); // (1)
function inner1(){
console.log(i); // (2)
}
// inner2中定义局部变量
function inner2(){
console.log(i); // (3)
var i = 1;
console.log(i); // (4)
}
inner1();
inner2();
console.log(i); // (5)
}
outer();//输出结果依次为:0 0 undefined 1 0
注释(1)处:outer()内未声明和定义局部变量i, 故js解析器会继续在函数外部寻找i变量,最终在全局变量中找到i,因此结果为0;
注释(2)处:inner1()和outer()内均未声明和定义局部变量i,因此结果为0。
总结1:js解析器查找变量,依次从作用域内部一层一层往外查找,找到后结束,找到最外部都没有找到,结果就为undefined(已声明,但未定义)。
注释(3)处:结果是undefined,为什么?
常见错误结果:0 inner2中变量i的声明与定义在此命令之后,找不到i变量,往函数外部查找,所以为全部变量i=0
常见错误结果:1 inner2中变量i已经声明定义,所以函数内部已经找到i了,所以为1;
注释(4)处:因为inner2()内部重新定义了局部变量i=1,故结果为1。
注释(5)处:outer()内未声明和定义局部变量i,因此结果为0。
总结2:js中存在变量提升(hoisting),但仅仅是变量提升,定义并未提升。
二、变量提升(hoisting)
定义
变量声明是在任意代码执行前处理的,在代码区中任意地方声明变量和在最开始(即变量对应的作用域范围的初始位置)的地方申明是一样的。也就是说,看起来一个变量可以在申明之前被使用!这种行为就是所谓的“hoisting”,也就是变量提升,看起来就像变量的申明被自动移动到了函数或全局代码的最顶上。
说明:
声明变量在ES5中包括用以下关键字声明的对象:
(1)var
(2)function
声明变量在ES6中还包括用以下关键字声明的对象:
(3)function *
(4)let
(5)const
(6)class
应用
var temp = new Date();
f();
document.write("<br>"+"------------------分割线-------------------"+"<br><br>");
m();
document.write("<br>"+"---全局变量---temp="+temp+"<br>");
function f(){
document.write("f in! 0 temp="+temp+"<br>");
if(false){
var temp = "hello";
document.write("f in! 1 temp="+temp+"<br>");
}
document.write("f in! 2 temp="+temp+"<br>");
}
function m(){
document.write("m in! 0 temp="+temp+"<br>");
if(true){
var temp = "hello";
document.write("m in! 1 temp="+temp+"<br>");
}
document.write("m in! 2 temp="+temp+"<br>");
}
图一:
图二:
分析:
(1)图一在声明和定义之前就调用执行f()和m()函数,说明f()和m()的声明和定义分别进行了提升。
(2)f()和m()中分别定义了局部变量temp,由于在任何情况下,变量的声明都会提升到其作用域的顶部,因此f()和m()中的局部变量temp的声明会提升到各自函数的顶部,即图一等价于图二。
(3)由于改变的是每个函数中的局部变量temp的值,因此全局变量temp的值并不会被影响。
特殊情况的说明:
(1)变量重复声明
var a = 100;
alert(a);//100
var a = 200;
alert(a);//200
function fun2() {
alert(a);//underfind;
var a = 3;
alert(a);//3
}
fun2();
alert(typeof a);//unmber
var a = function() {}
alert(typeof a);//function
等价于以下的代码
var a;
var a ;
var a ;//多次声明会合并为一个对象
a=100;//赋值操作
alert(a);
a=200;
alert(a);
function fun2() {
var a;
alert(a);//underfind;
a = 3;//确定变量的数据类型,赋值
alert(a);//3
}
fun2();
alert(typeof a);//unmber
a=function(){};
alert(typeof a);//function
分析:
js的var变量只有全局作用域和函数作用域(局部作用域)两种,且声明会被提升,因此实际上a只会在最顶上开始的地方申明一次,故var a=200的声明会被忽略,仅用于赋值。
(2)函数的提升:
函数提升分为两种情况:
第一种:函数申明。如:function foo(){}
这种形式(在提升的时候,会被整个提升上去,包括函数定义的部分)
第二种:函数表达式。如:var foo=function(){}
这种形式(其实是var变量的声明定义,故只提升声明,不提升定义)
(3)实例分析:
案例一:
案例二:
分析:
(1)通常通过var声明一个变量,在没有赋值的情况下只做声明,没有定义变量的数据类型。
(2)通过function声明一个变量,在声明的同时会定义变量的数据类型为funtion。
(3)在同级作用域内,相同名称的变量会合并为一个变量,也就是后者会覆盖前者。
综上所述:
function foo(){};--------定义一个foo变量,数据类型为function;
var foo;-------------------定义一个foo变量,覆盖前者,然而没有定义变量类型。
因此注释(1)处foo的数据类型仍然是function。
当为foo变量重新赋值为500时,相当于重新定义了foo变量的类型
因此注释(2)处的foo数据类型为number。
三、总结
要彻底理解JS的作用域和变量提升(Hoisting),只要记住以下三点即可:
1、所有声明都会被提升到作用域的最顶端,变量声明提升仅仅是声明提升,定义不提升;函数声明会连带定义一起被提升;
2、同一个变量声明只进行一次,因此同一个变量的其他声明都会被忽略,从而变成赋值操作;
如:var i = 0 ; var i = 1 相当于 var i ; i = 0 ; i = 1;
3、函数声明的优先级优于变量声明,且函数声明会连带定义一起被提升。