一、var关键字的特性
1.变量作用域
在引入ES 6的 let 和 const 关键字之前,使用var关键字声明的变量只有全局作用域和函数作用域,是没有块级作用域的。这意味着,变量在声明它们的函数体内以及这个函数体嵌套的其他函数体内都是有定义的。这意味着,在函数体内的其他块子句中使用var关键字声明的变量,在整个函数作用域内都是可用的。
function test(){ { var i = 0; } if(i===0){ var j=1; for(var k=0;k<10;k++){ i = 2; //在其他块作用域里仍然能够调用其他块里用var声明的变量 } console.log(k); //k虽然定义在for块结构里,但在for结构外面仍然可以调用。输出k = 10 } console.log(j); // 同样,j是在if块里定义的,但外部仍然可以调用。输出j = 1 console.log(i); // i = 2 function f(){ var m = 0; } f(); // console.log(m);ReferenceError,因为var声明的变量具有函数作用域,外部无法获取函数作用域内声明的变量m } test();
2.全局声明
在浏览器环境中,使用var关键字声明的变量或函数都会成为window对象的一个属性,且该属性是不可配置的(configurable:false),因此无法使用delete删除。与var关键字不同,使用let在全局作用域声明的变量不会成为window对象的属性,但由于该变量的声明是在全局作用域发生的,因此它仍会在页面的生命周期内存续。
var a = 1; console.log('a' in window); //true function f(){} console.log('f' in window); //true,函数f()也成为window对象的一个属性
console.log(delete a); //false,删除失败
cosole.log(window.a); //仍然是1 let b = 2; const c = 3; console.log('b' in window); //false console.log('c' in window); //false
二、遗漏的声明
在非严格模式下,给一个未声明的变量赋值也相当于创建了一个全局变量,但以这种方式创建的全局变量是作为window对象的可配置属性(configurable:true),因此可以使用delete删除该属性。
a = 1; //全局作用域遗漏的声明 function f1(){ b=2; //函数作用域遗漏的声明 } f1(); console.log('a' in window); //true console.log('b' in window); //true,即使变量b的赋值语句是在f()函数体内执行的,它也会成为window对象的一个属性
console.log(delete a); //true,删除成功
console.log(window.a); //undefined
三、声明提升(hoisting)与暂时性死区(temporal dead zone)
1.声明提升
"声明提升(hoisting)"其实是在程序执行过程中对变量、类或函数的声明语句赋予更高的优先级,使得计算机在处理其他代码之前先处理声明语句,从而在运行时能够在某个变量或函数的声明语句前面调用这个变量或函数,不会报错。在JavaScript中,var关键字声明的变量以及函数声明是具备"hoisting"特性的(按照计算机的解析顺序,其实let关键字声明的变量可能也有"提升",只不过被"暂时性死区"限制了)。注意区分变量初始化与变量赋值的区别!
console.log(a); //在声明语句var a前面调用变量a,输出初始值undefined f1(); //成功调用,输出"func" f2(); //TypeError,因为var f2先被提升,没有完成后续的赋值操作之前,它不是一个函数 //var a = 1实际上应该拆分为两个动作:①声明语句var a;(声明的同时被初始化为undefined,被提升) ②赋值语句a = 1; var a = 1; function f1(){ console.log('func1'); } //函数表达式实际上可以拆分为两个动作:①声明语句var f2;(被提升) ②赋值语句f2 = function(){...} var f2 = function (){ console.log('func2') }
补充:从《你不知道的JavaScript(上卷)》来理解"声明提升"的原理
首先理解一下JavaScript引擎对 var a = 2;这段程序的解读。在引擎看来,这里有两个完全不同的声明:一个是声明语句'var a;',由编译器在编译时处理;一个是赋值操作'a = 2',由引擎在运行时处理。具体过程如下:
- 遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中(有可能是上一层作用域的变量),如果有,则忽略该声明(这也是var关键字允许重复声明并且忽略后面的重复声明的原因),继续编译;如果没有,则要求作用域在当前作用域的集合中声明一个新的变量,命名为a并初始化为undefined。
- 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫做a的变量,如果有就使用它;如果没有就沿着作用域链继续查找该变量。最终都没有找到则抛出一个ReferenceError的异常!
2.函数提升与变量提升的区别
记住:函数会先被提升,然后才是变量提升。
var a; function a(){} console.log(Object.prototype.toString.call(a)); //[object Function] 相当于下面代码: function a(){} var a; //重复声明,被忽略 console.log(Object.prototype.toString.call(a));
注意下面误区:不要误认为"变量先于函数提升",也不要误认为"重复声明时后面声明语句覆盖前面声明"。
var a=1; function a(){} console.log(Object.prototype.toString.call(a)); //[object Number]
这里实际上是这样的:
function a(){}
var a; //重复声明,被忽略
a = 1; //是赋值语句"覆盖"了函数声明,使得变量a的类型发生改变
console.log(Object.prototype.toString.call(a)); //[object Number]
3.暂时性死区
"暂时性死区(temporal dead zone ,TDZ)"是块中一个变量无法被访问的区域,直到变量被初始化(注意,这里的初始化指的是将初始值undefined赋予变量,而不是指赋值)。使用let或const关键字声明的变量,在执行到声明语句之前,前面的区域都是暂时性死区,在这些区域中调用它们都会报错。同时,const关键字还要求声明变量的同时必须对其显式初始化,且后续尝试修改const声明的变量会导致运行时错误,可以理解为"常量声明"。
console.log(a); //undefined console.log(b); //ReferenceError: Cannot access 'b' before initialization c(); //ReferenceError: Cannot access 'c' before initialization var a = 1; let b = 2; let c = function (){ console.log('func') }
let d;
console.log(d); //undefined
实际上计算机的解析顺序是这样的:
//---------------------暂时性死区------------------------------------
var a; //首先解析这一句,会初始化a = undefined,因此变量被var提升的瞬间就已经结束暂时性死区了
let b; //解析的第二句,JavaScript会自动将它放入"暂时性死区",直到它被初始化(并不会像var那样自动初始化为undefined)
let c;
let d;
console.log(a);
console.log(b);
c(); //注意,这里不再是TypeError,而是变量c先报错
//----------------------暂时性死区-----------------------------------
b = 2;
c = function(){...}
d = undefined;
console.log(d);
四、使用var声明的弊端
- 使用var关键字声明的变量会作为全局对象window的一个属性添加进去,容易造成属性命名冲突;
- var声明具有声明提前的特性,同时允许重复声明,容易造成问题,而let与const是不允许重复声明的;
- var声明没有块级作用域;
参考资料
- var - JavaScript | MDN (mozilla.org)
- let - JavaScript | MDN (mozilla.org)
- 《你不知道的JavaScript(上卷)》
- Temporal Dead Zone (TDZ) and Hoisting in JavaScript – Explained with Examples (freecodecamp.org)