博客

  • (又一个)一行代码导致的血案-“is-promise”问题的粗浅分析

    (Updated)开发者自己做了一个事后分析,比我讲的清晰多了,当然主要原因还是JS module依赖设计的复杂度https://medium.com/javascript-in-plain-english/is-promise-post-mortem-cab807f18dcc 在这一点上我很看好deno这个新工具。

    ——————————-

    看到微信群里有人转关于“is-promise”更新导致的问题,公众号文章说的非常夸张,但又语焉不详,看了毫无头绪。于是看了些英文文章,做了点技术分析。

    首先声明,因为水平不够理解不深,这篇文章并没有解释清楚问题的原因,只是说了一下我看到的现象,确保将来写类似代码的时候,心里有点谱。

    “is-promise“有问题的版本更新是2.2.0,看了更新diff没发现什么明显问题。更新的目的主要是增加ES module风格的默认import。

    首先得熟悉一下ES Module的import,还有commonjs的require。

    先说一下ES6标准化的module,在浏览器中可以通过<script type=”module”>导入模块脚本,代码是这样的:”import xxx from ‘module-name’”。如果用CommonJS呢,就是“const package = require(‘module-name’)”。

    在Nodejs v12之前使用的是commonjs,在v12的时候引入了ES Module的支持。在node中可以通过条件导入同时支持两种方式(麻烦!)https://nodejs.org/dist/latest-v12.x/docs/api/esm.html#esm_conditional_exports

    在nodejs的官方文档中https://nodejs.org/dist/latest-v12.x/docs/api/esm.html#esm_dual_commonjs_es_module_packages 特意解释了双模式package的问题,其中特意提到了“Dual Package Hazard”(双Package风险),说实话这部分有些复杂,我也没有太理解。在后面提到了“Differences Between ES Modules and CommonJS“,其中第一条就是”Mandatory file extensions“,但是文档说的还是不清不楚的。

    我们看一下is-promise的bug fix能理解更深一些。https://github.com/then/is-promise/commit/0b69f52ee73c9f6ad4480f02c9bccb14d2038656 可以看到主要就是对文件路径指定,变成使用”./xxx.js”方式。为什么原来有问题,现在就能fix,说实话我还是没看懂。我在本地做了一个极为简单的node js脚本文件,引入了is-promise 2.2.0版本,尝试了import和require两种方式,没发现有问题。我也尝试从bug report中找点线索,但是大多数都是说xxx有问题了,没有细节。我甚至到create-react-app中查找is-promise,根本就没有,简直是磕了。

    先这样吧,等我理解更多了再更新此文。

    另外在HN上关于这件事的讨论https://news.ycombinator.com/item?id=22979245 也是非常热烈,不过没人分析原因,要么吐槽,要么建议用npm指定版本方式(版本号不用^和~)。

  • async/await仅仅是promise的语法糖么?

    简单的说结论:可以这么说。但我不能确定它是百分百等同,因为标准没有这么说。

    根据这篇文章https://mathiasbynens.be/notes/async-stack-traces Mathisas提到的,以及MDN上面的这一篇https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await 当我们使用await的时候,JS Engine会block当前运行代码,然后启动新的Promise(task)运行,等待它的结束或者异常抛出。

    这个部分我可能还要看一下emcascript的spec,确定真的理解了。但是很明显这与C#是截然不同的,差别就在于JS Engine是单线程的,而C#中会自动启动一个新的Task来做事情,当前的主Task和新启动Task是并行运行的。

    在async文档中有slow async和fast async的例子,便于更深入的理解https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

    那到底是用async/await还是promise?我的建议是,能用async/await的尽量用,也尽量不要和promise混用(我见过这么干的)。更细节上的判断可以参考MDN https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Choosing_the_right_approach

    另外需要注意的一点是,如果await多个promise,那么await和Promise.all在时序上和错误处理上是有所不同的。知道了这一点,trouble shooting的时候可以更仔细的研究细节。在这里Google Chrome团队就写了一篇很实用的文章,里面提到了如何对应转换promise代码成为async/await代码 https://developers.google.com/web/fundamentals/primers/async-functions

    先理解到这层面吧,如果有更深入了解再更新。

  • 重新学习JavaScript(2)

    继续看mdn的文档,今天看regexp部分。我想到一个小功能,把字符串中每个首字母大写。我看到的代码使用了split加上reduce,我呢,对于reduce总有些疑虑,因为它不直观,需要想一想才能把流程在脑子里理清楚。这样的功能用正则表达式正好,虽然正则也不容易理解,但是这貌似就是正则能打的领域,不用都可惜了。

    看了一会文档,拼凑出一个代码来,在chrome devtool跑了一下是期望的结果。

    function replacer(match, p1, p2, p3, offset, string) {
        console.log(arguments);
        const x = match.charAt(0).toLowerCase();
        return x + match.substring(1);
    }
    let re = /(\w+)/g;
    let str = 'john smith haha';
    let newstr = str.replace(re, replacer);
    console.log(newstr); 

    其实我并不是从string的replace函数看起的,而是看到了一个奇怪的用法。https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@replace 我在怎么写replacer一开始没搞懂,因为没有例子。甚至跑到这里看测试用例https://github.com/tc39/test262/tree/master/test/built-ins/RegExp/prototype/Symbol.replace 后来看ecmascript的文档才发现提到了string.replace,这才把知识点串联起来。

    var re = /-/g; 
    var replacer = function() {
        console.log(arguments);
        return '.';
    };
    var newstr = re[Symbol.replace]('2016-01-01', replacer);

    今天少写一点,就这些吧。

  • 重新学习JavaScript(1)

    学习JavaScript,就从MDN开始,其中包含了大量的值得深入学习和理解的内容。在这篇博客我介绍一些最近看到的。

    1,class expression

    首先就是类表达式https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/class 说实话这是我最近才知道的用法。它的语法如下,非常类似正常的类定义语句(class declaration statement),但是可以赋值给一个变量。如果使用typeof查一下就知道,MyClass类型还是function

    const MyClass = class [className] [extends otherClassName] {
        // class body
    };

    类表达式可以重复定义,这个和类定义方式不同。另外className可以省略,如果命名了,那这个名字也只在class body有效,这个和function expression是一样的 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function

    2, this

    this在JavaScript面试中几乎是必考的点,https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this 这里值得注意的几点,一个是call和apply怎么记忆呢?最简单的办法就是apply第二个参数接受的是数组类型,也就是array,a对a就能容易记住了。

    下面这段代码,其中有个知识点就是如果第一个参数不是object类型,就会调用内部的ToObject函数,比如7就会变成Number,而字符串字面值‘foo’就会转成String。

    function bar() {
      console.log(Object.prototype.toString.call(this));
    }
    bar.call(7);     // [object Number]
    bar.call('foo'); // [object String]

    而bind需要注意的是只会绑定一次

    function f() {
      return this.a;
    }
    var g = f.bind({a: 'azerty'});
    console.log(g()); // azerty
    var h = g.bind({a: 'yoo'}); // bind only works once!
    console.log(h()); // azerty

    3,Destructuring assignment解构赋值

    这个词有点难理解,多看看里面的示例会好很多,大致上分成两类数组解构和对象解构 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

    下面这个用法有点费解,最后得到的两个变量不是a和b,而是aa和bb。

    const {a: aa = 10, b: bb = 5} = {a: 3};
    console.log(aa); // 3
    console.log(bb); // 5

    继续加深理解

    const user = {
      id: 42,
      displayName: 'jdoe',
      fullName: {
        firstName: 'John',
        lastName: 'Doe'
      }
    };
    function userId({id}) {
      return id;
    }
    function whois({displayName, fullName: {firstName: name}}) {
      return `${displayName} is ${name}`;
    }
    console.log(userId(user)); // 42
    console.log(whois(user));  // "jdoe is John"

    注意与下面的例子的区别,主要在参数的声明格式上。一个用的等号,一个用的冒号。

    function drawChart({size = 'big', coords = {x: 0, y: 0}, radius = 25} = {}) {
      console.log(size, coords, radius);
    }
    drawChart({
      coords: {x: 18, y: 30},
      radius: 30
    });

    这个例子更复杂些,属性是计算得来

    let key = 'z';
    let {[key]: foo} = {z: 'bar'};
    console.log(foo); // "bar"

    还可以用rest 属性… 顺序也没有关系,只要能和属性名对上就行

    let {c, a, ...rest} = {a: 10, b: 20, c: 30, d: 40}
    a; // 10
    c; // 30
    rest; // { b: 20, d: 40 }
    const foo = { 'fizz-buzz': true };
    const { 'fizz-buzz': fizzBuzz } = foo;
    console.log(fizzBuzz); // "true"

    数组解构和对象解构组合起来

    const props = [
      { id: 1, name: 'Fizz'},
      { id: 2, name: 'Buzz'},
      { id: 3, name: 'FizzBuzz'}
    ];
    const [,, { name }] = props;
    console.log(name); // "FizzBuzz"
  • 在JavaScript中应用Mixin模式

    编程中Mixin是什么?我最近看到这个词,有点感兴趣起来。我以前也见过这个词,依稀记得是在介绍Ruby的文章当中,但是JavaScript中的Mixin干什么用呢?有些公众号提到express.js和vue.js的实现应用了mixin模式,我最近主要也是做这些方面,所以得关注了解啊。

    最权威的还是先看看wiki怎么说:https://en.wikipedia.org/wiki/Mixin 头一段说明有点绕,不是很容易理解,那就看看JS代码吧。Wiki列出了三种实现,分别是extend,Object.assign以及Flight-Mixin。

    第一种extend方式的参考实现,就是把源对象上面的key,逐个赋值到目标对象上。其它的两种大致也都是如此,Flight-Mixin模式其实就是IIFE直接执行的变形。

    剥掉了神秘感,剩下的就很简单了。有什么限制呢?首先,mixin的源对象操作this的属性,一定要在目标对象上存在,否则就会出现undefined问题了。其次mixin在JS这种动态类型的编程语言是合适的,如果用在c#这样的静态类型语言,就可以用扩展方法来实现,更灵活的就得用dynamic来定义this了。

    那mixin的用处,大致就是mixin源对象可以定义behavior,然后可以动态的绑定到其它对象上,只要这些对象满足mixin的constraints就可以了,把行为抽象出来。就此推论,mixin也很容易实现成decorator。mixin还要注意的一点是,它实质是一种浅拷贝,浅拷贝可能有的问题它也会有。

    在core-decorators项目中的代码更加完整 https://github.com/jayphelps/core-decorators/blob/5b754256a30c23a0aef846c1b45f261e0c7b21a2/src/mixin.js,其中使用了getOwnPropertyDescriptors以及defineProperty这样更为特定的函数处理不同的情况。

    mixin有什么问题么?当然有,什么设计模式都有局限性和适用的场景。比如react就提到https://reactjs.org/blog/2016/07/13/mixins-considered-harmful.html 简单说,mixin在大的codebase中增加了隐形依赖复杂度(Mixins introduce implicit dependencies),命名冲突问题(Mixins cause name clashes),滚雪球复杂度(Mixins cause snowballing complexity)。所以在express.js这样的轻量级框架中使用mixin说的通,但是复杂的企业级应用,就不得不考虑复杂度带来的各种问题。

    我的建议是如果软件超过7个人开发,那就考虑angular/TypeScript/rxjs这一套。如果是小项目,可以考虑react或者vue.js,但是最好也配上babel加ES最新标准。

    感兴趣的可以留言讨论。