【学习笔记】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 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。

    1. 首先遍历所有数值键,按照数值升序排列。
    2. 其次遍历所有字符串键,按照加入时间升序排列。
    3. 最后遍历所有 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指向当前模块

posted @ 2019-01-20 16:56  我不吃饼干呀  阅读(576)  评论(0编辑  收藏  举报