var声明与变量提升
变量提升(hoisting):使用var关键字声明的变量,无论声明位置在何处,都会被视为声明于所在函数的顶部(如果声明不在任意函数内,则视为在全局作用域的顶部)。
function getValue(condition) {
if (condition) {
var value = "blue";
// 其他代码
return value;
} else {
// value 在此处可访问,值为 undefined
return null;
}
// value 在此处可访问,值为 undefined
}
刚开始,你可能会认为仅当condition的值为true时,变量value才会被创建。但实际上,JS引擎在后台对getValue进行了调整,像这样:
function getValue(condition) {
var value;
if (condition) {
value = "blue";
// 其他代码
return value;
} else {
return null;
}
}
如上代码,value变量的声明被提升到了顶部,而初始化工作则保留在原处。
为了解决这个问题,ES6引入了块级作用域,让变量的生命周期更加可控。
块级声明
块级声明 :让所声明的变量在指定块的作用域外无法被访问。块级作用域(词法作用域)在如下情况被创建:
- 在一个函数内部
- 在一个代码块({})内部
let声明
同样是上面的代码范例:
function getValue(condition) {
if (condition) {
let value = "blue";
// 其他代码
return value;
} else {
// value 在此处不可用
return null;
}
// value 在此处不可用
}
let声明不会将变量提升到函数顶部。
禁止重复声明
如果一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符进行let声明会抛出错误。eg:
var a=20
// 语法错误
let a=200
然而在嵌套的作用域内使用let声明一个同名变量是正常的:
var a=10
// 不会抛出错误
if (condition) {
let a=100
// 其他代码
}
未报错的原因是,在if代码块内部,这个新变量会屏蔽全局的a变量,从而在局部阻止对于后者的访问。
常量声明
使用 const 声明的变量会被认为是常量(constant),所有的 const 变量都需要在声明时进行初始化,eg:
// 有效的常量
const hi=30
// 语法错误:未进行初始化
const hi
对比常量声明与 let 声明
1.常量声明与 let 声明一样,都是块级声明。即在语法块外无法访问,声明也不会被提升;
if (condition) {
const maxItems = 5;
// 其他代码
}
// maxItems 在此处无法访问
2.const 声明会在同一个作用域(全局或是函数作用域)内定义一个已有变量时抛出错误;
var message = "Hello!";
let age = 25;
// 二者均会抛出错误
const message = "Goodbye!";
const age = 30;
3.对之前用 const 声明的常量进行赋值会抛出错误,无论严格模式还是非严格模式;
const maxItems = 5;
maxItems = 6; // 抛出错误
与其他语言的常量类似, maxItems 变量不能被再次赋值。然而与其他语言不同, JS 的常量
如果是一个对象,它所包含的值是可以被修改的。
使用 const 声明对象
const person = {
name: "Nicholas"
};
// 工作正常
person.name = "Greg";
// 抛出错误
person = {
name: "Greg"
};
const
阻止的是对于变量绑定的修改,而不阻止对成员值的修改。
暂时性死区
if(condition){
console.log(typeof value); // 引用错误
let value = 'blue';
}
value
位于被JS社区称为暂时性死区(temporal dead zone,TDZ)的区域内,替换为 const
会有相同情况。
当JS引擎检视接下来的代码块并发现变量声明时,它会在面对var
的情况下将声明提升到函数或全局作用域的顶部,而面对let
或const
时会将声明放在暂时性死区内。只有执行到变量声明语句时,变量才可安全使用,否则报runtime error
。
对比以下代码:
console.log(typeof value); // undefined
if(condition){
left value = 'blue';
}
为什么和上面的结果不一样呢?
因为当typeof
运算符被使用时,value
并没有在暂时性死区内。
循环中的块级绑定
请对比以下代码
for (var i = 0; i < 10; i++) {
process(items[i]);
}
// i 在此处仍然可被访问
console.log(i); // 10
for (let i = 0; i < 10; i++) {
process(items[i]);
}
// i 在此处不可访问,抛出错误
console.log(i);
这一次欣赏到了变量提升的魔性吗!
循环内的函数
var funcs = [];
for (var i = 0; i < 10; i++) {
funcs.push(function() { console.log(i); });
}
funcs.forEach(function(func) {
func(); // 输出数值‘10’十次
});
你原本可能预期这段代码会输出 0
到 9
的数值,但它却在同一行将数值 10
输出了十次。这是因为变量 i
在循环的每次迭代中都被共享了,意味着循环内创建的那些函数都拥有对于同一变量的引用。在循环结束后,变量 i
的值会是 10
,因此当console.log(i)
被调用时,每次都打印出 10
。循环内使用立即调用函数表达式(IIFEs)解决这个问题。
var funcs = [];
for (var i = 0; i < 10; i++) {
funcs.push((function(value) {
return function() {
console.log(value);
}
}(i)));
}
funcs.forEach(function(func) {
func(); // 从 0 到 9 依次输出
});
这种写法在循环内使用了 IIFE 。变量i
被传递给 IIFE ,从而创建了value
变量作为自身副本并将值存储于其中。 value
变量的值被迭代中的函数所使用,因此在循环从 0
到 9
的过程中调用每个函数都返回了预期的值。更简单的方法请往下看:
循环内的let声明
var funcs = [];
for (let i = 0; i < 10; i++) {
funcs.push(function() {
console.log(i);
});
}
funcs.forEach(function(func) {
func(); // 从 0 到 9 依次输出
})
在循环中,let
声明每次都创建了一个新的 i
变量,因此在循环内部创建的函数获得了各自的 i
副本,而每个 i
副本的值都在每次循环迭代声明变量的时候被确定了。这种方式在 for-in
和 for-of
循环中同样适用,如下所示:
var funcs = [],
object = {
a: true,
b: true,
c: true
};
for (let key in object) {
funcs.push(function() {
console.log(key);
});
}
funcs.forEach(function(func) {
func(); // 依次输出 "a"、 "b"、 "c"
});
如果使用var
来声明 key
,则所有函数都只会输出 "c"
。
需要重点了解的是:
let
声明在循环内部的行为是在规范中特别定义的,而与不提升变量声明的特征没有必然联系。事实上,在早期let
的实现中并没有这种行为,它是后来才添加的。
循环内的常量声明
var funcs = [];
// 在一次迭代后抛出错误
for (const i = 0; i < 10; i++) {
funcs.push(function() {
console.log(i);
});
}
在此代码中,i
被声明为一个常量。循环的第一次迭代成功执行,此时 i
的值为 0
。在i++
执行时,一个错误会被抛出,因为该语句试图更改常量的值。
var funcs = [],
object = {
a: true,
b: true,
c: true
};
// 不会导致错误
for (const key in object) {
funcs.push(function() {
console.log(key);
});
}
funcs.forEach(function(func) {
func(); // 依次输出 "a"、 "b"、 "c"
});
const
能够在 for-in
与 for-of
循环内工作,是因为循环为每次迭代创建了一个新的变量绑定,而不是试图去修改已绑定的变量的值。
全局块级绑定
// 在浏览器中
var RegExp = "Hello!";
console.log(window.RegExp); // "Hello!"
var ncz = "Hi!";
console.log(window.ncz); // "Hi!"
尽管全局的 RegExp
是定义在 window
上的,它仍然不能防止被 var
重写。这个例子声明了一个新的全局变量 RegExp
而覆盖了原有对象。类似的, ncz
定义为全局变量后就立即成为了 window
的一个属性。这就是 JS
通常的工作方式。
// 在浏览器中
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
若想让代码能从全局对象中被访问,你仍然需要使用
var
。在浏览器中跨越帧或窗口去访问代码时,这种做法非常普遍。
块级绑定新的最佳实践&&总结
块级绑定当前的最佳实践就是:在默认情况下使用 const
,而只在你知道变量值需要被更改的情况下才使用let
。这在代码中能确保基本层次的不可变性,有助于防止某些类型的错误。
你的赞是我前进的动力
求赞,求评论,求转发...