深入理解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 相关文章推荐
在IE6下发生Internet Explorer cannot open the Internet site错误
Jun 21 Javascript
jQuery选择id属性带有点符号元素的方法
Mar 17 Javascript
JavaScript焦点事件、鼠标事件和滚轮事件使用详解
Jan 15 Javascript
javascript设计模式Constructor(构造器)模式
Aug 19 Javascript
深入理解vue.js双向绑定的实现原理
Dec 05 Javascript
bootstrap导航栏、下拉菜单、表单的简单应用实例解析
Jan 06 Javascript
深入理解Angular4中的依赖注入
Jun 07 Javascript
vue中遇到的坑之变化检测问题(数组相关)
Oct 13 Javascript
Bootstrap table使用方法汇总
Nov 17 Javascript
微信小程序基于slider组件动态修改标签透明度的方法示例
Dec 04 Javascript
微信小程序手机号码验证功能的实例代码
Aug 28 Javascript
JavaScript常用工具函数汇总(浏览器环境)
Sep 17 Javascript
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
php实现的一个很好用HTML解析器类可用于采集数据
2013/09/23 PHP
详解PHP导入导出CSV文件
2014/11/03 PHP
带你了解PHP7 性能翻倍的关键
2015/11/19 PHP
深入理解Yii2.0乐观锁与悲观锁的原理与使用
2017/07/26 PHP
Array.prototype.slice.apply的使用方法
2010/03/17 Javascript
各浏览器对link标签onload/onreadystatechange事件支持的差异分析
2011/04/27 Javascript
JQuery教学之性能优化
2014/05/14 Javascript
一个JavaScript处理textarea中的字符成每一行实例
2014/09/22 Javascript
js使用for循环与innerHTML获取选中tr下td值
2014/09/26 Javascript
浅谈javascript属性onresize
2015/04/20 Javascript
Nodejs进阶:核心模块net入门学习与实例讲解
2016/11/21 NodeJs
利用Node.js对文件进行重命名
2017/03/12 Javascript
JS中IP地址与整数相互转换的实现代码
2017/04/10 Javascript
BootStrap 动态表单效果
2017/06/02 Javascript
解决在Bootstrap模糊框中使用WebUploader的问题
2018/03/22 Javascript
详解Vue CLI 3.0脚手架如何mock数据
2018/11/23 Javascript
vue 中Virtual Dom被创建的方法
2019/04/15 Javascript
使用Vant完成Dialog弹框案例
2020/11/11 Javascript
python3.3实现乘法表示例
2014/02/07 Python
Mac下Supervisor进程监控管理工具的安装与配置
2014/12/16 Python
详解 Python中LEGB和闭包及装饰器
2017/08/03 Python
python中 logging的使用详解
2017/10/25 Python
Python实现简易版的Web服务器(推荐)
2018/01/29 Python
flask-socketio实现WebSocket的方法
2018/07/31 Python
mvc框架打造笔记之wsgi协议的优缺点以及接口实现
2018/08/01 Python
python实现大战外星人小游戏实例代码
2019/12/26 Python
CSS3 函数技巧 用css 实现js实现的事情(clac Counters Tooltip)
2017/08/15 HTML / CSS
css3 clip实现圆环进度条的示例代码
2018/02/07 HTML / CSS
荷兰网上买鞋:MooieSchoenen.nl
2017/09/12 全球购物
优秀班干部事迹材料
2014/01/26 职场文书
厂办主管岗位职责范本
2014/02/28 职场文书
中学生英语演讲稿
2014/04/26 职场文书
土地租赁意向书
2014/07/30 职场文书
安全标兵事迹材料
2014/08/17 职场文书
go语言中json数据的读取和写出操作
2021/04/28 Golang
Python闭包的定义和使用方法
2022/04/11 Python