关于JavaScript中的声明提升

声明提升

变量提升

两个示例

观察如下代码:

a = 1;
var a;
console.log(a);

将js代码从上往下一行一行地分析:

  1. 第一行声明了a全局变量,并赋值1(在非严格模式下,引擎进行LHS查询,在全局作用域中创建一个具有名称a的变量)
  2. 第二行再次声明变量a,将a重新默认值undefined
  3. 打印a

按照以上思路,最终打印的结果应该是undefined,但实际上我们打印出了1。这是因为发生了变量提升。代码的实际执行顺序如下:

var a;
a = 1;
console.log(a); // 1

再看另一段代码:

console.log(a);
var a = 1;

同样,如果将js代码从上往下一行一行地分析:

  1. 第一行代码发生RHS查询,对一个未声明的变量a进行取值,会抛出ReferenceError异常
  2. 第二行代码不再执行

但事实上,代码不会抛出任何错误,而是打印出undefined。这是因为出现了变量提升。代码的实际执行顺序如下:

var a;
console.log(a);	// a被赋予默认值undefined
a = 1;
变量提升的位置

那么,变量是提升到哪里了呢?是当前作用域的最前方,还是全局作用域的最前方?
var声明变量时(下面示例以此为前提,ES6中的let完美地解决了以下问题),当其所在作用域为块作用域时,var声明的变量将属于外部作用域(污染了外部作用域)。而当其所在作用域为函数作用域时,外部作用域将无法访问包装函数内部的任何内容,此时,var声明的变量就成功地成为了局部变量。

看如下代码:

{
	a = 1;
	var a;
	console.log("作用域内",a);
}
console.log("作用域外",a);

a的声明在块作用域中,但它实际上属于外部作用域。再根据之前谈到的变量提升。两个console.log打印的a都为1。此时,变量被提升到了全局作用域,也就是污染了全局作用域。

(function f(){
	a = 1;
	var a;
	console.log("作用域内",a);
})();  // 函数立即执行
console.log("作用域外",a);

a的声明在函数作用域中,a成为了该函数作用域内的局部变量。因此在外部作用域调用时将抛出ReferenceError错误,而在函数内部正常输出1。

由此可见,将变量封装在函数内部可以避免变量污染全局。如果想让代码正常执行,可以采用函数自调用。

函数提升

函数声明会被提升,函数表达式不会被提升。首先简单地了解一下什么是函数声明和函数表达式。

函数声明和函数表达式辨析
// 函数声明
function f() {
	console.log("函数声明");
}
// 函数表达式
var a = function() {
	console.log("函数表达式");
}

函数表达式就是将函数赋值给某一变量。该函数可以是匿名函数也可以是具名函数(例子中为匿名函数)。

函数声明的提升
f();
function f() {
	console.log(1);
}

在该代码中,函数声明提升到函数调用之前。函数调用正常执行,打印出1。

函数表达式不会出现提升
  1. 匿名函数表达式
f();
var f = function() {
	console.log(1);
}

抛出TypeError错误,代码可以被理解为:

var f;	// f为默认值undefined
f();	// 对undefined进行调用抛出TypeError错误
f = function() {
	console.log(1);
}
  1. 具名函数表达式
f();
var f = function g() {
	console.log(1);
};

与上面代码一样,抛出TypeError错误

g();
var f = function g() {
	console.log(1);
};

可以理解为:

var f;
g(); // ReferenceError
f = function g() {
	console.log(1);
};

g未被声明,抛出ReferenceError错误。

函数提升优先

当变量提升和函数提升同时存在时,会优先函数提升。
如下这段代码:

console.log(foo);
function foo() {
	console.log(1);
}
foo = 2;

会打印出

ƒ foo() {
	console.log(1);
}

对函数冲突和变量冲突的处理

对于冲突的函数声明,后面的声明会覆盖前面的声明。
对于冲突的变量声明,后面的声明会被忽略
其实,对于这个很好理解,函数声明时是有函数体的,因此设置后面的内容优先是很合理的。而变量声明时,变量并没有被赋值(初始化),因此再多的变量声明也都是一样。
下面,来简单测试一下:
变量冲突:

consle.log(f);
		
function f(){
	console.log(1);
}

var f = 1;

首先遇到函数声明,然后遇到变量声明(被忽略),打印出的依然是函数f。

ƒ f(){
	console.log(1);
	}

函数冲突:

console.log(f);
var f = 1;
function f(){
	console.log(1);
}

首先遇到变量声明,然后遇到函数声明(覆盖变量声明),依然打印出函数f。

ƒ f(){
	console.log(1);
	}

这个机制也很好的反应出了上面提到的函数提升优先的特性。

为什么会出现声明提升?

我们知道,引擎在解释JavaScript代码之前会先进行编译。编译过程中会找到所有的声明,并用合适的作用域将他们关联起来。也就是说,所有声明都会在代码被执行之前首先被处理。因此,声明发生在编译阶段,赋值发生在执行阶段。所以在代码执行时,所有的声明已经结束,也就是出现了声明提升。

有var和没有var的区别

由以上例子,我们可以发现,有var时声明提升正常进行。那没有var时,又会发生什么呢?
观察如下代码:

a = 1;
console.log(a);

正常打印出1。当给a前面加上var时,也正常打印出1。依据LHS查询,引擎会在全局作用域中创建一个具有名称a的变量。在本示例中,代码就在全局作用域中,因此,加不加var输出结果都一样。

再看这段涉及变量提升的代码:

console.log(a);
a = 1;

当有var时,输出undefined。当没有var时,抛出ReferenceError错误。可见在这里没有出现变量提升,也就是说全局变量a并没有被创建。因为代码在编译时,没有找到a的声明,在执行阶段中,console.log(a)发生RHS查询,找不到变量a。自然抛出ReferenceError错误。由此可见,a = 1创建一个具有名称a的全局变量这个过程是在执行阶段发生的。当没有执行到这一步时,就没有变量a。

所以规范的代码书写(将变量清晰地声明在相关作用域,避免未声明就使用和作用域污染)将有利于代码编译,从而提高代码执行效率。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值