参考资料:
开言
为什么要专门来记录一篇es6,而不是像之前一样以面试题的方式单独记录,主要是最近在项目中遇到一个问题,webpack中使用import二次导入失败,就是最开始加载的时候能正常显示,但是第二次加载就没用了。
问了老师,说是那个js没有export模块,我试着修改了一下,并且将所有import改为require,目前对它的理解仅仅是require能写进函数里,成功解决问题后,我想了解一下这里面是什么原理,结果查资料就看到commonJS和es6等,我基础不好,确实是被这些词绕的头晕,有见过有看过,但是具体是为什么会出现这些一概不知。
这就是我专门记录这个模块的原由,同时正好找到几份不错的资料,也算结合一下提升技能。记录的大部分都是总结,比较基础和浅显,可能会分比较多的章数,因为一些中间不太了解的知识点要另外去总结和搜索。
记录
es6简介
ES6全称ECMAScript 6.0,最开始就是针对JavaScript定制的,只是因为java名称权限以及体现中立性,所以采用的是标准化组织ECMA的前缀进行命名的。
ECMAScript 2015和ES6的关系,首先ES6的第一个版本是在2015年发布的,正式名称就是《ECMAScript 2015 标准》(简称 ES2015),在此之后每天6月发布这一年的新版本,每个版本通过年份标记,因此ES6既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本书中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript 语言”
Babel转码器
Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在老版本的浏览器执行。这意味着,你可以用 ES6 的方式编写程序,又不用担心现有环境是否支持。比如写了一个箭头函数,使用转码器就会转为普通函数,这样就能在不支持箭头函数的js环境中执行了。
let和const
ES6 新增了let和const
命令,用来声明变量。它们的用法类似于var
,但是所声明的变量,只在let和const
命令所在的代码块内有效,可以浅显理解为{}中,也就是说比如在一个循环体中,使用var声明的化,在循环结束后,循环体外依旧能打印出来最后一个循环数据,但是使用这两个块级变量就可以解决这个问题。
另外,for
循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。下面代码正确运行,输出了 3 次abc
。这表明函数内部的变量i
与循环变量i
不在同一个作用域,有各自单独的作用域(同一个作用域不可使用 let
重复声明同一个变量)
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
变量提升
var
命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined,let和const必须在声明后使用,否则报错。
暂时性死区
在代码块内,使用let
命令声明变量之前,该变量都是不可用的,这在语法上,称为“暂时性死区”。只要块级作用域内存在let
命令,它所声明的变量就“绑定”这个区域,不再受外部的影响。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
上面代码中,存在全局变量tmp
,但是块级作用域内let
又声明了一个局部变量tmp
,导致后者绑定这个块级作用域,所以在let
声明变量前,对tmp
赋值会报错。
在没有let
之前,typeof
运算符是百分之百安全的,永远不会报错,但在let出现后,在let声明前调用typeof判断就会导致抛出ReferenceError
块级作用域
ES6 允许块级作用域的任意嵌套,下面代码使用了一个五层的块级作用域,每一层都是一个单独的作用域。第四层作用域无法读取第五层作用域的内部变量
{{{{
{let insane = 'Hello World'}
console.log(insane); // 报错
}}}};
匿名IIFE
块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了
匿名立即执行函数表达式(IIFE)在JavaScript中常被用于创建一个独立的作用域,以避免变量污染全局作用域。通过这种方式,IIFE内部的变量和函数对外部是不可见的,这有助于封装和保护代码
在ES6之前,JavaScript只有全局作用域和函数作用域,这导致了一些场景下的不合理性,比如内层变量可能会覆盖外层变量,或者循环变量可能会泄漏为全局变量。ES6引入了块级作用域,它允许在代码块(由大括号{}包围的代码区域)内部声明变量,并且这些变量只在块级作用域内部有效,外部无法访问。
由于块级作用域提供了类似于IIFE的封装能力,但语法更为简洁和直观,因此它实际上成为了IIFE的一种更为方便和现代的替代方案。开发者现在可以直接在需要的地方使用块级作用域来声明变量,而无需再编写额外的IIFE函数。
// IIFE 写法
// 第一种
(function () {
var tmp = ...;
...
}());
// 第二种
(function () {
var tmp = ...;
...
})();
// 块级作用域写法
{
let tmp = ...;
...
}
变量的解构赋值
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构。
以前为变量赋值只能指定值,es6允许写成下面这种形式,表示从数组中提取值按照对应的位置对变量进行赋值,本质上这种写法属于模式匹配,只要等号两边模式相同,左边的变量就会被赋予相应的值。
let [a, b, c] = [1, 2, 3];
解构不成功,变量的值就等于undefined。以下两种情况都属于解构不成功。
let [foo] = [];
let [bar, foo] = [1];
另一种是不完全解构,即等号左边的模式只匹配一部分等号右边的数组,这种情况下解构依然成功。但是如果等号的右边不是数组或者说不是可遍历的解构,就会报错。事实上只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值。
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
默认值
解构赋值允许指定默认值,但是ES6内部使用严格运算符(===)判断一个位置是否有效,所以只有成员严格等于undefined,默认值才会生效。按照下面这段代码,null不等于undefined,所以默认值无效。
let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null
如果默认值是一个表达式,那么这个表达式是惰性请求的,即只有在用到的时候才会求值。
按照下面这段代码来分析,let [x = f()] = [1]; 是一个解构赋值表达式,它尝试从右侧的数组 [1] 中解构出元素来赋值给左侧的变量 x。由于数组 [1] 的第一个元素(索引为0的元素)是 1,这个值被直接赋给了 x。由于 x 能够从数组 [1] 中成功获取到值(即 1),因此默认值 f() 不会被执行。这是因为解构赋值中的默认值表达式是惰性求值的:只有当对应的解构位置没有值(即 undefined)时,默认值表达式才会被求值
function f() {
console.log('aaa');
}
let [x = f()] = [1];
对象的解构赋值
数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
下面的代码中,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined。
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar
如果变量名与属性名不一致,必须写成下面这样
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。下面代码中,foo
是匹配的模式,baz
才是变量。真正被赋值的是变量baz
,而不是模式foo
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined
注意点
下面代码中,因为 JavaScript 引擎会将{x}
理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题
// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error
// 正确的写法
let x;
({x} = {x: 1});
由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
字符串的解构赋值
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
let {length : len} = 'hello';
len // 5
数值和布尔值的解构赋值
在JavaScript中,原始类型(如数字、字符串、布尔值)在需要时被当作它们的包装对象(如Number、String、Boolean)来处理,这被称为装箱(boxing)。不过,在解构赋值中,这种装箱行为并不会直接创建一个新的对象实例,而是以一种特殊的方式访问这些原始类型对应对象原型上的方法。
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象,由于undefined
和null
无法转为对象,所以对它们进行解构赋值,都会报错。下面代码中解析第一个,第二个同理。
- let {toString: s} = 123; 这行代码尝试从数字123(一个原始类型)中解构出toString属性。然而,123本身并没有属性,因为它是一个原始类型,不是对象。但是,JavaScript在解构时,会尝试将原始值视为一个对象,并尝试访问该对象的toString属性。
- 在这里,JavaScript实际上并没有为123创建一个新的Number对象实例。相反,它直接访问了Number.prototype上的toString方法。因此,解构赋值的结果s直接引用了Number.prototype.toString方法。
- 所以,s === Number.prototype.toString的结果是true,因为s确实是对Number.prototype.toString的引用
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
函数参数的解构赋值
在这个函数中,{x = 0, y = 0} = {}
是一个对象解构赋值,但这里的{}
是函数的默认参数值,而不是解构的目标对象。解构赋值发生在函数参数被实际传入的对象上。如果没有传入任何对象(即undefined
),则使用{}
作为默认值进行解构,但这里的x = 0
和y = 0
是在解构过程中为未定义的属性提供的默认值,而不是为整个对象提供的默认值。
move({x: 3, y: 8});
传入的对象有x
和y
属性,直接解构为x: 3, y: 8
。move({x: 3});
传入的对象只有x
属性,解构时y
属性未定义,因此使用默认值0
。move({});
传入的对象没有x
和y
属性,因此都使用默认值0
。move();
没有传入任何对象,使用默认值{}
进行解构,x
和y
同样使用默认值0
{}作为默认参数值,它的主要作用是确保当没有提供任何参数给move函数时,解构赋值操作有一个对象可以操作,从而避免抛出错误。这个默认对象{}本身不包含x或y属性,但解构赋值中的x = 0和y = 0确保了即使{}中没有这些属性,x和y也会被赋予默认值0。即{}作为默认参数值确保了即使在没有提供任何参数的情况下,函数也能正常运行而不会抛出错误。
function move1({ x = 0, y = 0 } = {}) {
console.log([x, y]) ;
}
move1({ x: 3, y: 8 }); // [3, 8]
move1({ x: 3 }); // [3, 0]
move1({}); // [0, 0]
move1(); // [0, 0]
move2 的默认值是在参数对象整体上设置的,也就是说,只有当没有传入参数时,整个参数对象才会使用默认值 { x: 0, y: 0 }。如果传入了一个对象 {},即使该对象没有 x 或 y,它也不会触发默认值 0,因为解构后的值是 undefined
- move2({ x: 3, y: 8 }); 传入了完整的对象 { x: 3, y: 8 },所以输出 [3, 8]
- move2({ x: 3 }); 传入了 { x: 3 },但是没有传入 y,所以 y 的值是 undefined,输出 [3, undefined]
- move2({}); 传入了一个空对象 {},由于 x 和 y 都没有提供,它们的值都是 undefined,所以输出 [undefined, undefined]
-
move2(); 没有传入任何参数,所以会使用默认值 { x: 0, y: 0 },输出 [0, 0]
move2() 没有传入任何参数,因此会使用函数定义中提供的默认值 { x: 0, y: 0 } 作为整个对象的默认值。move2({}) 传入了一个空对象 {},虽然该对象没有 x 或 y 属性,但因为传递了对象,默认值 { x: 0, y: 0 } 不会被使用。解构时没有找到属性的值,结果是 undefined
function move2({ x, y } = { x: 0, y: 0 }) {
console.log([x, y]) ;
}
move2({ x: 3, y: 8 }); // [3, 8]
move2({ x: 3 }); // [3, undefined]
move2({}); // [undefined, undefined]
move2(); // [0, 0]
用途
交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
从函数返回多个值
// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
提取 JSON 数据
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]
遍历 Map 结构
任何部署了 Iterator 接口的对象,都可以用for...of
循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
console.log(key + " is " + value);
}
// 获取键名
for (let [key] of map) {
// ...
}
// 获取键值
for (let [,value] of map) {
// ...
}
字符串的扩展
ES6 加强了对 Unicode 的支持,允许采用\uxxxx
形式表示一个字符,其中xxxx
表示字符的 Unicode 码点,但是,这种表示法只限于码点在\u0000
~\uFFFF
之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。
ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符
"\u0061"
// "a"
"\uD842\uDFB7"
// "𠮷"
"\u20BB7"
// " 7"
"\u{20BB7}"
// "𠮷"
"\u{41}\u{42}\u{43}"
// "ABC"
let hello = 123;
hell\u{6F} // 123
'\u{1F680}' === '\uD83D\uDE80'
// true
字符串遍历器接口
ES6 为字符串添加了遍历器接口,使得字符串可以被for...of
循环遍历,除了遍历字符串,这个遍历器最大的优点是可以识别大于0xFFFF
的码点,传统的for
循环无法识别这样的码点。
for (let codePoint of 'foo') {
console.log(codePoint)
}
// "f"
// "o"
// "o"
let text = String.fromCodePoint(0x20BB7);
for (let i = 0; i < text.length; i++) {
console.log(text[i]);
}
// " "
// " "
for (let i of text) {
console.log(i);
}
// "𠮷"
模版字符串
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量
// 普通字符串
`In JavaScript '\n' is a line-feed.`
// 多行字符串
`In JavaScript this is
not legal.`
console.log(`string text line 1
string text line 2`);
// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中,可以使用trim
方法消除
$('#list').html(`
<ul>
<li>first</li>
<li>second</li>
</ul>
`.trim());
大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。还能调用函数,如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的toString
方法