原文连接:Variable Scope in Modern JavaScriptjavascript
译者:OFEDhtml
当与其余 JavaScript 开发人员交谈时,令我常常感到惊讶的是,有不少人不知道变量做用域是如何在 JavaScript 里起做用的。这里咱们说的做用域指的是代码里变量的可见性;或者换句话说,哪部分代码能够访问和修改变量。我发现你们在代码中常常用 var
声明变量,而并不知道 JavaScript 将如何处理这些变量。java
过去几年中,JavaScript 经历了一些巨大的变化;这些变化包括新的变量声明关键字以及新的做用域处理方式。ES6(ES2015) 新增了 let
和 const
命令,至今已经有三年时间,浏览器支持很好,对于其余新增特性,可使用 Babel 将 ES6 转换成普遍支持的 JavaScript。如今是时候回顾下如何声明变量,以及增长对做用域的了解了。node
在这篇博文中,我将经过大量 JavaScript 示例来展现全局、局部和块级做用域是如何工做的。咱们还将为那些仍不熟悉这部份内容的人演示如何使用 let
和 const
来声明变量。git
让咱们从全局做用域提及。全局定义的变量在代码中任何地方均可以访问和修改(几乎均可以,可是咱们稍后会提到例外的状况)。github
声明在任何函数以外的顶层做用域的变量就是全局变量。web
var a = 'Fred Flinstone'; // 全局变量
function alpha() {
console.log(a);
}
alpha(); // 输出 'Fred Flinstone'
复制代码
在这个例子中,a
是一个全局变量;所以,在任何函数中都能被轻松获取。所以,咱们能够从方法 alpha
输出 a
的值。当咱们调用 alpha
方法时,控制台输出 Fred Flinstone
。面试
在 web 浏览器声明全局变量时,它会做为全局 window
对象的属性。看看这个例子:api
var b = 'Wilma Flintstone';
window.b = 'Betty Rubble';
console.log(b); // 输出 'Betty Rubble'
复制代码
b
能够做为 window
对象的属性(window.b
)被访问/修改。固然,没有必要经过 window
对象修改 b
的值,这只是为了证实这一点。咱们更有可能将上述状况写成下面的形式:数组
var b = 'Wilma Flintstone';
b = 'Betty Rubble';
console.log(b); // 输出 'Betty Rubble'
复制代码
使用全局变量要当心。它们会致使代码的可读性变差,同时变得很难测试。我已经看到许多开发人员在查找变量值什么时候被重置时遇到了意想不到的问题。将变量做为参数传递给函数要比依赖全局变量好得多。全局变量应该尽可能少用。
若是你确实须要使用全局变量,最好定义命名空间,使它们成为全局对象的属性。例如,建立一个名为 globals
或 app
的全局对象。
var app = {}; // 全局对象
app.foo = 'Homer';
app.bar = 'Marge';
function beta() {
console.log(app.bar);
}
beta(); // 输出 'Marge'
复制代码
若是你正在使用 NodeJS,则顶层做用域与全局做用域不一样。若是在 NodeJS 模块中使用 var foobar
,则它是该模块的局部变量。要在 NodeJS 中定义全局变量,咱们须要使用全局命名空间对象,global
。
global.foobar = 'Hello World!'; // 在 NodeJS 里是一个全局变量
复制代码
须要注意的是,若是没有使用关键字 var
、let
或 const
之一来声明变量,那么变量属于全局做用域。
function gamma() {
c = 'Top Cat';
}
gamma();
console.log(c); // 输出 'Top Cat'
console.log(window.c); // 输出 'Top Cat'
复制代码
咱们推荐始终使用一种变量关键字定义变量。这样,代码中的每个变量做用域是可控的。正如以上例子,但愿你能意识到不用关键字的潜在危险。
如今咱们回到局部做用域
var a = 'Daffy Duck'; // a 是全局变量
function delta(b) {
// b 是传入 delta 的局部变量
console.log(b);
}
function epsilon() {
// c 被定义成局部做用域变量
var c = 'Bugs Bunny';
console.log(c);
}
delta(a); // 输出 'Daffy Duck'
epsilon(); // 输出 'Bugs Bunny'
console.log(b); // 抛出错误:b 在全局做用域未定义
复制代码
在函数内部定义的变量,做用域限制在函数内。以上例子中,b 和 c 对于各自的函数而言是局部的。但是出现如下的写法,输出结果会是什么呢?
var d = 'Tom';
function zeta() {
if (d === undefined) {
var d = 'Jerry';
}
console.log(d);
}
zeta();
复制代码
答案是 'Jerry',这多是常考的面试题之一。zeta 函数内部定义了一个新的局部变量 d,当用 var
定义变量的时候,JavaScript 会在当前做用域的顶部初始化它,无论它在代码的哪一部分。
var d = 'Tom';
function zeta() {
var d;
if (d === undefined) {
d = 'Jerry';
}
console.log(d);
}
zeta();
复制代码
这被称之为提高,它是 JavaScript 的特性之一,并且须要注意的是,没在做用域的顶部初始化变量,容易引发一些 bug。还好 let
和 const
的出现解救了咱们。那么让咱们看看如何使用 let
建立块级做用域。
几年前随着 ES6 的到来,出现了两个用于声明变量的新关键词: let
和 const
。这两个关键字都容许咱们将做用域扩大到代码块,即介于两个大括号{ }之间的内容。
许多人认为 let
是对现有 var
的替代。然而,这并不彻底正确,由于它们声明变量的做用域不一样。let
声明的是块级做用域的变量,然而var
语句容许咱们建立局部做用域的变量。固然,函数内咱们可使用 let
声明块级做用域,就像咱们之前使用 var
同样。
function eta() {
let a = 'Scooby Doo';
}
eta();
复制代码
这里 a
的做用域为函数 eta
内。咱们还能够扩展到条件块和循环。块级做用域包括变量定义的顶层块中包含的任何子块。
for (let b = 0; b < 5; b++) {
if (b % 2) {
console.log(b);
}
}
console.log(b); // 'ReferenceError: b is not defined'
复制代码
在本例中,b
在 for
循环范围内的块级做用域(其中包括条件块)内起做用。所以,它将输出奇数 1 和 3 ,而后抛出一个错误,由于咱们不能在它的做用域以外访问 b
。
咱们以前看到 JavaScript 奇怪的变量提高而影响到函数 zeta
的结果,若是咱们重写函数使用let会发生什么呢?
var d = 'Tom';
function zeta() {
if (d === undefined) {
let d = 'Jerry';
}
console.log(d);
}
zeta();
复制代码
这一次 zeta
输出 “Tom” ,由于 d 被限定为做用在条件块内,可是这是否意味着这里没有提高?不,当咱们使用 let
或 const
时, JavaScript 仍然会将变量提高到做用域的顶部,可是和 var
不一样的是,var
声明的变量提高后初始值为 undefined
,let
和 const
声明的变量提高后没有初始化,它们存在于暂时性死区中。
让咱们看一下在初始化声明以前使用一个块级做用域的变量会发生什么。
function theta() {
console.log(e); // 输出 'undefined'
console.log(f); // 'ReferenceError: d is not defined'
var e = 'Wile E. Coyote';
let f = 'Road Runner';
}
theta();
复制代码
所以,调用 theta
将为局部做用域的变量 e
输出 undefined
,并为块级做用域的变量 f
抛出一个错误。在启动 f
以前,咱们不能使用它,在这种状况下,咱们将其值设置为 “Road Runner”。
在继续以前咱们须要说明一下,在let和var之间还有一个重要的区别。当咱们在代码的最顶层使用var时,它会变成一个全局变量,并在浏览器中添加到window对象中。使用let,虽然变量将变为全局变量,由于它的做用域是整个代码库的块,但它不会成为window对象的属性。
var g = 'Pinky';
let h = 'The Brain';
console.log(window.g); // 输出 'Pinky'
console.log(window.h); // 输出 undefined
复制代码
我以前顺便提到过 const
。这个关键字与 let
一块儿做为 ES6 的一部分引入。就做用域而言,它与 let
的工做原理相同。
if (true) {
const a = 'Count Duckula';
console.log(a); // 输出 'Count Duckula'
}
console.log(a); // 输出 'ReferenceError: a is not defined'
复制代码
在本例中,a
的做用域是 if
语句,所以能够在条件语句内部访问,但在条件语句外部是 undefined
。
与 let
不一样,const
定义的变量不能经过从新赋值来改变。
const b = 'Danger Mouse';
b = 'Greenback'; // 抛出 'TypeError: Assignment to constant variable'
复制代码
然而,当使用数组或对象时,状况有点不一样。咱们仍然没法从新赋值,所以如下操做将失败
const c = ['Sylvester', 'Tweety'];
c = ['Tom', 'Jerry']; // 抛出 'TypeError: Assignment to constant variable'
复制代码
可是,咱们能够修改常量数组或对象,除非咱们在变量上使用 Object.freeze()
使其不可变。
const d = ['Dick Dastardly', 'Muttley'];
d.pop();
d.push('Penelope Pitstop');
Object.freeze(d);
console.log(d); // 输出 ["Dick Dastardly", "Penelope Pitstop"]
d.push('Professor Pat Pending'); // 抛出错误
复制代码
当咱们在局部做用域从新定义已经存在的全局变量时会发生什么呢。
var a = 'Johnny Bravo'; // 全局做用域
function iota() {
var a = 'Momma'; // 局部做用域
console.log(a); // 输出 'Momma'
console.log(window.a); // 输出 'Johnny Bravo'
}
iota();
console.log(a); // 输出 'Johnny Bravo'
复制代码
当咱们在局部做用域重定义全局变量的时候,JavaScript 初始化了一个新的局部变量。例子中,已有一个全局变量 a,函数 iota 内部又建立了一个新的局部变量 a。新的局部变量并无修改全局变量,若是咱们想在函数内部访问全局变量的值,须要使用全局的 window
对象。
对我而言,如下代码更易读,使用全局命名空间代替了全局变量,使用块级做用域重写了咱们的函数:-
var globals = {};
globals.a = 'Johnny Bravo'; // 全局做用域
function iota() {
let a = 'Momma'; // 局部做用域
console.log(a); // 输出 'Momma'
console.log(globals.a); // 输出 'Johnny Bravo'
}
iota();
console.log(globals.a); // 输出 'Johnny Bravo'
复制代码
但愿如下的代码如你所愿。
function kappa() {
var a = 'Him'; // 局部做用域
if (true) {
let a = 'Mojo Jojo'; // 块级做用域
console.log(a); // 输出 'Mojo Jojo'
}
console.log(a); // 输出 'Him'
}
kappa();
复制代码
以上代码并非特别易读,可是块级做用域变量只能在定义的块级内访问。在块级做用域外面修改块级变量毫无效果,用 let
重定义变量 a,一样没效果,以下例:
function kappa() {
let a = 'Him';
if (true) {
let a = 'Mojo Jojo';
console.log(a); // 输出 'Mojo Jojo'
}
console.log(a); // 输出 'Him'
}
kappa();
复制代码
我但愿此篇做用域的总结能让你们更好的理解 JavaScript 如何处理变量。贯穿全文的示例中我使用 var,let 和 const 定义变量。伴随着 ES6 的降临,咱们大可使用 let 和 const 取代 var。
那么 var 多余了吗?没有真正对错的答案,可是我的而言,我依旧习惯用 var 定义顶层的全局变量。但是,我会保守地使用全局变量,而是用全局命名空间代替。此外,再也不改变的变量我用 const,剩余的其余状况我用 let。
最终你会如何定义变量呢,仍是但愿你能更好的理解代码中的做用域范围。