JavaScript理解模块化的作用域 | 作用域 - 执行上下文 - 模块化

文章目录

  • JavaScript模块化与作用域
    • 作用域和作用域链 - 静态
    • 执行上下文 - 动态
      • 执行上下文的两个阶段: 创建阶段和执行阶段
        • 1.创建阶段
        • 2.执行阶段
    • 模块化
      • ES6 模块化
        • export与import的使用
        • import()函数 - 实现动态引入
      • Node模块化
        • module.exports 和 exports
        • 模块执行的原理
        • 模块加载过程
      • commonJs和es module的区别

JavaScript模块化与作用域

作用域和作用域链 - 静态

  • 作用域:一个代码段所在区域
  • 核心:作用域是静态的,编写代码时就确定
  • 作用:绑定变量在这个作用域有效,隔离变量,不同作用域下同名变量不会有冲突
  • 作用域链:多个作用域嵌套,就近选择,先在自己作用域找,然后去就近的作用域找。

作用域分类
1.全局作用域
2.函数作用域
3.块级作用域

全局作用域可以理解为window.变量,函数作用域函数名.变量,块级作用域块唯一标识符.变量。(这里只是方便理解,并不是知识点)

案例
aaa执行时,现在当前函数作用域找a(l理解为aaa.a),没找到。去父级作用域window里面找,找到a=10

let a = 10;  
function aaa() {
    console.log(a);
}
function bbb() {
    let a = 20;
    aaa();
}
bbb();

执行上下文 - 动态

抽象当前JavaScript的执行环境,包括变量、this指向等信息。每当JavaScript开始执行时,它都在执行上下文中运行。

全局执行上下文:在执行全局代码前将window确定为全局执行上下文,在整个页面生存周期内存在。

  • let定义的全局变量变量提升,添加为window的属性
  • function声明的全局函数 --> 赋值(函数体),添加为window的方法
  • this --> 赋值window
  • 开始执行全局代码
  • 函数执行上下文:在调用函数,准备执行函数体之前,创建对应的函数执行上下文对象

函数执行上下文,每当调用一个函数时,都会创建一个新的函数执行上下文,函数执行上下文在函数执行结束后被销毁。

  • 形参变量 --> 赋值(实参)–> 添加到函数执行上下文的属性
  • arguments(形参列表封装成的伪数组)–>赋值(实参列表),添加到函数执行上下文的属性
  • let 变量提升,添加为函数执行上下文的属性
  • function声明的函数–>赋值(函数体),添加为函数执行上下文的方法
  • this–>赋值(调用函数的对象)
  • 开始执行函数体代码

执行上下文的两个阶段: 创建阶段和执行阶段

1.在全局代码执行前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象
2.在全局执行上下文(window)确定后,将其添加到栈中(压栈)
3.在函数执行上下文创建后,将其添加到栈中(压栈)
4.在当前函数执行完成后,将栈顶的对象移除(出栈)
5.当所有的代码执行完后,栈中只剩下window

作用域 执行上下文
定义了几个函数 + 1 = 几个作用域 执行了几个函数 + 1 = 几个执行上下文
函数定义时就确定了,一直存在,不会再变化,是静态的 全局执行上下文环境实在全局作用域确定之后,js代码执行之前创建的
调用函数时创建,函数调用结束被释放,是动态的

JavaScript理解模块化的作用域 | 作用域 - 执行上下文 - 模块化_第1张图片

1.创建阶段

1.创建变量对象:根据上下文类型创建一个空的对象
2.建立作用域链:作用域链是一个指向父级作用域的链,用于查找变量的值。
3.确定this的指向
4.初始化变量对象:将函数的参数(仅函数执行上下文?)、函数声明和变量添加到变量对象中。

2.执行阶段

1.执行代码:按照代码的顺序执行,对变量赋值等操作。
2.访问变量:通过作用域链查找变量的值。
3.执行函数:在函数上下文中,执行函数体内的代码。

模块化

script标签直接引入的方式,没有模块化,script内部的变量是可以相互渲染的。

模块的概念:一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取。

  • CommonJS 服务于服务器 => NodeJS、Browserify
  • ES6模块化 服务于服务器和浏览器

ES6 模块化

ES6模块化的设计思想使尽量静态化,使得编译时就能确定模块的依赖关系,以及输入输出的变量。但是也支持动态引入的方式。

特点

  1. ES6 module 的引入和导出是静态的,import 会自动提升到代码的顶层 import , export 不能放在块级作用域或条件语句中。

    import关键字是静态导入,import()函数可以实现动态导入。
    这种静态语法,在编译过程中确定了导入和导出的关系,所以更方便去查找依赖,更方便去 tree shaking 。

  2. 使用import 导入的变量是只读的,可以理解默认为 const 装饰,无法被赋值

  3. 使用 import 导入的变量是与原变量绑定/引用的,可以理解为 import 导入的变量无论是否为基本类型都是引用传递。

  4. import导入文件时或仅导入文件的部分变量时,都会执行该文件

  5. import导入相对路径上的./不能省略

export与import的使用

前提:script标签加上一个属性type=module,那么该script就是一个es模块,可以使用模块的导入import与导出export

默认暴露export default

export default 表达式
import 给抛出的表达式命名 from '地址'

分别暴露于统一暴露export

// 分别暴露
export 变量
export const name = "ranan";
export const fn = function(){};
import { fn } from '地址'
// 统一暴露
let name = "ranan";
let fun = function(){};

//注意{}不是对象的意思是特定的语法,并给name重命名
export {name1:name,fun};
import { name } from '地址'

import引入

可以通过as关键字给引入部分起别名。

//通用引入 将所有输出的变量放在m1对象中,采用m1.xx的方式使用
import * as m1 from  './xxx'
/*
统一暴露或分别暴露,这里的{}也是特定语法
并且{ }内的变量名称需要和export导出的变量名称相同
*/
import {name,fun} from './xxx'
//默认暴露的写法,必须要写别名,因为default是关键字
import {default as data} from "path" 
//简便形式 针对默认暴露
import data from "path"

说明

  1. export不支持直接导出变量和值

    变量只存在声明时,声明之后变量都会作为表达式使用。错误写法:

    // 错误案例
    const name = 'aaa'
    export name // 导出变量
    export 'aaa' // 导出值	
    
  2. 在一个文件或模块中exportimport 可以有多个,export default 仅有一个

import()函数 - 实现动态引入

语法:
返回值:promise对象pormise返回的成功值就是暴露的对象。
作用:懒加载/按需加载 -> import() 可以很轻松的实现代码分割。避免一次性加载大量 js 文件,造成首次加载白屏时间过长的情况。

import() 动态加载一些内容,可以放在条件语句或者函数执行上下文中

if(isRequire){
    const result  = import('./b')
}

Node模块化

Node 是 CommonJS 在服务端一个具有代表性的实现

特点

  • commonJs中每个js文件都是一个单独的模块module
  • 使用require()方法加载其他模块时,会执行被加载模块中的代码
  • module.exports 指向暴露的对象,exports是简写情况。默认情况下,exportsmodule.exports指向同一个对象 exports==module.exports。最终引入的是module.exports
  • require() 可以在任意的位置,动态加载(运行时加载)模块,不会提升到最开头
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • CommonJS 同步加载并执行模块文件,CommonJS 模块在执行阶段分析模块依赖。采用深度优先算法
module.exports 和 exports

在具体引擎的实现中

module.exports = {}
exports = module.exports

所以使用 exports.xx = 'xxx' 其实就是往 module.exports = {} 这个对象中添加属性

引入的是module.exports属性
情况1:exports默认情况下是指向module.exports,下面案例重新给exports赋值了。由于引入的是module.exports属性,所以导入的数据是{}。这种方式无法暴露变量。

exports = {
    name: '123'
}

情况2:module.exports重新赋值了,由于引入的是module.exports属性,所以导入的数据是{}

exports.name = "123"
module.exports = {}
模块执行的原理

1.在编译过程中,commonJS对js的代码块进行了收尾包装
每个模块文件上存在requiremoduleexports方法,但是这三个变量在文件中是没有定义的。 commonJS会将我们写代码包装起来,形成包装函数,requiremoduleexports本质是通过形参的方式传递到包装函数中的。
require: 引入模块的方法
module: 记录当前模块信息
exports:当前模块导出的属性
2.在模块加载的时候,会传入requiremoduleexports等参数

const sayName = require('./hello.js')
module.exports = function say(){
    return {
        name:sayName(),
        author:'ranan'
    }
}
//包装后
(function(exports,require,module,__filename,__dirname){
	const sayName = require('./hello.js')
module.exports = function say(){
    return {
        name:sayName(),
        author:'ranan'
    	}
	}
})

作用域案例

执行node test1.js时,输出alex。每个js文件都是一个单独的模块,每个模块在编译包装后形成函数,所以每个模块内容都在一个函数作用域里。引入模块,相当于执行这个函数并取出module.exports的内容。
在本案例中fninfotest2.jsmodule.exports都指向同一个地址,所以fn调用时,实际是调用getInfo 函数。name会先在getInfo 作用域中找,没有找到,再去其父级作用域test2.js中寻找。

//test2.js
const name = "alex"
const getInfo = () =>(info={name: name})
module.exports = getInfo
//test3.js
const name = 'ranran'
const info = require('./test2');
module.exports = info
//test1.js
const fn = require('./test3');
console.log(fn())
模块加载过程
  1. 使用require()方法加载其他模块时,会执行被加载模块中的代码。
  2. require() 可以在任意的位置,动态加载(运行时加载)模块,不会提升到最开头

每个module上保存了一个 loaded 属性,该属性表示该模块是否被加载。没有加载过就先缓存后执行,已经加载过,就直接去缓存不用执行来避免循环引用问题

/*
根据文件标识符,先查找有没有缓存,有缓存直接返回缓存的内容
没有缓存,创建module对象,将其缓存到module上,然后加载文件,将 loaded 属性设置为 true ,然后返回 module.exports 对象。
*/
 // id 为路径标识符
function require(id) {
   /* 查找  Module 上有没有已经加载的 js  对象*/
   const  cachedModule = Module._cache[id]
   
   /* 如果已经加载了那么直接取走缓存的 exports 对象  */
  if(cachedModule){
    return cachedModule.exports
  }
 
  /* 创建当前模块的 module  */
  const module = { exports: {} ,loaded: false , ...}

  /* 将 module 缓存到  Module 的缓存属性中,路径标识符作为 id */
  //只会在第一次加载时运行一次,后面都会从缓存中读取,  
  Module._cache[id] = module
  /* 加载文件 */
  runInThisContext(wrapper('module.exports = "123"'))(module.exports, require, module, __filename, __dirname)
  /* 加载完成 *//
  module.loaded = true 
  /* 返回值 */
  return module.exports
}

commonJs和es module的区别

- commonJs es module
导入方式 require()动态加载模块,可以在任意的位置,不会被提升到最前面 导入方式分为静态导入和动态导入
静态导入:不能放在块级作用域和条件中,会提升到最前面
动态导入import()类似require(),但他是异步加载的
构建模块依赖的时期 require同步加载并执行模块文件,CommonJS 模块在执行阶段分析模块依赖。 在编译阶段就建立起了模块之间的依赖关系
ES6 模块在预处理阶段分析模块依赖,在执行阶段执行模块
输出的值 输出值的拷贝值,一旦输出了某个值,如果模块内部发生变化,不会影响外部的值 输出的是值的引用,JS 引擎对脚本静态分析的时候。遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行的时候,再根据这个只读引用,到被加载的那个模块里去取值。所以内部发生变化会影响外部的值。

你可能感兴趣的:(面试题,javascript,开发语言,ecmascript)