ES6的学习

ES6变量声明

let命令
  • 声明变量,只在所在的代码块有效,处于暂时性死区
  • for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
  • 变量不提升,不允许重复声明
  • 允许在块级作用域内声明函数,函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
  • ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
const命令
  • const声明一个只读的常量。一旦声明,常量的值就不能改变。且必须立即初始化,不能留到以后赋值。只声明不赋值,就会报错。
  • const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
将对象彻底冻结
  • 简单冻结 :const foo = Object.freeze({});

  • 对象及属性都冻结:

    var constantize = (obj) => {

    Object.freeze(obj);

    Object.keys(obj).forEach((key,i) =>{

    if( typeof obj[key] === ‘object’) {

    constantize(obj[key]);}

    });};

import命令
class命令
顶层对象
  • 浏览器环境中的顶层对象指window对象,node指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。
    • 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window
    • 浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self
    • Node 里面,顶层对象是global,但其他环境都不支持。
  • 一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。
  • 同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this变量,但是有局限性。
    • 全局环境中,this会返回顶层对象。但是,Node 模块和 ES6 模块中,this返回的是当前模块。
    • 函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined
    • 不管是严格模式,还是普通模式,new Function('return this')(),总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么evalnew Function这些方法都可能无法使用。

数组解构

  • 只需模式匹配即可用数组方式为各种数据类型赋值
  • 解构赋值允许指定默认值。只有当一个数组成员严格等于undefined,默认值才会生效,如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined

this

  • 全局环境中,this会返回顶层对象(浏览器是window,Node是global)。但是,Node模块和ES6模块中,this返回当前块
  • 函数里面的this,如果函数不是作为对象的方法执行,而是单纯作为函数运行,this会指向顶层对象。但是在严格模式下,这时的this会返回undefined
  • 不管是严格模式还是普通模式,new Function(‘return this’)(),总是返回全局对象。但是,如果浏览器用了CSP(Content Security Policy,内容安全策略),那么eval,new Function这些方法都可能不能使用。

字符串

  • 字符串的遍历器接口

    • for(let 变量名 of ‘遍历对象’){}使得字符串如字符串数组般被遍历。

    • 除了遍历字符串,这个遍历器最大的优点是可以识别大于0xFFFF的码点,传统的for循环无法识别这样的码点

      let text = String.fromCodePoint(0x20BB7);

      for(let i = 0;i < text.length; i++){

      ​ console.log(text[i]);

      }

      //“ “

      // ” “

      //for循环认为其包含两个字符

      for(let i of text){

      ​ console.log(text[i]);

      }

      //”吉“

      //for…of循环会正确识别出这一个字符

  • 为了确保返回的是合法的 UTF-8 字符,ES2019 改变了JSON.stringify()的行为。如果遇到0xD8000xDFFF之间的单个码点,或者不存在的配对形式,它会返回转义字符串,留给应用自己决定下一步的处理。

    1
    2
    JSON.stringify('\u{D834}') // ""\\uD834""
    JSON.stringify('\uDF06\uD834') // ""\\udf06\\ud834""
  • 模板字符串

    • 模板字符串(template string)是增强版的字符串,用反引号(~)标识。他可以当作普通字符串使用,也可以用来定义多行字符串,或在字符串中嵌入变量。

      1
      2
      3
      4
      5
      6
      7
      > $('#result').append(
      > 'There are <b>' + basket.count + '</b> ' +
      > 'items in your basket, ' +
      > '<em>' + basket.onSale +
      > '</em> are on sale!'
      > );
      >
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      > // 普通字符串
      > `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}?`
      >
    • 上面的代码都是用反引号表示,如果在模板字符串中需要使用反引号,则前面要用反斜杠转义

      1
      2
      > let greeting = `\`Yo\` World!`;
      >
    • 如果使用模板字符串表示多行字符串,所有空格和缩进都会被保留在输出之中。如不想这个换行,可使用trim方法消除他。

      1
      2
      3
      4
      5
      6
      7
      > $('#list').html(`
      > <ul>
      > <li>first</li>
      > <li>second</li>
      > </ul>
      > `.trim());
      >
    • 模板字符串嵌入变量,需要将变量名写在${}之中。还可以调用函数大括号内部可以放入任意的js表达式,可进行运算,以及引用对象属性。

    • 如果大括号内的值不是字符串,将按照一般规则转为字符串。若是字符串则会调用对象的toString方法。若模板字符串的变量没有声明,将会报错。

  • 标签模板

    • 它可紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。

      1
      2
      3
      alert `123`
      //等价于
      alert(123);
    • 模板字符串里面如果有变量,就不是简单调用,而是将模板字符串先处理成多个参数,在调用函数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      let a = 5;
      let b = 10;

      tag`Hello ${ a + b } world ${ a * b }`;
      // 等同于
      tag(['Hello ', ' world ', ''], 15, 50);
      function tag(stringArr, ...values){
      // ...
      }

字符串新增方法

  • String.fromCodePoint()

    • 可以识别大于0xFFFF的字符,弥补了String.fromCharCode()方法的不足。
  • String.raw()

    • 该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法。
  • codePointAt()

  • normalize()

    • 用来将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化。
确定一个字符串是否包含在另一个字符串中
  • indexof()

  • includes():返回布尔值,表示是否找到了参数字符串。

  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。

  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。

  • repeat():返回一个新字符串,表示将原字符串重复n次。
    • 其参数会先进行往0方向取整再代值,如是负数或Infinity会报错。参数NaN等同于 0
1
2
3
'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""
  • padStart()用于头部补全,padEnd()用于尾部补全。如果某个字符串不够指定长度,会在头部或尾部补全。
1
2
3
4
5
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'

'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
  • trimStart()trimEnd()这两个方法。它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。
  • matchAll()方法返回一个正则表达式在当前字符串的所有匹配

正则表达式

  • 如果正则构造函数的第一个参数是一个正则对象,那么就可以使用第二个参数指定修饰符。且返回的正则表达式会忽略原有的正则表达式修饰符,只使用新指定的修饰符。

    1
    2
    new RegExp(/abc/ig,'i').flags
    //"i"
  • 字符串对象共有 4 个方法,可以使用正则表达式: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]
具名组匹配
1
2
3
4
5
6
7
> const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;
>
> const matchObj = RE_DATE.exec('1999-12-31');
> const year = matchObj[1]; // 1999
> const month = matchObj[2]; // 12
> const day = matchObj[3]; // 31
>
  • 允许为每一个组匹配指定一个名字。模式的头部添加“问号 + 尖括号 + 组名”,然后就可以在exec方法返回结果的groups属性上引用该组名。

    1
    2
    3
    4
    5
    6
    7
    > 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
    >
  • RegExp.prototype.flags 属性返回正则表达式的修饰符。

先行断言

“先行断言”指的是,x只有在y前面才匹配,必须写成/x(?=y)/。比如,只匹配百分号之前的数字,要写成/\d+(?=%)/。“先行否定断言”指的是,x只有不在y前面才匹配,必须写成/x(?!y)/。比如,只匹配不在百分号之前的数字,要写成/\d+(?!%)/

1
2
/\d+(?=%)/.exec('100% of US presidents have been male')  // ["100"]
/\d+(?!%)/.exec('that’s all 44 of them') // ["44"]
后行断言

“后行断言”正好与“先行断言”相反,x只有在y后面才匹配,必须写成/(?<=y)x/。比如,只匹配美元符号之后的数字,要写成/(?<=\$)\d+/。“后行否定断言”则与“先行否定断言”相反,x只有不在y后面才匹配,必须写成/(?<!y)x/。比如,只匹配不在美元符号后面的数字,要写成/(?<!\$)\d+/

1
2
/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill')  // ["100"]
/(?<!\$)\d+/.exec('it’s is worth about €90') // ["90"]
属性
  • RegExp.prototype.unicode属性表示是否设置了u修饰符。
  • y修饰符隐含了头部匹配的标志^

函数的拓展

  • ES6允许为函数的参数设置默认值,即直接写在参数定义的后面

    function log(x,y=’world’){

    ​ console.log(x,y);

    }

    • 好处:阅读代码的人可立即意识到哪些参数可以省略,不用查看函数体或文档。其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数也不会导致以前的代码无法运行

    • 参数变量是默认声明的,故不能用let和const再次声明。否则会报错

    • 使用参数默认值时,函数不能有同名参数

    • 参数默认值不是传值的,而是每次都重新计算默认值表达式的值,即参数默认值是惰性求值的。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      > let x = 99;
      > function foo(p = x + 1) {
      > console.log(p);
      > }
      >
      > foo() // 100
      >
      > x = 100;
      > foo() // 101
      > //参数p的默认值是x + 1。这时,每次调用函数foo,都会重新计算x + 1,而不是默认p等于 100。
      >
  • 函数与解构默认值结合使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    > function foo({x, y = 5}) {
    > console.log(x, y);
    > }
    >
    > foo({}) // undefined 5
    > foo({x: 1}) // 1 5
    > foo({x: 1, y: 2}) // 1 2
    > foo() // TypeError: Cannot read property 'x' of undefined
    >
    • 上面代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当foo的参数是一个对象时,变量x和y才会通过解构赋值生成。如果函数foo调用时没提供参数,变量x和y就不会生成,从而报错。通过提供函数参数的默认值就可避免这种情况。

      function foo({x,y=5} = {}){

      ​ console.log(x,y);

      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      > // 函数没有参数的情况
      > m1() // [0, 0]
      > m2() // [0, 0]
      >
      > // x 和 y 都有值的情况
      > m1({x: 3, y: 8}) // [3, 8]
      > m2({x: 3, y: 8}) // [3, 8]
      >
      > // x 有值,y 无值的情况
      > m1({x: 3}) // [3, 0]
      > m2({x: 3}) // [3, undefined]
      >
      > // x 和 y 都无值的情况
      > m1({}) // [0, 0];
      > m2({}) // [undefined, undefined]
      >
      > m1({z: 3}) // [0, 0]
      > m2({z: 3}) // [undefined, undefined]
      >
    • 参数默认值一般是尾参数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      > function f(x, y = 5, z) {
      > return [x, y, z];
      > }
      >
      > f() // [undefined, 5, undefined]
      > f(1) // [1, 5, undefined]
      > f(1, ,2) // 报错
      > f(1, undefined, 2) // [1, 5, 2]
      >
  • 函数的length属性

    • 指定了默认值后,函数的length属性将返回没有指定默认值的参数个数。也就是说指定了默认值后,length属性将失真。

    • length属性的含义是该函数预期传入的参数个数,某个指定了默认之后,预期传入的参数个数就不包括这个参数了。

    • 如果设置默认值的参数不是尾参数,那么length属性将不再计入后面的参数了。

      1
      2
      3
      4
      5
      6
      7
      > (function(a){}).length  //1
      > (function(a=5){}).length //0
      > (function(a,b,c=9){}).length //2
      > (function(...args){}).length //0
      > (function(a=0,b,c){}).length //0
      > (function(a,b=2,c){}).length //1
      >
  • 作用域

    • 一旦设置了参数默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用于就会消失。这种情况在没有默认参数值是不会出现的。

      var x = 1;

      function f(x, y = x){

      ​ console.log(y);

      }

      f(2) //2

      //上面代码中,参数y的默认值等于变量x。调用函数时,参数会形成一个单独的作用域。在这个作用域里,默认变量x指向第一个参数x,而不是全局变量x,所以输出的是2

      let x = 1;

      function f(y=x){

      ​ let x = 2;

      ​ console.log(y);

      }

      f() //1

      //函数f调用时,参数y = x形成一个单独的作用域。这个作用域里面,变量x本身没有定义,所以指向外层的全局变量x。函数调用时,函数体内部的局部变量x影响不到默认值变量x。若全局变量x不存在,就会报错。

    • 参数x = x形成一个单独作用域。实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错”x 未定义“。

      1
      2
      3
      4
      5
      6
      7
      8
      > var x = 1;
      >
      > function foo(x = x) {
      > // ...
      > }
      >
      > foo() // ReferenceError: x is not defined
      >
    • 函数的参数默认值是一个匿名函数,当函数参数形成的单独作用域里面并没有定义该变量时,该变量就会指向外层的全局变量。若函数外层并无该变量,则会报错。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      > var x = 1;
      > function foo(x, y = function() { x = 2; }) {
      > var x = 3;
      > y();
      > console.log(x);
      > }
      >
      > foo() // 3
      > x // 1
      >
  • rest参数

    ​ function 函数名(…变量名){}

    • rest参数搭配的变量是一个数组,该变量会将多余的参数放入数组中。
    • 与arguments对象不同,arguments对象是一个类数组。
    • rest参数之后不能有其他参数(及只能是是最后一个参数),否则会报错。当然,函数的length属性也不包括rest参数。
  • 严格模式下函数参数不能使用默认值,解构赋值或拓展运算符。两种解决方法:

    • 设定全局式的严格模式。

      ‘use strict’;

      function (a,b=a){

      //code

      }

    • 把函数包在一个无参数的立即执行函数中。

      const doSomething = (function(){

      ‘use strict’;

      return function(value = 42){

      ​ return value;

      };

      }());

  • name属性返回函数的函数名

    • 如果将一个匿名函数赋值给一个变量,Es5的name属性会返回空字符串,ES6返回实际函数名。

    • 如果将一个具名函数赋值给一个变量,两者都返回这个具名函数原本的名字。

      const bar = function baz(){};

      //ES5

      bar.name //“baz”

      //ES6

      bar.name //“baz”

    • Function构造函数返回的函数实例,name属性的值为anonymous

      (new Function).name //“anonymous”

    • bind返回的函数,name属性值会加上bound前缀

      1
      2
      3
      4
      5
      > function foo() {};
      > foo.bind({}).name // "bound foo"
      >
      > (function(){}).bind({}).name // "bound "
      >
箭头函数(=>)

var f= v => v;

//等同于

var f = function(v){

return v;

};

  • 由于大括号被解释为代码块,故如果箭头函数直接返回一个对象会报错,必须在对象外面加上括号;

    //报错

    let gettempItem = id => { id: id, name: “temp” };

    //不报错

    let gettempItem = id => ({ id: id, name: “temp”})

  • 使用箭头函数注意点

    • 函数体内的this对象,也就是定义时所在的对象,而不是使用是所在的对象。this对象的指向在js中是可变的。但在箭头函数中则是固定的。
    • 不可以当作构造函数,即不可使用new命令,否则会抛出一个错误。
    • 不可使用arguments对象,该对象在函数体内不存在。如果要用,可用rest参数替换。也没有super,new.target
    • 不可使用yield命令,因此箭头函数不能用作Generator函数。
  • this指向的固定化,并不是因为内部有绑定this机制,实际原因是箭头函数没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

  • 箭头函数不适用场景

    • 定义对象的方法,且该方法内部包含this。调用cat.jumps()时,如果是普通函数,该方法内部的this指向cat;如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域。

      1
      2
      3
      4
      5
      6
      7
      8
      > const cat = {
      > lives: 9,
      > jumps: () => {
      > this.lives--;
      > }
      > }
      > //上面的this指向的是全局作用域,不是cat
      >
    • 需要动态this时也不应使用箭头函数。下面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。

      var button = document.getElementById(‘press’);

      button.addEventListener(‘click’,()=>{

      ​ this.classList.toggle(‘on’);

      });

尾调用(Tail Call)
  • 尾调用优化只在严格模式下开启,正常模式是无效的,因为正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。

    • func.arguments:返回调用时函数的那个参数。

    • func.caller:返回调用函数当前函数的那个函数。

      1
      2
      3
      4
      5
      6
      function restricted() {
      'use strict';
      restricted.caller; // 报错
      restricted.arguments; // 报错
      }
      restricted();
  • 尾调用是函数式编程的一个重要概念。指某个函数的最后一步是调用另一个函数。

  • 尾调用之所以与其他调用不同就在于其调用位置。我们知道,函数调用会在内存中形成一个”调用记录“,又称“调用帧”,保存调用的位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方还会形成B的调用帧。等到B运行结束,将结果返回到AB的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。

  • 尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。

  • 尾调用优化,即只保留内层函数的调用帧。如果所有函数都是尾调用,就可以完全做到每次执行一次时,调用帧只有一项,这将大大节省内存。注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。

尾递归
  • 尾部调用自身即为尾递归。

  • 递归是非常耗内存的,因为需要同时保存成千上万个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

    function factorial(n,total){

    ​ if(n === 1) return total;

    ​ return factorial(n-1,n*total);

    }

  • 尾调用优化的Fibonacci数列

    function Fibonacci2(n,ac1 = 1, ac2 = 1){

    ​ if(n<=1) {return ac2};

    ​ return Fibonacci2(n-1,ac2,ac1+ac2);

    }

数组

  • Array.from()将两类对象(类数组和可便利的对象)转为真正的数组。

    • 只要是部署了Iterator接口的数据结构,Array.from就都可以将其转为真正的数组。
    • 扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个对象就无法转换。
    • 类数组对象本质是有length属性,即只要有length属性的对象都可以通过Array.from方法转为数组。

    //es5

    var arr1 = [].slice.call(arrayLike);

    //es6

    let arr2 = Array.from(arrayLike);

    //NodeList对象

    let ps = document.querySelectorAll(‘p’);

    Array.from(ps).filter(p => {

    ​ return p.textContent.length.length >100;

    });

    //arguments对象

    function foo(){

    var args = Array.from(arguments);

    }

    • 对于没有部署该方法的浏览器可使用Array.prototype.slice方法替换。

      const toArray = ( () =>

      ​ Array.from ? Array.from : obj => [].slice.call(obj);

      )( );

    • Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

      Array.from(arrayLike,x => x * x);

      Array.from(arrayLike).map( x=> x*x);

      //两者等价

  • Array.of()用于将一组值转换为数组。

    1
    2
    3
    4
    > Array.of(3, 11, 8) // [3,11,8]
    > Array.of(3) // [3]
    > Array.of(3).length // 1
    >
    • 下面代码中,Array方法没有参数、一个参数、三个参数时,返回结果都不一样。只有当参数个数不少于 2 个时,Array()才会返回由参数组成的新数组。参数个数只有一个时,实际上是指定数组的长度。

      1
      2
      3
      4
      > Array() // []
      > Array(3) // [, , ,]
      > Array(3, 11, 8) // [3, 11, 8]
      >
  • Array.prototype.copyWithin(target,stsart=0,end=this.length)

    • 数组实例的copyWithin()方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
  • 查找

    • find():用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找到第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。回调函数可接受三个参数,依次为当前的值,当前的位置和原数组。

      [1,2,3,10,15].find(function(value,index,arr){

      ​ return value > 9;

      }) //10

    • findIndex():返回第一个符合条件的数组成员的位置,如果都不符合则返回-1;

    • 这两个方法都可以接受第二个参数,用于绑定回调函数的this对象。

      function f(v){

      ​ return v > this.age;

      }

      let person = {name:’john’,age:20};

      [12,15,26,42].find(f,person); //26

    • 都可发现NaN,弥补数组中indexOf方法的不足。indexOf方法无法识别数组的NaN成员,但是findIndex方法可以借助Object.is方法做到。

      [NaN].indexOf(NaN)

      //-1

      [NaN].findIndex(y => Object.is(NaN,y))

      //0

  • fill()使用给定值进行数组填充

    • 如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。
    • fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。
    • fill方法从 1 号位开始,向原数组填充 7,到 2 号位之前结束。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    > ['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()——用于遍历数组。它们都返回一个遍历器对像,可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

    • 如果不使用for...of循环,可以手动调用遍历器对象的next方法,进行遍历。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      > for (let [index, elem] of ['a', 'b'].entries()) {
      > console.log(index, elem);
      > }
      > // 0 "a"
      > // 1 "b"
      > let letter = ['a', 'b', 'c'];
      > let entries = letter.entries();
      > console.log(entries.next().value); // [0, 'a']
      > console.log(entries.next().value); // [1, 'b']
      > console.log(entries.next().value); // [2, 'c']
      >
  • includes()

    • Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似

    • 该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。

      1
      2
      3
      4
      > [1, 2, 3].includes(2)     // true
      > [1, 2, 3].includes(4) // false
      > [1, 2, NaN].includes(NaN) // true
      >
      1
      2
      3
      > [1, 2, 3].includes(3, 3);  // false
      > [1, 2, 3].includes(3, -1); // true
      >
    • 没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值.

      1
      2
      3
      4
      5
      6
      7
      8
      > if (arr.indexOf(el) !== -1) {
      > // ...
      > }
      > [NaN].indexOf(NaN)
      > // -1
      > [NaN].includes(NaN)
      > // true
      >
      • indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。
      1
      2
      3
      4
      5
      6
      7
      8
      > const contains = (() =>
      > Array.prototype.includes
      > ? (arr, value) => arr.includes(value)
      > : (arr, value) => arr.some(el => el === value)
      > )();
      > contains(['foo', 'bar'], 'baz'); // => false
      > //简易的替代版本。
      >
    • 另外,Map 和 Set 数据结构有一个has方法,需要注意与includes区分。

      • Map 结构的has方法,是用来查找键名的,比如Map.prototype.has(key)WeakMap.prototype.has(key)Reflect.has(target, propertyKey)
      • Set 结构的has方法,是用来查找值的,比如Set.prototype.has(value)WeakSet.prototype.has(value)
  • flat()和flatMap()

    • 数组的成员有时还是数组,Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

    • flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为

    • 可以用Infinity关键字作为参数。

    • 如果原数组有空位,flat()方法会跳过空位。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      > [1, 2, [3, 4]].flat()
      > // [1, 2, 3, 4]
      > [1, 2, [3, [4, 5]]].flat(2)
      > // [1, 2, 3, 4, 5]
      > [1, [2, [3]]].flat(Infinity)
      > // [1, 2, 3]
      > [1, 2, , 4, 5].flat()
      > // [1, 2, 4, 5]
      >
    • flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。flatMap()只能展开一层数组。

      1
      2
      3
      4
      5
      6
      7
      > // 相当于 [[2, 4], [3, 6], [4, 8]].flat()
      > [2, 3, 4].flatMap((x) => [x, x * 2])
      > // [2, 4, 3, 6, 4, 8]
      > // 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
      > [1, 2, 3, 4].flatMap(x => [[x * 2]])
      > // [[2], [4], [6], [8]]
      >
    • flatMap()方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。flatMap()方法还可以有第二个参数,用来绑定遍历函数里面的this

      1
      2
      3
      4
      > arr.flatMap(function callback(currentValue[, index[, array]]) {
      > // ...
      > }[, thisArg])
      >
  • 数组中的空位

    • 数组的空位指,数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位。空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。

    • 1
      2
      3
      > 0 in [undefined, undefined, undefined] // true
      > 0 in [, , ,] // false
      >
    • ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位.

      • forEach(), filter(), reduce(), every()some()都会跳过空位。
      • map()会跳过空位,但会保留这个值
      • join()toString()会将空位视为undefined,而undefinednull会被处理成空字符串。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      > // forEach方法
      > [,'a'].forEach((x,i) => console.log(i)); // 1
      >
      > // filter方法
      > ['a',,'b'].filter(x => true) // ['a','b']
      >
      > // every方法
      > [,'a'].every(x => x==='a') // true
      >
      > // reduce方法
      > [1,,2].reduce((x,y) => x+y) // 3
      >
      > // some方法
      > [,'a'].some(x => x !== 'a') // false
      >
      > // map方法
      > [,'a'].map(x => 1) // [,1]
      >
      > // join方法
      > [,'a',undefined,null].join('#') // "#a##"
      >
      > // toString方法
      > [,'a',undefined,null].toString() // ",a,,"
      >
    • Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。

    • 扩展运算符(...)也会将空位转为undefined

    • copyWithin()会连空位一起拷贝。

    • fill()会将空位视为正常的数组位置。

    • for...of循环也会遍历空位.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      > Array.from(['a',,'b'])
      > // [ "a", undefined, "b" ]
      > [...['a',,'b']]
      > // [ "a", undefined, "b" ]
      > [,'a','b',,].copyWithin(2,0) // [,"a",,"a"]
      > new Array(3).fill('a') // ["a","a","a"]
      > let arr = [, ,];
      > for (let i of arr) {
      > console.log(1);
      > }
      > // 1
      > // 1
      >
    • entries()keys()values()find()findIndex()会将空位处理成undefined。由于空位的处理规则非常不统一,所以建议避免出现空位。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      > // entries()
      > [...[,'a'].entries()] // [[0,undefined], [1,"a"]]
      >
      > // keys()
      > [...[,'a'].keys()] // [0,1]
      >
      > // values()
      > [...[,'a'].values()] // [undefined,"a"]
      >
      > // find()
      > [,'a'].find(x => true) // undefined
      >
      > // findIndex()
      > [,'a'].findIndex(x => true) // 0
      >

对象的扩展

属性遍历
  • for…in:遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。

  • Object.keys(obj):返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。

  • Object.getOwnPropertyNames(obj):返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。

  • Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有 Symbol 属性的键名。

  • Reflect.ownKeys(obj):返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

  • 以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。

    • 首先遍历所有数值键,按照数值升序排列。
    • 其次遍历所有字符串键,按照加入时间升序排列。
    • 最后遍历所有 Symbol 键,按照加入时间升序排列。
    1
    2
    3
    > Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
    > // ['2', '10', 'b', 'a', Symbol()]
    >
super关键字
  • this关键字总是指向函数所在的当前对象,关键字super则是指向当前对象的原型对象,super只能用在对象的方法中,用在其他地方会报错。

  • JavaScript 引擎内部,super.foo等同于Object.getPrototypeOf(this).foo(属性)或Object.getPrototypeOf(this).foo.call(this)(方法)。

    const proto ={foo:’hello’};

    const obj = {

    ​ foo:’world’,

    ​ find(){

    ​ return super.foo;

    ​ }

    }

    Object.setPrototypeOf(obj,proto);

    obj.find(); //“hello”

    //报错

    const obj = {foo:super.foo; //对象的属性}

    //只有对象方法的简写式才可以被js引擎确认定义的是对象的方法。

    //以下两种都报错报错

    const obj = {

    ​ foo:() => super.foo

    }

    1
    2
    3
    4
    5
    6
    > const obj = {
    > foo: function () {
    > return super.foo
    > }
    > }
    >
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const proto = {
x: 'hello',
foo() {
console.log(this.x);
},
};

const obj = {
x: 'world',
foo() {
super.foo();
}
}

Object.setPrototypeOf(obj, proto);

obj.foo() // "world"

上面代码中,super.foo指向原型对象protofoo方法,但是绑定的this却还是当前对象obj,因此输出的就是world

解构赋值
  • 扩展运算符的解构赋值,不能复制继承自原型对象的属性。
1
2
3
4
5
6
7
> let o1 = { a: 1 };
> let o2 = { b: 2 };
> o2.__proto__ = o1;
> let { ...o3 } = o2;
> o3 // { b: 2 }
> o3.a // undefined
>

对象新增方法

  • Object.is():在所有环境中,只要两个值是一样的,他们就相等

    • 与严格相等(===)功能差不多,但是弥补了NaN不等于自身的缺陷。

    • +0不等于-0

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      > Object.is('foo', 'foo')
      > // true
      > Object.is({}, {})
      > // false
      > +0 === -0 //true
      > NaN === NaN // false
      >
      > Object.is(+0, -0) // false
      > Object.is(NaN, NaN) // true
      >
  • Object.assign()用于对象的合并,将源对象的所有可枚举属性,复制到目标对象。

    • Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。

    • 如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

    • 如果只有一个参数,Object.assign会直接返回该参数。

    • 如果该参数不是对象,则会先转成对象,然后返回。由于undefinednull无法转成对象,所以如果它们作为参数,就会报错。

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      > const target = { a: 1, b: 1 };
      >
      > const source1 = { b: 2, c: 2 };
      > const source2 = { c: 3 };
      >
      > Object.assign(target, source1, source2);
      > target // {a:1, b:2, c:3}
      >
      > const obj = {a: 1};
      > Object.assign(obj) === obj // true
      > typeof Object.assign(2) // "object"
      > Object.assign(undefined) // 报错
      > Object.assign(null) // 报错
      >
    • 如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefinednull不在首参数,就不会报错。其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。因为只有字符串的包装对象,会产生可枚举属性。

      1
      2
      3
      4
      > let obj = {a: 1};
      > Object.assign(obj, undefined) === obj // true
      > Object.assign(obj, null) === obj // true
      >
      1
      2
      3
      4
      5
      6
      7
      > const v1 = 'abc';
      > const v2 = true;
      > const v3 = 10;
      >
      > const obj = Object.assign({}, v1, v2, v3);
      > console.log(obj); // { "0": "a", "1": "b", "2": "c" }
      >

数组中的遍历

  • reduce求和,求平均值

    1
    2
    3
    4
    5
    arr.reduce((tmp,item,index) => { 
    //tmp是一个前一次的总和
    //item是第几个数的值
    //index是下标值,因第一个tmp是第一个arr[0],所以index是从1开始
    })
  • map 映射

    1
    2
    3
    4
    5
    arr.map((item) => {
    //item是第几个数的值
    return item*2;
    });
    arr.map(item => item*2);
  • filter过滤器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    arr.filter(item => {
    //返回true者留下,返回false消失
    if(item % 3 == 0){
    return true;
    }else{
    return false;
    }
    })
    //相当于
    arr.filter(item => item%3 == 0);
  • forEach 循环迭代

    1
    2
    3
    arr.forEach( (item,index) => {

    })

字符串新增方法

  • startsWith:检测str是否以某些字符的开头
  • endsWith:检测str是否以某个字符的结尾
  • 字符串模板 :${变量}

面向对象class类

1
2
3
4
5
6
7
8
9
class User{
constructor(name,pass){
this.name = name;
this.pass = pass;
}
showName(){
alert(this.name);//不可以使用function,不用加逗号,表示属性,而非方法
}
}
  1. class关键字,构造器和类分开

  2. class里面直接加方法

继承

老版写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function User(name,pass){
this.name = name;
this.pass = pass;
}
User.prototype.showName = function(){
alert(this.name);
}
User.prototype.showPass = function(){
alert(this.pass);
}
function vipUser(name,pass,level){
User.call(this,name,pass);
this.level = level;
}
vipUser.prototype = new User();
vipUser.prototype.constructor = vipUser;
vipUser.prototype.showlevel = function(){
alert(this.level);
};
var v1 = new vipUser('blue','12345',10);
v1.showName();
v1.showlevel();

super——超类,就相当于父类

extends——继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class User{
constructor(name,pass){
this.name = name;
this.pass = pass;
}
showName(){
alert(this.name);
}
showPass(){
alert(this.pass);
}
}
class VipUser extends User{
constructor(name,pass,level){
super(name,pass);
this.level = level;
}
showLevel(){
alert(this.level);
}
}
var v1 = new VipUser('nikita','15243',10);
v1.showName();
v1.showLevel();

json

标准写法:
  • 只能用双引号。
  • 所有名字都必须用引号包起来。
方法:
  • 字符串化:

    1
    2
    JSON.stringify({"a":12,"name":"nikita"});
    //"{"a":12,"name":"nikita"}"
  • 解析为一个json对象:

    1
    2
    JSON.parse({"a":12,"name":"nikita"});
    //{name: "nikita", age: 12}

Promise(消除异步操作)

  • Promise对象只有三种状态:peding(进行中),fulfilled(已成功),rejected(已失败)。

  • Promise实例具有then方法,then返回一个新的Promise实例。Promise对象,如果该对象状态变为resolved,则会调用then方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch方法指定的回调函数,处理这个错误。另外,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。

    1
    2
    3
    4
    5
    6
    p.then((val) => console.log('fulfilled:', val))
    .catch((err) => console.log('rejected', err));

    // 等同于
    p.then((val) => console.log('fulfilled:', val))
    .then(null, (err) => console.log("rejected:", err));
  • 用同步一样的方式来书写异步代码。接收一个含resolve和reject的函数参数。

  • resolve——解决,即成功 reject——拒绝,即失败

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    let p = new Promise(function(resolve,reject){
    //异步代码
    $.ajax({
    url:'arr.txt',
    dataType:'json',
    success(arr){
    resolve(arr);
    },
    error(err){
    reject(err);
    }
    })
    });
    //then函数有两个参数,一个是成功的回调函数,一个是失败的回调函数
    p.then(function(){
    alert('成功了');
    },function(){
    alert('失败了');
    })
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    //若有两个Promise对象,可使用Promise.all进行全部处理
    //其中的arr是p1和p2中的数据
    function createPromise(url){
    return new Promise(function(resolve,reject){
    $.ajax({
    url,
    dataType:'json',
    success(arr){
    resolve(arr);
    },
    error(err){
    reject(err);
    }
    })
    });
    }
    Promise.all([
    createPromise('data/arr.txt'),
    createPromise('data/json.txt')
    ]).then(function(arr){
    //解构赋值
    let [res1,res2] = arr;
    alert('全部成功')
    },function(){
    alert('至少有一个失败');
    })

    高版本jquery也封装了promise对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Promise.all([
    $.ajax({url:'data/arr.txt',dataType:'json'}),
    $.ajax({url:'data/json.txt',dataType:'json'})
    ]).then(function(results){
    let [arr,json] = results;
    alert('成功了');
    },function(err){
    alert(err);
    })

    Promise.race 竞速,即先到先执行

    1
    2
    3
    4
    5
    6
    Promise.race([
    $.ajax({url:'http://a2.taobai.com/data/users'})
    $.ajax({ur4:'http://a2.taobai.com/data/users'})
    $.ajax({ur7:'http://a2.taobai.com/data/users'})
    $.ajax({ur2:'http://a2.taobai.com/data/users'})
    ])
Promise.prototype.finally()
  • 该方法为不管Promise对象最后如何,都会执行的代码。

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

1
2
3
4
5
server.listen(port)
.then(function () {
// ...
})
.finally(server.stop);
Promise.all()
  • 用于将多个Promise实力包装为一个新的Promise实例。

    1
    const p = Promise.all([p1,p2,p3]);

    p的状态由p1p2p3决定,分成两种情况。

    (1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

    (2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

1
2
3
4
5
6
7
8
9
10
// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON('/post/' + id + ".json");
});

Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
Fetch API是js接口
  • 用于访问和操作HTTP管道的部分,例如请求和响应。

Generator函数

  • 是ES6提供的一种异步编程解决方案,是一个状态机,封装了很多内部状态。执行Generator函数会返回一个遍历器对象,说明Generator函数同时还是一个遍历器对象生成函数,返回的遍历器对象可以一次遍历每个状态。

  • 特征 * yield

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function* test(value1){
    let value2 = yield value1;
    let value3 = yield value2;
    return value3;
    }
    //第一个yield后的值是传入的参数,第二个是第二个next方法传入的...
    var hw = test(1);
    hw.next(5);
    //第一个next传入的参数无效,不管test有无传参
    //{value:1,done:false}
    hw.next(7)
    //{value:7,done:false}
    hw.next(9)
    //{value:9,done:true}

    调用test函数时,该函数并未执行,而是返回一个指向内部状态的指针对象。而调用该函数的next方法则是将指针移向下一个状态,next有位置记忆功能,故每次调用next方法时,都从上一次停下来的地方执行,直到下一个yield或return为止。return与yield功能相似,但return无位置记忆功能,且每个函数只能有一个return语句。且将return语句后面的表达式作为对象的value属性值,若无return,则返回undefined

    当Generator函数运行完毕,但还使用next方法时,返回{value:undefined,done:true},便是遍历已结束。

  • next的参数表示上一个yield表达式的返回值,所以第一个next传递参数是无效的。

  • yield表达式若用在另一表达式中,必须放在圆括号内。

  • yield若用作函数参数或放在赋值表达式的右边,可不加括号。

    1
    2
    3
    4
    5
    console.log('hello' + (yield 123));
    function* demo(){
    foo(yield 'a',yield 'b');
    let input = yield;
    }
与Symbol.iterator的关系
  • 任意一个对象的Symbol.iterator方法等同于该对象的遍历器生成函数。把Generator赋值给对象的Symbol.iterator属性,可使该对象具有Iterator接口。
1
2
3
4
5
6
var myIterable = {};
myIterable[Symbol.iterator] = function* (){
yield 1;
yield 2;
}
[...myIterable] //1,2
  • Generator函数执行后返回一个遍历器对象,该对象本身具有iterator接口,执行后返回自身。
1
2
3
4
function* gen(){}
var g = gen();
g[Symbol.iterator] === g
//true
原生js对象添加Iterator接口的方法
  • 除了for...of循环以外,扩展运算符(...)、解构赋值和Array.from方法内部调用的,都是遍历器接口。

  • 原生js对象没有遍历接口,无法使用for…of循环,可通过generator函数为其加上接口便可使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
function* objectEntries(obj){
let propKeys = Reflect.ownKeys(obj);//将obj对象转为数组
for(let propKey of propKeys){
yield [propkey,obj[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };

for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
  • 将Generator函数加到对象的symbol.irerator属性上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* objectEntries() {
let propKeys = Object.keys(this);

for (let propKey of propKeys) {
yield [propKey, this[propKey]];
}
}

let jane = { first: 'Jane', last: 'Doe' };

jane[Symbol.iterator] = objectEntries;

for (let [key, value] of jane) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
Generator.prototype.throw
  • Generator函数返回的遍历器对象都有一个throw方法,可在函数体外抛出错误,然后在Generator函数体内捕获。
Generator.prototype.return
  • Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数。
  • 如果 Generator 函数内部有try...finally代码块,且正在执行try代码块,那么return方法会导致立刻进入finally代码块,执行完以后,整个函数才会结束。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
yield*表达式
  • yield*表达式返回一个表遍历器对象。

  • 如果Generator函数内部调用另一个Generator函数需要在前者函数体内,利用for..of手动完成遍历。若不想手动,则可利用yield*

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}

// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}

// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}

for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
  • yield和yield*都只能放在Generator函数中,不能放在普通函数里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//遍历嵌套数组
var arr = [1,[[2,3],4],[5,6]];
var flag = function* (a){
var length = a.length;
for(let i = 0;i < length;i++){
var item = a[i];
if(typeof item != 'number'){
yield* flag(item);
}else{
yield item;
}
}
}
for(var f of flag(arr)){
console.log(f);
}
// 1, 2, 3, 4, 5, 6
协程
  • 传统的编程语言早就有异步编程的解决方案,即多任务的解决方案。其中一种就是协程,意思是多个线程相互协作,完成异步操作。
    • 第一步,协程A开始执行。
    • 第二步,协程A执行到一半,进入暂停,执行权转移到协程B
    • 第三步,(一段时间后)协程B交还执行权。
    • 第四步,协程A恢复执行。
  • 异步任务的封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var fetch = require('node-fetch');
function* gen(){
var url = 'http://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
//How people build software.
Thunk函数
  • Thunk函数时自动执行Generator函数的一种方法。
  • 参数求值策略有“传值调用”和”传名调用”两种,Thunk函。编译器的“传名调用”实现,往往是将参数放到一个临时函数中,再将这个临时函数传入函数体中,这个函数体即为Thunk函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function f(m){return m*2;}
f(x+5);
//等同于
vat thunk = function(){ return x+5; }
function f(thunk){ return thunk()*2; }

//Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);
Thunkify模块
1
2
3
4
const thunkify = require('thunkify');
const fs = require('fs');
let read = thunkify(fs.readFile);
read('package.json')(function(err,data){});
co模块
  • co模块让你不用编写Generator函数的执行器,Generator函数只要传入co函数即可执行。

    1
    2
    3
    var co = require('co');
    co(generator);//返回一个promise对象,故可用then方法添加回调函数
    co.then(function(){})

co模块的原理

为什么 co 可以自动执行 Generator 函数?

前面说过,Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。

两种方法可以做到这一点。

(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。

(2)Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。

co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数或 Promise 对象。如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co。

基于Promise对象的自动执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const fs = require('fs')
let readFile = function (fileName){
return new Promise(function (resolve,reject){
fs.readFile(fileName,function(err,data){
if(err) return reject(err);
return resolve(data);
});
});
}
var gen = function* (){
let f1 = yield readFile('1.txt');
let f2 = yield readFile('2.txt');
console.log(f1.toString());
console.log(f2.toString());
}
function run(gen){
var g = gen();
function next(data){
var result = g.next(data);
if(result.done) return result.value;
result.value.then(function(data){
next(data);
})
}
next();
}
run(gen);

async函数

  • 引入async函数,使异步操作更方便。跟Generator函数类似,只需将*改为ansyc,yield改为await。
  • async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
ansyc较Generator函数的优点
  • 内置执行器。Generator函数的执行必须靠执行器,才co模块,而async函数自带执行器。即async函数的执行与普通函数一样,使用函数名调用即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const fs = require('fs')
let readFile = function (fileName){
return new Promise(function (resolve,reject){
fs.readFile(fileName,function(err,data){
if(err) return reject(err);
return resolve(data);
});
});
}
//generator函数需要再封装一个run函数自执行
// var gen = function* (){
// let f1 = yield readFile('1.txt');
// let f2 = yield readFile('2.txt');
// console.log(f1.toString());
// console.log(f2.toString());
// }
// function run(gen){
// var g = gen();
// function next(data){
// var result = g.next(data);
// if(result.done) return result.value;
// result.value.then(function(data){
// next(data);
// })
// }
// next();
// }
// run(gen)

//async函数有内置执行器
const asyncReadFile = async function(){
const f1 = await readFile('1.txt');
const f2 = await readFile('2.txt');
console.log(f1.toString());
console.log(f2.toString());
}
asyncReadFile()
  • 更好的语义

    • async:异步
    • await表示紧跟在后面的表达式需要等待结果
  • 更广的适应性。

    • co模块约定,yield命令后面只能是Thunk函数或Promise’对象,而async函数的await命令后面可以是Promise对象和原始类型的值(number,string,boolean,但这时会转为立即resolved的promise对象)。
  • 返回值是 Promise。

    async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

    进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

async使用形式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 函数声明
async function foo() {}

// 函数表达式
const foo = async function () {};

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}

async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

// 箭头函数
const foo = async () => {};

async函数内部return语句返回的值会成为then方法回调函数的参数。

async函数抛出的错误会导致返回的Promise对象变为reject状态。抛出的错误会被catch方法回调函数接收到。

1
2
3
4
5
6
7
8
9
10
async function f(flag){
if(flag) throw new Error('出错了');
return 'hello';
}
f(false).then(
v => console.log(v),
e => console.log(e)
)
//hello
//当flag为true时,返回’出错了‘
  • async函数返回的Promise对象必须等到内部所有await命令后面的Promise对象执行完毕才会发生状态改变,除非遇到return语句或抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
await 命令
  • 正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

  • 另一种情况是,await命令后面是一个thenable对象(即定义then方法的对象),那么await会将其等同于 Promise 对象。

  • 任何一个await语句后面的Promise对象变为reject状态,那么整个async函数就都会中断执行。

    • 要想不中断,可将其放入try…catch结构中。
    • 或者在await后面的Promise对象再跟一个catch方法,处理前面可能出现的错误。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    async function f(){
    try{
    await Promise.reject('出错了');
    }catch(e){
    }
    return await Promise.resolve('hello');
    }
    f().then(v => { console.log(v)});//hello

    async function f(){
    await Promise.reject('出错了')
    .catch(e => {console.log(e);});
    return await Promise.resolve('hello')
    }
    f().then(v => { console.log(v)});
    //出错了
    //hello

    async function f() {
    await new Promise(function (resolve, reject) {
    throw new Error('出错了');
    });
    }

    f()
    .then(v => console.log(v))
    .catch(e => console.log(e))
    // Error:出错了

class类

  • 生成实例对象的传统方法是通过构造函数,引入class类概念,利用constructor构造方法创建实例,类中方法不需要加function,毗邻方法间不许用逗号相隔,加了会报错。在类实例上调用方法其实就是调用原型上的方法。
  • prototype对象的constructor属性,直接指向类本身,与ES5相似。constructor是类的默认方法,无显式指定时会模认自动生成。
1
2
3
4
5
6
class B{
constructor(){}
}
let b = new B();
b.constructor === B.prototype.constructor //true
B.prototype.constructor === B //true
  • 由于类的方法都定义在prototype对象上,故类的新方法可添加在prototype对象上。Object.assign可一次添加多个方法。
1
2
3
4
Object.assign(Point.prototype,{
toString(){},
toValue(){}
});
  • 类内部定义的方法都不可枚举。
1
2
3
4
5
6
Object.keys(Point.prototype)
//Object.keys()返回可枚举的键名
//[]
Object.getOwnPropertyNames(Point.prototype)
//["constructor","toString"]
//返回自身全部属性方法名(包括不可枚举属性)
  • constructor方法默认返回实例对象(即this),可指定返回另外一个对象。
1
2
3
4
class Foo{
constructor(){ return Object.create(null); }
}
new Foo() instanceof Foo
类的几种表示法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class B{}
let b = new B();

const MyClass = class Me{}; //Me可省略,只能在内部使用
let inst = new MyClass();//外部使用类只能用MyClass

//立即执行的类
let person = new class{
constructor(name){
this.name = name;
}
sayName(){
console.log(this.name);
}
}('张三')
person.sayName(); //“张三”
noticePoint
  • 类和模块默认就是严格模式。

  • 类不存在变量提升,类使用在前,定义在后会报错。

  • name属性。ES6的类只是ES5的构造函数的一层包装,故函数的许多特性都被Class继承,包括name属性,name属性总是返回紧跟在class关键字后面的类名。

  • 如果类的Symbol.iterator方法前有一星号,就是Generator方法。该方法会返回类的默认遍历器,for…of循环自动调用这个遍历器。

  • 类内部如果有this默认指向类的实例。printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是undefined),从而导致找不到print方法而报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}

print(text) {
console.log(text);
}
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined

解决方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//1.给构造函数绑定this
class Logger{
constructor(){
this.printName = this.printName.bind(this);
}
}

//2.使用箭头函数
class Obj{
constructor(){
this.getThis = () => this;
}
}
const myObj = new Obj();
myObj.getThis() === myobj //true

//3.使用Proxy,获取方法时自动绑定this
function selfish(target){
const cache = new WeakMap();
const handler = {
get(target,key){
const value = Reflect.get(target,key);
if(typeof value !== 'function'){
return value;
}
if(!cache.has(value)){
cache.set(value,value.bind(target));
}
return cache.get(value);
}
};
const proxy = new Proxy(target,handle);
return proxy;
}
const logger = selfish(new Logger());
  • 类中的方法前加上static关键字,该不会被实例继承,调用只能通过累来调用,称为静态方法。如果静态方法上包含this关键字,这个this指的是类,而不是实例。故静态方法可与非静态方法重名。
  • 父类的静态方法可被子类继承,并可通过super对象屌用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Foo{
static bar(){this.baz();}
static baz(){console.log('helloBar');}
static classMethod(){return 'hello';}
}
Foo.classMethod() //‘hello'
Foo.bar() //'helloBar'
var foo = new Foo();
foo.classMethod()
//TypeError:foo.classMethod is not a function

class Bar extends Foo{
static sonMethod(){
return super.classMethod() + ',too';
}
}
Bar.classMethod() //'hello'
Bar.sonMethods() //'hello,too'
  • 实例属性this._count定义在constructor()方法里面,可将属性也可以定义在类的最顶层,其他都不变。实例属性_count与取值函数value()increment()方法,处于同一个层级。这时,不需要在实例属性前面加上this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class IncreasingCounter {
constructor() {
this._count = 0;
}
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}

class IncreasingCounter {
_count = 0;
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}
  • 静态属性即Class本身属性,必须在前面加上static关键字。
1
2
3
calss Foo{
static prop = 1;
}
  • 子类继承父类,除了使用extends外,必须在子类的constructor上调用super对象,否则new实例时会报错。这是因为子类自己的this对象必须经过父类的构造函数完成塑造,得到与父类同样的实例属性和方法再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。如果在调用super之前使用this会报错。

  • Object.getPrototypeOf()用于在子类中获取父类,可用该方法判断一个类是否继承了另一个类。

1
2
Object.getPrototypeOf(ColorPoint) === Point
//true
super
  • 既可当函数调用,也可当对象使用。
  • super作为函数调用时,代表父类的构造函数。ES6规定,子类的constructor函数必须执行一次super(),且super()函数只能用在子类的构造函数内,用在其他地方会报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
class A{
constructor(){
console.log(new.target.name);//new.target指向当前正在执行的函数
}
}
class B extends A{
constructor(){
super(); //等价于A.prototype.constructor.call(this)
//super内部的this指向子类的实例
}
}
new A() //A
new B() //B
  • super作为对象时,在普通方法中,指向父类的原型对象;静态方法中,指向父类。这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {
constructor() {
this.p = 2;
}
}
A.prototype.x = 3;

class B extends A {
get m() {
return super.p;
}
get x(){
return super.x;
}
}

let b = new B();
b.m // undefined
b.x //3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A {
constructor() {
this.x = 1;
}
}

class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined
console.log(this.x); // 3
}
}

let b = new B();

上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined

  • 如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。
  • 在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Parent {
constructor(){
this.x = '实例的x';
}
static print(){
console.log(this.x);
}
static myMethod(msg) {
console.log('static', msg);
}

myMethod(msg) {
console.log('instance', msg);
}
}

class Child extends Parent {
static m(){
super.print();
}
static myMethod(msg) {
super.myMethod(msg);
}

myMethod(msg) {
super.myMethod(msg);
}
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2

Child.x = '这是类的x';
Child.m() //'这是类的x'
类的prototype属性和__proto__属性
  • 每个对象都有__proto__属性,指向对应的构造函数的prototype属性。
  • 子类的__proto__属性表示构造函数的继承,总是指向父类。
  • 子类的prototype属性的__proto__属性表示方法的继承,总是指向父类的prototype属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A{}
class B extends A{}
B.__proto__ === A //true
B.prototype.__proto__ === A.prototype //true

//B的实例继承A的实例
Object.setPrototypeOf(B.prototype,A.prototype);
//B继承A的静态属性
Object.setPrototypeOf(B,A);

//Object.setPrototypeOf方法实现
Object.setPrototypeOf = function(obj,proto){
obj.__proto__ = proto;
return obj;
}

这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(__proto__属性)是父类(A);作为一个构造函数,子类(B)的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例。

上面代码的A,只要是一个有prototype属性的函数,就能被B继承。由于函数都有prototype属性(除了Function.prototype函数),因此A可以是任意函数。

以下两种特殊情况

  • 子类继承Object类
1
2
3
4
class A extends Object{}
A.__proto__ === Object //true
A.prototype.__proto__ === Object.prototype //true
//A其实就是构造函数Object的复制,A的实例就是Object的实例
  • 不存在任何继承
1
2
3
class A{}
A.__proto__ === Function.prototype //true
A.prototype.__proto__ === Object.prototype //true

A作为一个基类,即不存在任何继承,就是一个普通函数,所以直接继承Function.prototype。但是,A调用后返回一个空对象即object实例,所以A.prototype.__proto__指向构造函数Object的prototype属性。

module模块

  • ES6之前,社区制定了一些模块加载方案。主要是CommonJS——服务器,AMD——浏览器。
1
2
3
4
5
6
7
8
// CommonJS模块,运行时加载,无法做到“运行时加载”
let { stat, exists, readFile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
  • ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
1
2
// ES6模块
import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

除了静态加载带来的各种好处,ES6 模块还有以下好处。

  • 不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
  • 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
  • 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

严格模式主要有以下限制。
  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protectedstaticinterface

其中,尤其需要注意this的限制。ES6 模块之中,顶层的this指向undefined,即不应该在顶层代码使用this

exprot与import
  • export写法:必须提供对外的接口。实质是在接口名与模块内部变量之间建立一一对应的关系。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 报错
export 1;
// 报错
var m = 1;
export m;

// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};

// 报错
function f() {}
export f;
// 正确
export function f() {};
// 正确
function f() {}
export {f};

export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。

  • import命令具有提升效果,会提升到整个模块的头部,首先执行。import命令是编译阶段执行的,在代码运行之前。
  • 使用export default 时,对应的import语句不需要使用大括号;使用export时,对应的import语句需要使用大括号。
1
2
3
4
5
export default function crc32(){}
import crc32 from 'crc32';

export function crc32(){}
import {crc32} from 'crc32';

export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。故import后面才不用加大括号,因为只可能唯一对应export default命令。

本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。

1
2
3
4
5
6
7
8
9
10
11
12
// 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';
异步加载
  • 浏览器异步加载js脚本的方法。<script>标签遇到defer或async属性,脚本就会异步加载,渲染引擎遇到这一命令就会开始下载外部脚本,但不会等他下载和执行,而是直接执行后面的命令。
1
2
<script src=""  defer></script>
<script src="" async></script>

defer与async的区别

defer要等到整个页面在内存中正常渲染结束(DOM结构完全生成,以及脚本执行),才会执行。渲染完再执行,若有多个defer脚本,则按顺序加载。

async是下载后执行,即一旦下载完成,渲染引擎就会中断渲染执行这个脚本后再继续渲染。故不能保证加载顺序。

  • 加载ES6模块,对于带有type="module"的脚本都是异步加载,相当于默认是defer属性,渲染完再加载,可设为async属性。

<script type="module" src=""></script>

  • 外部脚本注意事项:
    • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
    • 模块脚本自动采用严格模式,不管有没有声明use strict
    • 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。
    • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。
    • 同一个模块如果加载多次,将只执行一次。
ES6模块与CommonJS模块的差异
  • commonJS模块输出的是一个值的拷贝,且运行时加载。
    • 值拷贝表明一旦输出后,内部值的改变不影响这个值。
  • ES6模块输出的是值的引用,编译时输出接口。
    • JS引擎对脚本静态分析时,遇到模块加载命令import就会生成一个只读引用,等到脚本真正执行时再根据这个只读引用到被加载模块取值。
CommonJS加载原理
  • CommonJS(加载时执行)的一个模块就是一个脚本文件,require命令第一次加载该脚本就会执行整个脚本,然后在内存生成一个对象。
1
2
3
4
5
6
7
{
//node内部加载模块完成后生成的对象
id:'...', //模块名
exports:{...}, //模块输出的各个接口
loaded:true, //该模块脚本是否执行完毕
...
}

以后用到这个模块时就回到exports属性上取值,即使再执行require也不会再次执行该模块而是到缓存中取值,即CommonJS无论加载多少次都只在第一次加载时运行一次,以后再加载只会返回第一次加载的结果,除非手动清除缓存。

ES6模块转码
  • Babel
  • 转码器:ES6 module transpiler(将es6转为CommonJS或AMD模块)
1
2
3
4
5
npm i -g es6-module-transpiler
//转码
compile-modules convert file1.js file2.js
//-o指定转码后的文件名
compile-modules convert -o out.js file1.js
  • 在网页中导入system.js文件
1
2
3
4
5
<script src="system.js"></script>
<script>
//使用System.import加载模块文件,返回Promise对象
System.import('./模块文件').then(function(){})
</script>

编程风格

  • 注意区分Object与Map,只有模拟现实世界的实体对象时才使用Object。如果只是需要key:value的数据结构,使用Map结构,因其有内建的遍历机制。
  • 总是用class代替prototype操作。使用extends实现继承,不会破环instanceof运算的危险。
  • 模块默认输出一个函数,函数名首字母应该小写,默认输出对象时,对象名首字母大写。

异步遍历器

  • Iterator接口是一种数据遍历的协议,只要调用遍历器对象的next方法就会得到一个对象,表示当前遍历指针所在的那个位置的信息。next方法必须是同步的,返回value和done两个属性值。
  • 若next方法是一个异步操作,可将异步操作包装成Thunk函数或Promise对象,即next方法的返回值的value属性是一个Thunk函数或Promise对象,等待以后返回真正的值,而done则还是同步操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function idMaker(){
let index = 0;
return {
next:function(){
return{
value:new Promise(resolve => setTimeOut(() => resolve(index++),1000)),
done:false
}
}
}
}
const it = idMaker();
it.next().value.then(o => console.log(o)) //1
it.next().value.then(o => console.log(o)) //2
  • 一个对象的同步遍历器的接口部署在Symbol.iterator属性上。
  • for…of遍历同步的iterator接口,for await...of循环遍历异步的Iterator接口。createAsyncIterable()返回一个拥有一部遍历器接口的对象
1
2
3
4
5
6
async function f(){
for await (const x of createAsyncIterable(['a','b'])){
console.log(x);
}
}
//a b
  • 异步Generator函数
1
async function* map(){ yield 'hello'; }
文章目录
  1. 1. ES6变量声明
    1. 1.0.1. let命令
    2. 1.0.2. const命令
    3. 1.0.3. 将对象彻底冻结
    4. 1.0.4. import命令
    5. 1.0.5. class命令
    6. 1.0.6. 顶层对象
  • 2. 数组解构
  • 3. this
  • 4. 字符串
  • 5. 字符串新增方法
    1. 5.0.1. 确定一个字符串是否包含在另一个字符串中
    2. 5.0.2. repeat():返回一个新字符串,表示将原字符串重复n次。
  • 6. 正则表达式
    1. 6.0.1. 具名组匹配
    2. 6.0.2. 先行断言
    3. 6.0.3. 后行断言
    4. 6.0.4. 属性
  • 7. 函数的拓展
    1. 7.0.1. 箭头函数(=>)
    2. 7.0.2. 尾调用(Tail Call)
    3. 7.0.3. 尾递归
  • 8. 数组
  • 9. 对象的扩展
    1. 9.0.1. 属性遍历
    2. 9.0.2. super关键字
    3. 9.0.3. 解构赋值
  • 10. 对象新增方法
  • 11. 数组中的遍历
  • 12. 字符串新增方法
  • 13. 面向对象class类
    1. 13.0.1. 继承
  • 14. json
    1. 14.0.1. 标准写法:
    2. 14.0.2. 方法:
  • 15. Promise(消除异步操作)
    1. 15.0.1. Promise.prototype.finally()
    2. 15.0.2. Promise.all()
    3. 15.0.3. Fetch API是js接口
  • 16. Generator函数
    1. 16.0.1. 与Symbol.iterator的关系
    2. 16.0.2. 原生js对象添加Iterator接口的方法
    3. 16.0.3. Generator.prototype.throw
    4. 16.0.4. Generator.prototype.return
    5. 16.0.5. yield*表达式
    6. 16.0.6. 协程
    7. 16.0.7. Thunk函数
    8. 16.0.8. Thunkify模块
    9. 16.0.9. co模块
  • 17. async函数
    1. 17.0.1. ansyc较Generator函数的优点
    2. 17.0.2. async使用形式
    3. 17.0.3. await 命令
  • 18. class类
    1. 18.0.1. 类的几种表示法
    2. 18.0.2. noticePoint
    3. 18.0.3. super
    4. 18.0.4. 类的prototype属性和__proto__属性
  • 19. module模块
    1. 19.0.1. 严格模式主要有以下限制。
    2. 19.0.2. exprot与import
    3. 19.0.3. 异步加载
    4. 19.0.4. ES6模块与CommonJS模块的差异
    5. 19.0.5. CommonJS加载原理
    6. 19.0.6. ES6模块转码
  • 20. 编程风格
  • 21. 异步遍历器
  • |