在JavaScript中使用var和function声明变量遇到的那些坑

这篇聊聊Javascript中的变量提升

声明一个变量

在JavaScript中,使用一个变量之前应当先声明。

变量是使用关键词var声明的:

var i;
var sum;

以下两种变量声明方式相同:

var a = 1,b = 10;
var c = 100, d;
var a = 1;
var b = 10;

var c = 100;
var d;

仅声明而没有赋值的变量的值为undefined。

var a;
console.log(a);	// undefined

变量作用域

在全局声明的变量在哪里都起作用(除非因为与局部变量同名而在局部作用域中被覆盖)。

在局部作用域中声明的变量只有在局部作用域起作用(ES5规范中进入函数执行上下文可以创建一个局部作用域),全局作用域或者其他局部作用域中无法直接获取(除非使用到类似闭包的机制导出局部作用域的信息)。

根据就近原则,局部变量优先级高于同名的全局变量。

var scope = 'global';
function checkScope(){
	var scope = 'local';
	return scope;
}
checkScope();  \\local

局部作用域必须使用var声明变量,不然就成了全局变量,甚至会把同名的全局变量修改了。

var scope = 'global scope';
function checkScope(){
	var scope = 'local scope';
	function nested(){
		var scope = 'nested scope';
		return scope;
	}
	return scope;
}
checkScope(); //local scope

块级作用域

在一些类似C语言的编程语言中,花括号内的每一段代码都具有各自的作用域,而且变量在声明它们的代码段之外是不可见的,我们称为块级作用域(block scope)。

JavaScript中没有块级作用域的概念(至少ES5及之前的JavaScript版本没有这个概念),JavaScript取而代之地使用了函数作用域:变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。

下方代码中 i k j三个变量在整个test函数体内均是有声明的。

function test(o){
	var i=0;
	if(typeof == 'object'){
		var j=0;
		for(var k=0; k<10; k++){
			console.log(k);
		}
		console.log(k);
	}
	console.log(j);
}

函数体内的所有变量的声明(但赋值不会提前,还是会js运行时在运行到该变量被赋值的位置才执行赋值)都被提前了。

var scope = 'global';
function f(){
	console.log(scope); //undefined
	var scope = 'local';
	console.log(scope); //local
}
f();

你可能会以为输出 ‘global’ 和 ‘local’,然而,因为在函数体内声明了和全局变量同名的变量 scope,并且根据作用域内声明变量时声明将提前的规则,所以在函数体内第一次打印scope之前已经声明了局部作用域scope,并默认赋值为undefined,因此,第一次才会打印出undefined。

函数体内第二次打印 scope 正确打印出 local,是因为在这条打印语句前面执行了赋值语句,所以这里可以打印成功。上面的代码相当于下面的代码,如果这样写你可能就明白为什么是undefined了:

var scope = 'global';
function f(){
	var scope;
	console.log(scope); //undefined
	scope = 'local';
	console.log(scope); //local
}
f();

上边的代码看着就理所当然了是吧~

在具有块级作用域的编程语言中,在狭小的作用域里让变量声明和使用变量的代码尽可能彼此靠近,通常来讲,这是一个不错的编程习惯。由于JavaScript没有块级作用域,因此一些程序员特意将变量声明放在函数体的顶部,而不是将生命靠近放在使用变量之处。这种做法使得他们的源代码非常清晰的反映了真是的变量作用域。

想想,如果代码改成下面的样子:

var scope = 'global';
function f(){
	console.log(scope); //global
	scope = 'local';
	console.log(scope); //local
}
f();

这时,函数体内没有对scope 进行局部变量的声明,因此不会有声明提前的发生,既然没有声明提前,那么根据全局作用域的变量,局部作用域都能访问的规则,当然第一次打印出的是全局变量 global,而第二次打印出的是 local(因为执行f函数后,全局变量被修改了。)

这说明了两个问题:

  • 1、如果没有特殊需要,一定要在函数体内先声明在给变量赋值,不然就把全局变量污染了。
  • 2、在局部作用域没有声明而直接给一个变量赋值,就相当于在全局作用域名中声明了一个变量并赋值(因为重复赋值是在JS里是被允许的),这是很危险的。

删除变量

首先看下面的示例代码:

var truevar = 1;  // 声明一个不可删除的全局变量
fakevar = 2; //创建全局对象的一个可删除的属性
this.fakevar2 = 3;  //同上
delete truevar; //false 变量没有被删除
delete fakevar; //true 变量被删除
delete this.fakevar2 // true 变量被删除

使用var声明的变量不能直接删除,而不使用var声明的变量直接赋值的变量是可以删除的。

JavaScript全局变量是全局对象的属性,这是在ECMAScript规范中强制规定的。对于局部变量则没有这样的规定,但是我们可以想象得到,局部变量当作跟函数调用相关的某个对象的属性。

作用域链

其实全局与局部是相对的,帅华君个人觉得,所谓的全局作用域其实也是某个更加全局作用域的某一个局部作用域,以此类推,这取决于运行环境。

所以,既然全局作用域有this全局对象存储所有全局变量,那么相对的,局部作用域也应该有一个“this局部对象存放所有局部作用域”,你觉得呢。

作用域链是一个对象列表或者链表,这组对象定义了这段代码“作用域中”的变量,当JavaScript需要查找变量 x 的时候(这个过程称为“变量解析”),它会从链中的第一个对象开始查找,如果这个对象有一个名为 x 的属性,则会直接使用这个属性的值,如果第一个对象中不存在名为 x 的属性,JavaScript会继续查找链上的下一个对象。如果第二个对象依然没有名为 x 的属性,则会继续查找下一个对象,以此类推。如果作用域链上没有任何一个对象含有属性 x ,那么就认为这段代码的作用域链上不存在在 x ,并最终抛出一个引用错误(ReferenceError)异常。

从上面这段解释中,个人认为从实现出发,作用域链和执行上下文栈有关系。

根据执行上下文的原理,包括三种执行上下文,分别为全局执行上下文、函数执行上下文和eval执行上下文。

每进入一个执行上下文环境,就向执行上下文栈压入一个执行上下文,所以,最上层的是当前活动中的执行上下文(可能是三种执行上下文的任意一个),最底层的当然就是全局执行上下文, 所以在查询作用域链的时候,应当时在执行上下文栈中自上而下的查找,直到查找到那个变量,否则抛出引用错误异常。


本篇~完

扩展阅读 : JavaScript代码执行上下文与this指向

下一篇《《乌合之众:大众心理研究》读书便笺》

上一篇《正则表达式的模式匹配还有哪些你不知道的》

永久链接 http://www.shuaihua.cc/article/var-function-declare-in-javascript

快速跳转 心头好文 - javascript - 《在JavaScript中使用var和function声明变量遇到的那些坑》

发布日期 2017年11月9日 星期四

版权声明 自由转载-非商用-非衍生-保持署名(创意共享3.0许可证