`

(转)JavaScript的作用域和提升机制

 
阅读更多
http://blogread.cn/it/article/7306?f=wb
你知道下面的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中的作用域(scope)

   JavaScript初学者最容易混淆的地方是作用域。实际上,不只是初学者。我遇到过许多经验丰富的JavaScript程序员,却不完全明白作用域。JavaScript的作用域如此容易混淆的原因是它看起来很像C家族的语言(类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和C家族的语言有块级作用域(block-level scope)。当控制流进入一个块,比如if语句,新的变量会在块作用域里声明,不会对外面作用域产生印象。这不适用于JavaScript。在Firebug里运行下面的代码:

var x = 1;
console.log(x); // 1
if (true) {
    var x = 2;
    console.log(x); // 2
}
console.log(x); // 2
   在这个例子中,Firebug将输出1,2,2。这是因为JavaScript有函数级作用域(function-level scope)。这一点和C家族完全不同。语句块,如if语言,不创建新的作用域。仅仅函数创建新作用域。

   很多程序员,像C,C++,C#或Java,都不知道这点,也不希望这样。幸运的是,因为JavaScript函数的灵活性,有一个解决方案。你若你必须要在函数内部创建一个临时作用域,像下面这样做:

function foo() {
    var x = 1;
    if (x) {
        (function () {
            var x = 2;
            // 此处省略一万个字
        }());
    }
    // x 仍然是 1.
}
   这方法实际上相当灵活,可以在你需要临时作用域的时候随意使用,不局限于块级语句内部。然而,我强烈建议你花时间去了解和欣赏JavaScript的作用域。它非常强大,是这门语言中我最喜欢的特性之一。如果你了解作用域,将更容易理解提升。

声明,名字和提升(Hoisting)

   在JavaScript中,作用域中的名字(属性名)有四种基本来源:

语言定义:默认所有作用域都有属性名this和arguments。

形参:函数可能有形式参数,其作用域是整个函数体内部。

函数声明:类似于function foo() {}这种形式。

   变量声明: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;
}
   注意变量声明中赋值的过程不会被提升。仅仅变量名字被提升了。这不适用于函数声明,整个函数体也会提升。但不要忘记有两种声明函数的方法。考虑下面的JavaScript代码:

function test() {
    foo(); // 类型错误 “foo 不是一个函数”
    bar(); // “这能运行”
    var foo = function () { // 将函数表达式赋值给本地变量“foo”
        alert("this won't run!");
    }
    function bar() { //  'bar'函数声明,分配“bar”名字
        alert("this will run!");
    }
}
test();
   在这种情况下,仅仅函数声明的函数体被提升到顶部。名字“foo”被提升,但后面的函数体,在执行的时候才被指派。

   这是全部的基本提升,看起来并不复杂和让人混淆。当然,这是JavaScript,在某些特殊性况下会更复杂一点。

名字解析顺序

   需要记住的最重要的特殊情况是名字的解析顺序。记住作用域中的名字有四种来源。上面我列出他们的顺序是他们被解析的顺序。一般来说,如果一个名字已经被定义过,那么它不会在被其他有相同名字的属性重写。这意味着函数声明优先于变量声明。这并不意味着为名字赋值的过程将不工作,仅仅声明的过程会被忽略。有几个例外情况:

函数的内置变量arguments比较奇怪。它看起来是在普通的函数参数之后才声明,其实是在函数声明之前。如果参数里面有名称为arguments的参数,它会比内置的那个优先级高,即使它是undefined。所以不要使用arguments作为为函数参数的名称。

尝试使用this作为标示符的地方都会造成一个语法错误。这是一个很好的特性。

如果多个参数具有相同的名字,那么最后一个参数会优先于先前的,即使它是undefined。

命名函数表达式

   你可以在函数表达式给中给函数命名,用这样的语法不能完成一个函数声明,下面有一些代码来说明我的意思:

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() {}; // 函数声明(“bar”和函数体会被提升)
var baz = function spam() {}; // 命名函数表达式(仅“baz”会被提升)

foo(); // valid
bar(); // valid
baz(); // valid
spam(); // ReferenceError "spam is not defined"
编码时如何使用这些知识

   现在你应该理解了作用域和提升(hoisting),那么我们在编写JavaScript的时候应该怎么做呢?最重要的事情就是始终用var表达式来声明你的变量。我强烈建议你使用单var模式(single var)。如果你强迫自己做到这一点,你将永远不会遇到任何与变量提升相关的混乱的问题。但是这样做也让我们很难跟踪那些在当前作用域中实际上已经声明的变量。我建议你使用JSLint和声明一次原则来进行实际操作,如果你这样做了,你的代码应该会看起来像这样:

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

   我翻了翻ECMAScript标准,想直接了解这些东西是如何工作的,发现效果不错。这里我不得不说关于变量声明和作用域(第12.2.2节)的内容:

   如果在一个函数中声明变量,这些变量就被定义在了在该函数的函数作用域中,见第10.1.3所述。不然它们就是被定义在全局的作用域内(即,它们被创建为全局对象的成员,见第10.1.3所述),当进入执行环境的时候,变量就被创建。一个语句块不能定义一个新的作用域。只有一个程序或者函数声明能够产生一个新的作用域。创建变量时,被初始化为undefined。如果变量声明语句里面带有赋值操作,则赋值操作只有被执行到声明语句的时候才会发生,而不是创建的时候。

   我希望这篇文章阐明了对JavaScript程序员来说最常见的迷惑问题,我试图讲的尽可能详尽,以避免造成更多的迷惑,如果我说错了或者有大的遗漏,请通知我。
分享到:
评论

相关推荐

    JavaScript作用域原理

    JavaScript作用域是编程中至关重要的概念,它定义了变量、函数和对象的可见性和生命周期。在JavaScript中,作用域主要分为两种类型:全局作用域和局部作用域。此外,随着ES6的引入,块级作用域和函数作用域也变得...

    javascript的作用域和块级作用域概念理解.doc

    JavaScript作用域和块级作用域概念理解 JavaScript中作用域的概念主要涉及变量和函数的可访问性,它影响程序中变量和函数在哪些区域是可见的,哪些区域是不可见的。在JavaScript中,主要有全局作用域和函数作用域两...

    JavaScript作用域原理(二) 预编译[9 29]

    总结一下,JavaScript作用域和预编译是编程实践中需要深入理解的核心概念。正确掌握它们能帮助开发者写出更健壮、更易于调试的代码。在学习过程中,建议通过实践和阅读相关资料来不断加深理解,确保在日常开发中能够...

    javascript作用域链(Scope Chain)初探.docx

    在理解JavaScript作用域链之前,我们需要先了解JavaScript的作用域机制,特别是词法作用域(lexical scope)。 **词法作用域**:变量的作用域是由其在源代码中定义的位置决定的,而不是由其实际执行位置决定的。这...

    JavaScript程序设计-变量作用域.pdf

    作用域链是JavaScript中实现作用域机制的一种方式,它是由一系列作用域构成的链状结构,每个函数都有自己的作用域链,用于在查找变量时提供路径。当尝试访问一个变量时,JavaScript会首先在当前作用域查找,如果没有...

    javascript 闭包、匿名函数、作用域链

    JavaScript中的闭包、匿名函数和作用域链是编程中至关重要的概念,它们是理解JavaScript运行机制的关键。在本文中,我们将深入探讨这三个概念,并通过实际示例来展示它们的运用。 首先,我们来讨论“闭包”。闭包是...

    【JavaScript源代码】JavaScript中变量提升和函数提升实例详解.docx

    变量提升是指在JavaScript语言中,变量声明会被提升到当前作用域的顶端。这意味着,即使变量声明在代码的后面,但是在执行时,它会被提升到当前作用域的顶端。例如,在 1中,`console.log(num)`会输出`undefined`,...

    js变量作用域

    本篇文章旨在通过一系列示例和解释,帮助读者掌握JavaScript中的变量作用域机制。 #### 二、全局变量与作用域划分 ##### 1. 全局变量 在JavaScript中,全局变量实际上是`window`对象的一个属性。例如,如果在一个...

    图解javascript作用域链

    理解JavaScript的作用域链对于防止变量污染、提升代码可维护性和性能优化至关重要。例如,通过避免全局变量的过度使用,可以减少内存消耗,同时避免命名冲突。此外,闭包也是利用作用域链的一种常见机制,允许函数...

    js作用域基本介绍.doc

    本文将深入探讨 JavaScript 的全局作用域和局部作用域,以及相关的变量定义、使用和赋值机制。 1. **全局作用域与局部作用域** - **全局作用域**:这是最大的作用域,它覆盖了整个脚本或网页。在全局作用域中定义...

    浅谈JavaScript作用域和闭包

    作用域和闭包是JavaScript语言中至关重要的概念,它们决定了变量和函数的可见性和生命周期。在JavaScript中,作用域可以分为全局作用域和局部作用域两大类,而闭包则是函数和声明该函数的词法环境的组合。下面将详细...

    你不懂JS:作用域与闭.pdf

    JavaScript并不使用动态作用域,但了解这一概念有助于对比和理解JavaScript的作用域机制。 最后,书中提到了`this`的关键点,`this`的值取决于函数调用方式,而不是函数定义的位置,这是JavaScript中常常让人困惑的...

    javascript函数作用域学习示例(js作用域)

    在学习JavaScript作用域之前,我们需要先了解在其他编程语言中常见的块级作用域(block scope)。在像C语言这样的编程语言中,每个用花括号{}包裹的代码块都有自己的作用域,这意味着在代码块内部声明的变量只能在该...

    Scope(作用域).md

    在JavaScript中,作用域主要分为两种类型:全局作用域和函数作用域。随着ES6的引入,又增加了一种新的作用域类型——块级作用域。 #### 二、作用域的分类与特性 ##### 1. 全局作用域 - **定义**:在任何函数外部...

    JavaScript作用域链示例分享

    JavaScript作用域链是编程语言中一个关键的概念,它在JavaScript中扮演着至关重要的角色,尤其对于变量的查找和管理有着深远的影响。理解作用域链有助于我们编写更清晰、更安全的代码,避免出现意外的变量污染和作用...

    简单了解JavaScript作用域

    理解JavaScript作用域对于编写高效、可维护的代码至关重要。掌握作用域可以帮助避免潜在的错误,提高代码质量,并有助于实现更高级的编程技术,如闭包和模块化。通过深入学习和实践,开发者可以更好地掌控JavaScript...

    浅析JavaScript中作用域和作用域链

    在JavaScript中,主要有两种作用域:全局作用域和局部作用域。 1. **全局作用域**:全局作用域指的是在函数外部定义的变量,这类变量在整个程序中都是可访问的。在浏览器环境中,全局变量被视为`window`对象的属性...

    详解JavaScript作用域、作用域链和闭包的用法

    JavaScript中的作用域、作用域链和闭包是理解JavaScript核心概念的关键部分,它们对代码的运行方式和变量的生命周期有着深远的影响。 1. **作用域** 作用域决定了变量和函数的可见性和生命周期。JavaScript主要有...

    Web-前端教程37 JS进阶:作用域.zip

    JavaScript(JS)是Web开发中的核心编程语言,尤其在前端...总之,深入理解JavaScript的作用域机制是成为专业前端开发者的关键一步。通过学习和实践这些概念,你将能更好地编写出高效、易于维护的代码,提升项目质量。

Global site tag (gtag.js) - Google Analytics