let命令
基本用法
类似于var,但是所声明的变量,只在let命令所在的代码块中有效。
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
上面代码中,分别用let,var 声明两个变量,然后再代码块之外调用两个变量,let变量报错,var变量返回正确的值,这表明,let声明的变量只在他的代码块中有效。
for循环的计数器,很适合let命令
for(let i =0 ;i<10;i++){}
console.log(i);
//ReferenceError: i is not defined
上面代码中计数器只在循环体中有效,循环体外引用就会报错
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
上述代码中,变量i是var声明的,所以每一轮循环,新的 i 值都会被覆盖,所以最后输出的是最后一轮的 i 的值是10
如果使用let,声明的变量仅在块级作用域中有效,最后输出的是6
var a = [];
for(let i = 0 ; i < 10; i ++){
a[i] = function(){
console.log(i);
}
}
a[6](); // 6
上面代码中,变量i 是let声明的,当前的 i 只在本轮循环中有效,所以每一次循环i 其实就是新的变量,到最后输出的是6
使用let不存在变量提升
let 不会像 var 那样会发生变量提升的现象,所以,变量变量一定在声明后使用,否则就会报错。
console.log(a) // undefined
console.log(b) // 报错ReferenceError
var a = 1;
let b = 2
上面代码中 a 用var 声明会变量提升,这时候提升只是a 提升没赋值,所以是undefined ,变量 b 不存在变量提升,所以报错
暂时性死区(TDZ)
只要块级作用域中存在let命令,那么他所声明的变量就绑定在这个区域,不再受到外部影响
var test = 123;
if(true){
test = "abc" // ReferenceError
let test
}
上面代码中,存在全局变量test , 但是在块级作用域中又声明个局部变量test,导致后者绑定块级作用域,所以在 let 声明前使用会导致报错。
“暂时性死区”也意味着typeof 不再是个百分之百安全的操作了.
type x; //ReferenceError
let x
作为比较,如果一个变量根本没有被声明,使用typeof 反而不会被报错
typeof test // underfined
上面test是不存在的变量名,结果返回undefined,所以在没有let 之前 ,typeof 是完全安全的。
有些“死区”比较隐蔽,不太容易发现。
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
上面代码中,调用函数之所以报错,是因为 x 默认是另一个函数y,而此时y 还没有声明,属于“死区”,如果y 的默认值是x,就不会报错,因为此时x已经声明了。
function bar(x = 2, y = x) {
return [x, y];
}
bar(); // [2, 2]
ES6规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在ES5是很常见的,现在有了这种规定,避免此类错误就很容易了。
总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
不允许重复声明
let 不允许在相同的作用域中,重复声明同一个变量。
// 报错
function () {
let a = 10;
var a = 1;
}
// 报错
function () {
let a = 10;
let a = 1;
}
因此不能再函数内部重新声明参数。
function func(arg) {
let arg; // 报错
}
function func(arg) {
{
let arg; // 不报错
}
}
ES6规定,块级作用域中,函数声明语句行为类似于let,在块级作用域之外不可引用。
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}());
上面代码在ES5下运行,会得到“I am outside” ,因为 if 内声明的函数 f 会提升到函数头部,实际运行代码如下
// ES5版本
function f() { console.log('I am outside!'); }
(function () {
function f() { console.log('I am inside!'); }
if (false) {
}
f();
}());
但是ES6就不一样了
// ES6版本
function f() { console.log('I am outside!'); }
(function () {
f();
}());
do表达式
本质上,块级作用域是一个语句,将多个操作封装在一起,没有返回值
{
let t = f();
t = t * t + 1;
}
上面代码中,块级作用域将两个语句封装在一起。但是,在跨级作用域以外不能得到 t 的值,除非 t 是全局变量。
变成do表达式可以获得返回值
let x = do {
let t = f();
t * t + 1;
};
上述代码中,变量x 会获得整个作用域中的返回值
const命令
const声明一个只读的常量。一旦声明,常量的值就不能改变。
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
上述代码表明 常量的值不能改变,会报错
const 常量的声明,意味着,const 一旦声明必须初始化,不能留到以后赋值。
const foo;
// SyntaxError: Missing initializer in const declaration
上面代码表示,对于const来说,只声明不赋值,就会报错。
const 与 let 一样:只在所在的块级作用域中有效
if (true) {
const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not defined
const 命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
if (true) {
console.log(MAX); // ReferenceError
const MAX = 5;
}
const 与 let 声明的变量一样不可重复声明
var message = "Hello!";
let age = 25;
// 以下两行都会报错
const message = "Goodbye!";
const age = 30;
对于复合型的变量,变量名不指向数据,而是指向数据所在的地址。const 命令指示保证变量名指向的地址不变,并不能保证该地址的数据不变,所以将一个对象声明为常量必须非常小心
const foo = {};
foo.prop = 123;
foo.prop
// 123
foo = {}; // TypeError: "foo" is read-only
上面代码中,常量 foo 储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。
下面另一个例子:
const a = [];
a.push('Hello'); // 可执行
a.length = 0; // 可执行
a = ['Dave']; // 报错
顶层对象的属性
顶层对象,在浏览器环境指的是window对象,在Node指的是global对象。ES5之中,顶层对象的属性与全局变量是等价的。
window.a = 1;
a // 1
a = 2;
window.a // 2
上面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事。
顶层对象的属性与全局变量挂钩,被认为是JavaScript语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。
ES6为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从ES6开始,全局变量将逐步与顶层对象的属性脱钩。
var a = 1;
// 如果在Node的REPL环境,可以写成global.a
// 或者采用通用方法,写成this.a
window.a // 1
let b = 1;
window.b // undefined