JavaScript中变量的作用域是什么?

JavaScript中变量的作用域是什么?它们在函数内部和外部的作用域相同吗?或者这有关系吗?此外,如果变量是全局定义的,它们存储在哪里?

565516 次浏览

全局声明的变量具有全局作用域。在函数中声明的变量的作用域为该函数,并阴影同名的全局变量。

(我相信真正的JavaScript程序员能够在其他答案中指出许多微妙之处。特别是我遇到了此页面关于this在任何时候的确切含义。希望这个更介绍性的链接足以让你开始。)

JavaScript使用作用域链来建立给定函数的作用域。通常有一个全局作用域,定义的每个函数都有自己的嵌套作用域。在另一个函数中定义的任何函数都有一个与外部函数链接的本地作用域。定义作用域的总是源中的位置。

作用域链中的元素基本上是一个带有指向其父作用域的指针的Map。

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

这里有一个例子:

<script>
var globalVariable = 7; //==window.globalVariable
function aGlobal( param ) { //==window.aGlobal();//param is only accessible in this functionvar scopedToFunction = {//can't be accessed outside of this function
nested : 3 //accessible by: scopedToFunction.nested};
anotherGlobal = {//global because there's no `var`};
}
</script>

您将需要研究闭包,以及如何使用它们来制作私人成员

TLDR

JavaScript具有词法(也称为静态)范围和闭包。这意味着您可以通过查看源代码来判断标识符的范围。

这四个范围是:

  1. 全球——一切可见
  2. 函数-在函数(及其子函数和块)中可见
  3. 块-在块(及其子块)内可见
  4. 模块-在模块内可见

在全局和模块作用域的特殊情况之外,变量使用var(函数作用域)、let(块作用域)和const(块作用域)声明。大多数其他形式的标识符声明在严格模式下都有块作用域。

概览

作用域是标识符有效的代码库区域。

词法环境是标识符名称和与之关联的值之间的映射。

作用域由词法环境的链接嵌套形成,嵌套中的每个级别对应于祖先执行上下文的词法环境。

这些链接的词法环境形成了一个范围“链”。标识符解析是沿着这条链搜索匹配标识符的过程。

标识符解析只发生在一个方向上:向外。这样,外部词汇环境就无法“看到”内部词汇环境。

在决定JavaScript中标识符范围时,有三个相关因素:

  1. 如何声明标识符
  2. 在哪里声明了标识符
  3. 无论你是严格模式还是非严格模式

可以声明标识符的一些方式:

  1. varletconst
  2. 函数参数
  3. 捕获块参数
  4. 函数声明
  5. 命名函数表达式
  6. 全局对象上隐式定义的属性(即,在非严格模式下缺少var
  7. import语句
  8. eval

可以声明一些位置标识符:

  1. 全球背景
  2. 职能机构
  3. 普通块
  4. 控制结构的顶部(例如,循环,如果,同时等)
  5. 控制结构主体
  6. 模块

声明样式

var

使用var有功能范围声明的标识符,除非它们直接在全局上下文中声明,在这种情况下,它们被添加为全局对象的属性并具有全局范围。它们在eval函数中的使用有单独的规则。

let和const

使用letconst有块范围声明的标识符,除非它们直接在全局上下文中声明,在这种情况下它们具有全局范围。

注意:letconstvar都被吊起来了。这意味着它们的逻辑定义位置是其封闭范围(块或函数)的顶部。然而,使用letconst声明的变量在控制通过源代码中的声明点之前无法读取或分配给。中间周期称为时间死区。

function f() {function g() {console.log(x)}let x = 1g()}f() // 1 because x is hoisted even though declared with `let`!

函数参数名称

函数参数名称的作用域是函数体。请注意,这有点复杂。声明为默认参数的函数靠近参数表,而不是函数体。

函数声明

函数声明在严格模式下有块作用域,在非严格模式下有函数作用域。注意:非严格模式是一组复杂的紧急规则,基于不同浏览器的古怪历史实现。

命名函数表达式

命名函数表达式的作用域为它们自己(例如,为了递归的目的)。

全局对象上隐式定义的属性

在非严格模式下,全局对象上隐式定义的属性具有全局范围,因为全局对象位于范围链的顶部。在严格模式下,这些是不允许的。

val

eval字符串中,使用var声明的变量将被放置在当前范围内,或者,如果间接使用eval,则作为全局对象的属性。

示例

下面的代码将抛出一个引用错误,因为名称xyz在函数f之外没有任何意义。

function f() {var x = 1let y = 1const z = 1}console.log(typeof x) // undefined (because var has function scope!)console.log(typeof y) // undefined (because the body of the function is a block)console.log(typeof z) // undefined (because the body of the function is a block)

下面的代码将为yz抛出一个引用错误,但不会为x抛出一个引用错误,因为x的可见性不受块的限制。定义ifforwhile等控制结构主体的块的行为类似。

{var x = 1let y = 1const z = 1}console.log(x) // 1console.log(typeof y) // undefined because `y` has block scopeconsole.log(typeof z) // undefined because `z` has block scope

在下面,x在循环之外可见,因为var具有函数范围:

for(var x = 0; x < 5; ++x) {}console.log(x) // 5 (note this is outside the loop!)

…由于这种行为,您需要小心关闭使用var in循环声明的变量。这里声明的变量x只有一个实例,它在逻辑上位于循环之外。

下面的代码打印5五次,然后在循环外为console.log打印5第六次:

for(var x = 0; x < 5; ++x) {setTimeout(() => console.log(x)) // closes over the `x` which is logically positioned at the top of the enclosing scope, above the loop}console.log(x) // note: visible outside the loop

下面打印undefined,因为x是块范围的。回调一个接一个异步运行。let变量的新行为意味着每个匿名函数都对名为x的不同变量关闭(与var不同),因此打印了整数04。:

for(let x = 0; x < 5; ++x) {setTimeout(() => console.log(x)) // `let` declarations are re-declared on a per-iteration basis, so the closures capture different variables}console.log(typeof x) // undefined

下面不会抛出ReferenceError,因为x的可见性不受块的约束;但是,它会打印undefined,因为变量尚未初始化(因为if语句)。

if(false) {var x = 1}console.log(x) // here, `x` has been declared, but not initialised

使用letfor循环顶部声明的变量的作用域为循环主体:

for(let x = 0; x < 10; ++x) {}console.log(typeof x) // undefined, because `x` is block-scoped

下面将抛出一个ReferenceError,因为x的可见性受到块的约束:

if(false) {let x = 1}console.log(typeof x) // undefined, because `x` is block-scoped

使用varletconst声明的变量都适用于模块:

// module1.js
var x = 0export function f() {}
//module2.js
import f from 'module1.js'
console.log(x) // throws ReferenceError

下面将在全局对象上声明一个属性,因为在全局上下文中使用var声明的变量被添加为全局对象的属性:

var x = 1console.log(window.hasOwnProperty('x')) // true

全局上下文中的letconst不向全局对象添加属性,但仍然具有全局范围:

let x = 1console.log(window.hasOwnProperty('x')) // false

函数参数可以认为是在函数体中声明的:

function f(x) {}console.log(typeof x) // undefined, because `x` is scoped to the function

Catch块参数的作用域为catch-block主体:

try {} catch(e) {}console.log(typeof e) // undefined, because `e` is scoped to the catch block

命名函数表达式的作用域仅限于表达式本身:

(function foo() { console.log(foo) })()console.log(typeof foo) // undefined, because `foo` is scoped to its own expression

在非严格模式下,全局对象上隐式定义的属性是全局范围的。在严格模式下,您会收到错误。

x = 1 // implicitly defined property on the global object (no "var"!)
console.log(x) // 1console.log(window.hasOwnProperty('x')) // true

在非严格模式下,函数声明具有函数作用域。在严格模式下,它们具有块作用域。

'use strict'{function foo() {}}console.log(typeof foo) // undefined, because `foo` is block-scoped

它在引擎盖下是如何工作的

作用域定义为标识符有效的代码的词汇区域。

在JavaScript中,每个函数对象都有一个隐藏的[[Environment]]引用,该引用是对创建它的执行上下文(堆栈框架)的词汇环境的引用。

当您调用函数时,会调用隐藏的[[Call]]方法。此方法创建一个新的执行上下文并在新的执行上下文和函数对象的词法环境之间建立链接。它通过将函数对象上的[[Environment]]值复制到新执行上下文的词法环境上的外部参考字段中来做到这一点。

请注意,新执行上下文和函数对象的词法环境之间的这种链接称为关闭

因此,在JavaScript中,范围是通过外部引用以“链”形式链接在一起的词法环境实现的。这个词法环境链称为范围链,匹配标识符的标识符解析由在链条上搜寻发生。

找出更多

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

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

据我所知,关键是JavaScript具有函数级作用域,而不是更常见的C块作用域。

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

我发现许多JavaScript新手很难理解继承在语言中默认可用,而函数作用域是唯一的作用域,到目前为止。我为去年年底写的一个名为JSPretty的美化器提供了一个扩展。特性在代码中为函数作用域着色,并始终将一种颜色与该作用域中声明的所有变量相关联。当具有一个作用域颜色的变量在不同的作用域中使用时,闭包就会直观地展示出来。

尝试以下功能:

查看演示:

查看代码:

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

试试这个奇怪的例子。在下面的例子中,如果a是初始化为0的数字,你会看到0,然后是1。除了a是一个对象,javascript会向f1传递a的指针而不是它的副本。结果是你两次都得到相同的警报。

var a = new Date();function f1(b){b.setDate(b.getDate()+1);alert(b.getDate());}f1(a);alert(a.getDate());

1)有一个全局作用域、一个函数作用域以及with和catch作用域。变量的一般没有“块”级别的作用域——with和catch语句为它们的块添加名称。

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

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

编辑:ECMAAScript 6(Harmony)规范支持let,我知道chrome允许“和谐”标志,所以也许它确实支持它。

Let将支持块级范围,但您必须使用关键字才能实现它。

编辑:基于Benjamin在评论中指出的with和catch语句,我编辑了这篇文章,并添加了更多。with和catch语句都将变量引入了它们各自的块中,是一个块范围。这些变量被别名为传递给它们的对象的属性。

 //chrome (v8)
var a = { 'test1':'test1val' }test1   // error not definedwith (a) { var test1 = 'replaced' }test1   // undefineda       // a.test1 = 'replaced'

编辑:澄清示例:

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

哎呀!小心使用'with'-就像var是一个noop,如果变量已经在函数中定义,它也是从对象导入的名称的noop!对已经定义的名称进行一点提醒会使这更安全。我个人永远不会因为这个而使用with。

全球范围:

全局变量就像全局明星(成龙、纳尔逊·曼德拉)。您可以从应用程序的任何部分访问它们(获取或设置值)。全局函数就像全局事件(新年、圣诞节)。您可以从应用程序的任何部分执行(调用)它们。

//global variablevar a = 2;
//global functionfunction b(){console.log(a);  //access global variable}

本地范围:

如果你在美国,你可能认识金·卡戴珊,一个声名狼藉的名人(她不知何故成功地登上了小报)。但美国以外的人不会认出她。她是当地的明星,被束缚在她的领土上。

局部变量就像局部星形。您只能在范围内访问它们(获取或设置值)。局部函数就像局部事件-您只能在该范围内执行(庆祝)。如果您想从范围外访问它们,您将收到引用错误

function b(){var d = 21; //local variableconsole.log(d);
function dog(){  console.log(a); }dog(); //execute local function}
console.log(d); //ReferenceError: dddddd is not defined

查看本文以深入了解范围

JavaScript只有两种作用域:

  1. 全球范围:全局只是一个窗口级别的作用域。在这里,变量存在于整个应用程序中。
  2. 职能范围:在具有var关键字的函数中声明的变量具有功能范围。

每当调用函数时,都会创建一个变量范围对象(并包含在范围链中),后跟JavaScript中的变量。

        a = "global";function outer(){b = "local";console.log(a+b); //"globallocal"}outer();

范围链-->

  1. 窗口级别-aouter函数位于作用域链的顶层。
  2. 当外部函数调用一个新的variable scope object(并包含在作用域链中)时,内部添加了变量b

现在,当变量a需要时,它首先搜索最近的变量范围,如果变量不存在,则将其移动到变量范围的下一个对象chain.which在这种情况下是窗口级别。

运行代码。希望这能给出一个关于范围的想法

Name = 'global data';document.Name = 'current document data';(function(window,document){var Name = 'local data';var myObj = {Name: 'object data',f: function(){alert(this.Name);}};
myObj.newFun = function(){alert(this.Name);}
function testFun(){alert("Window Scope : " + window.Name +"\nLocal Scope : " + Name +"\nObject Scope : " + this.Name +"\nCurrent document Scope : " + document.Name);}

testFun.call(myObj);})(window,document);

JS中只有函数范围。不是块范围!您也可以看到正在提升的内容。

var global_variable = "global_variable";var hoisting_variable = "global_hoist";
// Global variables printedconsole.log("global_scope: - global_variable: " + global_variable);console.log("global_scope: - hoisting_variable: " + hoisting_variable);
if (true) {// The variable block will be global, on true condition.var block = "block";}console.log("global_scope: - block: " + block);
function local_function() {var local_variable = "local_variable";console.log("local_scope: - local_variable: " + local_variable);console.log("local_scope: - global_variable: " + global_variable);console.log("local_scope: - block: " + block);// The hoisting_variable is undefined at the moment.console.log("local_scope: - hoisting_variable: " + hoisting_variable);
var hoisting_variable = "local_hoist";// The hoisting_variable is now set as a local one.console.log("local_scope: - hoisting_variable: " + hoisting_variable);}
local_function();
// No variable in a separate function is visible into the global scope.console.log("global_scope: - local_variable: " + local_variable);

最初由Brendan Eich设计时,JavaScript中范围的想法来自HyperCard脚本语言HyperTalk

在这种语言中,显示类似于一堆索引卡。有一张主卡被称为背景。它是透明的,可以看作是底部的卡片。这个基础卡上的任何内容都与放在它上面的卡片共享。放在上面的每张卡片都有自己的内容,优先于前一张卡片,但如果需要,仍然可以访问以前的卡片。

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

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

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

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

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

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

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

在此处输入图像描述

JavaScript中有两种类型的作用域。

  1. 全球范围:在全局范围内宣布的变量可以非常顺利地在程序的任何地方使用。例如:

    var carName = " BMW";
    // code here can use carName
    function myFunction() {// code here can use carName}
  2. Functional scope or Local scope: variable declared in this scope can be used in its own function only. For example:

    // code here can not use carNamefunction myFunction() {var carName = "BMW";// code here can use carName}

几乎只有两种类型的JavaScript作用域:

  • 每个var声明的作用域都与最直接的封闭函数相关联
  • 如果var声明没有封闭函数,则它是全局范围

因此,函数以外的任何块都不会创建新的作用域。这解释了为什么for循环覆盖外部作用域变量:

var i = 10, v = 10;for (var i = 0; i < 5; i++) { var v = 5; }console.log(i, v);// output 5 5

而是使用函数:

var i = 10, v = 10;$.each([0, 1, 2, 3, 4], function(i) { var v = 5; });console.log(i,v);// output 10 10

在第一个例子中,没有块作用域,所以最初声明的变量被覆盖了。在第二个例子中,由于函数的原因,有一个新的范围,所以最初声明的变量是SHADOWED,而不是覆盖。

就JavaScript范围而言,这几乎是你需要知道的全部,除了:

所以你可以看到JavaScript的作用域实际上非常简单,尽管并不总是直观的。需要注意的几件事:

  • var声明被提升到作用域的顶部。这意味着无论var声明发生在哪里,对编译器来说,var本身就好像发生在顶部
  • 合并同一作用域内的多个var声明

所以这个代码:

var i = 1;function abc() {i = 2;var i = 3;}console.log(i);     // outputs 1

相当于:

var i = 1;function abc() {var i;     // var declaration moved to the top of the scopei = 2;i = 3;     // the assignment stays where it is}console.log(i);

这可能看起来违反直觉,但从命令式语言设计者的角度来看,这是有道理的。

老式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代码并使用基于浏览器的转译器(如跟踪你巴别塔-独立),您可以安全地使用let语句,但是您的代码在性能方面可能不是最佳的。

  • 如果你正在编写客户端JavaScript代码并使用基于Node的转译器(如跟踪外壳脚本巴别塔),你可以安全地使用let语句。而且因为你的浏览器只会知道转译的代码,所以性能缺陷应该是有限的。

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

    以下是一些完全不支持let的浏览器:

    • Internet Explorer 10及以下
    • Firefox 43及以下
    • Safari9及以下
    • android浏览器4及以下
    • Opera 27及以下
    • Chome40及以下
    • Opera Mini黑莓浏览器的任何版本

在此处输入图片描述


如何跟踪浏览器支持

有关阅读此答案时哪些浏览器支持let语句的最新概述,请参阅此#1页


(*)全局和函数作用域的变量可以在声明之前初始化和使用,因为JavaScript变量是悬挂这意味着声明总是在作用域的顶部。

只是为了补充其他答案,范围是所有声明的标识符(变量)的查找列表,并强制执行一组严格的规则,说明当前执行的代码如何访问这些标识符(变量)。这种查找可能是为了分配变量,这是一个LHS(左侧)引用,或者可能是为了检索它的值,这是一个RHS(右侧)引用。这些查找是JavaScript引擎在编译和执行代码时在内部做的事情。

所以从这个角度来看,我认为一张图片会有所帮助,我在Kyle Simpson的Scopes and Clo的电子书中找到了:

图像

引用他的电子书:

建筑物代表我们程序的嵌套范围规则集。第一个建筑物的楼层代表您当前的执行范围,无论你在哪里。建筑物的顶层是全球范围。您通过查看当前楼层来解决LHS和RHS参考,如果你找不到,乘电梯到下一层,看那里,然后下一个,等等。一旦你到了顶层(全球范围),你要么找到你要找的东西,要么你

不要这样做,但无论如何都要停止。

值得一提的是,“范围查找在找到第一个匹配项后停止”。

这个“作用域级别”的概念解释了为什么“this”可以通过新创建的作用域进行更改,如果它是在嵌套函数中查找的。下面是所有这些细节的链接,你想知道的关于JavaScript作用域的一切

我的理解是有3个作用域:全局作用域,全局可用;局部作用域,对整个函数可用,无论块如何;块作用域,仅对使用它的块、语句或表达式可用。全局和局部作用域用关键字'var'表示,要么在函数内,要么在函数外,块作用域用关键字'let'表示。

对于那些认为只有全局和局部作用域的人,请解释为什么Mozilla会有一个完整的页面来描述JS中块作用域的细微差别。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let

现代Js、ES6+、'const'和'let'

您应该对您创建的每个变量使用块范围,就像大多数其他主要语言一样。var过时。这使您的代码更安全、更易于维护。

const应该用于95%的病例。它使变量参考无法更改。数组、对象和DOM节点属性可以更改,应该是const

let应该用于任何期望重新分配的变量。这包括在for循环中。如果您在初始化之后更改值,请使用let

块作用域意味着变量只能在声明它的括号内可用。这扩展到内部作用域,包括在您的作用域内创建的匿名函数。

在JavaScript中,有两种类型的作用域:

  • 本地范围
  • 全球范围

Below函数有一个局部作用域变量carName。这个变量不能从函数外部访问。

function myFunction() {var carName = "Volvo";alert(carName);// code here can use carName}

Below Class有一个全局范围变量carName。这个变量可以从类中的任何地方访问。

class {
var carName = " Volvo";
// code here can use carName
function myFunction() {alert(carName);// code here can use carName}}

在EcmaScript5中,主要有两个作用域,局部范围全球范围,但在EcmaScript6中,我们主要有三个作用域,局部作用域,全局作用域和一个名为块范围的新作用域。

块范围的示例是:-

for ( let i = 0; i < 10; i++){statement1...statement2...// inside this scope we can access the value of i, if we want to access the value of i outside for loop it will give undefined.}

ECMAScript 6引入了let和const关键字。这些关键字可以用来代替var关键字。与var关键字相反,let和const关键字支持在块语句中声明本地范围。

var x = 10let y = 10const z = 10{x = 20let y = 20const z = 20{x = 30// x is in the global scope because of the 'var' keywordlet y = 30// y is in the local scope because of the 'let' keywordconst z = 30// z is in the local scope because of the 'const' keywordconsole.log(x) // 30console.log(y) // 30console.log(z) // 30}console.log(x) // 30console.log(y) // 20console.log(z) // 20}
console.log(x) // 30console.log(y) // 10console.log(z) // 10

ES5及更早:

JavaScript中的变量最初(在ES6之前)是以词法函数作用域的。术语词法作用域意味着您可以通过“查看”代码来查看变量的作用域。

使用var关键字声明的每个变量的范围都限制在该函数内。但是,如果在该函数内声明了其他函数,则这些函数将有权访问外部函数的变量。这称为作用域链。它的工作方式如下:

  1. 当函数查找解析变量值时,它首先查看自己的范围。这是函数体,即花括号{}之间的所有内容(除了其他功能中的变量在此范围内)。
  2. 如果它无法在函数体中找到变量会爬上锁链并查看函数定义的地方中函数中的变量作用域。这就是词法作用域的含义,我们可以在定义该函数的代码中看到,因此只需查看代码即可确定作用域链。

示例:

// global scopevar foo = 'global';var bar = 'global';var foobar = 'global';
function outerFunc () {// outerFunc scopevar foo = 'outerFunc';var foobar = 'outerFunc';innerFunc(); 
function innerFunc(){// innerFunc scopevar foo = 'innerFunc';console.log(foo);console.log(bar);console.log(foobar);}}
outerFunc();

当我们尝试将变量foobarfoobar记录到控制台时会发生以下情况:

  1. 我们尝试将foo记录到控制台,foo可以在函数innerFunc本身中找到。因此,foo的值被解析为字符串innerFunc
  2. 我们尝试将bar记录到控制台,在函数innerFunc本身内部找不到bar。因此,我们需要爬上瞄准链。我们首先查看定义函数innerFunc的外部函数。这是函数outerFunc。在outerFunc的范围内,我们可以找到变量bar,它包含字符串'outerFunc'。
  3. fobar在innerFunc中找不到。因此,我们需要爬上瞄准链到innerFunc范围。在这里也找不到,我们爬上另一个级别到全球范围(即最外层的范围)。我们在这里找到保存字符串'global'的变量fobar。如果它在爬上作用域链后没有找到变量,JS引擎会抛出引用错误

ES6(ES 2015)及以上:

词法作用域和作用域链的相同概念仍然适用于ES6。但是引入了一种声明变量的新方法。有以下内容:

  • let:创建一个块范围的变量
  • const:创建一个必须初始化且不能重新分配的块范围变量

varlet/const之间最大的区别是var是函数作用域,而let/const是块作用域。这里有一个例子来说明这一点:

let letVar = 'global';var varVar = 'global';
function foo () {  
if (true) {// this variable declared with let is scoped to the if block, block scopedlet letVar = 5;// this variable declared with let is scoped to the function block, function scopedvar varVar = 10;}  
console.log(letVar);console.log(varVar);}

foo();

在上面的例子中,letVar记录全局值,因为用let声明的变量是块范围的。它们不再存在于各自的块之外,因此无法在if块之外访问变量。

内联处理程序

前端编码人员经常遇到的一个尚未描述的非常常见的问题是超文本标记语言中的内联事件处理程序可见的范围-例如

<button onclick="foo()"></button>

on*属性可以引用必须的变量的范围是:

  • 全局(工作的内联处理程序几乎总是引用全局变量)
  • 文档的属性(例如,querySelector作为独立变量将指向document.querySelector;罕见)
  • 处理程序附加到的元素的属性(如上所述;罕见)

否则,当调用处理程序时,你会得到一个引用错误。因此,例如,如果内联处理程序引用定义为里面window.onload$(function() {的函数,则引用将失败,因为内联处理程序可能只引用全局范围内的变量,并且函数不是全局的:

window.addEventListener('DOMContentLoaded', () => {function foo() {console.log('foo running');}});
<button onclick="foo()">click</button>

Properties of the document and properties of the element the handler is attached to may also be referenced as standalone variables inside inline handlers because inline handlers are invoked inside of two with blocks, one for the document, one for the element. The scope chain of variables inside these handlers is extremely unintuitive, and a working event handler will probably require a function to be global (and unnecessary global pollution should probably be avoided).

Since the scope chain inside inline handlers is so weird, and since inline handlers require global pollution to work, and since inline handlers sometimes require ugly string escaping when passing arguments, it's probably easier to avoid them. Instead, attach event handlers using Javascript (like with addEventListener), rather than with HTML markup.

function foo() {console.log('foo running');}document.querySelector('.my-button').addEventListener('click', foo);
<button class="my-button">click</button>

Modules (<script type="module">)

On a different note, unlike normal <script> tags, which run on the top level, code inside ES6 modules runs in its own private scope. A variable defined at the top of a normal <script> tag is global, so you can reference it in other <script> tags, like this:

<script>const foo = 'foo';</script><script>console.log(foo);</script>

但是ES6模块的顶层是没有全局。在ES6模块顶部声明的变量仅在该模块内可见,除非该变量显式地exported,或者除非它被分配给全局对象的属性。

<script type="module">const foo = 'foo';</script><script>// Can't access foo here, because the other script is a moduleconsole.log(typeof foo);</script>

ES6模块的顶层类似于普通<script>顶层的IIFE内部。该模块可以引用任何全局变量,除非模块明确为其设计,否则任何东西都不能引用模块内部的任何东西。

我真的很喜欢这个答案,但我想补充一下:

Scope收集并维护所有声明的标识符(变量)的查找列表,并强制执行一组严格的规则,说明当前执行的代码如何访问这些标识符。

作用域是一组用于通过标识符名称查找变量的规则。

  • 如果在直接作用域中找不到变量,Engine会咨询下一个包含外部作用域,一直持续到找到或到达最外层(也称为全局)作用域。
  • 是确定在何处以及如何查找变量(标识符)的一组规则。此查找可能是为了分配给变量,这是一个LHS(左侧)引用,也可能是为了检索其值,这是一个RHS(右侧)引用。
  • LHS引用来自赋值操作。与范围相关的赋值可以使用=运算符或通过将参数传递给(分配给)函数参数来发生。
  • JavaScript引擎首先在代码执行之前编译代码,在这样做的过程中,它将像var a=2;这样的语句拆分为两个单独的步骤:第一。首先,var a在该范围内声明它。这是在代码执行之前的开始执行的。第二。稍后,a=2查找变量(LHS引用)并在找到时分配给它。
  • LHS和RHS引用查找都从当前执行的作用域开始,如果需要(也就是说,它们在那里找不到要查找的内容),它们会在嵌套的作用域中向上移动,一次一个作用域(层),查找标识符,直到到达全局(顶层)并停止,然后要么找到它,要么没有找到它。未完成的RHS引用会导致抛出ReReference ceError。未完成的LHS引用会导致自动隐式创建的该名称的全局(如果不是在严格模式下),或者Re的。
  • 作用域由一系列“气泡”组成,每个气泡都充当一个容器或桶,其中声明了标识符(变量、函数)。这些气泡整齐地嵌套在彼此内部,这种嵌套是在作者时定义的。

(function foo() { console.log(foo) })();console.log(typeof foo); // undefined, because `foo` is scoped to its own expression
//but, like this(function foo() {console.log('1:', foo) // function foofoo = 100console.log('2:', foo) // function foo, is not 100, why?})()