JavaScript 中变量的范围是什么?

javascript 中变量的范围是什么?它们在函数内部和外部的作用域是否相同?还是有关系吗?另外,如果变量是全局定义的,这些变量将存储在哪里?

答案

我认为我能做的最好的就是给您一些例子来学习。实际上,JavaScript 程序员是根据他们对范围的理解程度来排名的。有时可能会违反直觉。

  1. 全局范围的变量

    // global scope
    var a = 1;
    
    function one() {
      alert(a); // alerts '1'
    }
  2. 当地范围

    // global scope
    var a = 1;
    
    function two(a) { // passing (a) makes it local scope
      alert(a); // alerts the given argument, not the global value of '1'
    }
    
    // local scope again
    function three() {
      var a = 3;
      alert(a); // alerts '3'
    }
  3. 中级JavaScript 中没有块作用域 (ES5; ES6 引入letconst

    一种。

    var a = 1;
    
    function four() {
      if (true) {
        var a = 4;
      }
    
      alert(a); // alerts '4', not the global value of '1'
    }

    b。

    var a = 1;
    
    function one() {
      if (true) {
        let a = 4;
      }
    
      alert(a); // alerts '1' because the 'let' keyword uses block scoping
    }

    C。

    var a = 1;
    
    function one() {
      if (true) {
        const a = 4;
      }
    
      alert(a); // alerts '1' because the 'const' keyword also uses block scoping as 'let'
    }
  4. 中级对象属性

    var a = 1;
    
    function Five() {
      this.a = 5;
    }
    
    alert(new Five().a); // alerts '5'
  5. 高级关闭

    var a = 1;
    
    var six = (function() {
      var a = 6;
    
      return function() {
        // JavaScript "closure" means I have access to 'a' in here,
        // because it is defined in the function in which I was defined.
        alert(a); // alerts '6'
      };
    })();
  6. 高级基于原型的范围解析

    var a = 1;
    
    function seven() {
      this.a = 7;
    }
    
    // [object].prototype.property loses to
    // [object].property in the lookup chain. For example...
    
    // Won't get reached, because 'a' is set in the constructor above.
    seven.prototype.a = -1;
    
    // Will get reached, even though 'b' is NOT set in the constructor.
    seven.prototype.b = 8;
    
    alert(new seven().a); // alerts '7'
    alert(new seven().b); // alerts '8'

  7. 全球 + 本地一个额外的复杂案例

    var x = 5;
    
    (function () {
        console.log(x);
        var x = 10;
        console.log(x); 
    })();

    因为 JavaScript 总是将变量声明(而不是初始化)移到范围的顶部,所以这将打印出undefined10而不是510 ,这使得代码等效于:

    var x = 5;
    
    (function () {
        var x;
        console.log(x);
        x = 10;
        console.log(x); 
    })();
  8. 捕获子句作用域变量

    var e = 5;
    console.log(e);
    try {
        throw 6;
    } catch (e) {
        console.log(e);
    }
    console.log(e);

    这将打印出565 。在 catch 子句内部, e遮盖了全局和局部变量。但是,此特殊作用域仅适用于捕获的变量。如果您写var f;在 catch 子句中,则与在 try-catch 块之前或之后定义它完全相同。

Javascript 使用范围链为给定功能建立范围。通常有一个全局范围,并且定义的每个函数都有其自己的嵌套范围。在另一个函数中定义的任何函数都具有与外部函数链接的局部作用域。定义范围的始终是源中的位置。

范围链中的元素基本上是一个 Map,具有指向其父范围的指针。

解析变量时,javascript 从最内部的范围开始并向外搜索。

全局声明的变量具有全局范围。函数内声明的变量的作用域为该函数,并且阴影全局变量具有相同的名称。

(我敢肯定有很多的奥妙,真正的 JavaScript 编程人员能够在其他的答案指出,特别是我碰上了这个网页约究竟this随时手段,希望这个更详细介绍的链接 ,就足以让你开始虽然。)

老派 JavaScript

传统上,JavaScript 实际上只有两种类型的作用域:

  1. 全局范围 :从应用程序的开始就在整个应用程序中知道变量(*)
  2. 功能范围 :从函数开头(*)开始,变量在函数中是已知

我不会对此进行详细说明,因为已经有许多其他答案解释了差异。


现代 JavaScript

现在, 最新的 JavaScript 规范也允许使用第三种范围:

  1. 块范围 :从声明变量开始(**)开始,变量就在声明的块内为人所知

如何创建块作用域变量?

传统上,您可以这样创建变量:

var myVariable = "Some text";

块范围变量是这样创建的:

let myVariable = "Some text";

那么功能范围和块范围之间有什么区别?

要了解功能范围和块范围之间的区别,请考虑以下代码:

// i IS NOT known here
// j IS NOT known here
// k IS known here, but undefined
// l IS NOT known here

function loop(arr) {
    // i IS known here, but undefined
    // j IS NOT known here
    // k IS known here, but has a value only the second time loop is called
    // l IS NOT known here

    for( var i = 0; i < arr.length; i++ ) {
        // i IS known here, and has a value
        // j IS NOT known here
        // k IS known here, but has a value only the second time loop is called
        // l IS NOT known here
    };

    // i IS known here, and has a value
    // j IS NOT known here
    // k IS known here, but has a value only the second time loop is called
    // l IS NOT known here

    for( let j = 0; j < arr.length; j++ ) {
        // i IS known here, and has a value
        // j IS known here, and has a value
        // k IS known here, but has a value only the second time loop is called
        // l IS NOT known here
    };

    // i IS known here, and has a value
    // j IS NOT known here
    // k IS known here, but has a value only the second time loop is called
    // l IS NOT known here
}

loop([1,2,3,4]);

for( var k = 0; k < arr.length; k++ ) {
    // i IS NOT known here
    // j IS NOT known here
    // k IS known here, and has a value
    // l IS NOT known here
};

for( let l = 0; l < arr.length; l++ ) {
    // i IS NOT known here
    // j IS NOT known here
    // k IS known here, and has a value
    // l IS known here, and has a value
};

loop([1,2,3,4]);

// i IS NOT known here
// j IS NOT known here
// k IS known here, and has a value
// l IS NOT known here

在这里,我们可以看到我们的变量j仅在第一个 for 循环中已知,而在之前和之后都不知道。但是,我们的变量i在整个函数中是已知的。

另外,请考虑在声明块范围变量之前不知道它们,因为它们没有被提升。您也不允许在同一块中重新声明相同的块范围变量。这使得块范围的变量比全局变量或功能范围的变量更不容易出错,全局变量或功能范围的变量被提升并且在有多个声明的情况下不会产生任何错误。


今天使用块作用域变量安全吗?

今天是否可以安全使用,取决于您的环境:

  • 如果您正在编写服务器端 JavaScript 代码( Node.js ),则可以安全地使用let语句。

  • 如果您正在编写客户端 JavaScript 代码并使用基于浏览器的编译器(例如Traceurbabel-standalone ),则可以安全地使用let语句,但是就性能而言,代码可能不是最佳选择。

  • 如果您正在编写客户端 JavaScript 代码并使用基于 Node 的编译器(例如traceur shell 脚本Babel ),则可以安全地使用let语句。并且由于您的浏览器仅会知道已转译的代码,因此应限制性能方面的弊端。

  • 如果您正在编写客户端 JavaScript 代码并且不使用翻译器,则需要考虑浏览器支持。

    以下是一些根本不支持let浏览器:

    • Internet Explorer 10及以下
    • Firefox 43及以下
    • Safari 9及以下
    • Android 浏览器 4及更低版本
    • Opera 27以下
    • Chome 40及以下
    • 任何版本的Opera MiniBlackberry 浏览器

在此处输入图片说明


如何跟踪浏览器支持

有关在阅读此答案时哪些浏览器支持let语句的最新概述,请参见Can I Use页面


(*)因为提升了 JavaScript 变量,所以可以在声明它们之前初始化和使用全局范围和功能范围的变量。这意味着声明始终在作用域的顶部。

(**)不提升块范围的变量

这是一个例子:

<script>

var globalVariable = 7; //==window.globalVariable

function aGlobal( param ) { //==window.aGlobal(); 
                            //param is only accessible in this function
  var scopedToFunction = {
    //can't be accessed outside of this function

    nested : 3 //accessible by: scopedToFunction.nested
  };

  anotherGlobal = {
    //global because there's no `var`
  }; 

}

</script>

您将要研究闭包,以及如何使用它们来成为私有成员

据我了解,关键是 Javascript 具有功能级别范围,而不是更常见的 C 块范围。

这是一篇关于该主题的好文章。

在 “Javascript 1.7”(Mozilla 对 Javascript 的扩展)中,还可以使用let语句声明块范围变量:

var a = 4;
 let (a = 3) {
   alert(a); // 3
 }
 alert(a);   // 4

Brendan Eich最初设计 JavaScript 时进行范围界定的想法来自HyperCard脚本语言HyperTalk

用这种语言,显示的过程类似于一堆索引卡。有一个称为背景的主卡。它是透明的,可以看作是底层卡片。此基本卡上的所有内容均与位于其上方的卡共享。放在顶部的每个卡都有其自己的内容,该内容优先于前一张卡,但如果需要,仍可以访问前一张卡。

这正是 JavaScript 作用域定义系统的设计方式。它只是具有不同的名称。 JavaScript 中的卡称为执行上下文ECMA 。这些上下文中的每一个都包含三个主要部分。可变环境,词法环境和 this 绑定。回到卡片参考,词法环境包含堆栈中较低位置的先前卡片的所有内容。当前上下文位于堆栈的顶部,声明的所有内容都将存储在变量环境中。在命名冲突的情况下,可变环境将优先。

此绑定将指向包含的对象。有时范围或执行上下文会在不更改包含对象的情况下发生变化,例如在声明的函数中,包含对象可能是window或构造函数。

这些执行上下文是在控制权转移时创建的。当代码开始执行时,将转移控制权,这主要是从函数执行开始。

这就是技术解释。在实践中,重要的是要记住在 JavaScript 中

  • 范围从技术上讲是 “执行上下文”
  • 上下文构成了存储变量的环境的堆栈
  • 堆栈的顶部优先(底部是全局上下文)
  • 每个函数都创建一个执行上下文(但并不总是新的 this 绑定)

将其应用于该页面上的先前示例之一(5.“结束”),可以遵循执行上下文堆栈。在此示例中,堆栈中有三个上下文。它们由外部上下文定义,由 var 6 调用的立即调用函数中的上下文,以及在 var 6 的立即调用的函数内部返回的函数中的上下文。

i )外部环境。它具有 a = 1 的可变环境
ii )IIFE 上下文,它的词法环境为 a = 1,但变量环境为 a = 6,该环境在堆栈中优先
iii )返回的函数上下文,它的词法环境为 a = 6,这是调用时在警报中引用的值。

在此处输入图片说明

1)有一个全局范围,一个功能范围以及 with 和 catch 范围。通常,变量没有 “块” 级作用域 - with 和 catch 语句将名称添加到其块中。

2)作用域一直由函数嵌套到全局作用域。

3)通过原型链解决属性。 with 语句将对象属性名称带到 with 块定义的词法范围内。

编辑:ECMAAScript 6(Harmony)被指定为支持 let,我知道 chrome 允许使用 “harmony” 标志,因此也许它确实支持它。

让我们为块级作用域提供支持,但是您必须使用关键字来实现它。

编辑:基于本杰明在评论中指出 with 和 catch 语句,我已经编辑了帖子,并添加了更多内容。 with 和 catch 语句都将变量引入其各自的块中,这就是块作用域。这些变量是传递给它们的对象的属性的别名。

//chrome (v8)

 var a = { 'test1':'test1val' }
 test1   // error not defined
 with (a) { var test1 = 'replaced' }
 test1   // undefined
 a       // a.test1 = 'replaced'

编辑:澄清示例:

test1 的作用域为 with 块,但别名为 a.test1。 “Var test1” 在上层词法上下文(函数或全局)中创建一个新变量 test1,除非它是 - 的属性。

kes!小心使用'with'- 就像 var 是 noop,如果变量已经在函数中定义,就从对象导入的名称而言,它也是 noop!对已经定义的名称稍加注意将使此操作更加安全。因此,我个人将永远不会使用。

我发现许多不熟悉 JavaScript 的人都难以理解,继承默认情况下是该语言可用的,并且函数作用域是迄今为止唯一的作用域。我提供了我去年年底写的名为 JSPretty 的美化工具的扩展。要素颜色在代码中作用域,并且始终将颜色与该作用域中声明的所有变量关联。当一个颜色的变量来自一个范围时,在另一个范围中使用可视化的方式显示了闭包。

请尝试以下功能:

观看演示:

在以下位置查看代码:

当前,该功能支持深度 16 个嵌套函数,但当前不为全局变量着色。