JavaScript作用域与声明提升【翻译】

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

JavaScript作用域与声明提升

你知道下面JavaScript执行后alert的值吗?

1 var foo = 1;
2 function bar() {
3     if (!foo) {
4         var foo = 10;
5     }
6     alert(foo);
7 }
8 bar();

如果因为结果为“10”而吃惊,那么可能你需要好好看看这篇文章:

1 var a = 1;
2 function b() {
3     a = 10;
4     return;
5     function a() {}
6 }
7 b();
8 alert(a);

这个呢?浏览器会alert为“1”,那么这些结果为何如此呢?确实这些看起来有点奇怪,危险,并且混乱,但这恰恰是说明JavaScript是一个强大和富有表现力的语言。我不知道这种特性的标准名称,但是我习惯叫它"提升“,这篇文章将尝试阐明这一机制,但首先我们需要先了解JavaScript的作用域。

JavaScript的作用域

许多初学者最容易混淆的就是“作用域”,实际上,它不只初学者容易混淆,我见过很多有经验的Javascript程序员也不能完全理解“作用域”。之所以弄不清JavaScript的作用域,是因为把它理解成了类C语言的作用域。思考下面的C程序:

 1 #include <stdio.h>
 2 int main() {
 3     int x = 1;
 4     printf("%d, ", x); // 1
 5     if (1) {
 6         int x = 2;
 7         printf("%d, ", x); // 2
 8     }
 9     printf("%d\n", x); // 1
10 }

这个程序的输出结果为1,2,1.这是因为C,和类C语言有块级作用域。当代码执行到一个块内,例如一对大括号内,新的变量声明在这个作用域内,不会影响大括号的外部。这是不同于JavaScript的。在Firebug下尝试以下代码:

1 var x = 1;
2 console.log(x); // 1
3 if (true) {
4     var x = 2;
5     console.log(x); // 2
6 }
7 console.log(x); // 2

在这种情况中,Firebug会显示1,2,2.这是因为JavaScript为函数作用域,这完全不同于类C语言的块级,比如在大括号内,它是不会创建新的作用域的。只有在函数才会。

而很多语言都使用的块级作用域,比如C,C++,C#,和Java,所以这很容易让刚学JavaScript的程序员无法理解,幸好,JavaScript的函数定义非常灵活,如果逆需要在一个函数内创建一个临时的作用域,你可以这样:

 1 function foo() {
 2     var x = 1;
 3     if (x) {
 4         (function () {
 5             var x = 2;
 6             // some other code
 7         }());
 8     }
 9     // x is still 1.
10 }

声明变量和提升

在JavaScript中,一个变量进入作用域有四种基本途径:

  1. 语言定义:全局作用域,默认情况下,有变量this和arguments。
  2. 形参:函数可以有形参,它的作用域为整个函数内。
  3. 函数声明:例如这种形式 function foo() {}
  4. 变量声明:例如这种形式 var foo;

函数声明和变量声明会在解析JavaScript程序是进行内部“提升”,形参和全局变量已经存在了,意味着提升的是3,4两种变量类型,意味着代码在解析后会像这样:

 1 function foo() {
 2     bar();
 3     var x = 1;
 4 }
 5 实际上解释后会像这样: 6
 7 function foo() {
 8     var x;
 9     bar();
10     x = 1;
11 }

事实证明,不管是否包含变量声明它都是存在的。下面两个函数是等价的:

 1 function foo() {
 2     if (false) {
 3         var x = 1;
 4     }
 5     return;
 6     var y = 1;
 7 }
 8 function foo() {
 9     var x, y;
10     if (false) {
11         x = 1;
12     }
13     return;
14     y = 1;
15 }

注意:当函数作为变量定义的值时声明不会被提升,这种情况只有变量名会被提升,这导致函数名提升了,但函数体没有被提升,但请记住函数声明有两种形式,考虑以下JavaScript:

 1 function test() {
 2     foo(); // TypeError "foo is not a function"
 3     bar(); // "this will run!"
 4     var foo = function () { // function expression assigned to local variable ‘foo‘
 5         alert("this won‘t run!");
 6     }
 7     function bar() { // function declaration, given the name ‘bar‘
 8         alert("this will run!");
 9     }
10 }
11 test();

在这个例子中,只有包含函数体的函数声明会被提升到顶部,而变量"foo"被提升了,但是它的主体在右边,只有在语句执行到此时才会被分配。

这就是提升的基本概念,这样看起来也不是那么复杂和容易混淆了吧。当然在写JavaScript时,会遇到一些特殊情况会稍微复杂点。

变量名解析顺序

最重要的是记住在特殊情况下的变量名解析顺序。它们有四种方式进入命名空间,这个顺序根据列表从上到下一次进行,这个顺序列表在下面列出,在一般情况下,如果一个命名已经被定义,那么它不会被另一个同名的所覆盖,这就意味着一个函数声明要优先于变量声明,这并不等于分配新的命名无效,只是声明将被屏蔽。他们也有些例外:

  • 内置arguments的怪异情况,它看起来在函数声明之前形参已经被声明,这意味着一个形参名arguments将优先于内置的arguments,即使它没有被定义,这是一个不好的特征。不要使用arguments作为一个形参。
  • 如果定义this这个命名在一些地方,会导致语法错误。这是一个好的特性。
  • 如果多个形参有相同的名字,那么形参中最后一个同名的将被优先,哪怕它没有被定义。

函数命名表达式

你可以使用函数表达式的形式将函数定义赋给一个函数名,语法像函数定义,但它不同于函数声明,这个命名没有进入作用域,函数体也没有提升。这里有一些代码作为例子来说明其含义:

 1 foo(); // TypeError "foo is not a function"
 2 bar(); // valid
 3 baz(); // TypeError "baz is not a function"
 4 spam(); // ReferenceError "spam is not defined"
 5
 6 var foo = function () {}; // anonymous function expression (‘foo‘ gets hoisted)
 7 function bar() {}; // function declaration (‘bar‘ and the function body get hoisted)
 8 var baz = function spam() {}; // named function expression (only ‘baz‘ gets hoisted)
 9
10 foo(); // valid
11 bar(); // valid
12 baz(); // valid
13 spam(); // ReferenceError "spam is not defined"

如何使用这个知识来写代码

现在你了解了作用域与命名提升,但如何写JavaScript代码呢?一个非常重要的事情是在任何声明变量的时候都使用var.我强烈建议你在每一个作用域内的头部使用var定义变量,如果你总是如此,你将不会因为提升的特性而混乱。然而,这样做很难区分当前作用域下实际被声明的变量和以有的变量。我建议使用JSLint的onevar选项来执行这些。如果你准备这样做,那么你的代码应该看起来是这样:

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

规范怎么说明的

我发现经常查阅ECMAScript Starndard去理解这些特性是如何工作的是非常有用的。这里是它说明变量声明和作用域(最新版的12.2.2部分)

如果变量声明在函数声明内部,那么这个变量被定义在当前函数作用域内,由10.1.3节所述。另外,他们都被定义在全局作用域(即,他们创建的是全局对象的成员,由10.1.3所述)的属性上,并具有属性的特性。变量被创建在当前执行的作用域内,一个块没法产生一个新的作用空间,只有程序和函数声明产生新的作用空间。变量被初始化时创建成一个undefined。一个变量真正被初始化是在使用表达式给变量分配一个存在的值。不是在变量被创建时。

我希望这篇文章解释清楚了JavaScript代码中的一些易混淆的特性。我试图全面的解释这些问题,避免产生新的问题,如果产生了更多的混淆,如果我翻了任何错误或重大遗漏,请让我知道。

时间: 2024-11-01 19:04:57

JavaScript作用域与声明提升【翻译】的相关文章

Javascript作用域和变量提升

下面的程序是什么结果? [javascript] view plain copy var foo = 1; function bar() { if (!foo) { var foo = 10; } alert(foo); } bar(); 结果是10: 那么下面这个呢? [javascript] view plain copy var a = 1; function b() { a = 10; return; function a() {} } b(); alert(a); 结果是1. 吓你一跳

【学习笔记】JavaScript编码规范-声明提升

变量声明应该在作用域的顶端,但是赋值没有. function example(){ var declaredButAssigned; //如下输出 declaredButNotAssigned 未定义 console.log(declaredButNotAssigned) declaredButNotAssigned = true } 匿名表达式能提升他们的变量名,但不能提升函数赋值. function example(){ console.log(anonymous); //未定义 anony

【翻译】JavaScript中的作用域和声明提前

原文:http://www.adequatelygood.com/JavaScript-Scoping-and-Hoisting.html ===翻译开始=== 你知道下面的JavaScript脚本执行结果是什么吗? 1 var foo=1; 2 function bar(){ 3 if(!foo){ 4 var foo=10; 5 } 6 alert(foo); 7 } 8 bar(); 如果你对弹出的结果是"10"感到惊讶的话,那么下面这段脚本会让你晕头转向的: 1 var a=1

JavaScript作用域及作用域链详解、声明提升

相信大家在入门JavaScript这门语言时对作用域.作用域链.变量声明提升这些概念肯定会稀里糊涂,下面就来说说这几个 Javascript 作用域 在 Javascript 中,只有局部作用域和全局作用域.而只有函数可以创建局部作用域,像 if,for 或者 while 这种块语句是没办法创建作用域的. (当然 ES6 提供了 let 关键字可以创建块作用域.) Javascript 的这种特性导致 for 循环里面创建闭包时会产生让人意想不到的结果.比如下面这个例子: var i = 20;

深入理解javascript作用域系列第三篇——声明提升(hoisting)

× 目录 [1]变量 [2]函数 [3]优先 前面的话 一般认为,javascript代码在执行时是由上到下一行一行执行的.但实际上这并不完全正确,主要是因为声明提升的存在.本文是深入理解javascript作用域系列第三篇——声明提升(hoisting) 变量声明提升 a = 2 ; var a; console.log( a ); 直觉上,会认为是undefined,因为var a声明在a = 2;之后,可能变量被重新赋值了,因为会被赋予默认值undefined.但是,真正的输出结果是2 c

JavaScript 变量声明提升

JavaScript 变量声明提升 一.变量提升的部分只是变量的声明,赋值语句和可执行的代码逻辑还保持在原地不动 二.在基本的语句(或者说代码块)中(比如:if语句.for语句.while语句.switch语句.for...in语句等),不存在变量声明提升 三.函数声明会提升,但是函数表达式的函数体就不会提升 1 fun(); // hello 2 function fun(){ 3 console.log("hello"); 4 } 5 // -------------- 6 //

javascript变量声明提升(hoisting)

javascript的变量声明具有hoisting机制,JavaScript引擎在执行的时候,会把所有变量的声明都提升到当前作用域的最前面. 先看一段代码 1 2 3 4 5 var v = "hello"; (function(){   console.log(v);   var v = "world"; })(); 这段代码运行的结果是什么呢? 答案是:undefined 这段代码说明了两个问题, 第一,function作用域里的变量v遮盖了上层作用域变量v.代

javascript变量声明提升

javascript变量声明提升(hoisting) javascript的变量声明具有hoisting机制,JavaScript引擎在执行的时候,会把所有变量的声明都提升到当前作用域的最前面. 先看一段代码 1 2 3 4 5 var v = "hello"; (function(){   console.log(v);   var v = "world"; })(); 这段代码运行的结果是什么呢?答案是:undefined这段代码说明了两个问题,第一,funct

JavaScript基本概念(1)-声明提升

声明提升: function > var > other var提升的时候,只是声明提升,但是赋值还是会在原来的位置. Javascript Hoisting:In javascript, every variable declaration is hoisted to the top of its declaration context.我的理解就是在Javascript语言中,变量的声明(注意不包含变量初始化)会被提升(置顶)到声明所在的上下文,也就是说,在变量的作用域内,不管变量在何处声