作为一个前端开发者,ES6则是必备技能了,针对自己的学习,总结一些笔记,供自己以后回顾....
今天学习的是let和const声明,参考阮一峰的《ECMAScript 6 入门》http://es6.ruanyifeng.com/
let和const声明
let 声明,我把它称为块级声明,因为它的作用域只在它所在的块级作用域里有效,所以它和var是不同的,var的作用域是全局作用域.如下代码:
{
let a = 10;
var b = 1;
}
console.log(a);//ReferenceError: a is not defined
console.log(b);1
在开发过程中,强烈建议使用let替换var声明变量,看如下代码:
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
上面代码中,因为i是var声明出来的,属于全局变量,每一次循环变量i都会发生改变,所以最后调用数组a索引为6的函数时,里面输出的i的值就是循环最后一次得到的i的值,所以结果为10.
这里一开始我是有点不清楚的,所以把执行步骤过一遍:
步骤1:var a = [],
步骤2:执行for循环,此例为10次,每次执行a[i] = function(){console.log(i)}
这时候,数组a里面的每个索引里的值都分别为function(){console.log(i)},如:
a[1] = function(){console.log(i)}
a[2] = function(){console.log(i)}
a[3] = function(){console.log(i)}
a[4] = function(){console.log(i)}
a[5] = function(){console.log(i)}
a[6] = function(){console.log(i)}
以此类推...
步骤3:调用a数组索引为6的方法
a[6]() //10
错误思维:认为console.log(i)中的i为独立的,所以当调用函数时,索引为6的i值也为6.这个思维时错误的,因为i是全局变量,当调用函数时,i值已经改变为最后一次循环的i值,则为10
换为let声明
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
此时变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
for循环的特别之处:
循环语句部分是一个父作用域,而循环体内部是一个单独的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。
let声明的特性
不存在变量提升
var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。
为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
暂时性死区
由于let声明不存在变量提升,因此使用let声明变量之前先使用了该变量,则会报错.这种情况则称为暂时性死区.
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
上面代码中,在let命令声明变量tmp之前,都属于变量tmp的“死区”。
还有一些比较隐蔽的死区,详情请移步阮一峰的ES6,本文也是参考这本书来总结笔记的.上面已经讲的非常详细了.总之造成暂时性死区的原因是因为变量未声明就使用.
不允许重复声明
使用let声明是不允许在相同作用域内被重复声明的,会报错.
先看看var重复声明代码:
function x() {
var x = 2;
var x = 3;
console.log(x);
}
x();//3
可见,var允许被重复声明,并且会覆盖前面的声明.
function x() {
let x = 2;
let x = 3;
console.log(x);
}
x();//aught SyntaxError: Identifier 'x' has already been declared
注意:函数的参数也是一种隐式的let声明,因此不能在函数内部声明参数,也会报错.
function func(arg) {
let arg;
}
func() // 报错
function func(arg) {
{
let arg;
}
}
func() // 不报错
块级作用域
ES5的时候,只有全局作用域,和函数作用域,没有块级作用域的概念,
ES6引入了块级作用域.学习了let之后应该也能明显的感受到块级作用域的存在了吧.因为let声明只作用在当前块级作用域,
function f1() {
let n = 5;
if (true) {
let n = 10;//变量n只在当前块有效
}
console.log(n); // 5
ES6允许块级作用域任意嵌套,每一层都是单独的作用,并且内层作用域可以与外层作用域有同名的变量.
{
let insane = 'Hello World';
{
{
{
{
let insane = 'Hello World'
let x = 'default'
}
console.log(x) //报错,因为当前块作用域未声明x,
}
}
}
};
块级作用域的出现,几乎可以替换掉广泛应用的匿名立即执行函数表达式(匿名 IIFE)
// IIFE 写法
(function () {
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
块级作用域与函数声明
ES5规定,函数只能在顶层作用域和函数作用域中声明,不能在块级作用域声明.
ES6允许函数在块级作用域中声明,并且仅作用于当前块,类似于let声明.
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}());
上例代码在ES5环境中运行,得到的结果为:"I am inside",因为在if内声明的函数f会被提升到函数头部,实际运行代码如下:
// ES5 环境
function f() { console.log('I am outside!'); }
(function () {
function f() { console.log('I am inside!'); }
if (false) {
}
f();
}());
ES6允许在块级作用域中声明函数,相当于使用let声明,因此,当前代码在ES6环境中,在if内声明的函数f不会被提升到函数头部,而且作用域仅在if语句的块中有效.
运行结果:
我的想法是会报错,报f is not defined
但实际上并不是报这个错,而是报了f is not a function,
原因是在ES6中声明函数有三条特殊的规定:
- 允许在块级作用域内声明函数。
- 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
- 同时,函数声明还会提升到所在的块级作用域的头部。
根据这三条规则,上面的代码实际运行代码如下:
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
var f = undefined;
if (false) {
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
强烈建议:避免在块级作用域中声明函数,如果实在需要,可以写成函数表达式,而不是函数声明语句
// 块级作用域内部的函数声明语句,建议不要使用
{
let a = 'secret';
function f() {
return a;
}
}
// 块级作用域内部,优先使用函数表达式
{
let a = 'secret';
let f = function () {
return a;
};
}
const
const声明一个只读的常量.顾名思义,常量就是一旦声明就不能改变的,改变常量的值就会报错.
const PI = 3.1415;
console.log(PI)//3.1415
PI = 3; // TypeError: Assignment to constant variable.
const声明之后一定要赋值,否则会报错
const foo;
// SyntaxError: Missing initializer in const declaration
const的特性和let完全一样,只在声明所在的块级作用域内有效,并且不存在变量提升,因此也会存在暂时性死区,同时也跟let一样不能重复被声明.详情可以查看阮一峰的ES6
重点注意
对于简单类型的数据(数据,字符串,布尔值),const声明的值是不可变的,
对于复杂类型的数据(数组,对象),const声明的值不能保证不可变,
原因是简单类型的数据保存在栈中,而复杂类型的数据保存在堆中,保存在栈中的只是一个指向实际数据指针,const声明只能保证保存在栈中的数据不可变,不能保证保存在堆中的数据不可变.
因此,如果需要对复杂类型进行冻结,不让它改变,可以使用Object.freeze方法。
来自MDN的解释:
Object.freeze() 方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。
这里不明白阮一峰写的这段话,希望有知道的同学能帮忙解释一下:
除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
这个Object.freeze()不是已经冻结了对象,然后对象的属性和值都不能被修改,不就代表属性也被冻结了吗.为什么还需要做递归冻结?
最后补充:
在let和const之间,建议优先使用const,尤其是在全局环境,不应该设置变量,只应设置常量。
const声明常量有两个好处,一是阅读代码的人立刻会意识到不应该修改这个值,二是防止了无意间修改变量值所导致的错误。
所以声明时优先考虑使用const,当遇到该值需要改变时,再将const改为let,这是比较好的代码习惯.
今天关于ES6的let和const声明学习笔记就到这里了, 希望能够帮助到正在阅读的你, 如文中有所纰漏, 希望能够指出, 感谢阅读.