原文:http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html

===翻译开始===

你知道下面的JavaScript脚本执行结果是什么吗?

 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",这到底是怎么回事呢?这看起来很奇怪,事实上这恰好是语言的一个强大而又富有表现力的特性。我不知道这种特别的行为是否有一个标准的命名,但我喜欢把它叫做"hoisting"。接下来我会试着分析一下这种机制,但是我们有必要先理解一下JavaScript的作用域。

JavaScript中的作用域

对于JavaScript初学者来说,作用域常让他们感到困惑。事实上,一些资深的JavaScript开发者也不是完全理解作用域。JavaScript的作用域之所以让人如此困惑,因为它跟C系语言有点相似,请看下面的C程序:

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

程序会依次输出1,2,1,这是因为C系语言有块级作用域。当程序运行到一个程序块的时候(比如if语句),在该程序块里定义的新变量不会影响到外部作用域。但在JavsScript中却不相同,试着执行下面的脚本:

 var x=1;
console.log(x);//
if(true){
var x=2;
console.log(x);//
}
console.log(x);//

脚本执行后会依次输出1,2,2,这是因为JavsScript只有函数级作用域,这和C系语言有着根本的不同,程序块(比如if语句)是不会创建新的作用域的,只有函数才会。

对于一些使用C、C++、C#或者Java语言的开发者来说,这简直让人难以接受。还好JavaScript的函数足够灵活,可以用其它变通方法。如果你一定要在函数内创建一个临时的作用域,可以这样做:

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

这个方法相当灵活,在任何需要的地方都可以使用,不止在块语句里。但是我强烈建议你花一些时间来真正理解和欣赏JavaScript的作用域,这是我最喜欢的语言特性之一,它真的非常强大。如果你理解了作用域,那么对于声明提前你会更容易理解。

声明,变量名,声明提前

在JavaScript中,一个变量可以通过以下四种方式之一进入作用域:

1、语言内置:所有作用域都默认包含"this"和"arguments"变量。
2、函数形参:函数可以拥有形参,所属作用域就是该函数体。
3、函数声明:形如"function foo(){}"的声明。
4、变量声明:形如"var foo;"的声明。

函数声明和变量声明总会被JavaScript解释器自动放到所属作用域的顶端,函数参数和语言内置的变量默认都是在最顶端。举个例子,有如下代码:

 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(){ //function expression assigned to local variable "foo"
alert('this won't run!');
};
function bar(){//function declaration,given the name
alert('this will run!');
}
}
test();

在这个例子里,只有使用函数声明的函数体会被提前至顶端,而使用函数表达式赋值方式,只有名字"foo"被提前至顶端,函数体是没有的。

上面的例子基本覆盖了自动提前的情况,看起来并不是那么复杂让人迷惑。当然,一些其它比较特别的例子还是有一些复杂的。

变量识别顺序

我们要特别记住变量的识别顺序,前面说过变量名进入作用域有四种方式,我上面列举的顺序就是它们被识别的顺序。通常,如果一个变量名已经定义了,那么它就不会被其它相同名称的变量所覆盖。这意味着函数声明比变量声明优先级高,但这并不影响赋值操作,只是声明部分会被忽略而已。

PS:补充一段代码,表达作者的意思

function foo(){}
var foo=3;
console.log(foo);// 这段代码会被解析为:
function foo(){}
//var foo; //这条语句就被忽略了
foo=3;
console.log(foo); 依我理解是这样子:
var foo;
foo=function foo(){}
foo=3;
console.log(foo);

接着原文,以下是几种特别情况:

1、内置的变量"arguments"表现比较奇怪,它好像定义在函数形参和函数声明之间。这意味着如果形参中有个变量为"arguments",那么它的优先级将高于内置的"arguments",即使它是undefined。这不是一个好的特性,不要使用"arguments"作为形参变量名。
2、使用"this"作为一个标识符会引起语法错误,这是一个好的特性。
3、如果多个形参中出现同名,那么最后一个将拥有最高的优先级,即使它是undefined。

带有名字的函数表达式

你也可以给函数表达式中的函数起个名字,采用类似函数声明的语法。但这并不能使它变成一个函数声明,并且这个函数名不会被添加到作用域,函数体也不会被提前至顶端,下面用一些代码来演示我说的意思:

 foo(); //TypeError "undefined is not a function"
bar(); //valid
baz(); //TypeError "undefined is not a function"
spam();//ReferenceError "spam is not defined" var foo=function(){}; //anonymous function expression('foo' get hoisted)
function bar(){}; //function declaration ('bar' and the function body get hoisted)
var baz=function spam(){};//named function expression('only 'baz' get hoisted) foo(); //valid
bar(); //valid
baz(); //valid
spam();//ReferenceError "spam is not defined"

怎么利用这些知识编程

现在你已经理解作用域和声明提前特性了,那么这些在JavaScript编程中有什么影响?最重要的是声明变量时要使用"var"关键字,我强烈建议你在每个作用域的顶端只写一个var语句(多变量的时候,用逗号连接)。如果你强制自己这样做,就不会对声明提升产生困惑了。不过,这么做会让你在当前作用域中寻找已经声明的变量变得更困难,我建议使用"JSLint"的"onevar"选项来验证代码,如果你照做了,你的代码看起来会像这样子:

 /*jslint onevar: true [...]*/
function foo(a,b,c){
var x=1,
bar,
baz="something";
}

看看规范怎么说

我发现经常查阅ECMAScript规范文档有助于直接理解这些机制是怎么运行的,以下是规范对于变量声明和作用域的描述:

 如果变量声明语句在函数声明里面,那么变量就是定义在函数内部作用域(参考章节10.1.3),否则它们就是定义在全局作用域内(作为全局对象的成员变量,参考章节10.1.3)。变量进入作用域的时候就会被创建,块语句不会定义一个新的执行作用域,只有程序和函数声明会产生新的作用域。变量在创建的时候会被初始化为"undefined",一个带有初始化语句的变量,在赋值语句执行的时候才会被赋上其赋值表达式对应的值,并不是变量创建的时候就赋值。

我希望这篇文章能够帮助JavaScript开发者理清一些困惑的问题,我已经尽可能的彻底把问题讲清楚,以免造成更多的疑惑。如果你发现我写错了或者遗漏了某些重要的东西,请一定让我知道。

===翻译完===

翻译参考:http://ju.outofmemory.cn/entry/85659

以下是一个例子:

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

【翻译】JavaScript中的作用域和声明提前的更多相关文章

  1. JavaScript中的作用域和声明提前

    [翻译]JavaScript中的作用域和声明提前 原文:http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html ===翻译 ...

  2. javascript中函数作用域和声明提前

    javascript不像java等其他强类型语句,没有块级作用域(括号内的代码都有自己的作用域,变量在声明它们的代码段之外不可见)一说,但有自己的独特地方,即函数作用域. 函数作用域:变量在声明它们的 ...

  3. JavaScript权威设计--JavaScript变量,作用域,声明提前(简要学习笔记四)

    1.宿主对象与宿主环境 宿主对象:由ECMAScript实现的宿主环境提供的对象,可以理解为:浏览器提供的对象.所有的BOM和DOM都是宿主对象.   宿主环境:一般宿主环境由外壳程序创建与维护,只要 ...

  4. JavaScript函数作用域和声明提前(3.10.1 page.57)

    <h4>3.函数作用域和声明提前</h4> <p> <!--<script type="text/javascript">-- ...

  5. JavaScript中的作用域

    很多(JavaScript)开发者都在讨论"作用域",但它是什么?它们在JavaScript中的任何地方!我发现很多年轻的开发者不知道作用域是什么.他们中大多数人可以用jQuery ...

  6. JS的作用域和声明提前

    首先介绍下Javascript的函数作用域的概念,然后了解下什么是作用域和声明提前,最后通过一个例子剖析Javascript的作用域链. 1.变量的作用域 稍微有些编程背景的都知道,变量的作用域分为两 ...

  7. 认识javascript中的作用域和上下文

    javascript中的作用域(scope)和上下文(context)是这门语言的独到之处,这部分归功于他们带来的灵活性.每个函数有不同的变量上下文和作用域.这些概念是javascript中一些强大的 ...

  8. 漫谈JavaScript中的作用域(scope)

    什么是作用域 程序的执行,离不开作用域,也必须在作用域中才能将代码正确的执行. 所以作用域到底是什么,通俗的说,可以这样理解:作用域就是定义变量的位置,是变量和函数的可访问范围,控制着变量和函数的可见 ...

  9. 深入理解JavaScript中的作用域和上下文

    介绍 JavaScript中有一个被称为作用域(Scope)的特性.虽然对于许多新手开发者来说,作用域的概念并不是很容易理解,我会尽我所能用最简单的方式来解释作用域.理解作用域将使你的代码脱颖而出,减 ...

随机推荐

  1. jetty9内嵌到应用,并在启动后加载WebApplicationInitializer,可运行jsp

    声明:本文所介绍的两功能都已经测试通过. 第一步先确保你用的是java 8,并依赖需要的相关jar包,以下是用gradle进行依赖的信息: ext { taglibsStandardVersion = ...

  2. maven install 读取jar包时出错;error in opening zip file

    错误信息: [INFO] ------------------------------------------------------------------------ [ERROR] Failed ...

  3. MySQL数据库基本指令

    对MySQL的指令不太熟悉,在此特别整理了一下一些常用的指令: 约定:大写字母写SQL关键字和函数名,小写字母写数据库.数据表和数据列的名字.(下述代码更新不同步,部分代码未依据此约定) 1 数据库的 ...

  4. 【iCore、iCore2、iBoard例程】【异步FIFO跨时钟域通信(通过ARM 读FPGA FIFO)】

    欢迎访问电子工程师学堂,以便了解更多内容:http://www.eeschool.org 一.本实验基于iCore2 完成,通过简单改动,即可用在 iCore 核心板.iBoard 电子学堂上. iC ...

  5. u-boot移植(四)---修改前工作:代码流程分析3---代码重定位

    一.重定位 1.以前版本的重定位 2.新版本 我们的程序不只涉及一个变量和函数,我们若想访问程序里面的地址,则必须使用SDRAM处的新地址,即我们的程序里面的变量和函数必须修改地址.我们要修改地址,则 ...

  6. 为什么main方法是public static void?

    Main方法是我们学习Java编程语言时知道的第一个方法,你是否曾经想过为什么main方法是public.static.void的.当然,很多人首先学的是C和C++,但是在Java中main方法与前者 ...

  7. reportng之测试报告升级美化

    背景:偶然看到一个人的自动化框架的测试报告好漂亮,心痒痒,今天弄了一下午,还是不行,结果到现在就现在,我特么成功了,不为什么 Mark一下: 本地化修改 获取源码,修改reportng.propert ...

  8. DVI与DVI-D的区别

    DVI-I兼容DVI-D和VGA,如果不使用VGA信号兼容,那么没有任何区别. 1) DVI接口是1999年由数字显示工作组DDWG(Digital Display Working Group)推出的 ...

  9. POJ 2395 Out of Hay(求最小生成树的最长边+kruskal)

    Out of Hay Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 18472   Accepted: 7318 Descr ...

  10. ASP.NET MVC学习(一)

    这几天在学习asp.net mvc 一上来就被书中的什么依赖注入,什么单元测试搞的晕晕呼呼,根本就不理解,前天开始做书中的运动商店项目,一上来就遇到个大难题,书中的连接字符串的写法,跟以往在winfo ...