标签: es6-系列

  • ES6 系列之模块加载方案

    前言
    本篇我们重点介绍以下四种模块加载规范:

    AMD
    CMD
    CommonJS
    ES6 模块

    最后再延伸讲下 Babel 的编译和 webpack 的打包原理。
    require.js
    在了解 AMD 规范之前,我们先来看看 require.js 的使用方式。
    项目目录为:
    * project/
    * index.html
    * vender/
    * main.js
    * require.js
    * add.js
    * square.js
    * multiply.js
    index.html 的内容如下:
    <!DOCTYPE html>
    <html>
    <head>
    <title>require.js</title>
    </head>
    <body>
    <h1>Content</h1>
    <script data-main=”vender/main” src=”vender/require.js”></script>
    </body>
    </html>
    data-main=”vender/main” 表示主模块是 vender 下的 main.js。
    main.js 的配置如下:
    // main.js
    require([‘./add’, ‘./square’], function(addModule, squareModule) {
    console.log(addModule.add(1, 1))
    console.log(squareModule.square(3))
    });
    require 的第一个参数表示依赖的模块的路径,第二个参数表示此模块的内容。
    由此可以看出,主模块依赖 add 模块和 square 模块。
    我们看下 add 模块即 add.js 的内容:
    // add.js
    define(function() {
    console.log(‘加载了 add 模块’);
    var add = function(x, y) { 
    return x + y;
    };

    return {      
    add: add
    };
    });
    requirejs 为全局添加了 define 函数,你只要按照这种约定的方式书写这个模块即可。
    那如果依赖的模块又依赖了其他模块呢?
    我们来看看主模块依赖的 square 模块, square 模块的作用是求出一个数字的平方,比如输入 3 就返回 9,该模块依赖一个乘法模块,该乘法模块即 multiply.js 的代码如下:
    // multiply.js
    define(function() {
    console.log(‘加载了 multiply 模块’)
    var multiply = function(x, y) { 
    return x * y;
    };

    return {      
    multiply: multiply
    };
    });
    而 square 模块就要用到 multiply 模块,其实写法跟 main.js 添加依赖模块一样:
    // square.js
    define([‘./multiply’], function(multiplyModule) {
    console.log(‘加载了 square 模块’)
    return {      
    square: function(num) {
    return multiplyModule.multiply(num, num)
    }
    };
    });
    require.js 会自动分析依赖关系,将需要加载的模块正确加载。
    requirejs 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/requirejs
    而如果我们在浏览器中打开 index.html,打印的顺序为:
    加载了 add 模块
    加载了 multiply 模块
    加载了 square 模块
    2
    9
    AMD
    在上节,我们说了这样一句话:

    requirejs 为全局添加了 define 函数,你只要按照这种约定的方式书写这个模块即可。
    那这个约定的书写方式是指什么呢?
    指的便是 The Asynchronous Module Definition (AMD) 规范。
    所以其实 AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。
    你去看 AMD 规范) 的内容,其主要内容就是定义了 define 函数该如何书写,只要你按照这个规范书写模块和依赖,require.js 就能正确的进行解析。
    sea.js
    在国内,经常与 AMD 被一起提起的还有 CMD,CMD 又是什么呢?我们从 sea.js 的使用开始说起。
    文件目录与 requirejs 项目目录相同:
    * project/
    * index.html
    * vender/
    * main.js
    * require.js
    * add.js
    * square.js
    * multiply.js
    index.html 的内容如下:
    <!DOCTYPE html>
    <html>
    <head>
    <title>sea.js</title>
    </head>
    <body>
    <h1>Content</h1>
    <script src=”vender/sea.js”></script>
    <script>
    // 在页面中加载主模块
    seajs.use(“./vender/main”);
    </script>
    </body>

    </html>
    main.js 的内容如下:
    // main.js
    define(function(require, exports, module) {
    var addModule = require(‘./add’);
    console.log(addModule.add(1, 1))

    var squareModule = require(‘./square’);
    console.log(squareModule.square(3))
    });
    add.js 的内容如下:
    // add.js
    define(function(require, exports, module) {
    console.log(‘加载了 add 模块’)
    var add = function(x, y) { 
    return x + y;
    };
    module.exports = {      
    add: add
    };
    });
    square.js 的内容如下:
    define(function(require, exports, module) {
    console.log(‘加载了 square 模块’)
    var multiplyModule = require(‘./multiply’);
    module.exports = {      
    square: function(num) {
    return multiplyModule.multiply(num, num)
    }
    };

    });
    multiply.js 的内容如下:
    define(function(require, exports, module) {
    console.log(‘加载了 multiply 模块’)
    var multiply = function(x, y) { 
    return x * y;
    };
    module.exports = {      
    multiply: multiply
    };
    });
    跟第一个例子是同样的依赖结构,即 main 依赖 add 和 square,square 又依赖 multiply。
    seajs 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/seajs
    而如果我们在浏览器中打开 index.html,打印的顺序为:
    加载了 add 模块
    2
    加载了 square 模块
    加载了 multiply 模块
    9
    CMD
    与 AMD 一样,CMD 其实就是 SeaJS 在推广过程中对模块定义的规范化产出。
    你去看 CMD 规范的内容,主要内容就是描述该如何定义模块,如何引入模块,如何导出模块,只要你按照这个规范书写代码,sea.js 就能正确的进行解析。
    AMD 与 CMD 的区别
    从 sea.js 和 require.js 的例子可以看出:
    1.CMD 推崇依赖就近,AMD 推崇依赖前置。看两个项目中的 main.js:
    // require.js 例子中的 main.js
    // 依赖必须一开始就写好
    require([‘./add’, ‘./square’], function(addModule, squareModule) {
    console.log(addModule.add(1, 1))
    console.log(squareModule.square(3))
    });
    // sea.js 例子中的 main.js
    define(function(require, exports, module) {
    var addModule = require(‘./add’);
    console.log(addModule.add(1, 1))

    // 依赖可以就近书写
    var squareModule = require(‘./square’);
    console.log(squareModule.square(3))
    });
    2.对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。看两个项目中的打印顺序:
    // require.js
    加载了 add 模块
    加载了 multiply 模块
    加载了 square 模块
    2
    9
    // sea.js
    加载了 add 模块
    2
    加载了 square 模块
    加载了 multiply 模块
    9
    AMD 是将需要使用的模块先加载完再执行代码,而 CMD 是在 require 的时候才去加载模块文件,加载完再接着执行。
    感谢
    感谢 require.js 和 sea.js 在推动 JavaScript 模块化发展方面做出的贡献。
    CommonJS
    AMD 和 CMD 都是用于浏览器端的模块规范,而在服务器端比如 node,采用的则是 CommonJS 规范。
    导出模块的方式:
    var add = function(x, y) { 
    return x + y;
    };

    module.exports.add = add;
    引入模块的方式:
    var add = require(‘./add.js’);
    console.log(add.add(1, 1));
    我们将之前的例子改成 CommonJS 规范:
    // main.js
    var add = require(‘./add.js’);
    console.log(add.add(1, 1))

    var square = require(‘./square.js’);
    console.log(square.square(3));
    // add.js
    console.log(‘加载了 add 模块’)

    var add = function(x, y) { 
    return x + y;
    };

    module.exports.add = add;
    // multiply.js
    console.log(‘加载了 multiply 模块’)

    var multiply = function(x, y) { 
    return x * y;
    };

    module.exports.multiply = multiply;
    // square.js
    console.log(‘加载了 square 模块’)

    var multiply = require(‘./multiply.js’);

    var square = function(num) { 
    return multiply.multiply(num, num);
    };

    module.exports.square = square;
    CommonJS 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/commonJS
    如果我们执行 node main.js,打印的顺序为:
    加载了 add 模块
    2
    加载了 square 模块
    加载了 multiply 模块
    9
    跟 sea.js 的执行结果一致,也是在 require 的时候才去加载模块文件,加载完再接着执行。
    CommonJS 与 AMD
    引用阮一峰老师的《JavaScript 标准参考教程(alpha)》:
    CommonJS 规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。
    由于 Node.js 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以 CommonJS 规范比较适用。
    但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用 AMD 规范。

    ES6
    ECMAScript2015 规定了新的模块加载方案。
    导出模块的方式:
    var firstName = ‘Michael’;
    var lastName = ‘Jackson’;
    var year = 1958;

    export {firstName, lastName, year};
    引入模块的方式:
    import {firstName, lastName, year} from ‘./profile’;
    我们再将上面的例子改成 ES6 规范:
    目录结构与 requirejs 和 seajs 目录结构一致。
    <!DOCTYPE html>
    <html>
    <head>
    <title>ES6</title>
    </head>
    <body>
    <h1>Content</h1>
    <script src=”vender/main.js” type=”module”></script>
    </body>
    </html>
    注意!浏览器加载 ES6 模块,也使用 <script> 标签,但是要加入 type=”module” 属性。
    // main.js
    import {add} from ‘./add.js’;
    console.log(add(1, 1))

    import {square} from ‘./square.js’;
    console.log(square(3));
    // add.js
    console.log(‘加载了 add 模块’)

    var add = function(x, y) {
    return x + y;
    };

    export {add}
    // multiply.js
    console.log(‘加载了 multiply 模块’)

    var multiply = function(x, y) { 
    return x * y;
    };

    export {multiply}
    // square.js
    console.log(‘加载了 square 模块’)

    import {multiply} from ‘./multiply.js’;

    var square = function(num) { 
    return multiply(num, num);
    };

    export {square}
    ES6-Module 项目 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/ES6
    值得注意的,在 Chrome 中,如果直接打开,会报跨域错误,必须开启服务器,保证文件同源才可以有效果。
    为了验证这个效果你可以:
    cnpm install http-server -g
    然后进入该目录,执行
    http-server
    在浏览器打开 http://localhost:8080/ 即可查看效果。
    打印的顺序为:
    加载了 add 模块
    加载了 multiply 模块
    加载了 square 模块
    2
    9
    跟 require.js 的执行结果是一致的,也就是将需要使用的模块先加载完再执行代码。
    ES6 与 CommonJS
    引用阮一峰老师的 《ECMAScript 6 入门》:

    它们有两个重大差异。

    CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
    CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

    第二个差异可以从两个项目的打印结果看出,导致这种差别的原因是:
    因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
    重点解释第一个差异。
    CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
    举个例子:
    // 输出模块 counter.js
    var counter = 3;
    function incCounter() {
    counter++;
    }
    module.exports = {
    counter: counter,
    incCounter: incCounter,
    };
    // 引入模块 main.js
    var mod = require(‘./counter’);

    console.log(mod.counter); // 3
    mod.incCounter();
    console.log(mod.counter); // 3
    counter.js 模块加载以后,它的内部变化就影响不到输出的 mod.counter 了。这是因为 mod.counter 是一个原始类型的值,会被缓存。
    但是如果修改 counter 为一个引用类型的话:
    // 输出模块 counter.js
    var counter = {
    value: 3
    };

    function incCounter() {
    counter.value++;
    }
    module.exports = {
    counter: counter,
    incCounter: incCounter,
    };
    // 引入模块 main.js
    var mod = require(‘./counter.js’);

    console.log(mod.counter.value); // 3
    mod.incCounter();
    console.log(mod.counter.value); // 4
    value 是会发生改变的。不过也可以说这是 “值的拷贝”,只是对于引用类型而言,值指的其实是引用。
    而如果我们将这个例子改成 ES6:
    // counter.js
    export let counter = 3;
    export function incCounter() {
    counter++;
    }

    // main.js
    import { counter, incCounter } from ‘./counter’;
    console.log(counter); // 3
    incCounter();
    console.log(counter); // 4
    这是因为
    ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的 import 有点像 Unix 系统的“符号连接”,原始值变了,import 加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
    Babel
    鉴于浏览器支持度的问题,如果要使用 ES6 的语法,一般都会借助 Babel,可对于 import 和 export 而言,只借助 Babel 就可以吗?
    让我们看看 Babel 是怎么编译 import 和 export 语法的。
    // ES6
    var firstName = ‘Michael’;
    var lastName = ‘Jackson’;
    var year = 1958;

    export {firstName, lastName, year};
    // Babel 编译后
    ‘use strict’;

    Object.defineProperty(exports, “__esModule”, {
    value: true
    });
    var firstName = ‘Michael’;
    var lastName = ‘Jackson’;
    var year = 1958;

    exports.firstName = firstName;
    exports.lastName = lastName;
    exports.year = year;
    是不是感觉有那么一点奇怪?编译后的语法更像是 CommonJS 规范,再看 import 的编译结果:
    // ES6
    import {firstName, lastName, year} from ‘./profile’;
    // Babel 编译后
    ‘use strict’;

    var _profile = require(‘./profile’);
    你会发现 Babel 只是把 ES6 模块语法转为 CommonJS 模块语法,然而浏览器是不支持这种模块语法的,所以直接跑在浏览器会报错的,如果想要在浏览器中运行,还是需要使用打包工具将代码打包。
    webpack
    Babel 将 ES6 模块转为 CommonJS 后, webpack 又是怎么做的打包的呢?它该如何将这些文件打包在一起,从而能保证正确的处理依赖,以及能在浏览器中运行呢?
    首先为什么浏览器中不支持 CommonJS 语法呢?
    这是因为浏览器环境中并没有 module、 exports、 require 等环境变量。
    换句话说,webpack 打包后的文件之所以在浏览器中能运行,就是靠模拟了这些变量的行为。
    那怎么模拟呢?
    我们以 CommonJS 项目中的 square.js 为例,它依赖了 multiply 模块:
    console.log(‘加载了 square 模块’)

    var multiply = require(‘./multiply.js’);

    var square = function(num) { 
    return multiply.multiply(num, num);
    };

    module.exports.square = square;
    webpack 会将其包裹一层,注入这些变量:
    function(module, exports, require) {
    console.log(‘加载了 square 模块’);

    var multiply = require(“./multiply”);
    module.exports = {
    square: function(num) {
    return multiply.multiply(num, num);
    }
    };
    }
    那 webpack 又会将 CommonJS 项目的代码打包成什么样呢?我写了一个精简的例子,你可以直接复制到浏览器中查看效果:
    // 自执行函数
    (function(modules) {

    // 用于储存已经加载过的模块
    var installedModules = {};

    function require(moduleName) {

    if (installedModules[moduleName]) {
    return installedModules[moduleName].exports;
    }

    var module = installedModules[moduleName] = {
    exports: {}
    };

    modules[moduleName](module, module.exports, require);

    return module.exports;
    }

    // 加载主模块
    return require(“main”);

    })({
    “main”: function(module, exports, require) {

    var addModule = require(“./add”);
    console.log(addModule.add(1, 1))

    var squareModule = require(“./square”);
    console.log(squareModule.square(3));

    },
    “./add”: function(module, exports, require) {
    console.log(‘加载了 add 模块’);

    module.exports = {
    add: function(x, y) {
    return x + y;
    }
    };
    },
    “./square”: function(module, exports, require) {
    console.log(‘加载了 square 模块’);

    var multiply = require(“./multiply”);
    module.exports = {
    square: function(num) {
    return multiply.multiply(num, num);
    }
    };
    },

    “./multiply”: function(module, exports, require) {
    console.log(‘加载了 multiply 模块’);

    module.exports = {
    multiply: function(x, y) {
    return x * y;
    }
    };
    }
    })
    最终的执行结果为:
    加载了 add 模块
    2
    加载了 square 模块
    加载了 multiply 模块
    9
    参考

    《JavaScript 标准参考教程(alpha)》
    《ECMAScript6 入门》
    手写一个CommonJS打包工具(一)

    ES6 系列
    ES6 系列目录地址:https://github.com/mqyqingfeng/Blog
    ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。
    如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

  • ES6 系列之 defineProperty 与 proxy

    前言
    我们或多或少都听过“数据绑定”这个词,“数据绑定”的关键在于监听数据的变化,可是对于这样一个对象:var obj = {value: 1},我们该怎么知道 obj 发生了改变呢?
    definePropety
    ES5 提供了 Object.defineProperty 方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
    语法
    Object.defineProperty(obj, prop, descriptor)
    参数
    obj: 要在其上定义属性的对象。

    prop: 要定义或修改的属性的名称。

    descriptor: 将被定义或修改的属性的描述符。
    举个例子:
    var obj = {};
    Object.defineProperty(obj, “num”, {
    value : 1,
    writable : true,
    enumerable : true,
    configurable : true
    });
    // 对象 obj 拥有属性 num,值为 1
    虽然我们可以直接添加属性和值,但是使用这种方式,我们能进行更多的配置。
    函数的第三个参数 descriptor 所表示的属性描述符有两种形式:数据描述符和存取描述符。
    两者均具有以下两种键值:
    configurable
    当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,也能够被删除。默认为 false。

    enumerable
    当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。

    数据描述符同时具有以下可选键值:
    value
    该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。

    writable
    当且仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变。默认为 false。

    存取描述符同时具有以下可选键值:
    get
    一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。

    set
    一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。

    值得注意的是:
    属性描述符必须是数据描述符或者存取描述符两种形式之一,不能同时是两者。这就意味着你可以:
    Object.defineProperty({}, “num”, {
    value: 1,
    writable: true,
    enumerable: true,
    configurable: true
    });
    也可以:
    var value = 1;
    Object.defineProperty({}, “num”, {
    get : function(){
    return value;
    },
    set : function(newValue){
    value = newValue;
    },
    enumerable : true,
    configurable : true
    });
    但是不可以:
    // 报错
    Object.defineProperty({}, “num”, {
    value: 1,
    get: function() {
    return 1;
    }
    });
    此外,所有的属性描述符都是非必须的,但是 descriptor 这个字段是必须的,如果不进行任何配置,你可以这样:
    var obj = Object.defineProperty({}, “num”, {});
    console.log(obj.num); // undefined
    Setters 和 Getters
    之所以讲到 defineProperty,是因为我们要使用存取描述符中的 get 和 set,这两个方法又被称为 getter 和 setter。由 getter 和 setter 定义的属性称做”存取器属性“。
    当程序查询存取器属性的值时,JavaScript 调用 getter方法。这个方法的返回值就是属性存取表达式的值。当程序设置一个存取器属性的值时,JavaScript 调用 setter 方法,将赋值表达式右侧的值当做参数传入 setter。从某种意义上讲,这个方法负责“设置”属性值。可以忽略 setter 方法的返回值。
    举个例子:
    var obj = {}, value = null;
    Object.defineProperty(obj, “num”, {
    get: function(){
    console.log(‘执行了 get 操作’)
    return value;
    },
    set: function(newValue) {
    console.log(‘执行了 set 操作’)
    value = newValue;
    }
    })

    obj.value = 1 // 执行了 set 操作

    console.log(obj.value); // 执行了 get 操作 // 1
    这不就是我们要的监控数据改变的方法吗?我们再来封装一下:
    function Archiver() {
    var value = null;
    // archive n. 档案
    var archive = [];

    Object.defineProperty(this, ‘num’, {
    get: function() {
    console.log(‘执行了 get 操作’)
    return value;
    },
    set: function(value) {
    console.log(‘执行了 set 操作’)
    value = value;
    archive.push({ val: value });
    }
    });

    this.getArchive = function() { return archive; };
    }

    var arc = new Archiver();
    arc.num; // 执行了 get 操作
    arc.num = 11; // 执行了 set 操作
    arc.num = 13; // 执行了 set 操作
    console.log(arc.getArchive()); // [{ val: 11 }, { val: 13 }]
    watch API
    既然可以监控数据的改变,那我可以这样设想,即当数据改变的时候,自动进行渲染工作。举个例子:
    HTML 中有个 span 标签和 button 标签
    <span id=”container”>1</span>
    <button id=”button”>点击加 1</button>
    当点击按钮的时候,span 标签里的值加 1。
    传统的做法是:
    document.getElementById(‘button’).addEventListener(“click”, function(){
    var container = document.getElementById(“container”);
    container.innerHTML = Number(container.innerHTML) + 1;
    });
    如果使用了 defineProperty:
    var obj = {
    value: 1
    }

    // 储存 obj.value 的值
    var value = 1;

    Object.defineProperty(obj, “value”, {
    get: function() {
    return value;
    },
    set: function(newValue) {
    value = newValue;
    document.getElementById(‘container’).innerHTML = newValue;
    }
    });

    document.getElementById(‘button’).addEventListener(“click”, function() {
    obj.value += 1;
    });
    代码看似增多了,但是当我们需要改变 span 标签里的值的时候,直接修改 obj.value 的值就可以了。
    然而,现在的写法,我们还需要单独声明一个变量存储 obj.value 的值,因为如果你在 set 中直接 obj.value = newValue 就会陷入无限的循环中。此外,我们可能需要监控很多属性值的改变,要是一个一个写,也很累呐,所以我们简单写个 watch 函数。使用效果如下:
    var obj = {
    value: 1
    }

    watch(obj, “num”, function(newvalue){
    document.getElementById(‘container’).innerHTML = newvalue;
    })

    document.getElementById(‘button’).addEventListener(“click”, function(){
    obj.value += 1
    });
    我们来写下这个 watch 函数:
    (function(){
    var root = this;
    function watch(obj, name, func){
    var value = obj[name];

    Object.defineProperty(obj, name, {
    get: function() {
    return value;
    },
    set: function(newValue) {
    value = newValue;
    func(value)
    }
    });

    if (value) obj[name] = value
    }

    this.watch = watch;
    })()
    现在我们已经可以监控对象属性值的改变,并且可以根据属性值的改变,添加回调函数,棒棒哒~
    proxy
    使用 defineProperty 只能重定义属性的读取(get)和设置(set)行为,到了 ES6,提供了 Proxy,可以重定义更多的行为,比如 in、delete、函数调用等更多行为。
    Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。我们来看看它的语法:
    var proxy = new Proxy(target, handler);
    proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。
    var proxy = new Proxy({}, {
    get: function(obj, prop) {
    console.log(‘设置 get 操作’)
    return obj[prop];
    },
    set: function(obj, prop, value) {
    console.log(‘设置 set 操作’)
    obj[prop] = value;
    }
    });

    proxy.time = 35; // 设置 set 操作

    console.log(proxy.time); // 设置 get 操作 // 35
    除了 get 和 set 之外,proxy 可以拦截多达 13 种操作,比如 has(target, propKey),可以拦截 propKey in proxy 的操作,返回一个布尔值。
    // 使用 has 方法隐藏某些属性,不被 in 运算符发现
    var handler = {
    has (target, key) {
    if (key[0] === ‘_’) {
    return false;
    }
    return key in target;
    }
    };
    var target = { _prop: ‘foo’, prop: ‘foo’ };
    var proxy = new Proxy(target, handler);
    console.log(‘_prop’ in proxy); // false
    又比如说 apply 方法拦截函数的调用、call 和 apply 操作。
    apply 方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组,不过这里我们简单演示一下:
    var target = function () { return ‘I am the target’; };
    var handler = {
    apply: function () {
    return ‘I am the proxy’;
    }
    };

    var p = new Proxy(target, handler);

    p();
    // “I am the proxy”
    又比如说 ownKeys 方法可以拦截对象自身属性的读取操作。具体来说,拦截以下操作:

    Object.getOwnPropertyNames()
    Object.getOwnPropertySymbols()
    Object.keys()

    下面的例子是拦截第一个字符为下划线的属性名,不让它被 for of 遍历到。
    let target = {
    _bar: ‘foo’,
    _prop: ‘bar’,
    prop: ‘baz’
    };

    let handler = {
    ownKeys (target) {
    return Reflect.ownKeys(target).filter(key => key[0] !== ‘_’);
    }
    };

    let proxy = new Proxy(target, handler);
    for (let key of Object.keys(proxy)) {
    console.log(target[key]);
    }
    // “baz”
    更多的拦截行为可以查看阮一峰老师的 《ECMAScript 6 入门》
    值得注意的是,proxy 的最大问题在于浏览器支持度不够,而且很多效果无法使用 poilyfill 来弥补。
    watch API 优化
    我们使用 proxy 再来写一下 watch 函数。使用效果如下:
    (function() {
    var root = this;

    function watch(target, func) {

    var proxy = new Proxy(target, {
    get: function(target, prop) {
    return target[prop];
    },
    set: function(target, prop, value) {
    target[prop] = value;
    func(prop, value);
    }
    });

    if(target[name]) proxy[name] = value;
    return proxy;
    }

    this.watch = watch;
    })()

    var obj = {
    value: 1
    }

    var newObj = watch(obj, function(key, newvalue) {
    if (key == ‘value’) document.getElementById(‘container’).innerHTML = newvalue;
    })

    document.getElementById(‘button’).addEventListener(“click”, function() {
    newObj.value += 1
    });
    我们也可以发现,使用 defineProperty 和 proxy 的区别,当使用 defineProperty,我们修改原来的 obj 对象就可以触发拦截,而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截。
    ES6 系列
    ES6 系列目录地址:https://github.com/mqyqingfeng/Blog
    ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。
    如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

  • ES6 系列之 Babel 是如何编译 Class 的(下)

    前言
    在上一篇 《 ES6 系列 Babel 是如何编译 Class 的(上)》,我们知道了 Babel 是如何编译 Class 的,这篇我们学习 Babel 是如何用 ES5 实现 Class 的继承。
    ES5 寄生组合式继承
    function Parent (name) {
    this.name = name;
    }

    Parent.prototype.getName = function () {
    console.log(this.name)
    }

    function Child (name, age) {
    Parent.call(this, name);
    this.age = age;
    }

    Child.prototype = Object.create(Parent.prototype);

    var child1 = new Child(‘kevin’, ’18’);

    console.log(child1);
    原型链示意图为:

    关于寄生组合式继承我们在 《JavaScript深入之继承的多种方式和优缺点》 中介绍过。
    引用《JavaScript高级程序设计》中对寄生组合式继承的夸赞就是:
    这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
    ES6 extend
    Class 通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
    以上 ES5 的代码对应到 ES6 就是:
    class Parent {
    constructor(name) {
    this.name = name;
    }
    }

    class Child extends Parent {
    constructor(name, age) {
    super(name); // 调用父类的 constructor(name)
    this.age = age;
    }
    }

    var child1 = new Child(‘kevin’, ’18’);

    console.log(child1);
    值得注意的是:
    super 关键字表示父类的构造函数,相当于 ES5 的 Parent.call(this)。
    子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类就得不到 this 对象。
    也正是因为这个原因,在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。
    子类的 __proto__
    在 ES6 中,父类的静态方法,可以被子类继承。举个例子:
    class Foo {
    static classMethod() {
    return ‘hello’;
    }
    }

    class Bar extends Foo {
    }

    Bar.classMethod(); // ‘hello’
    这是因为 Class 作为构造函数的语法糖,同时有 prototype 属性和 __proto__ 属性,因此同时存在两条继承链。
    (1)子类的 __proto__ 属性,表示构造函数的继承,总是指向父类。
    (2)子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的 prototype 属性。
    class Parent {
    }

    class Child extends Parent {
    }

    console.log(Child.__proto__ === Parent); // true
    console.log(Child.prototype.__proto__ === Parent.prototype); // true
    ES6 的原型链示意图为:

    我们会发现,相比寄生组合式继承,ES6 的 class 多了一个 Object.setPrototypeOf(Child, Parent) 的步骤。
    继承目标
    extends 关键字后面可以跟多种类型的值。
    class B extends A {
    }
    上面代码的 A,只要是一个有 prototype 属性的函数,就能被 B 继承。由于函数都有 prototype 属性(除了 Function.prototype 函数),因此 A 可以是任意函数。
    除了函数之外,A 的值还可以是 null,当 extend null 的时候:
    class A extends null {
    }

    console.log(A.__proto__ === Function.prototype); // true
    console.log(A.prototype.__proto__ === undefined); // true
    Babel 编译
    那 ES6 的这段代码:
    class Parent {
    constructor(name) {
    this.name = name;
    }
    }

    class Child extends Parent {
    constructor(name, age) {
    super(name); // 调用父类的 constructor(name)
    this.age = age;
    }
    }

    var child1 = new Child(‘kevin’, ’18’);

    console.log(child1);
    Babel 又是如何编译的呢?我们可以在 Babel 官网的 Try it out 中尝试:
    ‘use strict’;

    function _possibleConstructorReturn(self, call) {
    if (!self) {
    throw new ReferenceError(“this hasn’t been initialised – super() hasn’t been called”);
    }
    return call && (typeof call === “object” || typeof call === “function”) ? call : self;
    }

    function _inherits(subClass, superClass) {
    if (typeof superClass !== “function” && superClass !== null) {
    throw new TypeError(“Super expression must either be null or a function, not ” + typeof superClass);
    }
    subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } });
    if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
    }

    function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
    throw new TypeError(“Cannot call a class as a function”);
    }
    }

    var Parent = function Parent(name) {
    _classCallCheck(this, Parent);

    this.name = name;
    };

    var Child = function(_Parent) {
    _inherits(Child, _Parent);

    function Child(name, age) {
    _classCallCheck(this, Child);

    // 调用父类的 constructor(name)
    var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name));

    _this.age = age;
    return _this;
    }

    return Child;
    }(Parent);

    var child1 = new Child(‘kevin’, ’18’);

    console.log(child1);
    我们可以看到 Babel 创建了 _inherits 函数帮助实现继承,又创建了 _possibleConstructorReturn 函数帮助确定调用父类构造函数的返回值,我们来细致的看一看代码。
    _inherits
    function _inherits(subClass, superClass) {
    // extend 的继承目标必须是函数或者是 null
    if (typeof superClass !== “function” && superClass !== null) {
    throw new TypeError(“Super expression must either be null or a function, not ” + typeof superClass);
    }

    // 类似于 ES5 的寄生组合式继承,使用 Object.create,设置子类 prototype 属性的 __proto__ 属性指向父类的 prototype 属性
    subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } });

    // 设置子类的 __proto__ 属性指向父类
    if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
    }
    关于 Object.create(),一般我们用的时候会传入一个参数,其实是支持传入两个参数的,第二个参数表示要添加到新创建对象的属性,注意这里是给新创建的对象即返回值添加属性,而不是在新创建对象的原型对象上添加。
    举个例子:
    // 创建一个以另一个空对象为原型,且拥有一个属性 p 的对象
    const o = Object.create({}, { p: { value: 42 } });
    console.log(o); // {p: 42}
    console.log(o.p); // 42
    再完整一点:
    const o = Object.create({}, {
    p: {
    value: 42,
    enumerable: false,
    // 该属性不可写
    writable: false,
    configurable: true
    }
    });
    o.p = 24;
    console.log(o.p); // 42
    那么对于这段代码:
    subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } });
    作用就是给 subClass.prototype 添加一个可配置可写不可枚举的 constructor 属性,该属性值为 subClass。
    _possibleConstructorReturn
    函数里是这样调用的:
    var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name));
    我们简化为:
    var _this = _possibleConstructorReturn(this, Parent.call(this, name));
    _possibleConstructorReturn 的源码为:
    function _possibleConstructorReturn(self, call) {
    if (!self) {
    throw new ReferenceError(“this hasn’t been initialised – super() hasn’t been called”);
    }
    return call && (typeof call === “object” || typeof call === “function”) ? call : self;
    }
    在这里我们判断 Parent.call(this, name) 的返回值的类型,咦?这个值还能有很多类型?
    对于这样一个 class:
    class Parent {
    constructor() {
    this.xxx = xxx;
    }
    }
    Parent.call(this, name) 的值肯定是 undefined。可是如果我们在 constructor 函数中 return 了呢?比如:
    class Parent {
    constructor() {
    return {
    name: ‘kevin’
    }
    }
    }
    我们可以返回各种类型的值,甚至是 null:
    class Parent {
    constructor() {
    return null
    }
    }
    我们接着看这个判断:
    call && (typeof call === “object” || typeof call === “function”) ? call : self;
    注意,这句话的意思并不是判断 call 是否存在,如果存在,就执行 (typeof call === “object” || typeof call === “function”) ? call : self
    因为 && 的运算符优先级高于 ? :,所以这句话的意思应该是:
    (call && (typeof call === “object” || typeof call === “function”)) ? call : self;
    对于 Parent.call(this) 的值,如果是 object 类型或者是 function 类型,就返回 Parent.call(this),如果是 null 或者基本类型的值或者是 undefined,都会返回 self 也就是子类的 this。
    这也是为什么这个函数被命名为 _possibleConstructorReturn。
    总结
    var Child = function(_Parent) {
    _inherits(Child, _Parent);

    function Child(name, age) {
    _classCallCheck(this, Child);

    // 调用父类的 constructor(name)
    var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name));

    _this.age = age;
    return _this;
    }

    return Child;
    }(Parent);
    最后我们总体看下如何实现继承:
    首先执行 _inherits(Child, Parent),建立 Child 和 Parent 的原型链关系,即 Object.setPrototypeOf(Child.prototype, Parent.prototype) 和 Object.setPrototypeOf(Child, Parent)。
    然后调用 Parent.call(this, name),根据 Parent 构造函数的返回值类型确定子类构造函数 this 的初始值 _this。
    最终,根据子类构造函数,修改 _this 的值,然后返回该值。
    ES6 系列
    ES6 系列目录地址:https://github.com/mqyqingfeng/Blog
    ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。
    如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

  • ES6 系列之 Babel 是如何编译 Class 的(上)

    前言
    在了解 Babel 是如何编译 class 前,我们先看看 ES6 的 class 和 ES5 的构造函数是如何对应的。毕竟,ES6 的 class 可以看作一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
    constructor
    ES6 中:
    class Person {
    constructor(name) {
    this.name = name;
    }

    sayHello() {
    return ‘hello, I am ‘ + this.name;
    }
    }

    var kevin = new Person(‘Kevin’);
    kevin.sayHello(); // hello, I am Kevin
    对应到 ES5 中就是:
    function Person(name) {
    this.name = name;
    }

    Person.prototype.sayHello = function () {
    return ‘hello, I am ‘ + this.name;
    };

    var kevin = new Person(‘Kevin’);
    kevin.sayHello(); // hello, I am Kevin
    我们可以看到 ES5 的构造函数 Person,对应 ES6 的 Person 类的 constructor 方法。
    值得注意的是:类的内部所有定义的方法,都是不可枚举的(non-enumerable)
    以上面的例子为例,在 ES6 中:
    Object.keys(Person.prototype); // []
    Object.getOwnPropertyNames(Person.prototype); // [“constructor”, “sayHello”]
    然而在 ES5 中:
    Object.keys(Person.prototype); // [‘sayHello’]
    Object.getOwnPropertyNames(Person.prototype); // [“constructor”, “sayHello”]
    实例属性
    以前,我们定义实例属性,只能写在类的 constructor 方法里面。比如:
    class Person {
    constructor() {
    this.state = {
    count: 0
    };
    }
    }
    然而现在有一个提案,对实例属性和静态属性都规定了新的写法,而且 Babel 已经支持。现在我们可以写成:
    class Person {
    state = {
    count: 0
    };
    }
    对应到 ES5 都是:
    function Person() {
    this.state = {
    count: 0
    };
    }
    静态方法
    所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
    ES6 中:
    class Person {
    static sayHello() {
    return ‘hello’;
    }
    }

    Person.sayHello() // ‘hello’

    var kevin = new Person();
    kevin.sayHello(); // TypeError: kevin.sayHello is not a function
    对应 ES5:
    function Person() {}

    Person.sayHello = function() {
    return ‘hello’;
    };

    Person.sayHello(); // ‘hello’

    var kevin = new Person();
    kevin.sayHello(); // TypeError: kevin.sayHello is not a function
    静态属性
    静态属性指的是 Class 本身的属性,即 Class.propName,而不是定义在实例对象(this)上的属性。以前,我们添加静态属性只可以这样:
    class Person {}

    Person.name = ‘kevin’;
    因为上面提到的提案,现在可以写成:
    class Person {
    static name = ‘kevin’;
    }
    对应到 ES5 都是:
    function Person() {};

    Person.name = ‘kevin’;
    new 调用
    值得注意的是:类必须使用 new 调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用 new 也可以执行。
    class Person {}

    Person(); // TypeError: Class constructor Foo cannot be invoked without ‘new’
    getter 和 setter
    与 ES5 一样,在“类”的内部可以使用 get 和 set 关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
    class Person {
    get name() {
    return ‘kevin’;
    }
    set name(newName) {
    console.log(‘new name 为:’ + newName)
    }
    }

    let person = new Person();

    person.name = ‘daisy’;
    // new name 为:daisy

    console.log(person.name);
    // kevin
    对应到 ES5 中:
    function Person(name) {}

    Person.prototype = {
    get name() {
    return ‘kevin’;
    },
    set name(newName) {
    console.log(‘new name 为:’ + newName)
    }
    }

    let person = new Person();

    person.name = ‘daisy’;
    // new name 为:daisy

    console.log(person.name);
    // kevin
    Babel 编译
    至此,我们已经知道了有关“类”的方法中,ES6 与 ES5 是如何对应的,实际上 Babel 在编译时并不会直接就转成这种形式,Babel 会自己生成一些辅助函数,帮助实现 ES6 的特性。
    我们可以在 Babel 官网的 Try it out 页面查看 ES6 的代码编译成什么样子。
    编译(一)
    ES6 代码为:
    class Person {
    constructor(name) {
    this.name = name;
    }
    }
    Babel 编译为:
    “use strict”;

    function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
    throw new TypeError(“Cannot call a class as a function”);
    }
    }

    var Person = function Person(name) {
    _classCallCheck(this, Person);

    this.name = name;
    };
    _classCallCheck 的作用是检查 Person 是否是通过 new 的方式调用,在上面,我们也说过,类必须使用 new 调用,否则会报错。
    当我们使用 var person = Person() 的形式调用的时候,this 指向 window,所以 instance instanceof Constructor 就会为 false,与 ES6 的要求一致。
    编译(二)
    ES6 代码为:
    class Person {
    // 实例属性
    foo = ‘foo’;
    // 静态属性
    static bar = ‘bar’;

    constructor(name) {
    this.name = name;
    }
    }
    Babel 编译为:
    ‘use strict’;

    function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
    throw new TypeError(“Cannot call a class as a function”);
    }
    }

    var Person = function Person(name) {
    _classCallCheck(this, Person);

    this.foo = ‘foo’;

    this.name = name;
    };

    Person.bar = ‘bar’;
    编译(三)
    ES6 代码为:
    class Person {
    constructor(name) {
    this.name = name;
    }

    sayHello() {
    return ‘hello, I am ‘ + this.name;
    }

    static onlySayHello() {
    return ‘hello’
    }

    get name() {
    return ‘kevin’;
    }

    set name(newName) {
    console.log(‘new name 为:’ + newName)
    }
    }
    对应到 ES5 的代码应该是:
    function Person(name) {
    this.name = name;
    }

    Person.prototype = {
    sayHello: function () {
    return ‘hello, I am ‘ + this.name;
    },
    get name() {
    return ‘kevin’;
    },
    set name(newName) {
    console.log(‘new name 为:’ + newName)
    }
    }

    Person.onlySayHello = function () {
    return ‘hello’
    };
    Babel 编译后为:
    ‘use strict’;

    var _createClass = function() {
    function defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if (“value” in descriptor) descriptor.writable = true;
    Object.defineProperty(target, descriptor.key, descriptor);
    }
    }
    return function(Constructor, protoProps, staticProps) {
    if (protoProps) defineProperties(Constructor.prototype, protoProps);
    if (staticProps) defineProperties(Constructor, staticProps);
    return Constructor;
    };
    }();

    function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
    throw new TypeError(“Cannot call a class as a function”);
    }
    }

    var Person = function() {
    function Person(name) {
    _classCallCheck(this, Person);

    this.name = name;
    }

    _createClass(Person, [{
    key: ‘sayHello’,
    value: function sayHello() {
    return ‘hello, I am ‘ + this.name;
    }
    }, {
    key: ‘name’,
    get: function get() {
    return ‘kevin’;
    },
    set: function set(newName) {
    console.log(‘new name 为:’ + newName);
    }
    }], [{
    key: ‘onlySayHello’,
    value: function onlySayHello() {
    return ‘hello’;
    }
    }]);

    return Person;
    }();
    我们可以看到 Babel 生成了一个 _createClass 辅助函数,该函数传入三个参数,第一个是构造函数,在这个例子中也就是 Person,第二个是要添加到原型上的函数数组,第三个是要添加到构造函数本身的函数数组,也就是所有添加 static 关键字的函数。该函数的作用就是将函数数组中的方法添加到构造函数或者构造函数的原型中,最后返回这个构造函数。
    在其中,又生成了一个 defineProperties 辅助函数,使用 Object.defineProperty 方法添加属性。
    默认 enumerable 为 false,configurable 为 true,这个在上面也有强调过,是为了防止 Object.keys() 之类的方法遍历到。然后通过判断 value 是否存在,来判断是否是 getter 和 setter。如果存在 value,就为 descriptor 添加 value 和 writable 属性,如果不存在,就直接使用 get 和 set 属性。
    写在后面
    至此,我们已经了解了 Babel 是如何编译一个 Class 的,然而,Class 还有一个重要的特性就是继承,Class 如何继承,Babel 又该如何编译,欢迎期待下一篇《 ES6 系列之 Babel 是如何编译 Class 的(下)》
    ES6 系列
    ES6 系列目录地址:https://github.com/mqyqingfeng/Blog
    ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。
    如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

  • ES6 系列之 Babel 将 Async 编译成了什么样子

    前言
    本文就是简单介绍下 Async 语法编译后的代码。
    Async
    const fetchData = (data) => new Promise((resolve) => setTimeout(resolve, 1000, data + 1))

    const fetchValue = async function () {
    var value1 = await fetchData(1);
    var value2 = await fetchData(value1);
    var value3 = await fetchData(value2);
    console.log(value3)
    };

    fetchValue();
    // 大约 3s 后输出 4
    Babel
    我们直接在 Babel 官网的 Try it out 粘贴上述代码,然后查看代码编译成什么样子:
    “use strict”;

    function _asyncToGenerator(fn) {
    return function() {
    var gen = fn.apply(this, arguments);
    return new Promise(function(resolve, reject) {
    function step(key, arg) {
    try {
    var info = gen[key](arg);
    var value = info.value;
    } catch (error) {
    reject(error);
    return;
    }
    if (info.done) {
    resolve(value);
    } else {
    return Promise.resolve(value).then(
    function(value) {
    step(“next”, value);
    },
    function(err) {
    step(“throw”, err);
    }
    );
    }
    }
    return step(“next”);
    });
    };
    }

    var fetchData = function fetchData(data) {
    return new Promise(function(resolve) {
    return setTimeout(resolve, 1000, data + 1);
    });
    };

    var fetchValue = (function() {
    var _ref = _asyncToGenerator(
    /*#__PURE__*/ regeneratorRuntime.mark(function _callee() {
    var value1, value2, value3;
    return regeneratorRuntime.wrap(
    function _callee$(_context) {
    while (1) {
    switch ((_context.prev = _context.next)) {
    case 0:
    _context.next = 2;
    return fetchData(1);

    case 2:
    value1 = _context.sent;
    _context.next = 5;
    return fetchData(value1);

    case 5:
    value2 = _context.sent;
    _context.next = 8;
    return fetchData(value2);

    case 8:
    value3 = _context.sent;

    console.log(value3);

    case 10:
    case “end”:
    return _context.stop();
    }
    }
    },
    _callee,
    this
    );
    })
    );

    return function fetchValue() {
    return _ref.apply(this, arguments);
    };
    })();

    fetchValue();
    _asyncToGenerator
    regeneratorRuntime 相关的代码我们在 《ES6 系列之 Babel 将 Generator 编译成了什么样子》 中已经介绍过了,这次我们重点来看看 _asyncToGenerator 函数:
    function _asyncToGenerator(fn) {
    return function() {
    var gen = fn.apply(this, arguments);
    return new Promise(function(resolve, reject) {
    function step(key, arg) {
    try {
    var info = gen[key](arg);
    var value = info.value;
    } catch (error) {
    reject(error);
    return;
    }
    if (info.done) {
    resolve(value);
    } else {
    return Promise.resolve(value).then(
    function(value) {
    step(“next”, value);
    },
    function(err) {
    step(“throw”, err);
    }
    );
    }
    }
    return step(“next”);
    });
    };
    }
    以上这段代码主要是用来实现 generator 的自动执行以及返回 Promise。
    当我们执行 fetchValue() 的时候,执行的其实就是 _asyncToGenerator 返回的这个匿名函数,在匿名函数中,我们执行了
    var gen = fn.apply(this, arguments);
    这一步就相当于执行 Generator 函数,举个例子:
    function* helloWorldGenerator() {
    yield ‘hello’;
    yield ‘world’;
    return ‘ending’;
    }

    var hw = helloWorldGenerator();
    var gen = fn.apply(this, arguments) 就相当于 var hw = helloWorldGenerator();,返回的 gen 是一个具有 next()、throw()、return() 方法的对象。
    然后我们返回了一个 Promise 对象,在 Promise 中,我们执行了 step(“next”),step 函数中会执行:
    try {
    var info = gen[key](arg);
    var value = info.value;
    } catch (error) {
    reject(error);
    return;
    }
    step(“next”) 就相当于 var info = gen.next(),返回的 info 对象是一个具有 value 和 done 属性的对象:
    {value: Promise, done: false}
    接下来又会执行:
    if (info.done) {
    resolve(value);
    } else {
    return Promise.resolve(value).then(
    function(value) {
    step(“next”, value);
    },
    function(err) {
    step(“throw”, err);
    }
    );
    }
    value 此时是一个 Promise,Promise.resolve(value) 依然会返回这个 Promise,我们给这个 Promise 添加了一个 then 函数,用于在 Promise 有结果时执行,有结果时又会执行 step(“next”, value),从而使得 Generator 继续执行,直到 info.done 为 true,才会 resolve(value)。
    不完整但可用的代码
    (function() {
    var ContinueSentinel = {};

    var mark = function(genFun) {
    var generator = Object.create({
    next: function(arg) {
    return this._invoke(“next”, arg);
    }
    });
    genFun.prototype = generator;
    return genFun;
    };

    function wrap(innerFn, outerFn, self) {
    var generator = Object.create(outerFn.prototype);

    var context = {
    done: false,
    method: “next”,
    next: 0,
    prev: 0,
    sent: undefined,
    abrupt: function(type, arg) {
    var record = {};
    record.type = type;
    record.arg = arg;

    return this.complete(record);
    },
    complete: function(record, afterLoc) {
    if (record.type === “return”) {
    this.rval = this.arg = record.arg;
    this.method = “return”;
    this.next = “end”;
    }

    return ContinueSentinel;
    },
    stop: function() {
    this.done = true;
    return this.rval;
    }
    };

    generator._invoke = makeInvokeMethod(innerFn, context);

    return generator;
    }

    function makeInvokeMethod(innerFn, context) {
    var state = “start”;

    return function invoke(method, arg) {
    if (state === “completed”) {
    return { value: undefined, done: true };
    }

    context.method = method;
    context.arg = arg;

    while (true) {
    state = “executing”;

    if (context.method === “next”) {
    context.sent = context._sent = context.arg;
    }

    var record = {
    type: “normal”,
    arg: innerFn.call(self, context)
    };

    if (record.type === “normal”) {
    state = context.done ? “completed” : “yield”;

    if (record.arg === ContinueSentinel) {
    continue;
    }

    return {
    value: record.arg,
    done: context.done
    };
    }
    }
    };
    }

    window.regeneratorRuntime = {};

    regeneratorRuntime.wrap = wrap;
    regeneratorRuntime.mark = mark;
    })();

    “use strict”;

    function _asyncToGenerator(fn) {
    return function() {
    var gen = fn.apply(this, arguments);
    return new Promise(function(resolve, reject) {
    function step(key, arg) {
    try {
    var info = gen[key](arg);
    var value = info.value;
    } catch (error) {
    reject(error);
    return;
    }
    if (info.done) {
    resolve(value);
    } else {
    return Promise.resolve(value).then(
    function(value) {
    step(“next”, value);
    },
    function(err) {
    step(“throw”, err);
    }
    );
    }
    }
    return step(“next”);
    });
    };
    }

    var fetchData = function fetchData(data) {
    return new Promise(function(resolve) {
    return setTimeout(resolve, 1000, data + 1);
    });
    };

    var fetchValue = (function() {
    var _ref = _asyncToGenerator(
    /*#__PURE__*/
    regeneratorRuntime.mark(function _callee() {
    var value1, value2, value3;
    return regeneratorRuntime.wrap(
    function _callee$(_context) {
    while (1) {
    switch ((_context.prev = _context.next)) {
    case 0:
    _context.next = 2;
    return fetchData(1);

    case 2:
    value1 = _context.sent;
    _context.next = 5;
    return fetchData(value1);

    case 5:
    value2 = _context.sent;
    _context.next = 8;
    return fetchData(value2);

    case 8:
    value3 = _context.sent;

    console.log(value3);

    case 10:
    case “end”:
    return _context.stop();
    }
    }
    },
    _callee,
    this
    );
    })
    );

    return function fetchValue() {
    return _ref.apply(this, arguments);
    };
    })();

    fetchValue();
    请原谅我水了一篇文章……
    ES6 系列
    ES6 系列目录地址:https://github.com/mqyqingfeng/Blog
    ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。
    如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

  • ES6 系列之 Babel 将 Generator 编译成了什么样子

    前言
    本文就是简单介绍下 Generator 语法编译后的代码。
    Generator
    function* helloWorldGenerator() {
    yield ‘hello’;
    yield ‘world’;
    return ‘ending’;
    }
    我们打印下执行的结果:
    var hw = helloWorldGenerator();

    console.log(hw.next()); // {value: “hello”, done: false}
    console.log(hw.next()); // {value: “world”, done: false}
    console.log(hw.next()); // {value: “ending”, done: true}
    console.log(hw.next()); // {value: undefined, done: true}
    Babel
    具体的执行过程就不说了,我们直接在 Babel 官网的 Try it out 粘贴上述代码,然后查看代码被编译成了什么样子:
    /**
    * 我们就称呼这个版本为简单编译版本吧
    */
    var _marked = /*#__PURE__*/ regeneratorRuntime.mark(helloWorldGenerator);

    function helloWorldGenerator() {
    return regeneratorRuntime.wrap(
    function helloWorldGenerator$(_context) {
    while (1) {
    switch ((_context.prev = _context.next)) {
    case 0:
    _context.next = 2;
    return “hello”;

    case 2:
    _context.next = 4;
    return “world”;

    case 4:
    return _context.abrupt(“return”, “ending”);

    case 5:
    case “end”:
    return _context.stop();
    }
    }
    },
    _marked,
    this
    );
    }
    猛一看,好像编译后的代码还蛮少的,但是细细一看,编译后的代码肯定是不能用的呀,regeneratorRuntime 是个什么鬼?哪里有声明呀?mark 和 wrap 方法又都做了什么?
    难道就不能编译一个完整可用的代码吗?
    regenerator
    如果你想看到完整可用的代码,你可以使用 regenerator,这是 facebook 下的一个工具,用于编译 ES6 的 generator 函数。
    我们先安装一下 regenerator:
    npm install -g regenerator
    然后新建一个 generator.js 文件,里面的代码就是文章最一开始的代码,我们执行命令:
    regenerator –include-runtime generator.js > generator-es5.js
    我们就可以在 generator-es5.js 文件看到编译后的完整可用的代码。
    而这一编译就编译了 700 多行…… 编译后的代码可以查看 generator-es5.js
    总之编译后的代码还蛮复杂,我们可以从中抽离出大致的逻辑,至少让简单编译的那段代码能够跑起来。
    mark 函数
    简单编译后的代码第一段是这样的:
    var _marked = /*#__PURE__*/ regeneratorRuntime.mark(helloWorldGenerator);
    我们查看完整编译版本中 mark 函数的源码:
    runtime.mark = function(genFun) {
    genFun.__proto__ = GeneratorFunctionPrototype;
    genFun.prototype = Object.create(Gp);
    return genFun;
    };
    这其中又涉及了 GeneratorFunctionPrototype 和 Gp 变量,我们也查看下对应的代码:
    function Generator() {}
    function GeneratorFunction() {}
    function GeneratorFunctionPrototype() {}

    var Gp = GeneratorFunctionPrototype.prototype =
    Generator.prototype = Object.create(IteratorPrototype);

    GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype;

    GeneratorFunctionPrototype.constructor = GeneratorFunction;

    GeneratorFunctionPrototype[toStringTagSymbol] =
    GeneratorFunction.displayName = “GeneratorFunction”;
    这段代码构建了一堆看起来很复杂的关系链,其实这是参照着 ES6 规范构建的关系链:

    图中 +@@toStringTag:s = ‘Generator’ 的就是 Gp,+@@toStringTag:s = ‘GeneratorFunction’ 的就是 GeneratorFunctionPrototype。
    构建关系链的目的在于判断关系的时候能够跟原生的保持一致,就比如:
    function* f() {}
    var g = f();
    console.log(g.__proto__ === f.prototype); // true
    console.log(g.__proto__.__proto__ === f.__proto__.prototype); // true
    为了简化起见,我们可以把 Gp 先设置为一个空对象,不过正如你在上图中看到的,next()、 throw()、return() 函数都是挂载在 Gp 对象上,实际上,在完整的编译代码中,确实有为 Gp 添加这三个函数的方法:
    // 117 行
    function defineIteratorMethods(prototype) {
    [“next”, “throw”, “return”].forEach(function(method) {
    prototype[method] = function(arg) {
    return this._invoke(method, arg);
    };
    });
    }

    // 406 行
    defineIteratorMethods(Gp);
    为了简单起见,我们将整个 mark 函数简化为:
    runtime.mark = function(genFun) {
    var generator = Object.create({
    next: function(arg) {
    return this._invoke(‘next’, arg)
    }
    });
    genFun.prototype = generator;
    return genFun;
    };
    wrap 函数
    除了设置关系链之外,mark 函数的返回值 genFun 还作为了 wrap 函数的第二个参数传入:
    function helloWorldGenerator() {
    return regeneratorRuntime.wrap(
    function helloWorldGenerator$(_context) {

    },
    _marked,
    this
    );
    }
    我们再看下 wrap 函数:
    function wrap(innerFn, outerFn, self) {
    var generator = Object.create(outerFn.prototype);
    var context = new Context([]);
    generator._invoke = makeInvokeMethod(innerFn, self, context);

    return generator;
    }
    所以当执行 var hw = helloWorldGenerator(); 的时候,其实执行的是 wrap 函数,wrap 函数返回了 generator,generator 是一个对象,原型是 outerFn.prototype, outerFn.prototype 其实就是 genFun.prototype, genFun.prototype 是一个空对象,原型上有 next() 方法。
    所以当你执行 hw.next() 的时候,执行的其实是 hw 原型的原型上的 next 函数,next 函数执行的又是 hw 的 _invoke 函数:
    generator._invoke = makeInvokeMethod(innerFn, self, context);
    innerFn 就是 wrap 包裹的那个函数,其实就是 helloWordGenerato$ 函数,呐,就是这个函数:
    function helloWorldGenerator$(_context) {
    while (1) {
    switch ((_context.prev = _context.next)) {
    case 0:
    _context.next = 2;
    return “hello”;

    case 2:
    _context.next = 4;
    return “world”;

    case 4:
    return _context.abrupt(“return”, “ending”);

    case 5:
    case “end”:
    return _context.stop();
    }
    }
    }
    而 context 你可以直接理解为这样一个全局对象:
    var ContinueSentinel = {};

    var context = {
    done: false,
    method: “next”,
    next: 0,
    prev: 0,
    abrupt: function(type, arg) {
    var record = {};
    record.type = type;
    record.arg = arg;

    return this.complete(record);
    },
    complete: function(record, afterLoc) {
    if (record.type === “return”) {
    this.rval = this.arg = record.arg;
    this.method = “return”;
    this.next = “end”;
    }

    return ContinueSentinel;
    },
    stop: function() {
    this.done = true;
    return this.rval;
    }
    };
    每次 hw.next 的时候,就会修改 next 和 prev 属性的值,当在 generator 函数中 return 的时候会执行 abrupt,abrupt 中又会执行 complete,执行完 complete,因为 this.next = end 的缘故,再执行就会执行 stop 函数。
    我们来看下 makeInvokeMethod 函数:
    var ContinueSentinel = {};

    function makeInvokeMethod(innerFn, self, context) {
    var state = ‘start’;

    return function invoke(method, arg) {

    if (state === ‘completed’) {
    return { value: undefined, done: true };
    }

    context.method = method;
    context.arg = arg;

    while (true) {

    state = ‘executing’;

    var record = {
    type: ‘normal’,
    arg: innerFn.call(self, context)
    };
    if (record.type === “normal”) {

    state = context.done
    ? ‘completed’
    : ‘yield’;

    if (record.arg === ContinueSentinel) {
    continue;
    }

    return {
    value: record.arg,
    done: context.done
    };

    }
    }
    };
    }
    基本的执行过程就不分析了,我们重点看第三次执行 hw.next() 的时候:
    第三次执行 hw.next() 的时候,其实执行了
    this._invoke(“next”, undefined);
    我们在 invoke 函数中构建了一个 record 对象:
    var record = {
    type: “normal”,
    arg: innerFn.call(self, context)
    };
    而在 innerFn.call(self, context) 中,因为 _context.next 为 4 的缘故,其实执行了:
    _context.abrupt(“return”, ‘ending’);
    而在 abrupt 中,我们又构建了一个 record 对象:
    var record = {};
    record.type = ‘return’;
    record.arg = ‘ending’;
    然后执行了 this.complete(record),
    在 complete 中,因为 record.type === “return”
    this.rval = ‘ending’;
    this.method = “return”;
    this.next = “end”;
    然后返回了全局对象 ContinueSentinel,其实就是一个全局空对象。
    然后在 invoke 函数中,因为 record.arg === ContinueSentinel 的缘故,没有执行后面的 return 语句,就直接进入下一个循环。
    于是又执行了一遍 innerFn.call(self, context),此时 _context.next 为 end, 执行了 _context.stop(), 在 stop 函数中:
    this.done = true;
    return this.rval; // this.rval 其实就是 `ending`
    所以最终返回的值为:
    {
    value: ‘ending’,
    done: true
    };
    之后,我们再执行 hw.next() 的时候,因为 state 已经是 ‘completed’ 的缘故,直接就返回 { value: undefined, done: true}
    不完整但可用的源码
    当然这个过程,看文字理解起来可能有些难度,不完整但可用的代码如下,你可以断点调试查看具体的过程:
    (function() {
    var ContinueSentinel = {};

    var mark = function(genFun) {
    var generator = Object.create({
    next: function(arg) {
    return this._invoke(“next”, arg);
    }
    });
    genFun.prototype = generator;
    return genFun;
    };

    function wrap(innerFn, outerFn, self) {
    var generator = Object.create(outerFn.prototype);

    var context = {
    done: false,
    method: “next”,
    next: 0,
    prev: 0,
    abrupt: function(type, arg) {
    var record = {};
    record.type = type;
    record.arg = arg;

    return this.complete(record);
    },
    complete: function(record, afterLoc) {
    if (record.type === “return”) {
    this.rval = this.arg = record.arg;
    this.method = “return”;
    this.next = “end”;
    }

    return ContinueSentinel;
    },
    stop: function() {
    this.done = true;
    return this.rval;
    }
    };

    generator._invoke = makeInvokeMethod(innerFn, context);

    return generator;
    }

    function makeInvokeMethod(innerFn, context) {
    var state = “start”;

    return function invoke(method, arg) {
    if (state === “completed”) {
    return { value: undefined, done: true };
    }

    context.method = method;
    context.arg = arg;

    while (true) {
    state = “executing”;

    var record = {
    type: “normal”,
    arg: innerFn.call(self, context)
    };

    if (record.type === “normal”) {
    state = context.done ? “completed” : “yield”;

    if (record.arg === ContinueSentinel) {
    continue;
    }

    return {
    value: record.arg,
    done: context.done
    };
    }
    }
    };
    }

    window.regeneratorRuntime = {};

    regeneratorRuntime.wrap = wrap;
    regeneratorRuntime.mark = mark;
    })();

    var _marked = regeneratorRuntime.mark(helloWorldGenerator);

    function helloWorldGenerator() {
    return regeneratorRuntime.wrap(
    function helloWorldGenerator$(_context) {
    while (1) {
    switch ((_context.prev = _context.next)) {
    case 0:
    _context.next = 2;
    return “hello”;

    case 2:
    _context.next = 4;
    return “world”;

    case 4:
    return _context.abrupt(“return”, “ending”);

    case 5:
    case “end”:
    return _context.stop();
    }
    }
    },
    _marked,
    this
    );
    }

    var hw = helloWorldGenerator();

    console.log(hw.next());
    console.log(hw.next());
    console.log(hw.next());
    console.log(hw.next());
    ES6 系列
    ES6 系列目录地址:https://github.com/mqyqingfeng/Blog
    ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。
    如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

  • ES6 系列之异步处理实战

    前言
    我们以查找指定目录下的最大文件为例,感受从
    回调函数 -> Promise -> Generator -> Async
    异步处理方式的改变。
    API 介绍
    为了实现这个功能,我们需要用到几个 Nodejs 的 API,所以我们来简单介绍一下。
    fs.readdir
    readdir 方法用于读取目录,返回一个包含文件和目录的数组。
    fs.stat
    stat 方法的参数是一个文件或目录,它产生一个对象,该对象包含了该文件或目录的具体信息。此外,该对象还有一个 isFile() 方法可以判断正在处理的到底是一个文件,还是一个目录。
    思路分析
    我们基本的实现思路就是:

    用 fs.readdir 获取指定目录的内容信息
    循环遍历内容信息,使用 fs.stat 获取该文件或者目录的具体信息
    将具体信息储存起来
    当全部储存起来后,筛选其中的是文件的信息
    遍历比较,找出最大文件
    获取并返回最大文件

    然后我们直接上代码吧。
    回调函数
    var fs = require(‘fs’);
    var path = require(‘path’);

    function findLargest(dir, cb) {
    // 读取目录下的所有文件
    fs.readdir(dir, function(er, files) {
    if (er) return cb(er);

    var counter = files.length;
    var errored = false;
    var stats = [];

    files.forEach(function(file, index) {
    // 读取文件信息
    fs.stat(path.join(dir, file), function(er, stat) {

    if (errored) return;

    if (er) {
    errored = true;
    return cb(er);
    }

    stats[index] = stat;

    // 事先算好有多少个文件,读完 1 个文件信息,计数减 1,当为 0 时,说明读取完毕,此时执行最终的比较操作
    if (–counter == 0) {

    var largest = stats
    .filter(function(stat) { return stat.isFile() })
    .reduce(function(prev, next) {
    if (prev.size > next.size) return prev
    return next
    })

    cb(null, files[stats.indexOf(largest)])
    }
    })
    })
    })
    }
    使用方式为:
    // 查找当前目录最大的文件
    findLargest(‘./’, function(er, filename) {
    if (er) return console.error(er)
    console.log(‘largest file was:’, filename)
    });
    Promise
    var fs = require(‘fs’);
    var path = require(‘path’);

    var readDir = function(dir) {
    return new Promise(function(resolve, reject) {
    fs.readdir(dir, function(err, files) {
    if (err) reject(err);
    resolve(files)
    })
    })
    }

    var stat = function(path) {
    return new Promise(function(resolve, reject) {
    fs.stat(path, function(err, stat) {
    if (err) reject(err)
    resolve(stat)
    })
    })
    }

    function findLargest(dir) {
    return readDir(dir)
    .then(function(files) {
    let promises = files.map(file => stat(path.join(dir, file)))
    return Promise.all(promises).then(function(stats) {
    return { stats, files }
    })
    })
    .then(data => {

    let largest = data.stats
    .filter(function(stat) { return stat.isFile() })
    .reduce((prev, next) => {
    if (prev.size > next.size) return prev
    return next
    })

    return data.files[data.stats.indexOf(largest)]
    })

    }
    使用方式为:
    findLargest(‘./’)
    .then(function(filename) {
    console.log(‘largest file was:’, filename);
    })
    .catch(function() {
    console.log(error);
    });
    Generator
    var fs = require(‘fs’);
    var path = require(‘path’);

    var co = require(‘co’)

    var readDir = function(dir) {
    return new Promise(function(resolve, reject) {
    fs.readdir(dir, function(err, files) {
    if (err) reject(err);
    resolve(files)
    })
    })
    }

    var stat = function(path) {
    return new Promise(function(resolve, reject) {
    fs.stat(path, function(err, stat) {
    if (err) reject(err)
    resolve(stat)
    })
    })
    }

    function* findLargest(dir) {
    var files = yield readDir(dir);
    var stats = yield files.map(function(file) {
    return stat(path.join(dir, file))
    })

    let largest = stats
    .filter(function(stat) { return stat.isFile() })
    .reduce((prev, next) => {
    if (prev.size > next.size) return prev
    return next
    })

    return files[stats.indexOf(largest)]

    }
    使用方式为:
    co(findLargest, ‘./’)
    .then(function(filename) {
    console.log(‘largest file was:’, filename);
    })
    .catch(function() {
    console.log(error);
    });
    Async
    var fs = require(‘fs’);
    var path = require(‘path’);

    var readDir = function(dir) {
    return new Promise(function(resolve, reject) {
    fs.readdir(dir, function(err, files) {
    if (err) reject(err);
    resolve(files)
    })
    })
    }

    var stat = function(path) {
    return new Promise(function(resolve, reject) {
    fs.stat(path, function(err, stat) {
    if (err) reject(err)
    resolve(stat)
    })
    })
    }

    async function findLargest(dir) {
    var files = await readDir(dir);

    let promises = files.map(file => stat(path.join(dir, file)))
    var stats = await Promise.all(promises)

    let largest = stats
    .filter(function(stat) { return stat.isFile() })
    .reduce((prev, next) => {
    if (prev.size > next.size) return prev
    return next
    })

    return files[stats.indexOf(largest)]

    }
    使用方式为:
    findLargest(‘./’)
    .then(function(filename) {
    console.log(‘largest file was:’, filename);
    })
    .catch(function() {
    console.log(error);
    });
    ES6 系列
    ES6 系列目录地址:https://github.com/mqyqingfeng/Blog
    ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。
    如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

  • ES6 系列之 Generator 的自动执行

    单个异步任务
    var fetch = require(‘node-fetch’);

    function* gen(){
    var url = ‘https://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);
    });
    首先执行 Generator 函数,获取遍历器对象。
    然后使用 next 方法,执行异步任务的第一阶段,即 fetch(url)。
    注意,由于 fetch(url) 会返回一个 Promise 对象,所以 result 的值为:
    { value: Promise { <pending> }, done: false }
    最后我们为这个 Promise 对象添加一个 then 方法,先将其返回的数据格式化(data.json()),再调用 g.next,将获得的数据传进去,由此可以执行异步任务的第二阶段,代码执行完毕。
    多个异步任务
    上节我们只调用了一个接口,那如果我们调用了多个接口,使用了多个 yield,我们岂不是要在 then 函数中不断的嵌套下去……
    所以我们来看看执行多个异步任务的情况:
    var fetch = require(‘node-fetch’);

    function* gen() {
    var r1 = yield fetch(‘https://api.github.com/users/github’);
    var r2 = yield fetch(‘https://api.github.com/users/github/followers’);
    var r3 = yield fetch(‘https://api.github.com/users/github/repos’);

    console.log([r1.bio, r2[0].login, r3[0].full_name].join(‘\n’));
    }
    为了获得最终的执行结果,你可能要写成:
    var g = gen();
    var result1 = g.next();

    result1.value.then(function(data){
    return data.json();
    })
    .then(function(data){
    return g.next(data).value;
    })
    .then(function(data){
    return data.json();
    })
    .then(function(data){
    return g.next(data).value
    })
    .then(function(data){
    return data.json();
    })
    .then(function(data){
    g.next(data)
    });
    但我知道你肯定不想写成这样……
    其实,利用递归,我们可以这样写:
    function run(gen) {
    var g = gen();

    function next(data) {
    var result = g.next(data);

    if (result.done) return;

    result.value.then(function(data) {
    return data.json();
    }).then(function(data) {
    next(data);
    });

    }

    next();
    }

    run(gen);
    其中的关键就是 yield 的时候返回一个 Promise 对象,给这个 Promise 对象添加 then 方法,当异步操作成功时执行 then 中的 onFullfilled 函数,onFullfilled 函数中又去执行 g.next,从而让 Generator 继续执行,然后再返回一个 Promise,再在成功时执行 g.next,然后再返回……
    启动器函数
    在 run 这个启动器函数中,我们在 then 函数中将数据格式化 data.json(),但在更广泛的情况下,比如 yield 直接跟一个 Promise,而非一个 fetch 函数返回的 Promise,因为没有 json 方法,代码就会报错。所以为了更具备通用性,连同这个例子和启动器,我们修改为:
    var fetch = require(‘node-fetch’);

    function* gen() {
    var r1 = yield fetch(‘https://api.github.com/users/github’);
    var json1 = yield r1.json();
    var r2 = yield fetch(‘https://api.github.com/users/github/followers’);
    var json2 = yield r2.json();
    var r3 = yield fetch(‘https://api.github.com/users/github/repos’);
    var json3 = yield r3.json();

    console.log([json1.bio, json2[0].login, json3[0].full_name].join(‘\n’));
    }

    function run(gen) {
    var g = gen();

    function next(data) {
    var result = g.next(data);

    if (result.done) return;

    result.value.then(function(data) {
    next(data);
    });

    }

    next();
    }

    run(gen);
    只要 yield 后跟着一个 Promise 对象,我们就可以利用这个 run 函数将 Generator 函数自动执行。
    回调函数
    yield 后一定要跟着一个 Promise 对象才能保证 Generator 的自动执行吗?如果只是一个回调函数呢?我们来看个例子:
    首先我们来模拟一个普通的异步请求:
    function fetchData(url, cb) {
    setTimeout(function(){
    cb({status: 200, data: url})
    }, 1000)
    }
    我们将这种函数改造成:
    function fetchData(url) {
    return function(cb){
    setTimeout(function(){
    cb({status: 200, data: url})
    }, 1000)
    }
    }
    对于这样的 Generator 函数:
    function* gen() {
    var r1 = yield fetchData(‘https://api.github.com/users/github’);
    var r2 = yield fetchData(‘https://api.github.com/users/github/followers’);

    console.log([r1.data, r2.data].join(‘\n’));
    }
    如果要获得最终的结果:
    var g = gen();

    var r1 = g.next();

    r1.value(function(data) {
    var r2 = g.next(data);
    r2.value(function(data) {
    g.next(data);
    });
    });
    如果写成这样的话,我们会面临跟第一节同样的问题,那就是当使用多个 yield 时,代码会循环嵌套起来……
    同样利用递归,所以我们可以将其改造为:
    function run(gen) {
    var g = gen();

    function next(data) {
    var result = g.next(data);

    if (result.done) return;

    result.value(next);
    }

    next();
    }

    run(gen);
    run
    由此可以看到 Generator 函数的自动执行需要一种机制,即当异步操作有了结果,能够自动交回执行权。
    而两种方法可以做到这一点。
    (1)回调函数。将异步操作进行包装,暴露出回调函数,在回调函数里面交回执行权。
    (2)Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。
    在两种方法中,我们各写了一个 run 启动器函数,那我们能不能将这两种方式结合在一些,写一个通用的 run 函数呢?我们尝试一下:
    // 第一版
    function run(gen) {
    var gen = gen();

    function next(data) {
    var result = gen.next(data);
    if (result.done) return;

    if (isPromise(result.value)) {
    result.value.then(function(data) {
    next(data);
    });
    } else {
    result.value(next)
    }
    }

    next()
    }

    function isPromise(obj) {
    return ‘function’ == typeof obj.then;
    }

    module.exports = run;
    其实实现的很简单,判断 result.value 是否是 Promise,是就添加 then 函数,不是就直接执行。
    return Promise
    我们已经写了一个不错的启动器函数,支持 yield 后跟回调函数或者 Promise 对象。
    现在有一个问题需要思考,就是我们如何获得 Generator 函数的返回值呢?又如果 Generator 函数中出现了错误,就比如 fetch 了一个不存在的接口,这个错误该如何捕获呢?
    这很容易让人想到 Promise,如果这个启动器函数返回一个 Promise,我们就可以给这个 Promise 对象添加 then 函数,当所有的异步操作执行成功后,我们执行 onFullfilled 函数,如果有任何失败,就执行 onRejected 函数。
    我们写一版:
    // 第二版
    function run(gen) {
    var gen = gen();

    return new Promise(function(resolve, reject) {

    function next(data) {
    try {
    var result = gen.next(data);
    } catch (e) {
    return reject(e);
    }

    if (result.done) {
    return resolve(result.value)
    };

    var value = toPromise(result.value);

    value.then(function(data) {
    next(data);
    }, function(e) {
    reject(e)
    });
    }

    next()
    })

    }

    function isPromise(obj) {
    return ‘function’ == typeof obj.then;
    }

    function toPromise(obj) {
    if (isPromise(obj)) return obj;
    if (‘function’ == typeof obj) return thunkToPromise(obj);
    return obj;
    }

    function thunkToPromise(fn) {
    return new Promise(function(resolve, reject) {
    fn(function(err, res) {
    if (err) return reject(err);
    resolve(res);
    });
    });
    }

    module.exports = run;
    与第一版有很大的不同:
    首先,我们返回了一个 Promise,当 result.done 为 true 的时候,我们将该值 resolve(result.value),如果执行的过程中出现错误,被 catch 住,我们会将原因 reject(e)。
    其次,我们会使用 thunkToPromise 将回调函数包装成一个 Promise,然后统一的添加 then 函数。在这里值得注意的是,在 thunkToPromise 函数中,我们遵循了 error first 的原则,这意味着当我们处理回调函数的情况时:
    // 模拟数据请求
    function fetchData(url) {
    return function(cb) {
    setTimeout(function() {
    cb(null, { status: 200, data: url })
    }, 1000)
    }
    }
    在成功时,第一个参数应该返回 null,表示没有错误原因。
    优化
    我们在第二版的基础上将代码写的更加简洁优雅一点,最终的代码如下:
    // 第三版
    function run(gen) {

    return new Promise(function(resolve, reject) {
    if (typeof gen == ‘function’) gen = gen();

    // 如果 gen 不是一个迭代器
    if (!gen || typeof gen.next !== ‘function’) return resolve(gen)

    onFulfilled();

    function onFulfilled(res) {
    var ret;
    try {
    ret = gen.next(res);
    } catch (e) {
    return reject(e);
    }
    next(ret);
    }

    function onRejected(err) {
    var ret;
    try {
    ret = gen.throw(err);
    } catch (e) {
    return reject(e);
    }
    next(ret);
    }

    function next(ret) {
    if (ret.done) return resolve(ret.value);
    var value = toPromise(ret.value);
    if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
    return onRejected(new TypeError(‘You may only yield a function, promise ‘ +
    ‘but the following object was passed: “‘ + String(ret.value) + ‘”‘));
    }
    })
    }

    function isPromise(obj) {
    return ‘function’ == typeof obj.then;
    }

    function toPromise(obj) {
    if (isPromise(obj)) return obj;
    if (‘function’ == typeof obj) return thunkToPromise(obj);
    return obj;
    }

    function thunkToPromise(fn) {
    return new Promise(function(resolve, reject) {
    fn(function(err, res) {
    if (err) return reject(err);
    resolve(res);
    });
    });
    }

    module.exports = run;
    co
    如果我们再将这个启动器函数写的完善一些,我们就相当于写了一个 co,实际上,上面的代码确实是来自于 co……
    而 co 是什么? co 是大神 TJ Holowaychuk 于 2013 年 6 月发布的一个小模块,用于 Generator 函数的自动执行。
    如果直接使用 co 模块,这两种不同的例子可以简写为:
    // yield 后是一个 Promise
    var fetch = require(‘node-fetch’);
    var co = require(‘co’);

    function* gen() {
    var r1 = yield fetch(‘https://api.github.com/users/github’);
    var json1 = yield r1.json();
    var r2 = yield fetch(‘https://api.github.com/users/github/followers’);
    var json2 = yield r2.json();
    var r3 = yield fetch(‘https://api.github.com/users/github/repos’);
    var json3 = yield r3.json();

    console.log([json1.bio, json2[0].login, json3[0].full_name].join(‘\n’));
    }

    co(gen);
    // yield 后是一个回调函数
    var co = require(‘co’);

    function fetchData(url) {
    return function(cb) {
    setTimeout(function() {
    cb(null, { status: 200, data: url })
    }, 1000)
    }
    }

    function* gen() {
    var r1 = yield fetchData(‘https://api.github.com/users/github’);
    var r2 = yield fetchData(‘https://api.github.com/users/github/followers’);

    console.log([r1.data, r2.data].join(‘\n’));
    }

    co(gen);
    是不是特别的好用?
    ES6 系列
    ES6 系列目录地址:https://github.com/mqyqingfeng/Blog
    ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。
    如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

  • ES6 系列之我们来聊聊 Promise

    前言
    Promise 的基本使用可以看阮一峰老师的 《ECMAScript 6 入门》。
    我们来聊点其他的。
    回调
    说起 Promise,我们一般都会从回调或者回调地狱说起,那么使用回调到底会导致哪些不好的地方呢?
    1. 回调嵌套
    使用回调,我们很有可能会将业务代码写成如下这种形式:
    doA( function(){
    doB();

    doC( function(){
    doD();
    } )

    doE();
    } );

    doF();
    当然这是一种简化的形式,经过一番简单的思考,我们可以判断出执行的顺序为:
    doA()
    doF()
    doB()
    doC()
    doE()
    doD()
    然而在实际的项目中,代码会更加杂乱,为了排查问题,我们需要绕过很多碍眼的内容,不断的在函数间进行跳转,使得排查问题的难度也在成倍增加。
    当然之所以导致这个问题,其实是因为这种嵌套的书写方式跟人线性的思考方式相违和,以至于我们要多花一些精力去思考真正的执行顺序,嵌套和缩进只是这个思考过程中转移注意力的细枝末节而已。
    当然了,与人线性的思考方式相违和,还不是最糟糕的,实际上,我们还会在代码中加入各种各样的逻辑判断,就比如在上面这个例子中,doD() 必须在 doC() 完成后才能完成,万一 doC() 执行失败了呢?我们是要重试 doC() 吗?还是直接转到其他错误处理函数中?当我们将这些判断都加入到这个流程中,很快代码就会变得非常复杂,以至于无法维护和更新。
    2. 控制反转
    正常书写代码的时候,我们理所当然可以控制自己的代码,然而当我们使用回调的时候,这个回调函数是否能接着执行,其实取决于使用回调的那个 API,就比如:
    // 回调函数是否被执行取决于 buy 模块
    import {buy} from ‘./buy.js’;

    buy(itemData, function(res) {
    console.log(res)
    });
    对于我们经常会使用的 fetch 这种 API,一般是没有什么问题的,但是如果我们使用的是第三方的 API 呢?
    当你调用了第三方的 API,对方是否会因为某个错误导致你传入的回调函数执行了多次呢?
    为了避免出现这样的问题,你可以在自己的回调函数中加入判断,可是万一又因为某个错误这个回调函数没有执行呢?万一这个回调函数有时同步执行有时异步执行呢?
    我们总结一下这些情况:

    回调函数执行多次
    回调函数没有执行
    回调函数有时同步执行有时异步执行

    对于这些情况,你可能都要在回调函数中做些处理,并且每次执行回调函数的时候都要做些处理,这就带来了很多重复的代码。
    回调地狱
    我们先看一个简单的回调地狱的示例。
    现在要找出一个目录中最大的文件,处理步骤应该是:

    用 fs.readdir 获取目录中的文件列表;
    循环遍历文件,使用 fs.stat 获取文件信息
    比较找出最大文件;
    以最大文件的文件名为参数调用回调。

    代码为:
    var fs = require(‘fs’);
    var path = require(‘path’);

    function findLargest(dir, cb) {
    // 读取目录下的所有文件
    fs.readdir(dir, function(er, files) {
    if (er) return cb(er);

    var counter = files.length;
    var errored = false;
    var stats = [];

    files.forEach(function(file, index) {
    // 读取文件信息
    fs.stat(path.join(dir, file), function(er, stat) {

    if (errored) return;

    if (er) {
    errored = true;
    return cb(er);
    }

    stats[index] = stat;

    // 事先算好有多少个文件,读完 1 个文件信息,计数减 1,当为 0 时,说明读取完毕,此时执行最终的比较操作
    if (–counter == 0) {

    var largest = stats
    .filter(function(stat) { return stat.isFile() })
    .reduce(function(prev, next) {
    if (prev.size > next.size) return prev
    return next
    })

    cb(null, files[stats.indexOf(largest)])
    }
    })
    })
    })
    }
    使用方式为:
    // 查找当前目录最大的文件
    findLargest(‘./’, function(er, filename) {
    if (er) return console.error(er)
    console.log(‘largest file was:’, filename)
    });
    你可以将以上代码复制到一个比如 index.js 文件,然后执行 node index.js 就可以打印出最大的文件的名称。
    看完这个例子,我们再来聊聊回调地狱的其他问题:
    1.难以复用
    回调的顺序确定下来之后,想对其中的某些环节进行复用也很困难,牵一发而动全身。
    举个例子,如果你想对 fs.stat 读取文件信息这段代码复用,因为回调中引用了外层的变量,提取出来后还需要对外层的代码进行修改。
    2.堆栈信息被断开
    我们知道,JavaScript 引擎维护了一个执行上下文栈,当函数执行的时候,会创建该函数的执行上下文压入栈中,当函数执行完毕后,会将该执行上下文出栈。
    如果 A 函数中调用了 B 函数,JavaScript 会先将 A 函数的执行上下文压入栈中,再将 B 函数的执行上下文压入栈中,当 B 函数执行完毕,将 B 函数执行上下文出栈,当 A 函数执行完毕后,将 A 函数执行上下文出栈。
    这样的好处在于,我们如果中断代码执行,可以检索完整的堆栈信息,从中获取任何我们想获取的信息。
    可是异步回调函数并非如此,比如执行 fs.readdir 的时候,其实是将回调函数加入任务队列中,代码继续执行,直至主线程完成后,才会从任务队列中选择已经完成的任务,并将其加入栈中,此时栈中只有这一个执行上下文,如果回调报错,也无法获取调用该异步操作时的栈中的信息,不容易判定哪里出现了错误。
    此外,因为是异步的缘故,使用 try catch 语句也无法直接捕获错误。
    (不过 Promise 并没有解决这个问题)
    3.借助外层变量
    当多个异步计算同时进行,比如这里遍历读取文件信息,由于无法预期完成顺序,必须借助外层作用域的变量,比如这里的 count、errored、stats 等,不仅写起来麻烦,而且如果你忽略了文件读取错误时的情况,不记录错误状态,就会接着读取其他文件,造成无谓的浪费。此外外层的变量,也可能被其它同一作用域的函数访问并且修改,容易造成误操作。
    之所以单独讲讲回调地狱,其实是想说嵌套和缩进只是回调地狱的一个梗而已,它导致的问题远非嵌套导致的可读性降低而已。
    Promise
    Promise 使得以上绝大部分的问题都得到了解决。
    1. 嵌套问题
    举个例子:
    request(url, function(err, res, body) {
    if (err) handleError(err);
    fs.writeFile(‘1.txt’, body, function(err) {
    request(url2, function(err, res, body) {
    if (err) handleError(err)
    })
    })
    });
    使用 Promise 后:
    request(url)
    .then(function(result) {
    return writeFileAsynv(‘1.txt’, result)
    })
    .then(function(result) {
    return request(url2)
    })
    .catch(function(e){
    handleError(e)
    });
    而对于读取最大文件的那个例子,我们使用 promise 可以简化为:
    var fs = require(‘fs’);
    var path = require(‘path’);

    var readDir = function(dir) {
    return new Promise(function(resolve, reject) {
    fs.readdir(dir, function(err, files) {
    if (err) reject(err);
    resolve(files)
    })
    })
    }

    var stat = function(path) {
    return new Promise(function(resolve, reject) {
    fs.stat(path, function(err, stat) {
    if (err) reject(err)
    resolve(stat)
    })
    })
    }

    function findLargest(dir) {
    return readDir(dir)
    .then(function(files) {
    let promises = files.map(file => stat(path.join(dir, file)))
    return Promise.all(promises).then(function(stats) {
    return { stats, files }
    })
    })
    .then(data => {

    let largest = data.stats
    .filter(function(stat) { return stat.isFile() })
    .reduce((prev, next) => {
    if (prev.size > next.size) return prev
    return next
    })

    return data.files[data.stats.indexOf(largest)]
    })

    }
    2. 控制反转再反转
    前面我们讲到使用第三方回调 API 的时候,可能会遇到如下问题:

    回调函数执行多次
    回调函数没有执行
    回调函数有时同步执行有时异步执行

    对于第一个问题,Promise 只能 resolve 一次,剩下的调用都会被忽略。
    对于第二个问题,我们可以使用 Promise.race 函数来解决:
    function timeoutPromise(delay) {
    return new Promise( function(resolve,reject){
    setTimeout( function(){
    reject( “Timeout!” );
    }, delay );
    } );
    }

    Promise.race( [
    foo(),
    timeoutPromise( 3000 )
    ] )
    .then(function(){}, function(err){});
    对于第三个问题,为什么有的时候会同步执行有的时候回异步执行呢?
    我们来看个例子:
    var cache = {…};
    function downloadFile(url) {
    if(cache.has(url)) {
    // 如果存在cache,这里为同步调用
    return Promise.resolve(cache.get(url));
    }
    return fetch(url).then(file => cache.set(url, file)); // 这里为异步调用
    }
    console.log(‘1’);
    getValue.then(() => console.log(‘2’));
    console.log(‘3’);
    在这个例子中,有 cahce 的情况下,打印结果为 1 2 3,在没有 cache 的时候,打印结果为 1 3 2。
    然而如果将这种同步和异步混用的代码作为内部实现,只暴露接口给外部调用,调用方由于无法判断是到底是异步还是同步状态,影响程序的可维护性和可测试性。
    简单来说就是同步和异步共存的情况无法保证程序逻辑的一致性。
    然而 Promise 解决了这个问题,我们来看个例子:
    var promise = new Promise(function (resolve){
    resolve();
    console.log(1);
    });
    promise.then(function(){
    console.log(2);
    });
    console.log(3);

    // 1 3 2
    即使 promise 对象立刻进入 resolved 状态,即同步调用 resolve 函数,then 函数中指定的方法依然是异步进行的。
    PromiseA+ 规范也有明确的规定:
    实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
    Promise 反模式
    1.Promise 嵌套
    // bad
    loadSomething().then(function(something) {
    loadAnotherthing().then(function(another) {
    DoSomethingOnThem(something, another);
    });
    });
    // good
    Promise.all([loadSomething(), loadAnotherthing()])
    .then(function ([something, another]) {
    DoSomethingOnThem(…[something, another]);
    });
    2.断开的 Promise 链
    // bad
    function anAsyncCall() {
    var promise = doSomethingAsync();
    promise.then(function() {
    somethingComplicated();
    });

    return promise;
    }
    // good
    function anAsyncCall() {
    var promise = doSomethingAsync();
    return promise.then(function() {
    somethingComplicated()
    });
    }
    3.混乱的集合
    // bad
    function workMyCollection(arr) {
    var resultArr = [];
    function _recursive(idx) {
    if (idx >= resultArr.length) return resultArr;

    return doSomethingAsync(arr[idx]).then(function(res) {
    resultArr.push(res);
    return _recursive(idx + 1);
    });
    }

    return _recursive(0);
    }
    你可以写成:
    function workMyCollection(arr) {
    return Promise.all(arr.map(function(item) {
    return doSomethingAsync(item);
    }));
    }
    如果你非要以队列的形式执行,你可以写成:
    function workMyCollection(arr) {
    return arr.reduce(function(promise, item) {
    return promise.then(function(result) {
    return doSomethingAsyncWithResult(item, result);
    });
    }, Promise.resolve());
    }
    4.catch
    // bad
    somethingAync.then(function() {
    return somethingElseAsync();
    }, function(err) {
    handleMyError(err);
    });
    如果 somethingElseAsync 抛出错误,是无法被捕获的。你可以写成:
    // good
    somethingAsync
    .then(function() {
    return somethingElseAsync()
    })
    .then(null, function(err) {
    handleMyError(err);
    });
    // good
    somethingAsync()
    .then(function() {
    return somethingElseAsync();
    })
    .catch(function(err) {
    handleMyError(err);
    });
    红绿灯问题
    题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用 Promse 实现)
    三个亮灯函数已经存在:
    function red(){
    console.log(‘red’);
    }
    function green(){
    console.log(‘green’);
    }
    function yellow(){
    console.log(‘yellow’);
    }
    利用 then 和递归实现:
    function red(){
    console.log(‘red’);
    }
    function green(){
    console.log(‘green’);
    }
    function yellow(){
    console.log(‘yellow’);
    }

    var light = function(timmer, cb){
    return new Promise(function(resolve, reject) {
    setTimeout(function() {
    cb();
    resolve();
    }, timmer);
    });
    };

    var step = function() {
    Promise.resolve().then(function(){
    return light(3000, red);
    }).then(function(){
    return light(2000, green);
    }).then(function(){
    return light(1000, yellow);
    }).then(function(){
    step();
    });
    }

    step();
    promisify
    有的时候,我们需要将 callback 语法的 API 改造成 Promise 语法,为此我们需要一个 promisify 的方法。
    因为 callback 语法传参比较明确,最后一个参数传入回调函数,回调函数的第一个参数是一个错误信息,如果没有错误,就是 null,所以我们可以直接写出一个简单的 promisify 方法:
    function promisify(original) {
    return function (…args) {
    return new Promise((resolve, reject) => {
    args.push(function callback(err, …values) {
    if (err) {
    return reject(err);
    }
    return resolve(…values)
    });
    original.call(this, …args);
    });
    };
    }
    完整的可以参考 es6-promisif
    Promise 的局限性
    1. 错误被吃掉
    首先我们要理解,什么是错误被吃掉,是指错误信息不被打印吗?
    并不是,举个例子:
    throw new Error(‘error’);
    console.log(233333);
    在这种情况下,因为 throw error 的缘故,代码被阻断执行,并不会打印 233333,再举个例子:
    const promise = new Promise(null);
    console.log(233333);
    以上代码依然会被阻断执行,这是因为如果通过无效的方式使用 Promise,并且出现了一个错误阻碍了正常 Promise 的构造,结果会得到一个立刻跑出的异常,而不是一个被拒绝的 Promise。
    然而再举个例子:
    let promise = new Promise(() => {
    throw new Error(‘error’)
    });
    console.log(2333333);
    这次会正常的打印 233333,说明 Promise 内部的错误不会影响到 Promise 外部的代码,而这种情况我们就通常称为 “吃掉错误”。
    其实这并不是 Promise 独有的局限性,try..catch 也是这样,同样会捕获一个异常并简单的吃掉错误。
    而正是因为错误被吃掉,Promise 链中的错误很容易被忽略掉,这也是为什么会一般推荐在 Promise 链的最后添加一个 catch 函数,因为对于一个没有错误处理函数的 Promise 链,任何错误都会在链中被传播下去,直到你注册了错误处理函数。
    2. 单一值
    Promise 只能有一个完成值或一个拒绝原因,然而在真实使用的时候,往往需要传递多个值,一般做法都是构造一个对象或数组,然后再传递,then 中获得这个值后,又会进行取值赋值的操作,每次封装和解封都无疑让代码变得笨重。
    说真的,并没有什么好的方法,建议是使用 ES6 的解构赋值:
    Promise.all([Promise.resolve(1), Promise.resolve(2)])
    .then(([x, y]) => {
    console.log(x, y);
    });
    3. 无法取消
    Promise 一旦新建它就会立即执行,无法中途取消。
    4. 无法得知 pending 状态
    当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
    参考

    《你不知道的 JavaScript 中卷》
    Promise 的 N 种用法
    JavaScript Promise 迷你书
    Promises/A+规范
    Promise 如何使用
    Promise Anti-patterns
    一道关于Promise应用的面试题

    ES6 系列
    ES6 系列目录地址:https://github.com/mqyqingfeng/Blog
    ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。
    如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

  • express中使用es6

    express官网上给的javascript标准为es5,是不能直接跑es6的,想要在express中使用es6写法,可以用转码器Babel进行转码。
    开发环境中

    express项目中安装babel-cli
    $ npm install –save-dev babel-cli

    安装presets
    npm install –save-dev babel-preset-es2015 babel-preset-stage-2

    在package.json里添加运行的脚本
    {

    “scripts”: {
    “start”: “babel-node index.js –presets es2015,stage-2”
    }

    }

    到此就可以使用es6的写法了,写一段es6

    运行
    npm start

    刚开始学习express的时候,会遇到一个问题:每次改一点点代码,都需要重启服务。我们希望能够实现“热更新”的效果,接下来我们就可以使用nodemon监视文件修改,达到热更新效果,而不比每次都重启服务

    安装nodemon
    npm install –save-dev nodemon

    修改脚本
    {

    “scripts”: {
    “start”: “nodemon index.js –exec babel-node –presets es2015,stage-2”
    }

    }

    运行
    npm start
    现在更改js代码,不需要重启服务,就可以实现效果了