Javascript作用域及变量提升

因为javascript没有块级作用域(即if, for包裹的区域)的概念,在函数和变量的定义和使用中,可能会出现令人迷惑的结果。本文就js作用域和变量提升这两个话题,做一番讲解。
PS: ES6标准引入了let关键字,用它声明的变量,具有块级作用域。并且在ES6中,花括号{}内部就是一个块级作用域。

在Javascript中,名称(变量或函数)进入作用域的基本方式有以下4种:
1. 所有作用域内,都存在 thisarguments 这两个变量
2. 给函数指定的参数,参数名会自动包含在函数的作用域内
3. 函数的声明。如 function X() {}
4. 变量的声明。如 var Y;

重点来了,这4种类型的名称,会按照上述的优先级,被javascript引擎移动到它们所在的作用域(如函数内)的顶端。这就是所谓的“变量提升”。
下面让我们用实例讲解上面这段话的含义。

首先是变量声明。下面的函数A()其实会被js引擎解释为函数B()的形式:

function A() {
	bar();
	var x = 1;
}

function B() {
	var x;
	bar();
	x = 1;
}

由这个特性我们可以得出一个推论:给变量声明+赋值的代码即使在逻辑上执行不到,也不会妨碍变量被声明,只不过无法赋值而已。
比如下面这俩函数是一样的:

function A() {
	if (false) {
		var x = 1;
	}
	return;
	var y = 1;
}

function B() {
	var x, y;
	if (false) {
		x = 1;
	}
	return;
	y = 1;
}

也就是说,即使在函数A()里,x和y这两个变量也是存在的,只不过因为if和return的缘故,无法被赋值为1罢了。

接着是函数声明。和变量声明不同,函数声明的提升,是连同函数体一起提升了。当然,仅限于function X() {}这种写法的函数。而var X = function() {}这种,本质上是声明了一个变量,并把一个匿名函数给这个变量赋值,所以只会把变量名提升。如下示例:

function test() {
	A(); // 报错 TypeError "A is not a function"
	B(); // 没问题,打印出"this will run!"
	var A = function () { // 本质是变量声明 + 赋值
		alert("this won't run!");
	}
	function B() { // 函数声明
		alert("this will run!");
	}
}
test();

上述的4个优先级还会造成一个现象:因为一旦一个名称被声明,后面再重复声明是无效的,所以同一个作用域内的同名变量和函数,函数的声明永远会覆盖掉变量的声明。结合变量提升的原理,你应该知道是怎么回事了吧?当然,只是声明部分被覆盖,赋值部分还是会被执行。下面的例子可以清楚地看到这一点:

alert(typeof X);  // "function"
var X = 1;
function X() {}
alert(typeof X);  // "number"

不过也有特殊情况。其一,就比如作用域里默认生成的那个arguments变量。虽然把它放在优先级1里,但是实际上它真正的声明位置是在函数的参数之后,即优先级2和3之间的位置。因此如果在定义函数时,把函数的某个参数命名为arguments,那么这个参数就会覆盖掉默认的arguments,例如:

function X(param) {
	alert(arguments);
}
function Y(arguments) {
	alert(arguments);
}
X(1);  // "[Object Arguments]"
Y(1);  // "1"

其二,this也不能作为标识符使用,如作为函数的参数及函数名,否则会报SyntaxError,因此可以说this是这些名称里面优先级最高的。其三,如果一个函数拥有多个同名参数,那么最后那个参数会进入函数的内部作用域。这与在函数内反复声明变量时的结果正好相反。例如:

function X(a, a, a, b, a) {
	alert(a);
}
X(1);  // "undefined"
X(1, 2, 3, 4, 5)  // "5"

最后一个要点,是关于用命名函数来给变量赋值,那个命名函数是否会被提升的问题。答案是NO。
如下面的例子:

A(); // TypeError "A is not a function"
B(); // 没问题
C(); // TypeError "C is not a function"
X(); // ReferenceError "X is not defined"

var A = function () {}; // 变量声明+赋值,仅仅A这个名称被提升
function B() {}; // 函数声明。整个函数被提升
var C = function X() {}; // 变量声明+赋值,仅仅C这个名称被提升

A(); // 没问题
B(); // 没问题
C(); // 没问题
X(); // ReferenceError "X is not defined"

可以看到,X()不仅没有被提升,它根本没有进入作用域!

Reference:
[1] http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html

本文为悠然居(https://wordpress.youran.me/)的原创文章,转载请注明出处!

Leave a Reply

Your email address will not be published. Required fields are marked *