共计 4576 个字符,预计需要花费 12 分钟才能阅读完成。
一.var 声明与变量提升机制
在 JavaScript 中使用 var 定义一个变量,无论是定义在全局作用域函数函数的局部作用域中,都会被提升到其作用域的顶部,这也是 JavaScript 定义变量的一个令人困惑的地方。由于 es5 没有像其它类 C 语言一样的块级作用域, 因此 es6 增加了 let 定义变量,用来创建块级作用域。
我们来看一个 var 定义变量的示例:
function setName(){
if(condition){
var name = ‘loho’;
console.log(name);
}else{
console.log(name);
}
}
var student = ‘eveningwater’;
setName();
以上代码可以理解成如下:
var student;
function setName(){
var name;
if(condition){
name = ‘loho’;
console.log(name);//loho
}else{
console.log(name);//undefined
}
}
student = ‘eveningwater’;
setName();
二. 块级声明
块级声明意在指定一个块级作用域,使得块级作用域中所定义的变量无法再全局被访问到,块级作用域也被称为词法作用域。
块级作用域存在于两个地方:
函数内部。
指定代码块中。(即 ”{“ 和 ”}” 之间的区域)
1.let 声明
let 声明同 var 声明用法一致,唯一的区别在于,let 声明将变量限制在一个块内,这样就形成了一个块级作用域,因此也就不会存在变量的提升了。
例如前面的示例,我们可以写成如下:
let stundent = ‘eveningwater’;
function setName(){
if(condition){
let name = ‘loho’;
console.log(name);//loho
}else{
// 如果条件为 false 执行到这里
console.log(name);// 不返回值
}
}
setName();
2. 禁止重声明
在使用 let 定义变量之前如果已经声明了相同的变量,就会报错。因此不能重复声明变量。如以下示例:
var name = ‘eveningwater’;
// 报错, 重复声明
let name = ‘loho’;
当然这两个变量必须是在同一个作用域中,如果是不同作用域中,则不会报错。但有可能会遮蔽第一次声明的变量。如以下示例:
var name = ‘eveningwater’;
if(condition){
// 不会报错
let name = ‘loho’;
}
3.const 声明
使用 const 标识符所声明的变量必须要初始化,因此这个声明的就是一个常量。如下例:
const name=’eveningwater’;// 正确
const name;// 错误, 未初始化
const 声明同 let 声明一样,也是创建了一个块级作用域,在这个块级作用域之外是无法访问到所声明的变量的。换句话说,就是 const 所声明的变量不会有变量提升机制。如下例:
if(condition){
const name = ‘eveningwater’;
console.log(name);//’eveningwater’
}
// 错误
console.log(name);
同样的 const 也不能重复声明,如下例:
var name = ‘eveningwater’;
// 错误,不能重复声明
const name = ‘loho’;
但也可以在不同作用域中重复声明,如下例:
var name = ‘eveningwater’;
if(condition){
const name = ‘loho’;
console.log(name);//loho, 屏蔽全局定义的变量
}
尽管 const 声明与 let 声明有太多相似的地方,但 const 声明也有一处与 let 声明不同,那就是 const 声明的变量不能被赋值,无论是在非严格模式下还是在严格模式下,都不能对 const 声明的变量进行赋值。如下例:
const name = ‘eveningwater’;
// 错误
name = ‘loho’;
不过,如果定义的是一个对象,可以对对象的值进行修改,如下例:
const student = {
name:’eveningwater’
}
student.name = ‘loho’;// 没问题
// 错误,相当于赋值修改对象
student = {
name:’loho’
}
4. 临时死区
前面提到 let 和 const 声明的变量都不会提升到作用域的顶部,因此在使用这两个标识符声明之前访问会报错,即使是 typeof 操作符也会触发引用错误。如下例:
console.log(typeof name);// 报错
const name = ‘eveningwater’;
由于第一行代码就报错了,因此后续的声明变量语句不会执行,此时就出现了 JavaScript 社区所谓的 ” 临时死区 ”(temporal dead zone). 虽然这里示例是 const 声明,但 let 声明也是一样的。
但如果在 const 或 let 声明的变量的作用域之外使用 typeof 操作符监测却不会报错,只不过会返回 undefined。如下例:
console.log(typeof name);//undefined
if(condition){
let name = ‘eveningwater’;
}
5. 循环中的块级作用域绑定
我们在使用 var 声明变量的时候,总会遇到这样的情况,如下:
for(var i = 0;i < 100;i++){
// 执行某些操作
}
// 这里也能访问到变量 i
console.log(i);//100
我们可以使用 let 声明将变量 i 限制在循环中,此时再在循环作用域之外访问变量 i 就会报错了,因为 let 声明已经为循环创建了一个块级作用域。如下:
for(let i = 0;i < 100;i++){
// 执行某些操作
}
// 报错
console.log(i);
6. 循环中的创建函数
在使用 var 声明变量的循环中,创建一个函数非常的困难,如下例:
var func = [];
for(var i = 0;i < 5;i++){
func.push(function(){
console.log(i);
})
}
func.forEach(function(func){
func();
});
你可能预期想的是打印从 0 到 5 之间,即 0,1,2,3,4 的数字,但实际上答案并不是如此。由于函数有自己的作用域,因此在向数组中添加函数的时候,实际上循环已经运行完成,因此每次打印变量 i 的值都相当于是在全局中访问变量 i 的值,即 i = 5 这个值,因此实际上答案最终会返回 5 次 5.
在 es5 中,我们可以使用函数表达式 (IIFE) 来解决这个问题,因为函数表达式会创建一个自己的块级作用域。如下:
var func = [];
for(var i = 0;i < 5;i++){
(function(i){
func.push(function(){
console.log(i);
})
})(i)
}
func.forEach(function(func){
func();// 这就是你想要的答案,输出 0,1,2,3,4
})
;
但事实上有了 es6 的 let 声明,我们不必如此麻烦,只需要将 var 变成 let 声明就可以了,如下:
var func = [];
for(let i = 0;i < 5;i++){
func.push(function(){
console.log(i);
})
}
func.forEach(function(func){
func();// 输出 0,1,2,3,4
})
但是这里不能使用 const 声明,因为前面提到过,const 声明并初始化了一个常量之后是不能被修改的,只能在对象中被修改值。如以下示例就会报错:
// 在执行循环 i ++ 条件的时候就会报错
for(const i = 0;i < len;i++){
console.log(i);
}
因为 i ++ 这个语句就是在尝试修改常量 i 的值,因此不能将 const 声明用在 for 循环中,但可以将 const 声明用在 for-in 或者 for-of 循环中。如下:
var func = [];
var obj = {
name:’eveningwater’,
age:22
}
for(let key in obj){
func.push(function(){
console.log(key)
})
}
func.forEach(function(func){
func();//name,age
});
// 以下也没问题
var func = [];
var obj = {
name:’eveningwater’,
age:22
}
for(const key in obj){
func.push(function(){
console.log(key)
})
}
func.forEach(function(func){
func();//name,age
});
这里并没有修改 key 的值,因此使用 const 和 let 声明都可以,同理 for-of 循环也是一样的道理。for-of 循环是 es6 的新增的循坏。。
7. 全局作用域绑定
let,const 声明与 var 声明还有一个区别就是三者在全局作用域中的行为。当使用 var 声明一个变量时,会在全局作用域(通常情况下是浏览器 window 对象)中创建一个全局属性,这也就意味着可能会覆盖 window 对象中已经存在的一个全局变量。如下例:
console.log(window.Array);// 应该返回创建数组的构造函数, 即 f Array(){}
var Array = ‘ 这是数组 ’;
console.log(window.Array);// 返回 ’ 这是数组 ’;
从上例,我们可以知道即使全局作用域中已经定义了 Array 变量或者已经存在了 Array 属性,但我们之后定义的 Array 变量则会覆盖之前已经定义好的或者已经存在的 Array 变量(属性)。
但是 es6 的 let 和 const 声明则不会出现这种情况,let 和 const 声明会创建一个新的绑定,也就是说不会成为 window 对象的属性。换句话说,就是所声明的变量不会覆盖全局变量,而只会遮蔽它。如下例:
let Array = ‘ 这是数组 ’;
console.log(Array);//’ 这是数组‘;
console.log(window.Array);// 应该返回创建数组的构造函数, 即 f Array(){}
这也就是说 window.Array !== Array 这个等式返回布尔值 true。
8. 块级绑定的最佳实践
在使用 es6 块级声明变量中,最佳实践是如果确定后续不会改变这个变量的值,用 const 声明,如果确定要改变这个变量的值,则用 let 声明。因为预料外的变量值的改变时很多 bug 出现的源头。如下示例:
function eveningWater(){};
eveningWater.prototype.name = ‘eveningwater’;
let ew = new eveningWater();
// 定义不能被修改的变量,也就是用于判断实例类型的属性
const _constructor = ew.constructor;
// 可以改变自定义的名字属性
let name = ew.name;
if(_constructor === eveningWater || _constuctor === Object){
console.log(_constructor);
}else{
name = ‘loho’;
console.log(name)
}