本文为电子书《ECMAScript 6 入门》:ES6 入门教程学习笔记
笔者在个人项目前端开发中经常用到let和const命令,开始系统学习后才发现这两个命令来自于ES6的规范。
let命令
1. 作用域
let命令可以用来声明变量,类似于var,但是两者的区别在于所声明变量的生命周期。
对于let声明的变量来说,它的作用域只存在于声明它的代码块,而var声明的变量全局有效。同时了他不允许在相同作用域内重复声明同一个变量。
由此,let声明的变量天然适合做for循环的计数器,循环结束即销毁回收。除此之外,for循环设置变量的部分和大括号内的循环体不是一个作用域。例如以下代码:
for (let i = 0; i < 3; i++) {
let i = '-1';
console.log(i);
}
打印的结果就会是三个-1。
ES5中是只有全局作用域和函数作用域的,“实际上”是let命令的出现为JS带来了块级作用域,并允许块级作用域的任意嵌套,以下代码可以清晰表示这一点:
{{{{
{let tmp = 1}
console.log(tmp); // 报错
}}}};
ES6的块级作用域必须有大括号,如果没有大括号就不会识别到块级作用域。
// 第一种写法,报错
if (true) let x = 1;
// 第二种写法,不报错
if (true) {
let x = 1;
}
2. 变量提升
经常使用var声明变量的小伙伴可能会由于写错代码或其他原因遇到在声明变量之前就使用到变量的情况,这种现象就叫变量提升,此时变量的值为undefined,可能导致一些问题。let则修改了语法行为,当变量被let声明时会进行检测,在声明它之前该变量应该是不存在的,如果在声明之前就被使用过会抛出错误。
3. 暂时性死区
ES6中明确规定,如果一个代码块中存在let和const命令,这个代码块就形成了一个封闭作用域,即使在代码块之外声明过同名变量,在代码块内部声明前使用变量依然会出现防止变量提升导致的报错。让我们看例子:
let tmp = 1;
for(let i = 0; i < 3; i++) {
// 报错
tmp = 2;
console.log(tmp);
// 打印2
let tmp = 2;
console.log(tmp);
}
4. 函数声明
ES5规定函数只能在顶层作用域和函数作用域中声明,不能在块级作用域(指诸如if、for循环体内这种而非从上文生命周期的角度来说)中声明,但只是规定,浏览器为了兼容旧代码还是支持上述行为,不会报错,并在实际运行的时候会将块级作用域中的函数提升到头部。也就是说即使是在
if(false){
}
块内的代码也是会被执行的。
如以下代码两者都会打印:
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}());
在ES5环境中相当于:
// 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 () {
var f = undefined; // 提升到函数作用域顶部
if (false) {
// 块级作用域内的提升(不影响外层)
f = function() { console.log('I am inside!'); }
}
f(); // 此时f仍是undefined
}());
当然我觉得这里具体的提升规则不是很重要,只需要知道不建议在块级作用域内声明函数,如有需要可以写成函数表达式就可以了。
// 块级作用域内部的函数声明语句,建议不要使用
{
let a = 'secret';
function f() {
return a;
}
}
// 块级作用域内部,优先使用函数表达式
{
let a = 'secret';
let f = function () {
return a;
};
}
const命令
1. 作用域、变量提升、暂时性死区
const命令声明一个只读的常量,且在声明时必须赋值,其作用域与let相同,同样不提升、存在暂时性私曲。
2. 本质
const保证变量所指向的内存地址保存的数据不变,对于简单类型的数据(数值、字符串、布尔值)来说值就保存在变量指向的内存地址,因此相当于常量,而对于符合类型的数据来说,变量指向的内存地址保存的只是一个指向实际数据的指针,因此const只能保证指针固定,不能保证对象属性不变。如果想保证对象属性不变,可以使用Object.freeze方法。
const foo = Object.freeze({});
foo.prop = 123;
此时,新增属性不起作用。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
此时,一个对象被彻底冻结了。
顶层对象和全局对象
浏览器环境中的window对象、Node环境中的global对象都是顶层对象。
在ES5中,顶层对象的属性与全局变量等价。ES6为了改变该规定带来的一系列问题,如
// 编译时无法检测拼写错误
function init() {
var myVar = 1;
myVr = 2; // 拼写错误 → 意外创建全局变量 window.myVr
}
// 意外污染全局命名空间
function calculate() {
result = 0; // 未用 var/let/const → 自动成为 window.result
// ...计算逻辑
}
// 阻碍模块化编程
// 模块A.js
var config = { apiUrl: 'A' };
// 模块B.js
var config = { apiUrl: 'B' }; // 覆盖全局 config
并保持兼容性,规定var和function命令声明的全局变量依然是顶层对象的属性,而let、const、class声明的全局变量不属于顶层对象的属性。
接下来具体讲讲顶层对象,它来提供全局作用域,所有代码都运行在这个环境中,但是正如前面所说,顶层对象在各种实现里并不统一。
- 浏览器里面,顶层对象是
window
,但 Node 和 Web Worker 没有window
。 - 浏览器和 Web Worker 里面,
self
也指向顶层对象,但是 Node 没有self
。 - Node 里面,顶层对象是
global
,但其他环境都不支持。
一般我们使用this关键字获取顶层对象,但依然有局限性。
- 全局环境中,
this
会返回顶层对象。但是,Node.js 模块中this
返回的是当前模块,ES6 模块中this
返回的是undefined
。 - 函数里面的
this
,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this
会指向顶层对象。但是,严格模式下,这时this
会返回undefined
。 - 不管是严格模式,还是普通模式,
new Function('return this')()
,总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么eval
、new Function
这些方法都可能无法使用。
所以以下两种方法勉强可以普适地取到顶层对象。
// 方法一
(typeof window !== 'undefined'
? window
: (typeof process === 'object' &&
typeof require === 'function' &&
typeof global === 'object')
? global
: this);
// 方法二
var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};