commonjs 引入函数的疑问

a.js

let num = 1

function add(){
    console.log(num);
    ++num
    console.log(num);
}

module.exports = {num,add}

b.js

let {num,add} = require('./a')
console.log(num);
add()
console.log(num);

node b.js
运行结果是1,1,2,1

疑问点:
如果在a里执行add()函数,外部的num会变成2
在b里执行add()函数,内部输出的num是2了,外部的不变,这是为什么呢?

阅读 1.8k
2 个回答

情况1,在 a.js 中执行,结果是 1 1 2 2,这个跟普通函数的执行结果是一致的,add() 函数找不到 num 这个标识符,就会上溯作用域链尝试从祖先作用域查找,因为在 a.js 这个模块作用于中有且仅有一个 num 标识符,所以这个 num 是被共享的,换而言之所有关于 num 的读写操作都是统一的,你可以简单的把 a.js 当作一个函数局部作用域或区块作用域辅助理解,在 a.js 中代码实际效果如下:

{/* a.js 模块作用域 */
  let num = 1
  console.log(num)

  const add = () => {
    console.log(num)// 1
    num++
    console.log(num)// 2
  }
  add()

  console.log(num)// 2
}/* 如上,结果:1 1 2 2 */

情况2,在 b.js 中执行,CJS 的模块本质是一个 Module 实例对象,module.exports 导出的实际上是一个 JS 对象,也就是将 num 的值赋值给导出的对象的一个同名属性 num,在 b.js 中代码实际效果如下:

const o = require('./index')/* b.js 中导入对象 */
const {
  num, 
  add
} = o/* a.js 导出的对象 */

console.log(num)// 1,是 o.num
add()// JS 是词法作用域,add 使用的 num 是add 函数定义所在作用域的 num,这里不是定义是执行
console.log(num)// 1,是 o.num

看一下require是怎么实现的,重点在_extensions函数里

const fs = require('fs');
const path = require('path');
const vm = require('vm');
class Module{
    constructor(id){
        this.id = id
        this.exports={}
    }
    load(){
        let ext = path.extname(this.id); // 获取文件后缀名
        Module._extensions[ext](this);
    }
}
Module._cache = {}
Module._extensions = {
    '.js'(module){
        let script = fs.readFileSync(module.id,'utf8');//同步读取文件
        let templateFn = `(function(exports,module,require,__dirname,__filename){${script}})`;
        let fn = vm.runInThisContext(templateFn);//运行上下文,运行模板字符串
        // let fn1 = (function(exports,module,require,__dirname,__filename){
        //             eval(script)
        //         })
        let exports = module.exports;
        let thisValue = exports; // this = module.exports = exports;
        let filename = module.id;
        let dirname = path.dirname(filename);//取当前文件的父路径
        // 函数的call 的作用 1.改变this指向 2.让函数执行
        fn.call(thisValue,exports,module,req,dirname,filename); // 调用了a模块 module.exports = 100;
    },
    '.json'(module){
        let script = fs.readFileSync(module.id,'utf8');
        module.exports = JSON.parse(script)
    }
}
Module._resolveFilename = function(id){
    let filePath = path.resolve(__dirname,id)
    let isExists = fs.existsSync(filePath);//是否存在文件后缀,返回布尔值
    if(isExists) return filePath;//如果存在,直接返回
    // 如果不存在,for循环尝试添加后缀
    let keys = Object.keys(Module._extensions); // 以后Object的新出的方法 都会放到Reflect上
    for(let i =0; i < keys.length;i++){
       let newPath = filePath + keys[i];
       if(fs.existsSync(newPath)) return newPath
    }
    throw new Error('module not found')
}
function req(filename){
    filename = Module._resolveFilename(filename); // 1.创造一个绝对引用地址,方便后续读取
    let cacheModule = Module._cache[filename]
    if(cacheModule) return cacheModule.exports; // 直接将上次缓存的模块丢给你就ok了
    const module = new Module(filename); // 2.根据路径创造一个模块
    console.log(module)
    Module._cache[filename] = module; // 最终:缓存模块 根据的是文件名来缓存
    module.load(); // 就是让用户给module.exports 赋值
    return module.exports; // 默认是空对象
}
let a = req('./a.js');
a = req('./a.js');
a = req('./a.js');
console.log(a)
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题