ES6学习笔记:块级作用域绑定
过去,JavaScript的变量声明机制不像C语言一样,在声明变量的同时也会创建变量(绑定)。在以前的JavaScript中何时创建变量要看怎么声明变量。在以前的变量作用域有全局作用域和局部作用域,但不像其他的程序语言有块作用域一说。在ES6中新引入的块级作用域绑定机制。
var声明及变量提升
在函数作用域或全局作用域中通过var声明的变量,不管是在哪声明的,都会被当成在当前作用域顶部声明的变量,这也被称之为变量提升。拿个示例来说:
function getValue (condition) {
console.log(value); // => undefined
if (condition) {
var value = 'w3cplus';
console.log(value); // => w3cplus
return value; // => 如果condition为true,返回w3cplus
} else {
console.log(value); // => undefined
return null; // => 如果condition为false,返回null
}
console.log(value); // => undefined
}
getValue(true); // => w3cplus
getValue(false); // => null
刚接触JavaScript的时候,一直以为condition为true时才会创建value变量。而事实上,不管condition不管是为true还是false都已经创建了value变量。在预编译阶段,JavaScript引擎会将上面的getValue()函数修改成:
function getValue(condition) {
var value;
console.log(value);
if (condition) {
var value = 'w3cplus';
console.log(value);
return value;
} else {
console.log(value);
return null;
}
console.log(value);
}
变量value被提升到函数顶部,而初始化操作依旧留在原处执行,这也就是说else {} 中也可以访问到value变量,而且此时的value并未初始化,所以其值为undefined。
变量提升,简单的理解,就是把变量提升至函数的最顶部地方。需要说明的是:变量提升只是提升变量的声明,并不会把赋值也提升上来,没有赋值的变量初始值是undefined。所以上面就出现了声明为undefined的var,因为赋值在后面声明提升在了前面。
还有一点需要注意的是因为JavaScript是函数级作用域,只有函数才会创建新的作用域,而不像其他语言有块级作用域,比如if语句块。就上面的示例而言,不管会不会进入if语句块,函数声明都会提升到当前作用域的顶部,得到执行。在JavaScript并不会创建一个新的作用域。
扩展阅读:
- JavaScript的变量:变量提升
- JavaScript中的作用域
- 变量提升
- 变量作用域
- JavaScript的作用域和提升机制
- 一篇文章弄懂JavaScript中作用域和上下文
- 深入理解javascript中的作用域
- 解释 JavaScript 的作用域和闭包
- 图解Javascript上下文与作用域
块级声明
把上面的示例做一下调整,如下:
console.log(value); // => ReferenceError: value is not defined
function getValue(condition) {
console.log(value); // => undefined
if (condition) {
var value = 'w3cplus';
console.log(value); // => 如查condition为true, 输出w3cplus
return value;
} else {
console.log(value); // => 如果condition为false, 输出undefined
return null;
}
console.log(value); // => undefined
}
getValue(true); // => w3cplus
getValue(false); // => null
在函数外调用value会报错ReferenceError: value is not defined错误信息。也就是说在函数体内声明的变量,在函数体外是无法调用的。这里就涉及到了全局作用域和局部作用域相关的概念。这里暂且不说。但在函数内部的我们称之为块级作用域。上面的示例也说明,块级里面声明的变量只能经块级作用域中使用,在指定块的作用域之外无法访问块级声明。简而言之,块内声明的变量,在块外无法使用。
在JavaScript中块级作用域不仅存在于函数内部,也存在于块中,比如{}(if,for这样的语块)。如果在if或for这样的语句块中,使用var声明的变量,在外部(除函数体外)是可以被访问到的,只不过有可能其值是undefined。为了让JavaScript中能像其他的程序语言一样,所以引入了块级作用域,让JavaScript变得更灵活也更普通。
let和const
在ES6中引入了let和const关键词用来声明变量。let和var类似都是用来声明变量的,不同的是,let声明的变量的作用域名限制在当前代码块中。比如文章开头的示例,把if语句块中的var替换成let,结果就又将不一样:
function getValue(condition) {
console.log(value); // => ReferenceError: value is not defined
if (condition) {
let value = 'w3cplus';
console.log(value); // => w3cplus
return value; // => w3cplus
} else {
console.log(value); // => 如果condition为false,程序执行到此报错:ReferenceError: value is not defined
return null;
}
console.log(value); // => ReferenceError: value is not defined
}
getValue(true);
由let声明的变量,不会像var声明的变量一样被提升至函数顶部。执行流离开if语句块,value会立即被销毁。如果condition的值为false,就永远不会声明并初始化value。
let声明的变量没有变量提升。
ES6中还提供了const关键词来声明变量,但这个变量的值是不变的,也被称之为常量。其值一旦被设定后不可更改。因此,每个通过const声明的常量必须进行初始化。
// 有效的常量
const MAX_ITEMS = 30;
// 语法错误:常量未初始化
const MAX_ITEMS;
const和let类似,声明的变量都只能在块作用域下有效,所以常量也只在当前代码块内有效,一旦执行到块外会立即被销毁。也就是说,每个通过const声明的常量也不会有变量提升。
与var不同,let和const声明的变量不会被提升到作用域顶部,如果在声明之前访问这些变量,即使是相对安全的typeof操作符也会触发引用错误。
在JavaScript中,使用let和const声明变量有一个重要的特征,大家常称之为临时死区(TDZ),也常用TDZ来描述let和const的不提升效果。
JavaScript引擎在扫描代码发现变量时,要么将它们提升至作用域顶部(遇到var声明的变量),要么将声明放到TDZ中(遇到let和const声明的变量)。访问TDZ中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从TDZ中移出,然后方可正常访问。
有关于这方面的更多信息,可以阅读下面这些文章:
- 深入浅出ES6:
let和const - 变量声明
- ES6中的变量和作用域
- ES6中的变量和作用域
- Let’s use const! Here’s why.
- How
letandconstare scoped in JavaScript - ES6
letVSconstvariables - JavaScript的词法作用域
循环中的块作用域绑定
大家在ES6之前写for循环应该有碰到下面这样的场景:
// 场景一
var arr = [];
for (var i = 0; i < 3; i++) {
arr.push(function(){
return i;
})
}
console.log(arr.map(function(x){return x();})); // => [3, 3, 3]
// 场景二
for (var i = 0; i < 5; ++i) {
setTimeout(function (){
console.log(i); // => 输出'5' 五次
}, 100)
}
这不是我们想要的结果。长久以来,var声明让开发者在循环中创建函数变得异常困难,因为变量到了循环体外还是能被访问。正如上面的代码所示,场景一得到的是[3,3,3],场景二得到连续输出五次的5。这一切都是因为循环里的每次迭代同时共享着变量i,循环内部创建的函数全都保留了对相同变量的引用。循环结束时变量i的值为5,所以每次调用console.log(i)时就会输出数字5(上例中的场景二)。
为了解决这个问题,开发者们在循环中使用立即调用函数表达式( IIFE),以强制生成计数器变量的副本,如下所示:
var arr = [];
for (var i = 0; i < 3; i++) {
arr.push(function(value){
return function() {
console.log(value);
}
}(i));
}
arr.map(function(x){
return x(); // => [0, 1, 2]
})
在循环内部,IIFE表达式为接受的每一个变量i都创建了一个副本并存储为变量value。这个变量的值就是相应迭代创建的函数所使用的值,因此调用每个函数都会从0到2循环一样得到期望的值。
有关于JavaScript中IIFE相关资料:
- JavaScript中的立即执行函数
- 立即调用的函数表达式
- JavaScript中的立即执行函数表达式
- 立即执行函数表达式(IIFE)
- Understanding Immediately-Invoked Function Expressions 1
- Understanding Immediately-Invoked Function Expressions 2
- Immediately-Invoked Function Expression (IIFE)
- IMMEDIATELY INVOKED FUNCTION EXPRESSION
- What (function (window, document, undefined) {})(window, document); really means
在ES6中就不要这么蛋疼了,使用ES6中的let和const提供的块级绑定会让事情简单的多。
循环中的let
let声明模仿了上面所描述的IIFE所做的一切来简化循环过程,每次迭代循环都会创建一个新变量,并以之前迭代中同名变量的值将其初始化。也就是说,上面的代码换成这样即可得到我们想要的值:
var arr = [];
for (let i = 0; i < 3; i++) {
arr.push(function(){
return i;
})
}
console.log(arr.map(function(x){return x();})); // => [0, 1, 2]
循环中运到let声明的变量都会创建一个新的变量i,并将其初始化为i的当前值,所以循环内部创建的每个函数都能得到属于它们自己的i的副本。
除了在for中之外,在for-in和for-of循环中作用是类似的。
循环中的const
循环中的let声明的变量能得到IIFE的功效,那么是不是说const在for这样的循环体头部也能达到类似IIFE的功效呢?因为前面也说过,const有点类似于let。至于是不是如此,先把上同的示例换成下面的代码:
var arr = [];
for (const i = 0; i < 3; i++) {
arr.push(function(){
return i;
})
}
console.log(arr.map(function(x){return x();}));
并不如我们所想,如果把for循环中的let直接换成const之后,执行上面的代码会报错:
// => TypeError: Assignment to constant variable.
为什么会如此呢?仔细回忆一下。前面提到过,使用const声明的变量是一个常量,那么在上面的示例中,变量i就声明为常量。在循环的第一次迭代中,i是0,迭代执行成功。然后执行i++,代码试图修改常量。如此一来就违背了const的原则。使用const声明的常量,是不能修改的。这样一来就报TypeError错误。所以说,如果后续循环体内不会修改该变量,那么就可以使用const来声明,否则不能使用const声明变量。
在for-in或for-of循环中使用const时的行为与使用let一致。比如下面的代码就不会报错:
var funcs = [];
var obj = {
name: 'w3cplus'.
age: 7,
job: 'FE'
};
for (const key in obj) {
funcs.push(function (){
console.log(key);
})
}
funcs.map(function(x){
return x(); // => name, age, job
})
const在for-in和for-of循环中能正常运行,那是因为每次迭代不会像for循环一样修改已有绑定,而是会创建一个新绑定。
全局块作用域绑定
let和const与var的另一个区别是它们在全局作用域中的行为。当var被用于全局作用域时,它会创建一个新的全局变量作为全局对象的属性。这意味着用var很可能会无意中覆盖一个已经存在的全局变量,如下:
// 在浏览器中
var RegExp = 'w3cplus';
console.log(window.RegExp); // => w3cplus
var name = 'damo';
console.log(window.name); // => damo
如果在全局作用域中使用let或const,会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性。换句话说, 用let或const不能覆盖全局变量,而只能遮蔽它。如此一来,如果不想为全局对象创建属性,则使用let和const要安全得多。
var 和 let
简单的概括一下:
- 通过
var声明的变量,它的作用域是在function或任何外部已经被声明的function,是全域的 - 通过
let声明的变量,它的作用域是在一个块
比如:
function varvslet() {
console.log(i); // i 是 undefined 的,因为变量提升
// console.log(j); // ReferenceError: j 没有被定义
for( var i = 0; i < 3; i++ ) {
console.log(i); // 0, 1, 2
};
console.log(i); // 3
// console.log(j); // ReferenceError: j 没有被定义
for( let j = 0; j < 3; j++ ) {
console.log(j);
};
console.log(i); // 3
// console.log(j); // ReferenceError: j 没有被定义
}
两者区别:
- 变量提升:
let不会被提升到整个块的作用域。相比之下,var可以被提升 - 循环中的闭包:
let在每次循环可以重新被绑定,确保在它之前结束的循环被重新赋值,所以在闭名中它被用来避免一些问题
那我们应该用let替代var吗?
不是的,
let是新的块作用域。语法强调在var已经是区块作用域时,let应该替换var,否则请不要替换var。let改善了在 JavaScript 作用域的选项,而不是取代。var对于变量依旧是有用的,可被用在整个function之中。
块级绑定最佳实践
很多人都认为,在ES6中应该默认使用let而不是var。对于很多JavaScript开发者而言,let实际上与他们想要的var一样,直接替换也符合逻辑。这种情况下,对于需要写保护的变量则要使用const。
如果你开始使用ES6的话,默认使用const,只有确实需要改变变量的值时使用let。因为大部分变量的值在初始化之后不应该再改变,而预料外的变量值的改变是很多Bug的源头。
总结
块级作用域绑定的let和const为JavaScript引入词法作用域,它们声明的变量不会提升,而且只可以在声明这些变量的代码块中使用。虽然这个功能给我们带来很多方便之处,但也存在一个副作用:不能在声明变量前访问它们,就算是typeof这样安全的操作符也不行。在声明前访问块绑定会导致错误,因为绑定还在临时死区(TDZ)中。
let和const的行为很多时候和var一致。然而,它们在循环中的行为运不一样。在for-in和for-of循环中,let和const都会每次迭代时创建新绑定,从而使循环体内创建的函数可以访问到相应的迭代值,而非最后一次迭代后的值(像使用var一样)。let在for循环中同样如此,但在for循环中使用const声明则有可能会引发错误。
综合所述,在ES6中声明变量时,默认使用const,只在确实需要改变变量的值时使用let。这样就可以在某种程度中实现代码的不可变,从而防止某些错误的产生。
如需转载,烦请注明出处:https://www.w3cplus.com/javascript/es6-block-scoping.html



