JavaScript作用域与变量声明提升
本文由 小茗同学 发表于 2016-08-02 浏览(3042)
最后修改 2018-01-29 标签:javascript hoisting 作用域 变量声明 提升

原文发表于:2014-08-21

js作用域

为了更好的描述本文的核心:变量声明提升,我们先来复习一下js的作用域。

js没有块级作用域,函数是js唯一拥有自身作用域的结构(注:指在ES6出现以前,ES6开始出现了对块级作用域的支持)。

变量声明提升

什么是变量声明提升

这是一个比较老的话题,所谓变量声明提升hoisting,是指JavaScript引擎在执行的时候,会把所有变量的声明都提升到当前作用域的最前面,包括普通变量和函数。

先看个经典例子

var str = 'Hello Liuxianan!';
(function()
{
	console.log(str); // 输出 undefined
	var str = 'Hello xmy!';
})();

运行结果是:undefined

为什么这样呢?前面说了,js引擎会把变量声明放到当前作用域的最前面,又由于函数是js唯一拥有自身作用域的结构,所以,提升的代码其实是这样的:

var str = 'Hello Liuxianan!';
(function()
{
	var str;
	console.log(str);
	str = 'Hello xmy!';
})();

假如换成这样呢?不用说,这种情况下变量声明不会提升:

var str = 'Hello Liuxianan!';
if(true)
{
	console.log(str); // 输出 'Hello Liuxianan!'
	str = 'Hello xmy!';
})();

函数的提升

还是看段代码:

(function()
{
	f1(); // 输出 我是f1
	f2(); // 提示:f2 is not a function
	function f1(){console.log('我是f1');}
	var f2 = function(){};
})();

结论:
无论是普通变量,还是函数,声明都会被提升,但是,函数声明式(比如这里的f1)会把函数内容也一起提升,而函数表达式(比如这里的f2)只提升声明,不提升函数内容,这就是函数声明式和函数表达式的最大区别。

以上代码相当于:

(function()
{
	function f1(){console.log('我是f1');}
	var f2;
	f1(); // 输出 我是f1
	f2(); // 提示:f2 is not a function
	f2 = function(){};
})();

函数表达式和函数声明式提升的顺序

(function()
{
	console.log(foo); // 输出 function foo(){}
	var foo;
	function foo(){};
})();
(function()
{
	console.log(foo); // 输出 function foo(){}
	function foo(){};
	var foo;
})();

可以看出,无论var foo;是放在前面还是后面,它始终都被提升到了function foo(){}的前面。以上代码均相当于:

(function()
{
	var foo;
	function foo(){};
	console.log(foo); // 输出 function foo(){}
})();

但是由于只是声明提升,函数的内容并没有提升,所以如果是这的话:

(function()
{
	function foo(){console.log('111')};
	var foo = function(){console.log('222')};
	foo(); // 输出 222
})();
(function()
{
	var foo = function(){console.log('222')};
	function foo(){console.log('111')};
	foo(); // 还是输出 222
})();

函数声明表达式的提升

(function()
{
	f(); // TypeError: f is not a function
	foo(); // ReferenceError: foo is not defined
	var f = function foo(){console.log(typeof foo);};
	f(); // function
	foo(); // ReferenceError: foo is not defined
})();

以上代码相当于:

(function()
{
	var f;
	f(); // TypeError: f is not a function
	foo(); // ReferenceError: foo is not defined
	f = function(){var foo = f; console.log(typeof foo);};
	f(); // function
	foo(); // ReferenceError: foo is not defined
})();

附:函数声明式和函数表达式

所谓函数声明式,就是类似这样的定义:

function foo(){}

所谓函数表达式,就是类似这样的定义:

var foo = function(){}

那如果一个function是这样定义的呢?

var f = function foo(){};

这样的function好像有一个名字叫函数声明表达式,具体是不是专业名词俺没调查过,其实上面的代码相当于:

var f = function(){var foo = f;}

另外,突发奇想一个例子:

(function(foo)
{
	function foo(){}
	console.log(arguments); // 输出 [function(){}]
})('ss');

变量优先级

说明:下面的内容可能有严重错误,还没有完全研究透。

javascript中一个变量以四种方式进入作用域(scope),其优先级从低到高分别是(这里说的优先级的高低,是指优先级高的可以覆盖优先级低的):

  1. 语言内置:所有的作用域中都有 this 和 arguments 关键字
  2. 函数声明:形如function foo() {}
  3. 形式参数:函数的参数在函数作用域中都是有效的
  4. 变量声明:形如var bar;

以上内容属于转载,感觉有问题,有待继续参研。

看个例子:

(function(foo)
{
	function foo(){};
	var foo;
	console.log(foo); // 输出function foo(){}
})('ss');
(function(foo)
{
	var foo;
	function foo(){};
	console.log(foo); // 还是输出function foo(){}
})('ss');
(function(foo)
{
	var foo;
	console.log(foo); // 输出ss而不是undefined,说明形参的优先级比var声明高
})('ss');
(function(foo)
{
	function foo(){};
	console.log(foo); // 输出function foo(){},说明函数声明式覆盖了形参
})('ss');

以上例子我还没研究透,不过从上面可以大概看出,形参覆盖了var的声明,至于为何函数声明式覆盖了形参,还没弄明白。

再看个例子:

(function(foo)
{
	console.log(foo, arguments[0]); // 输出 function foo(){}, function foo(){}
	function foo(){};
	console.log(foo, arguments[0]); // 输出 function foo(){}, function foo(){}
})('ss');
(function(foo)
{
	console.log(foo, arguments[0]); // 输出 ss ss
	var foo = function(){};
	console.log(foo, arguments[0]); // 输出 function foo(){}, function foo(){}
})('ss');

可以看出arguments始终存放的是对形参的引用,形参内容变了arguments的内容也变了。

arguments可以被覆盖的:

(function(arguments)
{
	console.log(arguments); // 输出ss而不是['ss'],说明arguments可以被覆盖
	arguments = 111;
	console.log(arguments); // 输出111
})('ss');

参考

http://openwares.net/js/javascript_declaration_hoisting.html