• 说说 CommonJS 中的 require 和 ES6 中的 import 区别?


    提问

    CommonJS 中的 require/exports 和 ES6 中的 import/export 区别?

    回答

    • CommonJS 模块是运行时加载,ES6 Modules 是编译时加载并输出接口。
    • CommonJS 输出是值的拷贝;ES6 Modules输出的是值的引用,被输出模块的内部的改变会影响引用的改变。
    • CommonJs 导入的模块路径可以是一个表达式,因为它使用的是 require() 方法,甚至这个表达式计算出来的内容是错误的路径,也可以通过编译到执行阶段再出错;而ES6 Modules 只能是字符串,并且路径不正确,编译阶段就会抛错。
    • CommonJS this 指向当前模块,ES6 Modules this 指向 undefined
    • ES6 Modules 中没有这些顶层变量:arguments、require、module、exports、__filename、__dirname

    此总结出自 如何回答好这个高频面试题:CommonJS和ES6模块的区别?,笔者在这里做一些其他的分析

    关于第一个差异运行时加载和编译时加载

    这是最大的一个差别。commonjs 模块在引入时就已经运行了,它是“运行时”加载的;但 es6 模块在引入时并不会立即执行,内核只是对其进行了引用,只有在真正用到时才会被执行,这就是“编译时”加载(引擎在编译代码时建立引用)。很多人的误区就是 JS 为解释型语言,没有编译阶段,其实并非如此。举例来说 Chrome 的 v8 引擎就会先将 JS 编译成中间码,然后再虚拟机上运行。

    CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

    由此引发一些区别,如 require 理论上可以运用在代码的任何地方,可以在引入的路径里加表达式,甚至可以在条件判断语句里处理是否引入的逻辑。因为它是运行时的,在脚本执行时才能得知路径与引入要求,故而甚至时路径填写了一个压根不存在的地址,它也不会有编译问题,而在执行时才抛出错误。

    // ...a lot code
    if (true) {
      require(process.cwd() + '/a');    
    }
    
    

    但是 import 则不同,它是编译时的,在编译时就已经确定好了彼此输出的接口,可以做一些优化,而 require 不行。所以它必须放在文件开头,而且使用格式也是确定的,路径里不许有表达式,路径必须真实能找到对应文件,否则编译阶段就会抛出错误。

    import a from './a'
    
    // ...a lot code
    

    关于第一个差异,是因为CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

    关于第二点 CommonJS 输出的是值的拷贝 的补充

    // a.js
    
    var name = '张三';
    var sex = 'male';
    var tag = ['good look']
    
    setTimeout(function () {
      console.log('in a.js after 500ms change ', name)
      sex = 'female';
      tag.push('young');
    }, 500)
    
    // exports.name = name;
    // exports.sex = sex;
    // exports.tag = tag;
    
    module.exports = {
      name,
      sex,
      tag
    }
    
    
    // b.js
    var a = require('./a');
    setTimeout(function () {
      console.log(`after 1000ms in commonjs ${a.name}`, a.sex)
      console.log(`after 1000ms in commonjs ${a.name}`,  a.tag)
    }, 1000)
    console.log('in b.js');
    
    

    若运行 b.js,得到下面的输出

    $ node b.js
    in b.js
    in a.js after 500ms change  张三
    after 1000ms in commonjs 张三 male
    after 1000ms in commonjs 张三 [ 'good look', 'young' ]
    

    把 a 和 b 看成两个不相干的函数,a 之中的 sex 是基础属性当然影响不到 b,而 a 和 b 的 tag 是引用类型,并且是共用一份地址的,自然 push 能影响。

    补充说明 require 原理

    require 是怎么做的?先根据 require('x') 找到对应文件,在 readFileSync 读取, 随后注入exports、require、module三个全局变量再执行源码,最终将模块的 exports 变量值输出

    Module._extensions['.js'] = function(module, filename) {
      var content = fs.readFileSync(filename, 'utf8');
      module._compile(stripBOM(content), filename);
    };
    

    读取完毕后编译

    Module.prototype._compile = function(content, filename) {
      var self = this;
      var args = [self.exports, require, self, filename, dirname];
      return compiledWrapper.apply(self.exports, args);
    };
    

    上面代码等同于

    (function (exports, require, module, __filename, __dirname) {
      // 模块源码
    });
    

    模块的加载实质上就是,注入exports、require、module三个全局变量,然后执行模块的源码,然后将模块的 exports 变量的值输出。

    补充说明 Babel 下的 ES6 模块转化

    Babel 也会将 export/import的时候,Babel也会把它转换为exports/require的形式。

    // m1.js
    export const count = 0;
    
    // index.js
    import {count} from './m1.js'
    console.log(count)
    

    Babel 编译后就应该是

    // m1.js
    "use strict";
    
    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    exports.count = void 0;
    const count = 0;
    
    
    // index.js
    "use strict";
    
    var _m = require("./m1.js");
    
    console.log(_m.count);
    exports.count = count;
    

    正因为有 Babel 做了转化,所以 require 和 import 才能被混用在一个项目里,但是你应该知道这是两个不同的模块系统。

    题外话

    留个思考题给大家,这两种模块系统对于循环引用的区别?有关于循环引用是啥,参见我这篇Node 模块循环引用问题

  • 相关阅读:
    c/c++中两颗璀璨的明珠
    deepin软件中心打不开
    shell之rm -rf的别名设置
    历史命令脚本
    mysql之7xtrabackup
    python之3内置容器
    python之第一个例子hello world
    python之安装
    shell脚本练习(autocert)
    【转】nginx之逻辑运算
  • 原文地址:https://www.cnblogs.com/everlose/p/12897791.html
Copyright © 2020-2023  润新知