在写js代码时,不少人会使用var关键字来声明变量。var关键字声明的变量使得咱们不管在函数做用域仍是全局做用域中任意地方声明的一个变量都会被当成在当前做用域的顶部声明的,这就是咱们常说的变量提高(Hoisting)机制。用一个函数举例子:es6
function example(flag) {
console.log(value) //此处可访问变量value,值为undefined
if (flag) {
var value = 'hello'
console.log(value) //此处可访问变量value,值为hello
} else {
console.log(value) //此处可访问变量value,值为undefined
}
}
复制代码
不了解js的人可能认为只有当flag为true的时候才能访问到value变量,但其实var声明的value变量已经被提高到了example函数做用域的顶部了,这样在此函数的任意位置都是能够访问到value变量的,只不过只有当flag为true是value才会被初始化才有值。在预编译阶段,js引擎会将上面的example 函数解析成下面这样:浏览器
function example(flag) {
var value
console.log(value) //此处可访问变量value,值为undefined
if (flag) {
var value = 'hello'
console.log(value) //此处可访问变量value,值为hello
} else {
console.log(value) //此处可访问变量value,值为undefined
}
}
复制代码
刚接触js的开发者一般会花一下时间来习惯变量提高,有时还会由于误解致使程序中出现bug。为此es6中引入块级做用域来强化对变量生命周期的控制。安全
一般若咱们须要一个变量只能在指定块中访问时会用到块级声明。块级做用域(也称词法做用域)存在于:bash
let的声明的var的声明语法相同。经过let声明的变量只能在当前做用域只访问到,而且let声明的变量不会被提高,所以开发者一般会把let声明的变量放在做用域顶部以便整个做用域能够访问。如下是let声明的示例:函数
function example(flag) {
console.log(value) //此处不存在变量value
if (flag) {
console.log(value) //此处不存在变量value
let value = 'hello'
console.log(value) //此处可访问变量value,值为hello
} else {
console.log(value) //此处不存在变量value
}
}
复制代码
示例中let声明的变量value不会被提高到顶部,执行流离开if块,value就会马上被销毁,若是flag为false那么value变量将永远不会被声明和初始化。ui
若是let关键字声明了重复的某个标识符,那么就会抛出报错,举个例子:spa
var count = 30
//抛出语法错误
let count = 40
复制代码
在这个示例中,如上所说let不能重复声明已经存在的标识符,因此此处let声明会报错。可是若是let 声明的count变量的当前做用域被内嵌在另外一个做用域就能够被正常访问,好比:code
var count = 30
if (flag) {
//不会抛出语法错误
let count = 40
}
复制代码
此处的let 声明的count只在做用域if块中能被访问,而且在if块中let声明的count 会覆盖外面var声明的count变量对象
const是用来声明常量的关键字,其值一旦被肯定就不能被更改,因此每一个经过const声明的常量必须进行初始化。生命周期
//有效常量
const maxItems = 30
//语法错误: 常量未初始化
const name;
复制代码
const与let声明的都是块级标识符,因此const声明的常量只能在当前做用域访问到也不会被提高到顶部。一样,const也不能声明重复的标识符,不管该标识符时使用var(在全局或函数做用域中),仍是let(在块级做用域中)声明的。举例来讲:
var maxItems = 30
let name = 'coco'
//这两条语句都会抛出错误
const maxItems = 60
const name = 'koko'
复制代码
尽管const和let类似之处不少,可是他们仍是有一个最大的区别,就是const声明的常量不能够再次被赋值。es6中的常量这一点和其余语言很像,然而,与其余语言不一样的是若是js中常量是对象,则对象中的值能够修改。
const声明不容许修改绑定,但容许修改值,这就意味着用const生命的对象后,能够修改对象的属性值。举个例子:
const dog = {
name: 'koko'
}
//能够修改对象属性的值
dog.name = 'coco'
//抛出语法错误
dog = {
name: 'coco'
}
复制代码
在这段代码中修改dog的属性不会报错,由于修改的是dog包含的值,dog的绑定并无变。但若是改变的dog的绑定就会报错
let和const与var不一样,若是要在let或const声明变量以前访问这些变量,即便是相对安全的typeof操做也会触发引用错误,好比下面这段代码:
if (flag) {
console.log(typeof (value)); //引用错误
let value = 'b'
}
复制代码
由于console.log(typeof (value))抛出错误,因此value的声明和初始化不会被执行,此时的value还位于js社区所谓的“临时死区”(TDZ)中,人们经常使用他来描述let和const的不提高效果。(这里以let为例,可是const也是如此)。js引擎在扫描代码发现变量声明时,要么将他们提高到做用域顶部(遇到var声明),要么将声明放入TDZ中(遇到let和const声明)。访问TDZ中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从TDZ中移出,而后方可正常访问。
从上例可看出,即使是不容易出错的typeof也没法阻止引擎抛出错误,可是在let声明的做用域外对该变量使用typeof则不会报错,好比:
console.log(typeof (value)); //"undefined"
if (flag) {
let value = 'b'
}
复制代码
typeof是在声明变量value代码块外执行的,此时value并不在TDZ中。这也就意味着不存在value这个绑定,typeof操做最终返回‘undefined’。TDZ只是块级绑定的特点之一,而在循环中使用块级绑定也是一个特点。
开发者应该最但愿实现for循环中的块级做用域了,由于这样他们能够将随意声明的计数器变量限制在for循环中使用。像下面这种代码在js中是很常见的:
for (var i = 0; i < 10; i++) {
process(items[i]);
}
console.log(i) //在这里任然能够访问变量i
复制代码
示例代码中由于计数器变量i是由var声明的因此变量i会被提高至for循环外的做用域顶部,因此即使是在循环以外也能够访问变量i,但若是换用let声明就能够达到想要的效果:
for (let i = 0; i < 10; i++) {
process(items[i]);
}
console.log(i) //在这里不能够访问变量i,抛出错误
复制代码
在上例中,let声明的变量i只在for循环中可访问。在其它语言中(默认拥有块级做用域的)上面两个示例也能够正常运行,可是变量i始终只在for循环中可访问。
长久以来,使用var声明会让开发者在循环中建立函数变得很困难,由于每一次的循环的变量到了下一次循环任然可访问。好比:
var funArr = []
for (var i = 0; i < 10; i++) {
funArr.push(function () {
console.log(i)
})
}
funArr.forEach(function (func) {
func() //输出10次数字10
})
复制代码
上例中可能你想要的是输出0-9,但是他却一连串输出了10次10。这是由于循环里的每次迭代同时共享着变量i,循环内部建立的函数全都保留了对相同变量的引用(细细品味)。循环结束的时候i最后被赋予的值是10,因此以前循环中内部建立的函数里引用的变量i的值都是10,每次调用console.log(i)时就会输出数字10。
为此,开发者们再循环中使用当即调用函数表达式(IIFE),以强制生成计数器变量的副本,就像这样:
var funArr = []
for (var i = 0; i < 10; i++) {
funArr.push((function (value) {
return function () {
console.log(value)
}
}(i)))
}
funArr.forEach(function (func) {
func() //输出0-9
})
复制代码
上例中,IIFE表达式会为每次循环的变量i生成一个副本(参数value变量),变量value替代了每次循环中建立的函数中引用的变量i,且value的值为本次循环中i的值,每次循环生成的value都只在本次循环中可访问。所以调用函数就会生成咱们所指望的0-9。es6中let和const提供的块级绑定让咱们无需再这么折腾。
let声明和上例IIFE所作的事相似,也是每次迭代循环都会建立一个新变量,并以以前迭代中同名变量的值将其初始化。可是代码相对简洁。代码示例:
var funArr = []
for (let i = 0; i < 10; i++) {
funArr.push(function () {
console.log(i)
})
}
funArr.forEach(function (func) {
func() //输出0-9
})
复制代码
上例代码中每次循环let声明都会建立一个新变量i并初始化为i的当前值,因此每次循环建立的函数都能获得属于他们本身的i的副本。对于for-in循环和for-of循环来讲也是同样的,示例以下:
var funArr = [],
object = {
a: true,
b: true,
c: true
};
for (let key in object) {
funArr.push(function () {
console.log(key)
})
}
funArr.forEach(function (func) {
func() //输出a,b,c
})
复制代码
上例中for-in循环与上上例中for循环表现得行为一致,也是为每次循环中建立的函数赋予一个新变量(上例中为变量key)。但若是使用var声明则这些函数都会输出‘c’。
let声明在循环内部的行为是标准中专门定义的,他不必定与let的不提高特性相关,理解这一点相当重要。事实上,早期的let 实现不包含这一行为,他是后来加入的。
es6中没有明确指明循环中不容许使用const声明,在不一样类型的循环中使用const声明它会表现出不一样的行为。普通的for循环能够在初始化变量时使用const,可是一旦这个变量的值发生改变就会报错,就像这样:
var funArr = []
//完成一次迭代后抛出错误
for (const i = 0; i < 10; i++) {
funArr.push(function () {
console.log(i)
})
}
复制代码
在上例中,由const声明的常量i,在循环第一次迭代中,i=0迭代成功,而后执行i++试图改变常量i的值,因此会抛出错误。所以,const声明适用于后续循环不会修改该变量的循环中。
在for-in和for-of循环中使用const时的行为与使用let一致,下面这段代码应该不会产生错误:
var funArr = [],
object = {
a: true,
b: true,
c: true
};
//不会产生错误
for (const key in object) {
funArr.push(function () {
console.log(key)
})
}
funArr.forEach(function (func) {
func() //输出a, b, c
})
复制代码
这段代码与上上段代码几乎一致,惟一的区别是,循环内不能改变key的值。以前提到过,const声明的常量不能够修改他的绑定,可是能够修改常量的绑定的值(const声明的对象的属性)。之因此能够运用在for-in和for-of循环中,是由于每次迭代不会(像前面for循环的例子同样)修改已有绑定,而是会建立一个新的绑定。
let和const与var的另外一个区别就是他们在全局做用域中的行为。当var被用于全局做用域时,它会建立一个新的变量做为全局对象的属性(全局对象一般是浏览器环境中的window对象)。这意味着var极可能会无心中覆盖一个已经存在的全局属性,就像这样:
//在浏览器中为全局对象window建立全局属性RegExp
var RegExp = "hello";
console.log(window.RegExp); //“hello”
//全局属性RegExp被覆盖
var RegExp = "hi"
console.log(window.RegExp); //“hi”
复制代码
即使全局变量RegExp是定义在全局对象window上,也不能幸免于被var声明覆盖。示例中声明的全局变量RegExp会覆盖一个已经存在的全局属性。js过去一直都是这样。
若是在全局做用域中使用let或const声明,他们会在全局做用域下建立一个新的绑定,但该绑定不会添加为全局对象的属性。换句话说,let和const不会覆盖全局变量,只会遮盖他。示例以下:
let RegExp = "hello";
console.log(RegExp); //“hello”
console.log(window.RegExp === RegExp) //false
const ncz = "hi";
console.log(ncz); //“hi”
console.log("ncz" in window) //false
复制代码
这里let声明的RegExp建立了一个绑定并遮蔽了全局RegExp的变量。结果是window.RegExp和RegExp不相同,但不会破坏全局做用域。const也是同样。若是不想为全局对象建立属性使用let和const声明要安全不少。
若是但愿在全局对象下定义变量,任然可使用var。这种状况常见于跨frame或跨window访问代码。
默认使用const,当须要改变变量的值时使用let。由于大部分变量初始化后值不该再改变,而预料外的变量值的改变是不少bug的源头。