【JS知识体系】- 持续更新

194 阅读12分钟

作用域

  • 静态作用域 & 动态作用域

    JS采用的是静态作用域(也称 词法作用域)

静态作用域

函数的作用域在函数定义时就决定了

var val = 1;
function test() {
    console.log(val)
}
function bar() {
    var val = 2;
    test();
}
bar();

/*
    【JS采用静态作用域函数】
    - test()函数被调用,且内部未找到变量val
    - test()是在全局环境里定义的,所以其中的变量要从全局作用域里找,打印1
*/

动态作用域

函数的作用域在函数调用时才决定的

var val = 1;
function test() {
    console.log(val)
}
function bar() {
    var val = 2;
    test();
}
bar();
/*
    【动态作用域】
    - test()内部未找到变量val
    - test()在函数bar()中被调用,则直接在bar函数找变量val,打印2
*/

变量提升与函数提升

  • JS引擎读取JS代码时,执行两个过程

    • 解析整个JS代码
    • 执行代码

在解析代码过程中,JS引擎在遇到 var 声明的变量 和 function声明的函数时,会将其提升到当前作用域的最前面

变量提升

使用 var声明的变量才会有提升的说法

console.log(a); // undefined
var a = 'hello';
console.log(a); // hello

解析过程

var a;
console.log(a);
a = 'hello';
console.log(a);

函数提升

只针对具名函数,匿名函数不存在函数提升

console.log(a); // f a() { console.log('a - function')}
console.log(b); // undefined
function a() {
    console.log('a - function');    
}
var b = function() {
    console.log('b - function');    
}

解析过程

var a = function a() {
    console.log('a - function'); 
}
var b;
console.log(a);
console.log(b);
b = function() {
    console.log('b - function');    
}

变量提升与函数提升的优先级

  • 函数提升优先级 【高于】 变量提升优先级
  • 不会被同名变量声明覆盖,但会被同名变量赋值覆盖
console.log(a);
console.log(a());
var a = 1;
function a = () {
    console.log(2);
}
console.log(a);
a = 3;
console.log(a());

解析过程

// 函数提升
var a = function () {
    console.log(2);
}
// 变量提升。同名变量的声明不会覆盖函数
var a;
console.log(a); // f a() { console.log(2) }
console.log(a()); // 2
a = 1;
console.log(a); // 1 同名的变量赋值会覆盖函数
a = 3;
console.log(a()); // 报错,此时a是一个变量而非函数 Uncaught TypeError: a is not a function

执行上下文

JS引擎在解析到可执行代码片段(函数调用阶段)时,会做一些执行前的准备工作,这个”准备工作“被称为【执行上下文】或者【执行环境】

  • 包含三种类型上下文
    • 全局执行上下文
      • 默认的执行上下文
      • 一个程序只存在一个全局执行上下文,存在于执行栈中最底部,且不会被弹出栈销毁
    • 函数执行上下文
      • 当函数调用时被压入栈顶
    • eval函数执行上下文(不常用)

执行栈

存储执行上下文环境的栈内存

举个例子

var a = 10;
var b = 1
function foo() { 
    var b = 20; 
    function bar() { 
        var c = 30; 
        console.log(a + b + c); 
    } 
    bar(); 
} 
foo();

上面执行栈中存储的执行上下文(从栈底到栈顶):

全局执行上下文 -> foo函数执行上下文 -> bar函数执行上下文

执行栈入栈/出栈过程

  • 全局执行上下文入栈(栈底),包含两个变量a=10, b=1
  • foo函数调用,其对应的函数执行上下文入栈,包含b=20,bar(),其父环境是【全局环境】(看定义位置)
  • bar函数调用,其对应的函数执行上下文入栈,包含c=30,其父环境是【foo函数】(看定义位置)
  • console.log(a+b+c) 变量看自身环境是否定义,否则去其父环境找,a=10,b=20,c=30

变量对象

  • 变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量函数声明
  • 全局执行上下文:
    变量: a、b,
    函数: foo
    都可以通过thiswindow进行访问
    全局执行上下文的变量就是全局变量
    
  • 函数执行上下文
    • 进入执行上下文
      • 形参:由名称和对应值组成
      • 函数:由名称和对应值(function-object)组成。存在同名函数,则替换
      • 变量:由名称和对应值(undefined)组成。变量名与函数名重复,变量名不干扰函数名(函数优先级高)
    • 代码执行

举个例子

var a = 10;
var b = 1
function foo(d) { 
    var b = 20; 
    function bar() { 
        var c = 30; 
        console.log(a + b + c); 
    }
    var e = function() {}
    bar(); 
} 
foo(1);

过程解析

// 【进入执行上下文过程解析】
{
    arguments: {
        0: 1,
        length: 1
    },
    d: 1,
    b: undefined,
    bar: reference to function bar(){},
    e: undefined
}

// 【代码执行过程】
{
    arguments: {
        0: 1,
        length: 1
    },
    d: 1,
    b: 20,
    bar: reference to function bar(){},
    e: reference to function bar(){},
}

作用域链

作用域

解析

  • 变量的可访问性和可见性。即整个程序中哪些部分可以访问这个变量。

分类

  • 全局作用域
  • 函数作用域(局部作用域)
  • 块级作用域
    • ES6引入的 letconst。在大括号中使用 letconst 声明的变量存在于块级作用域中,大括号之外不能访问这些变量。
    {
        let a = 'Hello World!';
        var b = 'English';
        console.log(a); // Hello World! 
    }
    console.log(a); // 报错:Uncaught ReferenceError: b is not define
    console.log(b); // English 
    

作用域链

  • 当JS代码中读取一个变量时(以a为例)
    • JS引擎尝试在当前作用域寻找a
    • 没找到,则去其上层作用域寻找a
    • 以此类推,直到找到a,或者到了【全局作用域】
    • 若在全局作用域仍然找不到a,则报错(在非严格模式下,全局作用域内隐式声明a)
    像这样一层一层往上层作用域找变量的链路,就形成一条作用域链
词法环境

Lexical Enviroments

  • 环境记录器(Enviroment Records):存储变量和函数声明的实际位置
  • 外部环境的引用(outer):可以通过这个访问器父级词法环境【这是作用域链的关键所在】

创建时机

  • 函数声明时创建
  • 函数表达式 - 赋值时被创建
    var myFunc = function() {}
    
    // var myFunc = undefined - 1
    // myFunc = c function() {} - 2
    // 在2的时候,myFunc的函数词法环境被创建
    
词法作用域

Lexical Scope:作用域(JS中为静态作用域)

创建时机

  • 在代码编写时就被创建(比如,函数声明时就会创建其作用域)
    • 这也就能解释,为什么词法在函数的父级是看创建时的作用域,而不是调用时的作用域

this

结论

  • this永远指向一个对象
  • this的指向完全取决于函数调用的位置

一个例子

function fun(){
    console.log(this.s);
}

var obj = {
    s:'1',
    f:fun
}

var s = '2';

obj.f(); //1
fun(); //2

问题

  • this为什么会改变?
    • 函数在运行时需要确定其当前的运行环境,this会随着运行环境的改变而改变。所以this的作用就是定位当前运行环境的。
  • this什么时候改变?
    • 函数中的this只有在函数运行时才能最终确定。

this使用最常见的情况

事件绑定

行内绑定

<div onClick="btn()">点击</div>
<script>
    function btn() {
        this // 此时函数的运行环境在【window】对象下,所以this指向window
    }
</script>

<div onClick="this">点击</div>
<!-- 运行环境在节点对象中,this指向当前节点对象 -->

动态绑定 & 事件监听

<div id="btn"></div>
const btnNode = document.querySelector('#btn');
btnNode.onClick = function() {
    this // this指向本节点对象
}

btnNode.addListener('click', function() {
    this // this指向本节点对象
})
构造函数(new关键字的使用)
function fun() {
    this.a = 1,
    this.fun = function(){
        this // this指向当前对象
    }
}

const f = new fun()

new一个对象的过程

  • 创建一个空对象-f
  • 将空对象的显式原型-_proto_指向构造函数-fun()的隐式原型-prototype
  • 将构造函数-fun()中的 this 指向空对象
  • 执行构造函数-fun()的代码(为对象赋值)
  • 返回新对象-f
const f = {};
f._proto_ = fun.prototype
fun.call(f)
return f
定时器

setTimeoutsetInterval都是window对象内置方法,接收两个参数

  • 函数或一段可执行代码
  • 函数或代码的执行延迟/间隔时间
const obj = {
    fun:function(){
        this
    }
}

setInterval(obj.fun, 1000) // 函数
setInterval("obj.fun()", 1000) // 可执行代码

// 第一段,传递了一个方法的地址作为引用传递,1S后运行方法,this指向window对象
// 第二段,传入一段可执行代码,1S后通过obj对象找到fun(),this指向obj
函数对象的 call/apply方法
const test = {name:'test'}
function fun(age, sex) {
    console.log(`name:${this.name},age:${age}, sex:${sex}`)
}

<!-- call -->
// 将fun函数中的this指向对象test上
fun.call(test, 26, 'male')

<!-- apply -->
// 将fun函数中的this指向对象test上
fun.apply(test, [26, 'female'])

闭包

定义

  • 一个函数内部的函数
  • 能够读取其他函数内部变量的函数
  • 即使创建内部函数的上下文已经被销毁,它还是能访问上下文的变量(内部函数从父函数中返回)

举个例子

function fun1() {
    var a = 1;
    function fun2() {
        console.log(a);
    }
    return fun2;
}

const f = fun1()
f()

执行过程

  • 进入全局代码,创建全局执行上下文,并将其压入执行上下文栈
  • 全局执行上下文初始化
  • 执行fun1函数,创建fun1函数执行上下文,并将其压入执行上下文栈
  • fun1函数执行上下文初始化,创建变量对象,作用域链,this
  • fun1函数执行完毕,其执行上下文弹出执行上下文栈
  • 执行f函数,创建f函数执行上下文,并将其压入执行上下文栈
  • f1函数执行完毕,其执行上下文弹出执行上下文栈

问题

  • fun1函数执行完毕,其执行上下文已经弹出执行上下文栈了,f函数为何还能访问fun1()中的变量?
  • 原因是f()执行上下文维护了一个作用域链
fContext = { Scope: [AO, fun1Context.AO, globeContext.AO]}

就算,fun1Context被销毁了,f()在作用域链中还是可以读取到fun1Context.AO的值。所以也就解释了父函数的上下文被销毁了,子函数仍旧能获取父函数中的变量。这也会导致一个内存泄漏的问题

经典例子

for (var i = 0; i < 3; i++) {
    setTimeout(() => {
        console.log(' i -- ', i);
    })
}

结果 - 打印出来的都是6

分析 - 由于变量i是由var关键词声明的,所以i是个全局变量。在for循环结束之后,i的值会被覆盖掉。setTimeout是一个异步函数,其执行的时机是等JS主代码执行之后,所以setTimeout中的i值就是最后被覆盖的值6

解决 - 要解决这个问题,有两种方式:let闭包

// -- let -- //
for (let i = 0; i < 3; i++) {
    setTimeout(() => {
        console.log(' i -- ', i);
    })
}

// let声明的变量i是一个局部变量,不会被覆盖掉。
// 相当于创建了三个局部作用域 => {i=0},{i=1},{i=2},
// 每个作用域setTimeout(()=>{})异步函数获取i时,获取的也都是每个局部作用域里的i值。
// -- 闭包 -- //
for (var i = 0; i < 3; i++) {
    ((i) => {
        setTimeout(() => {
            console.log(' i -- ', i);
        })
    })(i)
}

// 创建一个闭包,相当于创建一个局部作用域。
// 将变量i作为参数传入局部作用域

API请求

两个API,一个技术,一个封装库

两个API

XMLHttpRequest

定义

XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。XMLHttpRequest 在 AJAX 编程中被大量使用。

XMLHttpRequest 可以用于获取任何类型的数据,而不仅仅是 XML。它甚至支持 HTTP 以外的协议(包括 file:// 和 FTP) (MDN)

构造函数

XMLHttpRequest()

// 该构造函数用于初始化一个 `XMLHttpRequest` 实例对象。
// 在调用下列任何其他方法之前,必须先调用该构造函数,或通过其他方式,得到一个实例对象。

属性

// 返回 一个无符号短整型(`unsigned short`)数字,代表请求的状态码
XMLHttpRequest.readyState 

// 当 `readyState` 属性发生变化时,调用的事件处理器
XMLHttpRequest.onreadystatechange  

// 返回一个 ArrayBuffer、Blob、Document,或 DOMString,具体是哪种类型取决于 XMLHttpRequest.responseType的值。其中包含整个响应实体(response entity body)
XMLHttpRequest.response

// 返回一个 DOMString,该 DOMString 包含对请求的响应,如果请求未成功或尚未发送,则返回 `null`
XMLHttpRequest.responseText

// 一个用于定义响应类型的枚举值(enumerated value)
XMLHttpRequest.responseType

// 返回经过序列化(serialized)的响应 URL,如果该 URL 为空,则返回空字符串。
XMLHttpRequest.responseURL

// 返回一个 Document,其中包含该请求的响应,如果请求未成功、尚未发送或是不能被解析为 XML 或 HTML,则返回 `null`。
XMLHttpRequest.responseXML

// 返回一个无符号短整型(`unsigned short`)数字,代表请求的响应状态
XMLHttpRequest.status

// 返回一个 DOMString,其中包含 HTTP 服务器返回的响应状态。与 XMLHTTPRequest.status不同的是,它包含完整的响应状态文本(例如,"`200 OK`")
XMLHttpRequest.statusText

// 一个无符号长整型(`unsigned long`)数字,表示该请求的最大请求时间(毫秒),若超出该时间,请求会自动终止
XMLHttpRequest.timeout

// XMLHttpRequestUpload代表上传进度。
XMLHttpRequest.upload 只读

// 一个布尔值,用来指定跨域 `Access-Control` 请求是否应当带有授权信息,如 cookie 或授权 header 头
XMLHttpRequest.withCredentials

事件

// 当 request 被停止时触发,例如当程序调用 XMLHttpRequest.abort()属性
abort

// 当 request 遭遇错误时触发。 也可以使用 onerror 属性
error

// XMLHttpRequest请求成功完成时触发。 也可以使用 onload 属性
load

// 当请求结束时触发,无论请求成功 (load) 还是失败 (abort 或 error)。 也可以使用 onloadend 属性
loadend

// 接收到响应数据时触发。 也可以使用 onloadstart 属性。
loadstart

// 当请求接收到更多数据时,周期性地触发。 也可以使用 onprogress 属性
progress

// 在预设时间内没有接收到响应时触发。 也可以使用 ontimeout 属性
timeout

Fetch

定义

全局的 fetch()  方法用于发起获取资源的请求。它返回一个 promise,这个 promise 会在请求响应后被 resolve,并传回 Response 对象。(MDN

语法

Promise<Response> fetch(input[, init]);
  • input

    定义要获取的资源。这可能是:

    • 一个 USVString 字符串,包含要获取资源的 URL。一些浏览器会接受 blob: 和 data: 作为 schemes.
    • 一个 Request 对象。
  • init

    一个配置项对象,包括所有对请求的设置。可选的参数有:

var myInit = {
    // 请求使用的方法  //如:GET, POST, PUT, DELETE等
    method: 'POST',

    // 请求的头信息
    headers: {	
      'user-agent': 'Mozilla/4.0 MDN Example',
      'content-type': 'application/json'
    },

    // 请求的 body 信息
    // 如:Blob、BufferSource、FormData、URLSearchParams 或者 USVString 对象
    // 注意 GET 或 HEAD 方法的请求不能包含 body 信息
    body: JSON.stringify(data), // 这里必须匹配 'Content-Type' 

    // 请求的 cache 模式
    // 如:default, no-cache, reload, force-cache, only-if-cached
    cache: 'no-cache', 	 

    // 请求的 credentials
    // 包括:omit、same-origin,include
    credentials: 'same-origin',  

    // 请求的模式
    // 如 cors、 no-cors 或者 same-origin。
    mode: 'cors', 

    // 重定向模式
    // 如 follow|自动重定向, error|如果产生重定向将自动终止并且抛出一个错误, manual|手动处理重定向
    redirect: 'follow', 

    // USVString
    // 如 no-referrer、client或一个 URL。默认是 client
    referrer: 'no-referrer',
}

实例

var data = {username: "example"}; 
fetch('https://example.com/profile', { 
   method: 'POST', 
   body: JSON.stringify(data), 
   headers: new Headers({ 
       'Content-Type': 'application/json' 
   }) 
})
.then(res => res.json()) // fetch获取结果,必须进行数据转化。此处是一个json数据的转化
.then(response => console.log('Success:', response));
.catch(error => console.error('Error:', error)) 

一个技术

Ajax

定义

AJAX(Asynchronous JavaScript And XML )是一种使用 XMLHttpRequest 技术构建更复杂,动态的网页的编程实践。

AJAX 允许只更新一个 HTML 页面的部分 DOM,而无须重新加载整个页面。AJAX 还允许异步工作,这意味着当网页的一部分正试图重新加载时,你的代码可以继续运行(相比之下,同步会阻止代码继续运行,直到这部分的网页完成重新加载)。

通过交互式网站和现代 Web 标准,AJAX 正在逐渐被 JavaScript 框架中的函数和官方的 Fetch API 标准取代。

实例

function requestDataApi (obj) {
    // 定义一个XMLHttpRequest对象,ajax基于此对象与服务器交互
    const xhr = new XMLHttpRequest(); 
    
    // 设置请求方法、url、同/异步
    xhr.open(obj.method ? obj.method : 'GET', obj.url, true);
    
    // 添加http头,发送信息至服务器时内容编码类型(可选)
    // POST传数据时,用来添加 HTTP 头,然后send(data),注意data格式;
    // GET发送信息时直接加参数到url上就可以,比如url?a=a1&b=b1。
    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    
    // xhr状态改变回调函数
    // 每当 readyState 改变时,onreadystatechange 函数就会被执行。
    xhr.onreadystatuchange = function() {
        // readyState为4说明请求已完成
        // 0: 请求未初始化(代理被创建,但尚未调用 open() 方法)
        // 1: 服务器连接已建立(`open`方法已经被调用)
        // 2: 请求已接收(`send`方法已经被调用,并且头部和状态已经可获得)
        // 3: 请求处理中(下载中,`responseText` 属性已经包含部分数据)
        // 4: 请求已完成,且响应已就绪(下载操作已完成)
        if (xhr.readyState === 4) {
            if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
                console.log(xhr.response);
            } else {
                console.log('请求失败');
            }
        }
    }
    
   // 发送请求 
    xhr.send();
}

注:解决跨域的问题原生JS实现Ajax及Ajax的跨域请求 - 换个影子 - 博客园 (cnblogs.com)

一个封装库

[axios](Axios中文文档 | Axios中文网 (axios-http.cn))

安装

npm i axios

CDN

<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

基本用例

发送一个GET请求

const axios = require('axios');

// 向给定ID的用户发起请求
axios.get('/user?ID=12345')
  .then(function (response) {
    // 处理成功情况
    console.log(response);
  })
  .catch(function (error) {
    // 处理错误情况
    console.log(error);
  })
  .finally(function () {
    // 总是会执行
  });

// 上述请求也可以按以下方式完成(可选)
axios.get('/user', {
    params: {
      ID: 12345
    }
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  })
  .finally(function () {
    // 总是会执行
  });  

// 支持async/await用法
async function getUser() {
  try {
    const response = await axios.get('/user?ID=12345');
    console.log(response);
  } catch (error) {
    console.error(error);
  }
}

发送一个POST请求

axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

并发请求

function getUserAccount() {
  return axios.get('/user/12345');
}

function getUserPermissions() {
  return axios.get('/user/12345/permissions');
}

const [acct, perm] = await Promise.all([getUserAccount(), getUserPermissions()]);

// OR

Promise.all([getUserAccount(), getUserPermissions()])
  .then(function ([acct, perm]) {
    // ...
  });

自定义axios实例

import axios from 'axios'

// axios实例1 
const instance1 = axios.create({
  baseURL: 'https://some-domain.com/api1/',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});

// axios实例2
const instance2 = axios.create({
  baseURL: 'https://some-domain.com/api2/',
  timeout: 5000,
  headers: {'X-Custom-Header': 'foobar'}
});

export default {
  instance1,
  instance2
}
import { instance1, instance2 } from '...'

instance1({ 
    url:'/home/multidata'', 
    params:{ type:'pop', page:3 } 
}) 
.then(res=>{ console.log(res) })
.catch(err=>{ console.log(err) })

拦截器

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error);
  });

附加

Promise

Promise是为了解决回调函数层层嵌套而导致的回调地狱问题,通过链式调用的一种异步编程解决方案。

未使用Promise的代码

$.ajax(url1, function(data1){
    // do something1...
    $.ajax(url2, function(data2){
        // do something2...
        $.ajax(url3, function(data3){
            // do something3...
        })
    });
});

层层嵌套,很不直观。

基本使用

const p = new Promise((resolve,reject)=>{
setTimeout(()=>{
 resolve('123')
 },1000)
}).then(res=>{
 console.log(res) //1秒后打印123
})

const p = new Promise((resolve,reject)=>{
setTimeout(()=>{
 reject('123')
 },1000)
}).catch(res=>{
 console.log(res) //1秒后打印123
})

链式调用

function promiseFunc() {
  return new Promise((resolve, reject) => {
    console.log('1 -- ', 1)
    resolve(2)
  })
  .then(function(value){ 
    console.log('2 -- ', value);
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(3)
      })
    })
  })
  .then(function(value){ 
    console.log('3 -- ', value);
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(4)
      });
    })
  })
  .then(function(value){ 
    console.log('4 -- ', value); 
  });
}

async/await

async/await是基于Promise的,处理异步代码的新方式,以同步的方式处理异步代码。

async函数会隐式地返回一个Promise,该promise的resolve值就是return的值

// Promise 方式
Promise.resolve().then(data=>{//promise
    console.log(data)
    return "aaa"
})


// async/await 方式
async function func() {
    await console.log(await getJSON)
    return 'aaa'
}

ES6CommonJS 的对比

ES6ECMScript 2015)和 CommonJS 都是 JavaScript 中模块化的解决方案,但他们也有一些关键的区别。

ES6模块

语法

ES6 模块使用 importexport 语法进行导入导出

// 导出 myModule
export const myFunction = () => {};
// 导入
import { myFunction } from './myModule'
myFunction()
​
// 或者// 导出
const myFunction = () => {};
export default = { myFunction }
// 导入
import moduleA from './myModule'
moduleA.myFunction()

静态

ES6的语法在代码编译阶段就已经确定,并且在运行时不会发生变化

静态语法分析

  • ES6的新语法特性在代码编译阶段就被静态的分析和确定。这与JavaScript在执行过程中动态解析代码并执行不同(运行时解析代码)。静态分析意味着在编译阶段就能够确定代码结构和语法是否正确,而不需要等到运行时才发现错误。

不可变性

  • ES6引入一些新特性,如constlet关键字,箭头函数等,强调了变量的不可变性。这种不可变性有助于代码在静态分析阶段更容易理解和优化。

模块化

  • ES6引入了模块化系统,其中exportimport语句使得模块之间的依赖关系在静态分析阶段就能够被确定。这使得编译器和工具能够更好的进行代码优化和减少包的大小。

总体而言,ES6的静态性质使得代码更容易被理解、分析和优化。这种静态性质并不是指所有的ES6特性在编译阶段就决定的,而是强调一些新特性在代码解析时就能确定,不会在运行时动态变化,从而提高代码的可读性和可维护性。

单例

ES6通过模块的导入和导出,实现单例。

当在一个模块中定义变量或函数时,该模块的导出会成为单例的实例

// singleModule.js
const instance = {}
export function getInstance() {
    return instance
}

在其他文件中导入该模块,将得到相同的实例

import { getInstance } from './singleModule'
const myInstance = getInstance()

这确保了singleModule模块在整个应用程序中只有一个实例

支持异步加载

ES6的异步加载主要通过两种机制实现:Promise 和 模块系统

  • Promise

    ES6 引入了 promise 对象,它代表一个异步操作的最终完成或失败的值

    function fetchData() {
        return new Promise((resolve, reject) => {
            // 异步操作,例如ajax
            setTimeout(() => {
                resolve()
            })
        })
    }
    ​
    fetchData().then().catch(error => {})
    
  • 模块系统

    ES6引入了一种原生的模块系统,可以通过 importexport 进行模块导入和导出。这种模块系统支持异步加载,可以在运行时按需加载模块(少见)

    // 模块文件 moduleA.js
    export const data = 'some data';
    ​
    // main.js
    import('./moduleA.js')
        .then(res => {
            console.log(res.data)
        })
        .catch(error => {
            console.log(error)
        })
    ​
    // import('./moduleA.js') 返回一个 Promise。这使得模块可以在需要的时候按需加载,而不是一开始就加载整个应用程序。
    

CommonJS

语法

CommonJS 使用 requiremodule.exports 进行导入和导出

// 导出
function myFunction() {}
module.exports = { myFunction }
// 或者
export.myFunction = function() {}
​
// 导入
const { myFunction }  = require('./myModule')

动态

CommonJS 模块的加载和执行是在运行时动态发生的,而不是在代码编译时静态确定的

运行时加载

  • CommonJS 模块系统中,模块的加载是在运行时动态进行的。当执行到 require 语句时,才会加载所需的模块,并且模块的内容在第一次加载时执行。

    // CommonJS 模块
    const otherModule = require('./otherModule')
    

模块缓存

  • CommonJS模块在第一次加载时,会执行模块中的代码,并将导出的内容缓存起来。如果同一个模块被多次 require, 除了第一次加载时执行并缓存结果,后续的 require 只会使用缓存,不会执行

    // moduleA.js
    console.log('Module A is being executed.')
    const data = 'data from Module A'
    module.export = data
    ​
    // moduleB.js
    console.log('Module B is beding executed.')
    const dataFromModuleA = require('./ModuleA')
    console.log('Data in Module B:', dataFromModuleA)
    ​
    // main.js
    require('./moduleB') 
    require('./moduleB') // 不再输出,直接使用缓存的结果。说明没有再次执行 moduleA 中的逻辑
    
  • 执行逻辑分析

    `main.js` 第一次 `require('./moduleB')` 时,打印
    ​
    Module B is being executed.
    Module A is being executed.
    Data in Module B: Data from Module A
    ​
    并缓存 moduleA/B 的执行结果
    ​
    在 `main.js` 中第二次 `require('./moduleB')` 时,不会再次执行 moduleB 的代码,而是直接从缓存中获取导出的结果。所以不会有打印语句输出
    

动态路径

  • CommonJS 允许在 require 中使用动态的路径表达式。这意味着你可以在运行时根据条件或变量的值动态决定加载哪个模块。

    const moduleName = someCondition ? 'moduleA' : 'moduleB';
    const dynamicModule = require(moduleName);
    

总而言之,CommonJS模块系统的动态性使得模块的加载和执行更灵活。在服务端得到广泛应用。在客户端,由于这种动态加载性质,不太适合(更适合ES6模块)

同步加载

阻塞式加载

  • 执行 require 语句时,会阻塞其后续代码的执行(模块的同步性)

模块执行顺序

  • 如果 模块A 包含了对 模块Brequire,那么 模块B 会在 模块A 中的 require 语句执行时立即加载和执行,而不会等到 模块A 执行完才会去加载 模块B