【学习笔记】ES6入门教程
2. let 和 const
let 不存在变量提升
在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
此时 typeof x; // ReferenceError
不允许重复声明
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数
块级作用域的函数声明当作let处理。
const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。只声明不赋值,就会报错。
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
完全不可变,冻结对象:const foo = Object.freeze({});
6 种声明变量的方法:var let const function import class
var命令和function命令声明的全局变量,依旧是顶层对象的属性;let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。
全局环境中,this会返回顶层对象。但是,Node 模块和 ES6 模块中,this返回的是当前模块。
函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined。
3. 变量的解构赋值
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
-
数组
从数组中提取值
let [a, b, c] = [1, 2, 3]; let [foo, [[bar], baz]] = [1, [[2], 3]]; let [x, , y] = [1, 2, 3]; let [head, ...tail] = [1, 2, 3, 4]; // 1 [2,3,4]
如果解构不成功 变量的值就是 undefined
只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。
解构赋值允许指定默认值。
let [foo = true] = [];
只有当一个数组成员严格等于undefined,默认值才会生效。
let [x = 1] = [undefined]; // x: 1 let [x = 1] = [null]; // x: null
-
对象
let { bar, foo } = { foo: "aaa", bar: "bbb" };
赋值为同名变量 顺序无关 无匹配属性名返回 undefined
如果变量名与属性名不一致
let { foo: baz } = { foo: 'aaa', bar: 'bbb' }; // baz: 'aaa'
上面的表达式其实是
let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
的缩写对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
嵌套结构
let obj = { p: [ 'Hello', { y: 'World' } ] }; let { p, p: [x, { y }] } = obj; // x: "Hello" y: "World" p: ["Hello", {y: "World"}]
对象的解构也可以指定默认值。默认值生效的条件是,对象的属性值严格等于undefined。
如果解构失败,变量的值等于undefined。如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。
如果要将一个已经声明的变量用于解构赋值,必须非常小心。
let x; {x} = {x: 1};
因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。
({x} = {x: 1});
-
字符串
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello'; let {length : len} = 'hello'; // length: 5
-
数值和布尔值
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。
建议不要在解构赋值中放置圆括号 情况太多我记不住不记了 😉
4. 字符串的扩展
-
字符的 Unicode 表示法
\uxxxx xxxx表示 Unicode 码点 需在
\u0000~\u9999
之间\u{xxx} 放入大括号无限制
-
codePointAt()
js每个字符固定为两个字节,需要用四个字节编码的字符,在js中会判断长度为2 charCodeAt 只能返回字符的前两个和后两个字节
codePointAt方法会正确返回 32 位的 UTF-16 字符的码点(十进制)。对于那些两个字节储存的常规字符,它的返回结果与charCodeAt方法相同。
let s = '𠮷a'; s.codePointAt(0) // 134071 s.codePointAt(1) // 57271 s.codePointAt(2) // 97
使用for...of循环,因为它会正确识别 32 位的 UTF-16 字符。
let s = '𠮷a'; for (let ch of s) { console.log(ch.codePointAt(0).toString(16)); } // 20bb7 // 61
codePointAt方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。
c.codePointAt(0) > 0xFFFF;
-
String.fromCodePoint()
ES5 提供String.fromCharCode方法,用于从码点返回对应字符,但是这个方法不能识别 32 位的 UTF-16 字符(Unicode 编号大于0xFFFF)。
String.fromCodePoint(0x20BB7)// "𠮷"
如果String.fromCodePoint方法有多个参数,则它们会被合并成一个字符串返回。
String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'// true
ES6 为字符串添加了遍历器接口(详见《Iterator》一章),使得字符串可以被for...of循环遍历。
-
normalize()
许多欧洲语言有语调符号和重音符号。为了表示它们,Unicode 提供了两种方法。
一种是直接提供带重音符号的字符, 另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符 -
includes(), startsWith(), endsWith()
-
repeat()
repeat方法返回一个新字符串,表示将原字符串重复n次。参数如果是小数,会被取整。
如果repeat的参数是负数或者Infinity,会报错。如果参数是 0 到-1 之间的小数,则等同于 0.参数NaN等同于 0。
如果repeat的参数是字符串,则会先转换成数字。
'na'.repeat(2.9) // "nana" 'na'.repeat(0) // ""
-
padStart() padEnd()
字符串长度补全。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。
padStart()和padEnd()一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。
如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串。
如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串。
如果省略第二个参数,默认使用空格补全长度。
'x'.padStart(4, 'ab') // 'abax' 'x'.padEnd(4, 'ab') // 'xaba' 'xxx'.padEnd(2, 'ab') // 'xxx' 'x'.padStart(4) // ' x' '1'.padStart(10, '0') // "0000000001"
-
matchAll() 返回一个正则表达式在当前字符串的所有匹配
-
模板字符串
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
标签模板模板字符串的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。
tag函数的第一个参数是一个数组,该数组的成员是模板字符串中那些没有变量替换的部分。tag函数的其他参数,都是模板字符串各个变量被替换后的值。
let a = 5, b = 10; tag`Hello ${ a + b } world ${ a * b }`; // 等同于 tag(['Hello ', ' world ', ''], 15, 50);
将各个参数按照原来的位置拼合回去。
function passthru(literals, ...values) { let output = ""; let index; for (index = 0; index < values.length; index++) { output += literals[index] + values[index]; } output += literals[index] return output; }
-
String.raw()
String.raw方法可以作为处理模板字符串的基本方法,它会将所有变量替换,而且对斜杠进行转义,方便下一步作为字符串来使用。
String.raw方法也可以作为正常的函数使用。这时,它的第一个参数,应该是一个具有raw属性的对象,且raw属性的值应该是一个数组。
5. 正则的扩展
-
构造函数
new RegExp(/abc/ig, 'i').flags // "i"
-
字符串的正则方法 match()、replace()、search()和split()。
ES6 将这 4 个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。
String.prototype.match 调用 RegExp.prototype[Symbol.match] String.prototype.replace 调用 RegExp.prototype[Symbol.replace] String.prototype.search 调用 RegExp.prototype[Symbol.search] String.prototype.split 调用 RegExp.prototype[Symbol.split]
-
u 修饰符 会正确处理四个字节的 UTF-16 编码。
-
RegExp.prototype.unicode 表示是否设置了u修饰符
-
y 修饰符 “粘连”(sticky)修饰符
y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。
-
RegExp.prototype.sticky 表示是否设置了y修饰符。
-
RegExp.prototype.flags 属性 会返回正则表达式的修饰符。
-
s 修饰符:dotAll 模式 即点(dot)代表一切字符。
正则表达式中,点(.)是一个特殊字符,代表任意的单个字符,但是有两个例外。一个是四个字节的 UTF-16 字符,这个可以用u修饰符解决;另一个是行终止符(line terminator character)。U+000A 换行符(\n)U+000D 回车符(\r)U+2028 行分隔符(line separator)U+2029 段分隔符(paragraph separator)
变通
/foo[^]bar/.test('foo\nbar')
s 修饰符
/foo.bar/s.test('foo\nbar') // true
-
RegExp.prototype.dotAll 表示该正则表达式是否处在dotAll模式。
-
后行断言
-
具名组匹配
“具名组匹配”在圆括号内部,模式的头部添加“问号 + 尖括号 + 组名”(?
), 然后就可以在exec方法返回结果的groups属性上引用该组名。同时,数字序号(matchObj[1])依然有效。
如果具名组没有匹配,那么对应的groups对象属性会是undefined。
const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/; const matchObj = RE_DATE.exec('1999-12-31'); const year = matchObj.groups.year; // 1999 const month = matchObj.groups.month; // 12 const day = matchObj.groups.day; // 31
- 解构赋值和替换
有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值。
let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar'); one // foo two // bar
字符串替换时,使用$<组名>引用具名组。
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u; '2015-01-02'.replace(re, '$<day>/$<month>/$<year>') // '02/01/2015'
引用 如果要在正则表达式内部引用某个“具名组匹配”,可以使用\k<组名>的写法。
const RE_TWICE = /^(?<word>[a-z]+)!\k<word>!\1$/; RE_TWICE.test('abc!abc!abc') // true RE_TWICE.test('abc!abc!ab') // false
- 解构赋值和替换
6. 数值的扩展
-
ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。
-
Number.isFinite(), Number.isNaN()
Number.isFinite()用来检查一个数值是否为有限的(finite),即不是Infinity。
注意,如果参数类型不是数值,Number.isFinite一律返回false。
Number.isNaN()用来检查一个值是否为NaN。
它们与传统的全局方法isFinite()和isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,Number.isFinite()对于非数值一律返回false, Number.isNaN()只有对于NaN才返回true,非NaN一律返回false。
-
Number.parseInt(), Number.parseFloat()
ES6 将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。
这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。
-
Number.isInteger()
Number.isInteger()用来判断一个数值是否为整数。如果参数不是数值,Number.isInteger返回false。
如果数值的精度超过限度,Number.isInteger可能会误判。
-
Number.EPSILON
Number.EPSILON 表示 1 与大于 1 的最小浮点数之间的差。是 JavaScript 能够表示的最小精度。
-
安全整数和 Number.isSafeInteger() -253到253
Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1 // true Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER // true
Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。
-
Math 对象的扩展
-
Math.trunc() 用于去除一个数的小数部分,返回整数部分。(直接截取)对于非数值,Math.trunc内部使用Number方法将其先转为数值。对于空值和无法截取整数的值,返回NaN。
-
Math.sign() 用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。
参数为正数,返回+1;参数为负数,返回-1;参数为 0,返回0;参数为-0,返回-0;其他值,返回NaN。
-
Math.cbrt() 用于计算一个数的立方根。对于非数值,Math.cbrt方法内部也是先使用Number方法将其转为数值。
-
Math.clz32() 将参数转为 32 位无符号整数的形式,然后这个 32 位值里面有多少个前导 0。
-
Math.imul方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。
Math.imul(2, 4) // 8 Math.imul(-1, 8) // -8 Math.imul(-2, -2) // 4
大多数情况下,Math.imul(a, b)与a * b的结果是相同的,即该方法等同于(a * b)|0的效果。之所以需要部署这个方法,是因为 JavaScript 有精度限制,超过 2 的 53 次方的值无法精确表示。这就是说,对于那些很大的数的乘法,低位数值往往都是不精确的,Math.imul方法可以返回正确的低位数值。
-
Math.fround() 返回一个数的32位单精度浮点数形式。
-
Math.hypot() 返回所有参数的平方和的平方根。
-
Math.expm1(x) 返回 e^x - 1,即 Math.exp(x) - 1。
-
Math.log1p(x) 返回 1 + x 的自然对数,即 Math.log(1 + x)。如果 x 小于-1,返回 NaN。
-
Math.log10(x)返回以 10 为底的 x 的对数。如果 x 小于 0,则返回 NaN。
-
Math.log2(x)返回以 2 为底的x的对数。如果x小于 0,则返回 NaN。
-
Math.sinh(x) 返回x的双曲正弦(hyperbolic sine)
-
Math.cosh(x) 返回x的双曲余弦(hyperbolic cosine)
-
Math.tanh(x) 返回x的双曲正切(hyperbolic tangent)
-
Math.asinh(x) 返回x的反双曲正弦(inverse hyperbolic sine)
-
Math.acosh(x) 返回x的反双曲余弦(inverse hyperbolic cosine)
-
Math.atanh(x) 返回x的反双曲正切(inverse hyperbolic tangent)
-
-
指数运算符 **
这个运算符的一个特点是右结合
2 ** 3 ** 2 // 相当于 2 ** (3 ** 2) // 512
指数运算符可以与等号结合,形成一个新的赋值运算符(**=)。
a **= 2; // 等同于 a = a ^ 2;
V8 引擎的指数运算符与Math.pow的实现不相同,对于特别大的运算结果,两者会有细微的差异。
7. 函数的扩展
-
函数参数的默认值
参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。let x = 99; function foo(p = x + 1) { console.log(p); } foo() // 100 x = 100; foo() // 101
-
与解构赋值默认值结合使用
function foo({x, y = 5} = {}) { console.log(x, y); } foo() // undefined 5
-
指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。 rest 参数也不会计入length属性。
(function (a) {}).length // 1 (function (a = 5) {}).length // 0 (function (a, b, c = 5) {}).length // 2
-
利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。
function throwIfMissing() { throw new Error('Missing parameter'); } function foo(mustBeProvided = throwIfMissing()) { return mustBeProvided; } foo() // Error: Missing parameter
-
ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,将多余的参数放入数组 rest 中 。
-
name 属性
函数的name属性,返回该函数的函数名。
如果将一个匿名函数赋值给一个变量,ES5 的name属性,会返回空字符串,而 ES6 的name属性会返回实际的函数名。
var f = function () {}; f.name // ES5 "" 、 ES6 "f"
如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的name属性都返回这个具名函数原本的名字。
Function构造函数返回的函数实例,name属性的值为anonymous。
bind返回的函数,name属性值会加上bound前缀。
function foo() {}; foo.bind({}).name // "bound foo" (function(){}).bind({}).name // "bound "
-
箭头函数
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。如果箭头函数只有一行语句,且不需要返回值:
let fn = () => void doesNotReturn();
函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误
不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。
-
双冒号运算符 (浏览器不支持)
-
尾调用优化
函数的最后一步调用其他函数 可以不用保存调用函数的 调用帧 (call frame)尾递归:函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。
-
函数参数的尾逗号
ES2017 允许函数的最后一个参数有尾逗号(trailing comma)。
8. 数组的扩展
-
扩展运算符
由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了。
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
const [...butLast, last] = [1, 2, 3, 4, 5]; // error
扩展运算符还可以将字符串转为真正的数组。此时能够正确识别四个字节的 Unicode 字符。
[...'hello'] // [ "h", "e", "l", "l", "o" ]
任何定义了遍历器(Iterator)接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组。
-
Array.from() 用于将两类对象转为真正的数组
类似数组的对象(array-like object)let arrayLike = { '0': 'a', '1': 'b', '2': 'c', length: 3 }; // ES5的写法 var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c'] // ES6的写法 let arr2 = Array.from(arrayLike); // ['a', 'b', 'c'] // NodeList对象 let ps = document.querySelectorAll('p'); Array.from(ps);
可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
Array.from('hello') // ['h', 'e', 'l', 'l', 'o'] let namesSet = new Set(['a', 'b']) Array.from(namesSet) // ['a', 'b']
如果参数是一个真正的数组,Array.from会返回一个一模一样的新数组。
Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。如果map函数里面用到了this关键字,还可以传入Array.from的第三个参数,用来绑定this。
Array.from(arrayLike, x => x * x); // 等同于 Array.from(arrayLike).map(x => x * x);
-
Array.of() 用于将一组值,转换为数组
Array.of(3, 11, 8) // [3,11,8] Array.of(3) // [3] Array.of(3).length // 1 Array.of() // [] Array.of(undefined) // [undefined]
-
数组实例的 copyWithin()
在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
Array.prototype.copyWithin(target, start = 0, end = this.length) // target(必需):从该位置开始替换数据。如果为负值,表示倒数。 // start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。 // end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。
-
数组实例的 find() 和 findIndex()
数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
[1, 5, 10, 15].find(function(value, index, arr) { return value > 9; }) // 10
数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1
-
数组实例的 fill() 使用给定值,填充一个数组。 fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
['a', 'b', 'c'].fill(7) // [7, 7, 7] new Array(3).fill(7) // [7, 7, 7] ['a', 'b', 'c'].fill(7, 1, 2) // ['a', 7, 'c']
-
数组实例的 entries(),keys() 和 values()
ES6 提供三个新的方法——entries(),keys()和values()——用于遍历数组。它们都返回一个遍历器对象,可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。
如果不使用for...of循环,可以手动调用遍历器对象的next方法,进行遍历。
-
数组实例的 includes()
Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。
该方法的第二个参数表示搜索的起始位置,默认为0。
对比 indexOf 可以判断 NaN
-
数组实例的 flat(),flatMap()
数组的成员有时还是数组,Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。
flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1。
如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。
如果原数组有空位,flat()方法会跳过空位。
flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。只能展开一层数组。
-
数组的空位
Array.from方法会将数组的空位,转为undefined
扩展运算符(...)也会将空位转为undefined。
copyWithin()会连空位一起拷贝。
fill()会将空位视为正常的数组位置。
for...of循环也会遍历空位。
entries()、keys()、values()、find()和findIndex()会将空位处理成undefined。
9. 对象的扩展
-
对象中方法简写, 属性名总是字符串
const obj = { class () {} }; // 等同于 var obj = { 'class': function() {} };
class是字符串,所以不会因为它属于关键字,而导致语法解析报错。
-
ES6 允许字面量定义对象时,用表达式作为对象的属性名,即把表达式放在方括号内。也可以用于定义方法名。
let propKey = 'foo'; let obj = { [propKey]: true, ['a' + 'bc']: 123, ['h' + 'ello']() { return 'hi'; } };
-
函数的name属性,返回函数名。对象方法也是函数,因此也有name属性。方法的name属性返回函数名(即方法名)
bind方法创造的函数,name属性返回bound加上原函数的名字;Function构造函数创造的函数,name属性返回anonymous。
如果对象的方法是一个 Symbol 值,那么name属性返回的是这个 Symbol 值的描述。
const key1 = Symbol('description'); const key2 = Symbol(); let obj = { [key1]() {}, [key2]() {}, }; obj[key1].name // "[description]" obj[key2].name // ""
-
可枚举性
目前,有四个操作会忽略enumerable为false的属性。
- for...in循环:只遍历对象自身的和继承的可枚举的属性。
- Object.keys():返回对象自身的所有可枚举的属性的键名。
- JSON.stringify():只串行化对象自身的可枚举的属性。
- Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。
所有 Class 的原型的方法都是不可枚举的。
-
属性的遍历
- for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
- Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
- Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
- Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。
- Reflect.ownKeys返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。
- 首先遍历所有数值键,按照数值升序排列。
- 其次遍历所有字符串键,按照加入时间升序排列。
- 最后遍历所有 Symbol 键,按照加入时间升序排列。
-
super
我们知道,this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。
JavaScript 引擎内部,super.foo等同于Object.getPrototypeOf(this).foo(属性)或Object.getPrototypeOf(this).foo.call(this)(方法)。
super关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。
10. 对象新增方法
-
Object.is() 和 === 行为基本一致 除了
Object.is(+0, -0) // false Object.is(NaN, NaN) // true
-
Object.assign() 用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
Object.assign(target, source1, source2);
如果只有一个参数,Object.assign会直接返回该参数。
如果该参数不是对象,则会先转成对象,然后返回。
由于undefined和null无法转成对象,所以如果它们作为参数,就会报错。
其他类型的值(即数值、字符串和布尔值,undefined和null)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。
属性名为 Symbol 值的属性,也会被Object.assign拷贝。
Object.assign可以用来处理数组,但是会把数组视为对象。
为对象添加方法
Object.assign(SomeClass.prototype, { someMethod(arg1, arg2) { // ··· }, anotherMethod() { // ··· } }); // 等同于下面的写法 SomeClass.prototype.someMethod = function (arg1, arg2) { // ··· }; SomeClass.prototype.anotherMethod = function () { // ··· };
-
Object.getOwnPropertyDescriptors() 返回指定对象所有自身属性(非继承属性)的描述对象。
-
__proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()
-
Object.keys(),Object.values(),Object.entries()
-
Object.fromEntries() Object.fromEntries()方法是Object.entries()的逆操作,用于将一个键值对数组转为对象。
const map = new Map().set('foo', true).set('bar', false); Object.fromEntries(map) // { foo: true, bar: false }
11. Symbol
Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。
Symbol 值不能与其他类型的值进行运算,会报错。
Symbol 值可以显式转为字符串。Symbol 值也可以转为布尔值,但是不能转为数值。
在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。
Symbol 值作为对象属性名时,不能用点运算符。在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。
Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的。
```js
const log = {};
log.levels = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn')
};
```
Symbol 作为属性名,该属性不会出现在for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有 Symbol 属性名。
由于以 Symbol 值作为名称的属性,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。
Symbol.for(),Symbol.keyFor()
有时,我们希望重新使用同一个 Symbol 值,Symbol.for方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
Symbol.keyFor方法返回一个已登记的 Symbol 类型值的key。
内置的 Symbol 值
-
Symbol.hasInstance
对象的
Symbol.hasInstance
属性,指向一个内部方法。当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo
在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)
。 -
Symbol.isConcatSpreadable 等于一个布尔值,表示该对象用于
Array.prototype.concat()
时,是否可以展开。 -
Symbol.species
Symbol.species的作用在于,实例对象在运行过程中,需要再次调用自身的构造函数时,会调用该属性指定的构造函数。它主要的用途是,有些类库是在基类的基础上修改的,那么子类使用继承的方法时,作者可能希望返回基类的实例,而不是子类的实例。 -
Symbol.match 对象的
Symbol.match
属性,指向一个函数。当执行str.match(myObject)
时,如果该属性存在,会调用它,返回该方法的返回值。 -
Symbol.replace
-
Symbol.search
-
Symbol.split
-
Symbol.iterator 对象的
Symbol.iterator
属性,指向该对象的默认遍历器方法。 -
Symbol.toPrimitive 指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。
-
Symbol.toStringTag
-
Symbol.unscopables
对象的Symbol.unscopables属性,指向一个对象。该对象指定了使用with关键字时,哪些属性会被with环境排除。
12. Set 和 Map 数据结构
-
Set
// 初始化 const s = new Set(); // 空 Set const set = new Set([1, 2, 3, 4, 4]); // 使用数组初始化 const set = new Set(document.querySelectorAll('div')); // 使用类数组初始化 const set = new Set('ababbc'); // 使用字符串初始化 Set(3) {"a", "b", "c"} // 常用操作 s.add(1); // 添加某个值,返回 Set 结构本身。 s.add(3).add(4) s.size; s.delete(value); // 删除某个值,返回一个布尔值,表示删除是否成功。 s.has(value); // 返回一个布尔值,表示该值是否为Set的成员。 s.clear(); // 清除所有成员,没有返回值。 // 遍历 Set的遍历顺序就是插入顺序。 s.keys(); // 返回键名的遍历器 keys方法和values方法的行为完全一致。 s.values(); // 返回键值的遍历器 s.entries(); // 返回键值对的遍历器 每次输出一个数组,它的两个成员完全相等。 s.forEach(); // 使用回调函数遍历每个成员 回调函数中 键值相等 // forEach方法还可以有第二个参数,表示绑定处理函数内部的this对象。
Set 中 使用 === 判断相等 除了 NaN
Set的遍历顺序就是插入顺序。
-
WeakSet
WeakSet 的成员只能是对象,而不能是其他类型的值。
WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
WeakSet 没有size属性,没有办法遍历它的成员。
WeakSet 的一个用处,是储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏。
-
Map
const m = new Map(); const map = new Map([ ['name', '张三'], ['title', 'Author'] ]); m.set(key, value); m.set(key1, value1).set(key2, value2); m.get(key); // 不存在返回 undefined m.has(key); m.delete(key); m.size; m.keys(); // 返回键名的遍历器 keys方法和values方法的行为完全一致。 m.values(); // 返回键值的遍历器 m.entries(); // 返回键值对的遍历器 每次输出一个数组,它的两个成员完全相等。 m.forEach(); // 使用回调函数遍历每个成员 回调函数中 键值相等 // forEach方法还可以有第二个参数,表示绑定处理函数内部的this对象。 // map to array 二维数组 [...myMap] // 二维数组 // array to map new Map([ [true, 7], [{foo: 3}, ['abc']] ]) // Map 转为 JSON // 一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。 // 另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。
Map 的遍历顺序就是插入顺序。
-
WeakMap
WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
WeakMap的键名所指向的对象,不计入垃圾回收机制。
WeakMap 没有遍历操作(即没有keys()、values()和entries()方法),也没有size属性。
13. Proxy
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
var proxy = new Proxy(target, handler);
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
15. Promise
调用resolve或reject并不会终结 Promise 的参数函数的执行。
跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。
Promise.resolve方法的参数分成四种情况。
- 参数是一个 Promise 实例:直接返回这个实例
- 参数是一个 thenable 对象: Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行 thenable 对象的 then 方法。
- 参数不是具有then方法的对象,或根本就不是对象: 则Promise.resolve方法返回一个新的 Promise 对象,状态为 resolved。
- Promise.resolve方法允许调用时不带参数,直接返回一个 resolved 状态的 Promise 对象。 Promise {
: undefined}
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。
16. Iterator
对象有 Symbol.iterator 方法 用来生成一个迭代器
迭代器是一个有 next 方法的对象,返回 { value, done }
let testItetator = {
[Symbol.iterator]: function() {
let i = 0;
return {
next: function() {
return { value: i++, done: i > 10 }
}
}
}
}
调用 Iterator 接口的场合
- 解构赋值 对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。
- 扩展运算符 扩展运算符(...)也会调用默认的 Iterator 接口。
yield*
后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
return方法的使用场合是,如果for...of循环提前退出(通常是因为出错,或者有break语句),就会调用return方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法。
17. Generator 函数的语法
Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象
Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
- 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
- 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
- next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
- 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
- 如果该函数没有return语句,则返回的对象的value属性值为undefined。
yield表达式如果用在另一个表达式之中,必须放在圆括号里面。 'Hello' + (yield 123);
// OK
第一次使用next方法时,传递参数是无效的。
Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b
内部没有捕获错误的话,错误将会发到外层
一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。即使这个错误被外部捕获。下一次调用 next() 会返回 { value: undefined, done: true }
return方法,可以返回给定的值,并且终结遍历 Generator 函数。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
如果 Generator 函数内部有try...finally代码块,且正在执行try代码块,那么return方法会推迟到finally代码块执行完再执行。
next()、throw()、return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。
next()是将yield表达式替换成一个值。throw()是将yield表达式替换成一个throw语句。return()是将yield表达式替换成一个return语句。
yield*表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。
任何数据结构只要有 Iterator 接口,就可以被yield*遍历 等同于在 Generator 函数内部,部署一个for...of循环。
function* gen(){
yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }
如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据。
如果一个对象的属性是 Generator 函数,可以简写成下面的形式。
let obj = {
* myGeneratorMethod() {
···
}
};
Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype对象上的方法。
Generator 函数返回的迭代器对象不是函数中 this 实例。
Generator 函数不能跟new命令一起用,会报错。
18. Generator 函数的异步应用
JS异步编程方法: 回调函数、事件监听、发布/订阅、Promise、Generator
GGenerator 类似协程 可以移交函数的执行权
Generator 函数可以暂停执行和恢复执行、函数体内外的数据交换和错误处理机制。
Thunk 函数: 编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。
JS 中的 Thunk 函数 :任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。
// ES6版本
const Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
19. async 函数
async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。
正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。
可以通过try catch 让程序继续执行 或者promise后面加 .catch
async function f() {
try {
await Promise.reject('出错了');
} catch(e) {
}
return await Promise.resolve('hello world');
}
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject。
多个异步操作没有先后顺序时 尽量同步进行
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
map 是并发执行的
20. Class 的基本语法
class Point {
constructor(){
// ...
}
}
Point.prototype.constructor === Point // true
类的内部所有定义的方法,都是不可枚举的(non-enumerable)。
类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。
Class 表达式
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
// 这个类的名字是MyClass而不是Me,Me只在 Class 的内部代码可用,指代当前类。
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
采用 Class 表达式,可以写出立即执行的 Class。
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('张三');
person.sayName(); // "张三"
类不存在变量提升(hoist)
new Foo(); // ReferenceError
class Foo {}
name 属性 Point.name // "Point"
静态方法
如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
父类的静态方法,可以被子类继承。
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod() // 'hello'
实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层。
class IncreasingCounter {
_count = 0;
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}
静态属性(提案)
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // 42
}
}
new.target 属性
ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。
function Person(name) {
if (new.target !== undefined) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
21. Class 的继承
super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
- 第一种情况,super作为函数调用时,代表父类的构造函数。
super虽然代表了父类的构造函数,但是 super 内部的this指的是子类的实例。
- 第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。
由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。
class A {
}
class B {
}
class B extends A {
}
// 相当于
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);
原生构造函数是指语言内置的构造函数。extends 可以继承自原生构造函数。
22. Decorator 装饰器
修饰器是一个对类进行处理的函数。修饰器函数的第一个参数,就是所要修饰的目标类。
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;
如果觉得一个参数不够用,可以在修饰器外面再封装一层函数。
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true
@testable(false)
class MyClass {}
MyClass.isTestable // false
也就是说 @
后面可以是一个值为函数表达式
应用
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}
修饰器不仅可以修饰类,还可以修饰类的属性。修饰器第一个参数是类的原型对象,第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象。
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}
function readonly(target, name, descriptor){
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor;
}
readonly(Person.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor);
如果同一个方法有多个修饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。
function dec(id){
console.log('evaluated', id);
return (target, property, descriptor) => console.log('executed', id);
}
class Example {
@dec(1)
@dec(2)
method(){}
}
// evaluated 1
// evaluated 2
// executed 2
// executed 1
也就是说,计算出修饰器的表达式是按顺序执行的,修饰器的执行是倒序的。
修饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。
23. Module 的语法
CommonJS 模块就是对象,输入时必须查找对象属性。是“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";。
export 语法
export var year = 1958;
export function multiply(x, y) {
return x * y;
};
export { year, multiply };
通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
// 代码输出变量foo,值为bar,500 毫秒之后变成baz。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新。
export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,import命令也是如此。
这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
import 语法
import {firstName, lastName, year} from './profile.js';
import { lastName as surname } from './profile.js';
// 整体加载
import * as circle from './circle';
import命令输入的变量都是只读的。 但是,如果a是一个对象,改写a的属性是允许的。
注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。
由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
import语句会执行所加载的模块,如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。也就是说,import语句是 Singleton 模式。
export default 默认导出模块,加载时可以指定任意名字
// 函数名在模块外部无效 视同匿名函数加载。
export default function foo() {
console.log('foo');
}
import customName from './export-default';
本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。
// modules.js
function add(x, y) {
return x * y;
}
export {add as default};
// 等同于
// export default add;
// app.js
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';
正是因为export default命令其实只是输出一个叫做default的变量,本质是将后面的值,赋给default变量,所以它后面不能跟变量声明语句。
// 正确
export var a = 1;
// 正确
var a = 1;
export default a;
// 错误
export default var a = 1;
如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
// 接口改名
export { foo as myFoo } from 'my_module';
// 整体输出
export * from 'my_module';
// 具名接口改为默认接口的写法如下。
export { es6 as default } from './someModule';
24. Module 的加载实现
<script>
标签打开defer或async属性,脚本就会异步加载。
- defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;
- async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。
- defer是“渲染完再执行”,async是“下载完就执行”。
- 如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
浏览器加载 ES6 模块,也使用<script>
标签,但是要加入type="module"属性。
模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。
<script type="module" src="./foo.js"></script>
<script type="module" src="./bar.js"></script>
js 文件
// foo.js
export const x = 1;
// bar.js 注意 这里 './foo.js' 的文件后缀不能省略!
import { x } from './foo.js';
console.log(x);
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
export通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。
ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块