javascript中变量的范围是什么?它们在函数内部和外部具有相同的范围吗?或者它甚至重要吗?另外,如果变量是全局定义的,它们存储在哪里?
27 回答
TLDR
JavaScript 具有词法(也称为静态)作用域和闭包。这意味着您可以通过查看源代码来判断标识符的范围。
四个范围是:
- 全球 - 一切可见
- 功能 - 在功能(及其子功能和块)中可见
- 块 - 在块(及其子块)中可见
- 模块 - 在模块内可见
var
在全局和模块范围的特殊情况之外,使用(函数范围)、let
(块范围)和(块范围)声明变量const
。大多数其他形式的标识符声明在严格模式下具有块范围。
概述
范围是标识符有效的代码库区域。
词法环境是标识符名称和与之关联的值之间的映射。
作用域由词汇环境的链接嵌套构成,嵌套中的每一级对应于祖先执行上下文的词汇环境。
这些链接的词法环境形成了一个作用域“链”。标识符解析是沿着该链搜索匹配标识符的过程。
标识符解析只发生在一个方向:向外。这样,外部词汇环境就无法“看到”内部词汇环境。
可以声明标识符的一些方法:
var
,let
和const
- 功能参数
- 捕获块参数
- 函数声明
- 命名函数表达式
var
全局对象上隐式定义的属性(即,在非严格模式下丢失)import
陈述eval
可以声明一些位置标识符:
- 全球背景
- 函数体
- 普通块
- 控制结构的顶部(例如,循环、if、while 等)
- 控制结构体
- 模块
声明样式
变量
使用声明的标识符var
具有函数范围,除了它们直接在全局上下文中声明时,在这种情况下,它们作为属性添加到全局对象上并具有全局范围。eval
它们在函数中的使用有单独的规则。
让和常量
let
使用和声明的标识符const
具有块作用域,除非它们直接在全局上下文中声明,在这种情况下它们具有全局作用域。
注:let
,const
和var
都是吊装的。这意味着它们的逻辑定义位置是它们封闭范围(块或函数)的顶部。但是,在控制通过源代码中的声明点之前let
,const
无法读取或分配使用声明的变量。过渡时期被称为时间死区。
function f() {
function g() {
console.log(x)
}
let x = 1
g()
}
f() // 1 because x is hoisted even though declared with `let`!
函数参数名称
函数参数名称的范围是函数体。请注意,这有点复杂。声明为默认参数的函数靠近参数列表,而不是函数体。
函数声明
函数声明在严格模式下具有块作用域,在非严格模式下具有函数作用域。注意:非严格模式是基于不同浏览器古怪的历史实现的一组复杂的紧急规则。
命名函数表达式
命名函数表达式的范围仅限于自身(例如,出于递归的目的)。
全局对象上隐式定义的属性
在非严格模式下,全局对象上隐式定义的属性具有全局范围,因为全局对象位于范围链的顶部。在严格模式下,这些是不允许的。
评估
在eval
字符串中,使用声明的变量var
将被放置在当前范围内,或者,如果eval
间接使用,则作为全局对象的属性。
例子
以下将引发 ReferenceError,因为名称x
、、y
和z
在函数之外没有任何意义f
。
function f() {
var x = 1
let y = 1
const 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)
以下将为y
and引发 ReferenceError z
,但不会为x
,因为 的可见性x
不受块的约束。定义控制结构体(如if
、for
和)的块的while
行为类似。
{
var x = 1
let y = 1
const z = 1
}
console.log(x) // 1
console.log(typeof y) // undefined because `y` has block scope
console.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
在循环中使用声明的变量。这里只x
声明了一个变量实例,它在逻辑上位于循环之外。
以下打印5
五次,然后为循环外打印5
第六次:console.log
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
),因此整数0
通过4
被打印。:
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
以下不会抛出 aReferenceError
因为可见性x
不受块的限制;但是,它会打印,undefined
因为变量尚未初始化(因为if
语句)。
if(false) {
var x = 1
}
console.log(x) // here, `x` has been declared, but not initialised
在循环顶部声明的变量for
usinglet
的作用域为循环体:
for(let x = 0; x < 10; ++x) {}
console.log(typeof x) // undefined, because `x` is block-scoped
以下将抛出 aReferenceError
因为 的可见性x
受到块的限制:
if(false) {
let x = 1
}
console.log(typeof x) // undefined, because `x` is block-scoped
var
使用或声明let
的变量const
都作用于模块:
// module1.js
var x = 0
export function f() {}
//module2.js
import f from 'module1.js'
console.log(x) // throws ReferenceError
以下将在全局对象上声明一个属性,因为var
在全局上下文中使用声明的变量将作为属性添加到全局对象:
var x = 1
console.log(window.hasOwnProperty('x')) // true
let
并且const
在全局上下文中不要向全局对象添加属性,但仍然具有全局范围:
let x = 1
console.log(window.hasOwnProperty('x')) // false
函数参数可以认为是在函数体中声明的:
function f(x) {}
console.log(typeof x) // undefined, because `x` is scoped to the function
捕获块参数的范围为捕获块主体:
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) // 1
console.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 使用范围链来建立给定函数的范围。通常有一个全局范围,并且定义的每个函数都有自己的嵌套范围。在另一个函数中定义的任何函数都具有链接到外部函数的局部范围。定义范围的始终是源中的位置。
作用域链中的元素基本上是一个带有指向其父作用域的指针的 Map。
解析变量时,javascript 从最里面的范围开始向外搜索。
全局声明的变量具有全局范围。在函数中声明的变量的作用域是该函数,并隐藏同名的全局变量。
(我敢肯定,真正的 JavaScript 程序员可以在其他答案中指出许多微妙之处。特别是我在任何时候都看到了这个页面,了解它的确切this
含义。希望这个更具介绍性的链接足以让你开始.)
老派 JavaScript
传统上,JavaScript 实际上只有两种类型的作用域:
- 全局范围:变量在整个应用程序中都是已知的,从应用程序开始(*)
- 功能范围:变量在声明它们的函数中是已知的,从函数的开头(*)
我不会详细说明这一点,因为已经有许多其他答案可以解释差异。
现代 JavaScript
最新的JavaScript 规范现在也允许第三个作用域:
- 块作用域:标识符在声明它们的作用域的顶部是“已知的” ,但在声明行之后才能分配或取消引用(读取)。这个过渡时期被称为“时间死区”。
如何创建块范围变量?
传统上,您可以像这样创建变量:
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 代码并使用基于浏览器的转译器(如Traceur或babel-standalone),您可以安全地使用该
let
语句,但是您的代码可能在性能方面并非最佳。如果您正在编写客户端 JavaScript 代码并使用基于节点的转译器(如traceur shell 脚本或Babel),则可以安全地使用该
let
语句。而且因为您的浏览器只会知道转译的代码,所以性能缺陷应该是有限的。如果您正在编写客户端 JavaScript 代码并且不使用转译器,则需要考虑浏览器支持。
这些是一些根本不支持
let
的浏览器:- Internet Explorer 10及以下版本
- 火狐 43及以下
- Safari 9及以下
- 安卓浏览器4及以下
- Opera 27及以下
- 丁目40以下
- 任何版本的Opera Mini和黑莓浏览器
如何跟踪浏览器支持
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 块范围。
let
在“Javascript 1.7”(Mozilla 对 Javascript 的扩展)中,还可以使用语句声明块范围变量:
var a = 4;
let (a = 3) {
alert(a); // 3
}
alert(a); // 4
最初由Brendan Eich设计的 JavaScript 范围的想法来自HyperCard脚本语言HyperTalk。
在这种语言中,显示的方式类似于一叠索引卡。有一张被称为背景的主卡。它是透明的,可以看作是底牌。此基础卡上的任何内容都与放置在其上的卡共享。放在最上面的每张卡片都有自己的内容,优先于前一张卡片,但如果需要,仍然可以访问之前的卡片。
这正是 JavaScript 范围系统的设计方式。它只是有不同的名称。JavaScript 中的卡片称为Execution Contexts ECMA。这些上下文中的每一个都包含三个主要部分。一个变量环境、一个词法环境和一个 this 绑定。回到卡片参考,词法环境包含堆栈中较低的先前卡片的所有内容。当前上下文位于堆栈的顶部,在那里声明的任何内容都将存储在变量环境中。在命名冲突的情况下,变量环境将优先。
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”标志,所以也许它确实支持它..
Let 将支持块级范围,但您必须使用关键字才能实现。
编辑:根据本杰明在评论中指出 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,除非它是 a 的属性 -- 它是。
哎呀!小心使用 'with' - 就像 var 是一个 noop 如果变量已经在函数中定义,它也是一个 noop 相对于从对象导入的名称!对已经定义的名称稍加注意会使这更安全。因此,我个人永远不会使用 with。
我发现许多 JavaScript 新手很难理解在语言中默认情况下继承是可用的,并且到目前为止,函数作用域是唯一的作用域。我为去年年底编写的美化器 JSPretty 提供了一个扩展。代码中的功能颜色函数作用域并始终将颜色与该作用域中声明的所有变量相关联。当具有来自一个范围的颜色的变量在不同的范围中使用时,可以直观地演示闭包。
在以下位置尝试该功能:
在以下位置查看演示:
查看代码:
- http://prettydiff.com/lib/jspretty.js
- https://github.com/austincheney/Pretty-Diff/blob/master/lib/jspretty.js
目前该功能支持深度为 16 个嵌套函数,但目前不为全局变量着色。
全球范围:
全局变量就像全局明星(成龙,纳尔逊曼德拉)。您可以从应用程序的任何部分访问它们(获取或设置值)。全局函数就像全局事件(新年、圣诞节)。您可以从应用程序的任何部分执行(调用)它们。
//global variable
var a = 2;
//global function
function b(){
console.log(a); //access global variable
}
本地范围:
如果你在美国,你可能认识臭名昭著的名人金·卡戴珊(她以某种方式设法制作了小报)。但是美国以外的人不会认出她。她是当地的明星,与她的地盘息息相关。
局部变量就像局部星星。您只能在范围内访问它们(获取或设置值)。本地函数就像本地事件 - 您只能在该范围内执行(庆祝)。如果你想从范围之外访问它们,你会得到一个参考错误
function b(){
var d = 21; //local variable
console.log(d);
function dog(){ console.log(a); }
dog(); //execute local function
}
console.log(d); //ReferenceError: dddddd is not defined
JavaScript 只有两种类型的范围:
- 全局范围:全局只是一个窗口级别的范围。在这里,变量存在于整个应用程序中。
- 功能范围:在带有
var
关键字的函数中声明的变量具有功能范围。
每当调用函数时,都会创建一个变量范围对象(并包含在范围链中),然后是 JavaScript 中的变量。
a = "global";
function outer(){
b = "local";
console.log(a+b); //"globallocal"
}
outer();
范围链 -->
- 窗口级别 -
a
和outer
功能位于范围链的顶层。 - 当外部函数调用一个新的
variable scope object
(并包含在范围链中)时,b
在其中添加了变量。
现在,当一个变量a
需要时,它首先搜索最近的变量范围,如果变量不存在,那么它会移动到变量范围链的下一个对象。在这种情况下是窗口级别。
运行代码。希望这将提供有关范围界定的想法
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);
只是为了添加其他答案,范围是所有声明的标识符(变量)的查找列表,并强制执行一组严格的规则来说明当前执行的代码如何访问这些。这种查找可能是为了分配给变量,这是一个 LHS(左侧)引用,或者它可能是为了检索它的值,这是一个 RHS(右侧)引用。这些查找是 JavaScript 引擎在编译和执行代码时在内部执行的操作。
所以从这个角度来看,我认为我在 Kyle Simpson 的 Scopes and Closures 电子书中找到的一张图片会有所帮助:
引用他的电子书:
该建筑物代表我们程序的嵌套范围规则集。大楼的一楼代表您当前执行的范围,无论您身在何处。建筑物的顶层是全局范围。您通过查看当前楼层来解析 LHS 和 RHS 引用,如果没有找到,乘电梯到下一层,查看那里,然后查看下一层,依此类推。一旦你到达顶层(全局范围),你要么找到你要找的东西,要么找不到。但无论如何你都必须停下来。
值得一提的是,“范围查找一旦找到第一个匹配项就会停止”。
这个“范围级别”的想法解释了为什么“this”可以用一个新创建的范围来改变,如果它在一个嵌套函数中被查找。这是一个包含所有这些细节的链接,你想知道的关于 javascript 范围的一切
内联处理程序
前端编码人员经常遇到的一个尚未描述的非常常见的问题是 HTML 中的内联事件处理程序可见的范围 - 例如,使用
<button onclick="foo()"></button>
on*
属性可以引用的变量范围必须是:
- 全局(工作内联处理程序几乎总是引用全局变量)
- 文档的属性(例如,
querySelector
作为独立变量将指向document.querySelector
; 罕见) - 处理程序附加到的元素的属性(如上;罕见)
否则,您将在调用处理程序时收到 ReferenceError。因此,例如,如果内联处理程序引用了定义在 window.onload
or内部的函数,$(function() {
则引用将失败,因为内联处理程序可能只引用全局范围内的变量,而该函数不是全局的:
window.addEventListener('DOMContentLoaded', () => {
function foo() {
console.log('foo running');
}
});
<button onclick="foo()">click</button>
处理程序附加到的元素的属性document
和属性也可以作为内联处理程序内的独立变量引用,因为内联处理程序是在两个块内调用的,with
一个用于document
,一个用于元素。这些处理程序内的变量范围链非常不直观,并且工作的事件处理程序可能需要一个全局函数(并且可能应该避免不必要的全局污染)。
由于内联处理程序内部的作用域链非常奇怪,而且内联处理程序需要全局污染才能工作,而且内联处理程序有时需要在传递参数时进行丑陋的字符串转义,因此避免它们可能更容易。addEventListener
相反,使用 Javascript(如 with )而不是 HTML 标记来附加事件处理程序。
function foo() {
console.log('foo running');
}
document.querySelector('.my-button').addEventListener('click', foo);
<button class="my-button">click</button>
模块 ( <script type="module">
)
<script>
另一方面,与在顶层运行的普通标签不同,ES6 模块内的代码在其自己的私有范围内运行。在普通标签顶部定义的<script>
变量是全局的,因此您可以在其他<script>
标签中引用它,如下所示:
<script>
const foo = 'foo';
</script>
<script>
console.log(foo);
</script>
但是 ES6 模块的顶层不是全局的。在 ES6 模块顶部声明的变量将仅在该模块内可见,除非该变量被显式export
编辑,或者除非它被分配给全局对象的属性。
<script type="module">
const foo = 'foo';
</script>
<script>
// Can't access foo here, because the other script is a module
console.log(typeof foo);
</script>
ES6 模块的顶层类似于普通<script>
. 模块可以引用任何全局变量,并且除非模块是为它明确设计的,否则没有任何东西可以引用模块内部的任何东西。
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 作用域而言,这几乎是您需要了解的全部内容,除了:
- try/catch 只为异常变量本身引入新作用域,其他变量没有新作用域
- with-clause 显然是另一个例外,但非常不鼓励使用 with-clause ( https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with )
所以你可以看到 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 scope
i = 2;
i = 3; // the assignment stays where it is
}
console.log(i);
这似乎违反直觉,但从命令式语言设计者的角度来看是有道理的。
现代 Js, ES6+, ' const
' 和 ' let
'
您应该对创建的每个变量使用块范围,就像大多数其他主要语言一样。var
已过时。这使您的代码更安全、更易于维护。
const
应该用于95% 的情况。它使变量引用不能改变。数组、对象和 DOM 节点属性可以更改并且应该是const
.
let
应该用于期望被重新分配的任何变量。这包括在 for 循环中。如果您在初始化之外更改值,请使用let
.
块作用域意味着该变量仅在声明它的括号内可用。这扩展到内部范围,包括在您的范围内创建的匿名函数。
试试这个奇怪的例子。在下面的示例中,如果 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());
JS 中只有函数作用域。不阻止范围!你也可以看到什么在提升。
var global_variable = "global_variable";
var hoisting_variable = "global_hoist";
// Global variables printed
console.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);
我的理解是有3个作用域:全局作用域,全局可用;本地范围,可用于整个函数,而不管块;和块作用域,仅对使用它的块、语句或表达式可用。全局和局部范围用关键字“var”表示,无论是在函数内部还是外部,块范围用关键字“let”表示。
对于那些认为只有全局作用域和局部作用域的人,请解释为什么 Mozilla 会有一个完整的页面来描述 JS 中块作用域的细微差别。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
在 JavaScript 中有两种类型的作用域:
- 本地范围
- 全球范围
下面的函数有一个局部范围变量carName
。并且这个变量不能从函数外部访问。
function myFunction() {
var carName = "Volvo";
alert(carName);
// code here can use carName
}
下面的类有一个全局范围变量carName
。这个变量可以从类中的任何地方访问。
class {
var carName = " Volvo";
// code here can use carName
function myFunction() {
alert(carName);
// code here can use carName
}
}
ES5
和更早:
Javascript 中的变量最初(pre ES6
)是词法函数范围的。词法范围一词意味着您可以通过“查看”代码来查看变量的范围。
var
用关键字声明的每个变量都作用于函数。但是,如果在该函数中声明了其他函数,则这些函数将可以访问外部函数的变量。这称为作用域链。它以下列方式工作:
- 当一个函数想要解析一个变量值时,它首先查看它自己的范围。这是函数体,即大括号 {} 之间的所有内容(此范围内的其他 函数内的变量除外)。
- 如果它在函数体内找不到变量,它将爬上链并查看函数中定义函数的变量范围。这就是词法作用域的含义,我们可以在代码中看到该函数被定义的位置,因此只需查看代码就可以确定作用域链。
例子:
// global scope
var foo = 'global';
var bar = 'global';
var foobar = 'global';
function outerFunc () {
// outerFunc scope
var foo = 'outerFunc';
var foobar = 'outerFunc';
innerFunc();
function innerFunc(){
// innerFunc scope
var foo = 'innerFunc';
console.log(foo);
console.log(bar);
console.log(foobar);
}
}
outerFunc();
当我们尝试将变量foo
、bar
和记录foobar
到控制台时会发生以下情况:
- 我们尝试将 foo 记录到控制台,可以在函数
innerFunc
本身内部找到 foo。因此, foo 的值被解析为 stringinnerFunc
。 innerFunc
我们尝试将 bar 记录到控制台,在函数本身内部找不到 bar 。因此,我们需要爬上作用域链。我们首先查看定义函数的外部函数innerFunc
。这就是功能outerFunc
。在范围内,outerFunc
我们可以找到变量 bar,其中包含字符串“outerFunc”。- 在 innerFunc 中找不到 foobar。. 因此,我们需要将作用域链爬升到 innerFunc 作用域。在这里也找不到,我们再爬一层到全局作用域(即最外层作用域)。我们在这里找到了变量 foobar,它保存了字符串 'global'。如果在爬升作用域链后它没有找到变量,JS 引擎将抛出一个referenceError。
ES6
(ES 2015)及更早版本:
词法作用域和作用域链的相同概念仍然适用于ES6
. 然而,引入了一种声明变量的新方法。有以下几种:
let
: 创建一个块范围的变量const
: 创建一个必须初始化且不能重新分配的块范围变量
var
和let
/之间的最大区别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 scoped
let letVar = 5;
// this variable declared with let is scoped to the function block, function scoped
var varVar = 10;
}
console.log(letVar);
console.log(varVar);
}
foo();
在上面的示例中,letVar 记录了全局值,因为用声明的变量let
是块范围的。它们不再存在于各自的块之外,因此无法在 if 块之外访问该变量。
我真的很喜欢接受的答案,但我想补充一下:
Scope 收集并维护所有已声明标识符(变量)的查找列表,并针对当前执行的代码如何访问这些标识符执行一组严格的规则。
范围是一组通过标识符名称查找变量的规则。
- 如果在直接作用域中找不到变量,引擎会查询下一个外部包含作用域,继续直到找到或到达最外层(也称为全局)作用域。
- 是一组规则,用于确定在何处以及如何查找变量(标识符)。此查找可能是为了分配给变量,这是一个 LHS(左侧)引用,或者它可能是为了检索它的值,这是一个 RHS(右侧)引用.
- LHS 引用来自赋值操作。与范围相关的赋值可以使用 = 运算符或通过将参数传递给(赋值给)函数参数来进行。
- JavaScript 引擎在执行之前首先编译代码,在此过程中,它会拆分语句,例如 var a = 2; 分为两个单独的步骤: 1。首先, var a 在该范围内声明它。这是在代码执行之前的开始执行的。第二。稍后,a = 2 查找变量(LHS 引用)并在找到时分配给它。
- LHS 和 RHS 引用查找都从当前执行的范围开始,如果需要(也就是说,它们在那里找不到他们要查找的内容),它们会沿着嵌套范围向上工作,一个范围(地板) 一次,寻找标识符,直到他们到达全局(顶层)并停下来,要么找到它,要么不找到它。未完成的 RHS 引用会导致 ReferenceError 被抛出。未实现的 LHS 引用会导致自动、隐式创建该名称的全局(如果不在严格模式下)或 ReferenceError(如果在严格模式下)。
- 范围由一系列“气泡”组成,每个气泡都充当容器或桶,其中声明了标识符(变量、函数)。这些气泡彼此整齐地嵌套在一起,并且这种嵌套是在作者时定义的。
(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 foo
foo = 100
console.log('2:', foo) // function foo, is not 100, why?
})()
在 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 = 10
let y = 10
const z = 10
{
x = 20
let y = 20
const z = 20
{
x = 30
// x is in the global scope because of the 'var' keyword
let y = 30
// y is in the local scope because of the 'let' keyword
const z = 30
// z is in the local scope because of the 'const' keyword
console.log(x) // 30
console.log(y) // 30
console.log(z) // 30
}
console.log(x) // 30
console.log(y) // 20
console.log(z) // 20
}
console.log(x) // 30
console.log(y) // 10
console.log(z) // 10
JavaScript 中有两种类型的作用域。
全局作用域:在全局作用域内声明的变量可以非常流畅地在程序的任何地方使用。例如:
var carName = " BMW"; // code here can use carName function myFunction() { // code here can use carName }
功能范围或本地范围:在此范围内声明的变量只能在其自己的函数中使用。例如:
// code here can not use carName function myFunction() { var carName = "BMW"; // code here can use carName }