深入理解Javascript作用域与变量提升


Posted in Javascript onDecember 09, 2013

下面的程序是什么结果?

var foo = 1;
function bar() {
 if (!foo) {
  var foo = 10;
 }
 alert(foo);
}
bar();

结果是10;

那么下面这个呢?

var a = 1;
function b() {
 a = 10;
 return;
 function a() {}
}
b();
alert(a);

结果是1.

吓你一跳吧?发生了什么事情?这可能是陌生的,危险的,迷惑的,同样事实上也是非常有用和印象深刻的javascript语言特性。对于这种表现行为,我不知道有没有一个标准的称呼,但是我喜欢这个术语:“Hoisting (变量提升)”。这篇文章将对这种机制做一个抛砖引玉式的讲解,但是,首先让我们对javascript的作用域有一些必要的理解。

Javascript的作用域

对于Javascript初学者来说,一个最迷惑的地方就是作用域;事实上,不光是初学者。我就见过一些有经验的javascript程序员,但他们对scope理解不深。javascript作用域之所以迷惑,是因为它程序语法本身长的像C家族的语言,像下面的C程序:

#include <stdio.h>
int main() {
 int x = 1;
 printf("%d, ", x); // 1
 if (1) {
  int x = 2;
  printf("%d, ", x); // 2
 }
 printf("%d\n", x); // 1
}

输出结果是1 2 1,这是因为C家族的语言有块作用域,当程序控制走进一个块,比如if块,只作用于该块的变量可以被声明,而不会影响块外面的作用域。但是在Javascript里面,这样不行。看看下面的代码:
var x = 1;
console.log(x); // 1
if (true) {
 var x = 2;
 console.log(x); // 2
}
console.log(x); // 2

结果会是1 2 2。因为javascript是函数作用域。这是和c家族语言最大的不同。该程序里面的if并不会创建新的作用域。

对于很多C,c++,java程序员来说,这不是他们期望和欢迎的。幸运的是,基于javascript函数的灵活性,这里有可变通的地方。如果你必须创建临时的作用域,可以像下面这样:

function foo() {
 var x = 1;
 if (x) {
  (function () {
   var x = 2;
   // some other code
  }());
 }
 // x is still 1.
}

这种方法很灵活,可以用在任何你想创建临时的作用域的地方。不光是块内。但是,我强烈推荐你花点时间理解javascript的作用域。它很有用,是我最喜欢的javascript特性之一。如果你理解了作用域,那么变量提升就对你显得更有意义。

变量声明,命名,和提升

在javascript,变量有4种基本方式进入作用域:

•1 语言内置:所有的作用域里都有this和arguments;(译者注:经过测试arguments在全局作用域是不可见的)

•2 形式参数:函数的形式参数会作为函数体作用域的一部分;

•3 函数声明:像这种形式:function foo(){};

•4 变量声明:像这样:var foo;

函数声明和变量声明总是会被解释器悄悄地被“提升”到方法体的最顶部。这个意思是,像下面的代码:

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

实际上会被解释成:
function foo() {
 var x;
 bar();
 x = 1;
}

无论定义该变量的块是否能被执行。下面的两个函数实际上是一回事:
function foo() {
 if (false) {
  var x = 1;
 }
 return;
 var y = 1;
}
function foo() {
 var x, y;
 if (false) {
  x = 1;
 }
 return;
 y = 1;
}

请注意,变量赋值并没有被提升,只是声明被提升了。但是,函数的声明有点不一样,函数体也会一同被提升。但是请注意,函数的声明有两种方式:
function test() {
 foo(); // TypeError "foo is not a function"
 bar(); // "this will run!"
 var foo = function () { // 变量指向函数表达式
  alert("this won't run!");
 }
 function bar() { // 函数声明 函数名为bar
  alert("this will run!");
 }
}
test();

这个例子里面,只有函数式的声明才会连同函数体一起被提升。foo的声明会被提升,但是它指向的函数体只会在执行的时候才被赋值。

上面的东西涵盖了提升的一些基本知识,它们看起来也没有那么迷惑。但是,在一些特殊场景,还是有一定的复杂度的。

变量解析顺序

最需要牢记在心的是变量解析顺序。记得我前面给出的命名进入作用域的4种方式吗?变量解析的顺序就是我列出来的顺序。

<script>
function a(){ 
}
var a;
alert(a);//打印出a的函数体
</script>
<script>
var a;
function a(){ 
}
alert(a);//打印出a的函数体
</script>
//但是要注意区分和下面两个写法的区别:
<script>
var a=1;
function a(){ 
}
alert(a);//打印出1
</script>
<script>
function a(){ 
}
var a=1;
alert(a);//打印出1
</script>

这里有3个例外:

1 内置的名称arguments表现得很奇怪,他看起来应该是声明在函数形式参数之后,但是却在函数声明之前。这是说,如果形参里面有arguments,它会比内置的那个有优先级。这是很不好的特性,所以要杜绝在形参里面使用arguments;

2 在任何地方定义this变量都会出语法错误,这是个好特性;

3 如果多个形式参数拥有相同的名称,最后的那个具有优先级,即便实际运行的时候它的值是undefined;

命名函数

你可以给一个函数一个名字。如果这样的话,它就不是一个函数声明,同时,函数体定义里面的指定的函数名( 如果有的话,如下面的spam, 译者注)将不会被提升, 而是被忽略。这里一些代码帮助你理解:

foo(); // TypeError "foo is not a function"
bar(); // valid
baz(); // TypeError "baz is not a function"
spam(); // ReferenceError "spam is not defined"
var foo = function () {}; // foo指向匿名函数
function bar() {}; // 函数声明
var baz = function spam() {}; // 命名函数,只有baz被提升,spam不会被提升。
foo(); // valid
bar(); // valid
baz(); // valid
spam(); // ReferenceError "spam is not defined"

怎么写代码

现在你理解了作用域和变量提升,那么这对于javascript编码意味着什么?最重要的一点是,总是用var定义你的变量。而且我强烈推荐,对于一个名称,在一个作用域里面永远只有一次var声明。如果你这么做,你就不会遇到作用域和变量提升问题。

语言规范怎么说

我发现ECMAScript参考文档总是很有用。下面是我找到的关于作用域和变量提升的部分:

如果变量在函数体类声明,则它是函数作用域。否则,它是全局作用域(作为global的属性)。变量将会在执行进入作用域的时候被创建。块不会定义新的作用域,只有函数声明和程序(译者以为,就是全局性质的代码执行)才会创造新的作用域。变量在创建的时候会被初始化为undefined。如果变量声明语句里面带有赋值操作,则赋值操作只有被执行到的时候才会发生,而不是创建的时候。

我期待这篇文章会对那些对javascript比较迷惑的程序员带来一丝光明。我自己也尽最大的可能去避免带来更多的迷惑。如果我说错了什么,或者忽略了什么,请告知。

译者补充

有位朋友提醒了我发现了IE下全局作用域下命名函数的提升问题:

我翻译文章的时候是这么测试的:

<script>
functiont(){
spam();
var baz = function spam() {alert('this is spam')};
}
t();
</script>

这种写法, 即非全局作用域下的命名函数的提升,在ie和ff下表现是一致的. 我改成:
<script>
spam();
var baz = function spam() {alert('this is spam')};
</script>

则ie下是可以执行spam的,ff下不可以. 说明不同浏览器在处理这个细节上是有差别的.

这个问题还引导我思考了另2个问题,1:对于全局作用于范围的变量,var与不var是有区别的. 没有var的写法,其变量不会被提升。比如下面两个程序,第二个会报错:

<script>
alert(a);
var a=1;
</script>

<script>
alert(a);
a=1;
</script>

2: eval中创建的局部变量是不会被提升的(它也没办法做到).
<script>
var a = 1;
function t(){
 alert(a);
 eval('var a = 2');
 alert(a);
}
t();
alert(a);
</script>
Javascript 相关文章推荐
Javascript 中的类和闭包
Jan 08 Javascript
Javascript 变量作用域 两个可能会被忽略的小特性
Mar 23 Javascript
使用jQuery.Validate进行客户端验证(初级篇) 不使用微软验证控件的理由
Jun 28 Javascript
鼠标放在图片上显示大图的JS代码
Mar 26 Javascript
javascript中parseInt()函数的定义和用法分析
Dec 20 Javascript
JS+CSS实现带有碰撞缓冲效果的竖向导航条代码
Sep 15 Javascript
使用微信小程序开发前端【快速入门】
Dec 05 Javascript
Angular实现svg和png图片下载实现
May 05 Javascript
微信小程序授权登录解决方案的代码实例(含未通过授权解决方案)
May 10 Javascript
jquery实现弹窗(系统提示框)效果
Dec 10 jQuery
vue 翻页组件vue-flip-page效果
Feb 05 Javascript
vue使用vant中的checkbox实现全选功能
Nov 17 Vue.js
Javascript全局变量var与不var的区别深入解析
Dec 09 #Javascript
jquery div拖动效果示例代码
Dec 08 #Javascript
jquery垂直公告滚动实现代码
Dec 08 #Javascript
jquery中交替点击事件toggle方法的使用示例
Dec 08 #Javascript
JavaScript 判断用户输入的邮箱及手机格式是否正确
Dec 08 #Javascript
jqplot通过ajax动态画折线图的方法及思路
Dec 08 #Javascript
JavaScript 32位整型无符号操作示例
Dec 08 #Javascript
You might like
中国第一家无线电行
2021/03/01 无线电
windows server 2008/2012安装php iis7 mysql环境搭建教程
2016/06/30 PHP
PHP判断文件是否被引入的方法get_included_files用法示例
2016/11/29 PHP
Yii2实现ActiveForm ajax提交
2017/05/26 PHP
支持汉转拼和拼音分词的PHP中文工具类ChineseUtil
2018/02/23 PHP
php查询内存信息操作示例
2019/05/09 PHP
php反序列化长度变化尾部字符串逃逸(0CTF-2016-piapiapia)
2020/02/15 PHP
Opacity.js
2007/01/22 Javascript
浅谈javascript 面向对象编程
2009/10/28 Javascript
ajax 缓存 问题 requestheader
2010/08/01 Javascript
js 编码转换 gb2312 和 utf8 互转的2种方法
2013/08/07 Javascript
基于jquery实现鼠标滚轮驱动的图片切换效果
2015/10/26 Javascript
AngularJS控制器controller正确的通信的方法
2016/01/25 Javascript
微信小程序 devtool隐藏的秘密
2017/01/21 Javascript
JavaScript提高加载和执行效率的方法
2017/02/03 Javascript
jQuery表单元素选择器代码实例
2017/02/06 Javascript
jquery tmpl模板(实例讲解)
2017/09/02 jQuery
深入理解 webpack 文件打包机制(小结)
2018/01/08 Javascript
微信小程序学习笔记之本地数据缓存功能详解
2019/03/29 Javascript
js实现随机div颜色位置 类似满天星效果
2019/10/24 Javascript
vuex实现像调用模板方法一样调用Mutations方法
2019/11/06 Javascript
vue el-table实现自定义表头
2019/12/11 Javascript
详解Vue的watch中的immediate与watch是什么意思
2019/12/30 Javascript
jQuery事件模型默认行为执行顺序及trigger()与 triggerHandler()比较实例分析
2020/04/30 jQuery
python批量设置多个Excel文件页眉页脚的脚本
2018/03/14 Python
Python3中在Anaconda环境下安装basemap包
2018/10/21 Python
Python如何在循环内使用list.remove()
2020/06/01 Python
Python 如何查找特定类型文件
2020/08/17 Python
详解CSS3选择器:nth-child和:nth-of-type之间的差异
2017/09/18 HTML / CSS
“型”走纽约上东区:Sam Edelman
2017/04/02 全球购物
英国购买威士忌网站:Master of Malt
2019/09/26 全球购物
什么是GWT的Module
2013/01/20 面试题
医学毕业生自我鉴定
2013/10/30 职场文书
企业消防安全制度
2014/02/02 职场文书
2014年学习雷锋活动总结
2014/03/01 职场文书
一帮一活动总结
2014/05/08 职场文书