• ECMAScript 6 笔记(五)


    Iterator和for...of循环

    1. Iterator(遍历器)的概念

      Iterator接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of循环

      遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

      Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。

      Iterator的遍历过程是这样的。

      (1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

      (2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

      (3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

      (4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

      每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

      在ES6中,有些数据结构原生具备Iterator接口(比如数组),即不用任何处理,就可以被for...of循环遍历,有些就不行(比如对象)。原因在于,这些数据结构原生部署了Symbol.iterator属性

    2. 调用Iterator接口的场合

    (1)解构赋值

      对数组和Set结构进行解构赋值时,会默认调用Symbol.iterator方法。

    let set = new Set().add('a').add('b').add('c');
    
    let [x,y] = set;
    // x='a'; y='b'
    
    let [first, ...rest] = set;
    // first='a'; rest=['b','c'];

    (2)扩展运算符

      扩展运算符(...)也会调用默认的iterator接口。

    // 例一
    var str = 'hello';
    [...str] //  ['h','e','l','l','o']
    
    // 例二
    let arr = ['b', 'c'];
    ['a', ...arr, 'd']
    // ['a', 'b', 'c', 'd']

    (3)yield*

      yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。

    3. 字符串的Iterator接口

    4. for...of循环 

      for...of循环,作为遍历所有数据结构的统一的方法。

      for...of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。

      for...of循环可以代替数组实例的forEach方法。

      JavaScript原有的for...in循环,只能获得对象的键名,不能直接获取键值。ES6提供for...of循环,允许遍历获得键值。

      对于普通的对象,for...of结构不能直接使用,会报错,必须部署了iterator接口后才能使用。但是,这样情况下,for...in循环依然可以用来遍历键名。

    var es6 = {
      edition: 6,
      committee: "TC39",
      standard: "ECMA-262"
    };
    
    for (let e in es6) {
      console.log(e);
    }
    // edition
    // committee
    // standard
    
    for (let e of es6) {
      console.log(e);
    }
    // TypeError: es6 is not iterable

      for...in循环有几个缺点。

    • 数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
    • for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
    • 某些情况下,for...in循环会以任意顺序遍历键名。

      总之,for...in循环主要是为遍历对象而设计的,不适用于遍历数组。

    • 有着同for...in一样的简洁语法,但是没有for...in那些缺点。
    • 不同用于forEach方法,它可以与break、continue和return配合使用。
    • 提供了遍历所有数据结构的统一操作接口。

    Generator 函数的语法

      形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield语句,定义不同的内部状态(yield在英语里的意思就是“产出”)。

      Generator 函数是 ES6 提供的一种异步编程解决方案,从语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

    function* helloWorldGenerator() {
      yield 'hello';
      yield 'world';
      return 'ending';
    }
    
    var hw = helloWorldGenerator();

      上面代码定义了一个Generator函数helloWorldGenerator,它内部有两个yield语句“hello”和“world”,即该函数有三个状态:hello,world和return语句(结束执行)。

      调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象

      下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield语句(或return语句)为止。

    hw.next()
    // { value: 'hello', done: false }
    
    hw.next()
    // { value: 'world', done: false }
    
    hw.next()
    // { value: 'ending', done: true }
    
    hw.next()
    // { value: undefined, done: true }

    1. yield语句

      由于Generator函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield语句就是暂停标志。

      yield语句与return语句既有相似之处,也有区别。

      1. 相似之处在于,都能返回紧跟在语句后面的那个表达式的值。

      2. 区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。

      3. 一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield语句。正常函数只能返回一个值,因为只能执行一次return;Generator函数可以返回一系列的值,因为可以有任意多个yield

      Generator函数可以不用yield语句,这时就变成了一个单纯的暂缓执行函数。

    function* f() {
      console.log('执行了!')
    }
    
    var generator = f();
    
    setTimeout(function () {
      generator.next()
    }, 2000);

      上面代码中,函数f如果是普通函数,在为变量generator赋值时就会执行。但是,函数f是一个Generator函数,就变成只有调用next方法时,函数f才会执行。

      另外需要注意,yield语句不能用在普通函数中,否则会报错。

      另外,yield语句如果用在一个表达式之中,必须放在圆括号里面。

    console.log('Hello' + yield); // SyntaxError
    console.log('Hello' + yield 123); // SyntaxError
    
    console.log('Hello' + (yield)); // OK
    console.log('Hello' + (yield 123)); // OK

      yield语句用作函数参数或赋值表达式的右边,可以不加括号。

    2. next方法的参数

      yield句本身没有返回值,或者说总是返回undefinednext方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。

    function* f() {
      for(var i = 0; true; i++) {
        var reset = yield i;
        if(reset) { i = -1; }
      }
    }
    
    var g = f();
    
    g.next() // { value: 0, done: false }
    g.next() // { value: 1, done: false }
    g.next(true) // { value: 0, done: false }

      上面代码先定义了一个可以无限运行的 Generator 函数f,如果next方法没有参数,每次运行到yield语句,变量reset的值总是undefined。当next方法带一个参数true时,变量reset就被重置为这个参数(即true),因此i会等于-1,下一轮循环就会从-1开始递增。

    3. for...of循环

      for...of循环可以自动遍历Generator函数时生成的Iterator对象,且此时不再需要调用next方法。

    function *foo() {
      yield 1;
      yield 2;
      yield 3;
      yield 4;
      yield 5;
      return 6;
    }
    
    for (let v of foo()) {
      console.log(v);
    }
    // 1 2 3 4 5

      上面代码使用for...of循环,依次显示5个yield语句的值。这里需要注意,一旦next方法的返回对象的done属性为truefor...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。

    Generator 函数的异步应用

    1. 基本概念 

    回调函数

      JavaScript 语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。

    Class

    1. Class基本语法 

      基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

    //es5语法
    function Point(x, y) {
      this.x = x;
      this.y = y;
    }
    
    Point.prototype.toString = function () {
      return '(' + this.x + ', ' + this.y + ')';
    };
    
    var p = new Point(1, 2);
    
    //es6语法
    //定义类
    class Point {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
    
      toString() {
        return '(' + this.x + ', ' + this.y + ')';
      }
    }

      Point类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。

      ES6的类,完全可以看作构造函数的另一种写法。

    class Point {
      // ...
    }
    
    typeof Point // "function"
    Point === Point.prototype.constructor // true
    //上面代码表明,类的数据类型就是函数,类本身就指向构造函数。

      使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。

      构造函数的prototype属性,在ES6的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。

    class Point {
      constructor(){
        // ...
      }
    
      toString(){
        // ...
      }
    
      toValue(){
        // ...
      }
    }
    
    // 等同于
    
    Point.prototype = {
      toString(){},
      toValue(){}
    };

      Object.assign方法可以很方便地一次向类添加多个方法。

    class Point {
      constructor(){
        // ...
      }
    }
    
    Object.assign(Point.prototype, {
      toString(){},
      toValue(){}
    });

      另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。

      类的属性名,可以采用表达式。

    let methodName = "getArea";
    class Square{
      constructor(length) {
        // ...
      }
    
      [methodName]() {
        // ...
      }
    }
    //上面代码中,Square类的方法名getArea,是从表达式得到的。

    2. Class的继承 

    基本用法

      Class之间可以通过extends关键字实现继承

    class ColorPoint extends Point {
      constructor(x, y, color) {
        super(x, y); // 调用父类的constructor(x, y)
        this.color = color;
      }
    
      toString() {
        return this.color + ' ' + super.toString(); // 调用父类的toString()
      }
    }

      子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

      Class作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

      (1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

      (2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

    class A {
    }
    
    class B extends A {
    }
    
    B.__proto__ === A // true
    B.prototype.__proto__ === A.prototype // true

    3. Class 的静态方法

      类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

    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'

      静态方法也是可以从super对象上调用的。

    class Foo {
      static classMethod() {
        return 'hello';
      }
    }
    
    class Bar extends Foo {
      static classMethod() {
        return super.classMethod() + ', too';
      }
    }
    
    Bar.classMethod();

    4. Class的静态属性和实例属性 

      静态属性指的是Class本身的属性,即Class.propname,而不是定义在实例对象(this)上的属性。

    class Foo {
    }
    
    Foo.prop = 1;
    Foo.prop // 1

      目前,只有这种写法可行,因为ES6明确规定,Class内部只有静态方法,没有静态属性。

    5. 类的私有属性

      目前,有一个提案,为class加了私有属性。方法是在属性名之前,使用#表示。

    修饰器

    1. 类的修饰 

      修饰器(Decorator)是一个函数,用来修改类的行为。修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。

    function testable(target) {
      target.isTestable = true;
    }
    
    @testable
    class MyTestableClass {}
    
    console.log(MyTestableClass.isTestable) // true

      上面代码中,@testable就是一个修饰器。它修改了MyTestableClass这个类的行为,为它加上了静态属性isTestable

      基本上,修饰器的行为就是下面这样。

    @decorator
    class A {}
    
    // 等同于
    
    class A {}
    A = decorator(A) || A;

      也就是说,修饰器本质就是编译时执行的函数。

      修饰器函数的第一个参数,就是所要修饰的目标类。

    2. 方法的修饰

    class Person {
      @readonly
      name() { return `${this.first} ${this.last}` }
    }
    //上面代码中,修饰器readonly用来修饰“类”的name方法。

    Module

      ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

    // ES6模块
    import { stat, exists, readFile } from 'fs';

    1. 严格模式

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

    2. export 命令

      模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

    // profile.js
    export var firstName = 'Michael';
    export var lastName = 'Jackson';
    export var year = 1958;
    
    // profile.js
    var firstName = 'Michael';
    var lastName = 'Jackson';
    var year = 1958;
    
    export {firstName, lastName, year};

      通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。

    function v1() { ... }
    function v2() { ... }
    
    export {
      v1 as streamV1,
      v2 as streamV2,
      v2 as streamLatestVersion
    };

      需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

    // 报错
    export 1;
    
    // 报错
    var m = 1;
    export m;

      上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出1,第二种写法通过变量m,还是直接输出1。1只是一个值,不是接口。正确的写法是下面这样。

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

      上面三种写法都是正确的,规定了对外的接口m。其他脚本可以通过这个接口,取到值1。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。

      export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错

    3. import 命令

      使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

    // main.js
    import {firstName, lastName, year} from './profile';
    
    function setName(element) {
      element.textContent = firstName + ' ' + lastName;
    }

      import语句会执行所加载的模块,因此可以有下面的写法。

    import 'lodash';
    import * as circle from './circle';
    
    console.log('圆面积:' + circle.area(4));
    console.log('圆周长:' + circle.circumference(14));

      为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

    // export-default.js
    export default function () {
      console.log('foo');
    }

       上面代码是一个模块文件export-default.js,它的默认输出是一个函数。

      其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

    // import-default.js
    import customName from './export-default';
    customName(); // 'foo'
    // 第一组
    export default function crc32() { // 输出
      // ...
    }
    
    import crc32 from 'crc32'; // 输入
    
    // 第二组
    export function crc32() { // 输出
      // ...
    };
    
    import {crc32} from 'crc32'; // 输入

      上面代码的两组写法,第一组是使用export default时,对应的import语句不需要使用大括号;第二组是不使用export default时,对应的import语句需要使用大括号。

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

    // modules.js
    function add(x, y) {
      return x * y;
    }
    export {add as default};
    // 等同于
    // export default add;
    
    // app.js
    import { default as xxx } from 'modules';
    // 等同于
    // import xxx from 'modules';

    4. export 与 import 的复合写法

    export { foo, bar } from 'my_module';
    
    // 等同于
    import { foo, bar } from 'my_module';
    export { foo, bar };

    5. ES6模块加载的实质

      ES6 模块加载的机制,与 CommonJS 模块完全不同。CommonJS模块输出的是一个值的拷贝,而 ES6 模块输出的是值的引用。

    6. 浏览器的模块加载 

      浏览器使用 ES6 模块的语法如下。

    <script type="module" src="foo.js"></script>

      上面代码在网页中插入一个模块foo.js,由于type属性设为module,所以浏览器知道这是一个 ES6 模块。

    7. 循环加载

      “循环加载”(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。

    // a.js
    var b = require('b');
    
    // b.js
    var a = require('a');

    8. 跨模块常量

      本书介绍const命令的时候说过,const声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),可以采用下面的写法。

    // constants.js 模块
    export const A = 1;
    export const B = 3;
    export const C = 4;
    
    // test1.js 模块
    import * as constants from './constants';
    console.log(constants.A); // 1
    console.log(constants.B); // 3
  • 相关阅读:
    伪造mysql服务端实现任意读取
    客户端session安全问题(flask)
    systemd教程
    MySQL的一些常用基本命令的使用说明
    AMD、CMD、CommonJs和ES6的区别
    for in与for of的区别,以及forEach,map,some,every,filter的区别
    EcmaScript 6 十大常用特性
    单行省略号与多行省略号
    Array.prototype.slice.call()详解及转换数组的方法
    返回顶部
  • 原文地址:https://www.cnblogs.com/chaoran/p/6368035.html
Copyright © 2020-2023  润新知