1. 基础
1. 基础
js是创始人Brendan Eich于1995.5在10天内设计的一种宽松脚本语言,目的是让网页开发者能快速上手,因此你能看到许多从其他语言得到的借鉴灵感和不少不合理的历史遗留--因为现在改变规则会使上亿的老页面发生改变。标准协会TC39负责js维护和规范js主要有两部分相关内容ECMAScript标准是js的主要基础规范,最出名的是ES6(2015),从这个版本之后改为每年迭代,比如ES2017Web API:浏览器中有很多可用的API。它们建立在核心JavaScript语言之上,为使用JavaScript代码提供额外的超强能力- 浏览器
API内置于Web浏览器中,能从浏览器和电脑周边环境中提取数据,或者通过浏览器完成复杂的操作,通过特定的抽象化的接口,调用浏览器实现的底层低级代码 - 第三方
API不会内置,通常需要从web中某个地方获取代码和信息,允许集成其他平台页面功能
- 浏览器
常见的页面
DOM操作就由Web API定义(定义了表示和修改文档的对象,描述了处理网页内容的方法和接口)。之前曾使用BOM用于描述浏览器和提供的对象接口集合,概念出现于90年代。现代浏览器中BOM的操作已经被并入Web API不同功能中,基本不再使用,Web API不仅覆盖了原有的BOM能力还提供了更丰富的功能MDN文档,尽可能阅读英文版,内容更新更全面
1.1. 基本操作
变量作用域:在
js中有局部作用域和全局作用域- 局部作用域有两种情况,函数内部作用域和块级作用域
{}(自己添加{}包裹的块),局部作用域中的变量仅在当前作用域有效。局部变量会在局部作用域结束后销毁回收 script标签和.js文件就是全局作用域,在此声明的变量在任何位置都可以访问,应该尽可能少地添加全局变量,不推荐在函数中使用var或直接在全局变量比如window上挂载变量。全局变量一般不会销毁,在页面关闭后才回收- 正常情况下,局部作用域会发生嵌套。在执行时会优先在当前作用域中查找变量,找不到会依次查找父级作用域,直到全局作用域,找不到会得到
undefined
- 局部作用域有两种情况,函数内部作用域和块级作用域
定义变量
var变量将声明到全局作用域或当前函数作用域中- 同作用域内可以重复声明,每次仅保留最新的值;之前的声明不在本作用域,将会创建当前作用域的新变量
- 不会受到块级作用域限制,可以在块级作用域外访问到,因此
for(var i = 0; i < 10; i++)中每次块级作用域中得到的都是同一个变量 var声明的全局变量会绑定到全局变量上,比如window上var声明会出现变量提升的情况,在赋值之前得到的是undefined
let和const的变量是局部变量,只能在当前作用域中访问,会受到块级作用域限制- 不允许重复声明,重复声明会报错
let和const变量不会绑定到全局变量上,比如window上- 没有变量提升特性,虽然也会被提升到作用域顶部,但存在暂时性死区,在定义之前访问会报错
// 全局变量,非函数定义,会同步挂载到window对象,window.a // 函数中定义,仅在当前函数作用域有效 // 无块级作用域 var a = 1; // 局部变量,仅在当前作用域或循环轮次有效 let b = 2; // 局部常量,仅在当前作用域或循环轮次有效 const c = 3;垃圾回收
- 数据内存分配:垃圾回收机制作用在堆空间
- 堆:存放所有对象(函数也是对象)、数组,需要垃圾回收,无法手动释放
- 常量池:在堆中有一个区域存放字符串和数字常量,减少占用
在部分情况下,存在需要手动释放的外部资源,但这些资源不属于
js内存,比如webGL的部分资源,Nodejs:fs打开的文件句柄 - 栈:存放基本数据类型(包括
number,boolean,null,undefined,symbol,bigint,string)、对象引用,由操作系统分配释放
- 堆:存放所有对象(函数也是对象)、数组,需要垃圾回收,无法手动释放
- 常见的浏览器垃圾回收算法
引用计数算法:
IE使用,如果一个内存不再有指向他的引用,回收对象,算法要求跟踪内存引用(创建变量记录,如果一个内存有添加引用就累加1,减少引用就减1),引用为0则回收。如果多个对象互相存储了引用,则无法回收function fn() { let a = {}; let b = {}; // 相互引用,无法回收 a.b = b; b.a = a; }标记清除算法:现代浏览器使用基于此方法的改进算法,将不再使用对象定义为无法到达的对象,从js全局变量出发,扫描内存中的对象,凡是能到达的对象,都是还需要使用的,其他无法到达的对象则被标记为垃圾对象,稍后进行回收
基本流程语句
if语句for语句switch语句while语句do...while语句let condition = true; let expression = 0; if (condition) {} else {} for (let i = 0; i < 10; i++) {} // 获取对象或数组中的值,需要对象可迭代 for (let v of array) {} // 获取索引键字符串,数组返回数字索引 // 谨慎使用,这会获取所有可迭代的属性键名 // 如果有必要遍历键,使用Object.keys()方法 for (let k in object) {} switch (expression) { case 0: break; case 1: break; } while (condition) {} do {} while (condition);
事件循环机制
- 由于
js被设计出来是用于处理用户交互、操作DOM的脚本语言,多线程会产生很多同步问题,因此浏览器的代码执行和渲染操作被设计为是单线程的,这也奠定了js的语言基础,应该不会再改变了 - 为了更好地实现功能,
js支持异步代码,而为了能更好地处理异步代码,浏览器为代码执行和渲染设计了事件循环Event Loop机制- 事件循环机制中所有的代码都被规范分类为了宏任务和微任务
- 宏任务
macro task:包括setTimeout、setInterval、I/O、事件触发回调等,这些任务现在可能分门别类,划分到不同的队列中,渲染主线程会根据规定的优先级选择其中的任务执行 - 微任务
micro task:包括Promise.then、MutationObserver、queueMicrotask(等效Promise.resolve().then())等,微任务优先其他的宏任务执行,浏览器将微任务添加到一个单独的队列中
- 宏任务
- 所有的
js代码实际上是在渲染主线程的调度下执行的,渲染主线程通常是一个永远不会停止的循环,会不断从队列中取出任务交给js引擎执行- 所有的任务都需要在主执行栈上执行,在同步代码执行的过程中,所有异步代码会被浏览器保管起来,在条件满足后由其他支持线程包装分类,放入宏任务(有的地方也称消息队列,标准中的说法是
task queue)和微任务队列中。每个队列中任务是先进先出的 - 每个队列的任务不存在优先级,但有多个不同的队列,队列存在优先级,其中微任务队列的优先级最高,其他宏任务队列的优先级高低依赖浏览器实现
- 宏任务中的异步代码都是需要
js执行线程外提供支持的方法,比如事件触发线程、定时器线程、网络请求线程提供的方法,这些回调会在条件满足时被添加到任务队列中,等待被执行
- 所有的任务都需要在主执行栈上执行,在同步代码执行的过程中,所有异步代码会被浏览器保管起来,在条件满足后由其他支持线程包装分类,放入宏任务(有的地方也称消息队列,标准中的说法是
- 开始时,主程序同步代码作为宏任务队列中的第一个任务进入执行栈执行。宏任务执行完毕(执行栈清空)后,事件循环进入微任务队列,依次清空所有的微任务之后,再等待渲染主线程取出宏任务队列中的下一个宏任务,开启下一轮循环
- 事件循环机制中所有的代码都被规范分类为了宏任务和微任务
- 一轮事件循环完成之后,如果需要更新一帧画面,渲染主线程会进行一次视图渲染,渲染间隔和屏幕帧率有关,在渲染之前先执行所有的
requestAnimationFrame回调,如果代码执行耗时过长阻塞了渲染,会造成视觉卡顿
通过事件循环机制,实际上异步的浏览器本身,而
js引擎的执行依旧是同步的基本操作符
- 基本运算:
+、-、*、/、%、**(幂运算)- 其中
+可以为单目运算,表示转换为数字,+"1"得到数字1 %会保留数字符号+支持拼接字符串,其他的运算符遇到字符串会得到NaN
- 其中
- 比较:
==、===、!=、!==、>、<、>=、<= - 逻辑:
&&、||、! - 位运算:
&、|、^、~、<<、>>、>>>- 位运算支持32位整数,会向0取整自动转换为整数,更大范围的数字溢出会被截断
- 赋值:
= - 判断特定键字符串是否存在:
in('name' in window)
- 基本运算:
;的添加,js允许在语句后面添加;分隔不同语句,在除了可能导致语义不清的情况下,;可以省略,要注意:for循环中循环条件中必须通过;分隔以
[]、()的语句,为了和之前的语句形成分隔,必须在前面添加;let a = 1, b = 2; // [] 开头的数组 [a, b] = [b, a]; // () 开头的立即执行函数 (function () {});{}开头作为变量直接进行运算的情况,需要添加()修改执行顺序,当然一般这样也没有效果,无内容被修改// 被浏览器理解为代码块 {}; +[]; // + 支持是一元的,相当于 Number([]) {} + [] // 需要添加() ({} + [])return、throw关键字后会自动添加;,如果需要返回值必须在同一行写,throw换行会报SyntaxError: Illegal newline after throw,但return换行不会报错(最终仍返回undefined)
1.2. 变量类型
Number:数字,包括整数和浮点数
let a = 1, b = 2.0;- 安全整数范围:,在这个范围外的整数会发生精度丢失,结果不一定正确,因为整数是使用
IEEE754浮点数存储的,仅尾数位(还有一个隐含位)用于存储数字 - 存在特殊全局变量
Infinity(正无穷大)、NaN(无意义数字),用于表示特殊值Infinity:正无穷大,比任何数都大,可以添加-表示负无穷大,通过Number.isFinite(value)判断是否为无穷大NaN:无意义数字,NaN不等于任何值,包括NaN自身,可以通过Number.isNaN(value)判断是否为NaN- 还有
+0和-0两种,在正常情况下,二者会被==和===判定为相等,如果需要更严格的判断,可以使用Object.is(a, b),见Object
BigInt大数ES2020:可以解决大整数精度丢失问题,需要在整数数字结尾添加n,如let a = 1n;,支持加减乘除取余幂运算(向下取整),支持除了>>>外的位运算,但不能和普通数字混合运算,也无法被序列化为json字符串
序列化和反序列化
- 序列化:把数据结构或对象转换成一种可以存储或传输的线性格式(通常是字符串或字节流)的过程,以便之后可以恢复原来的数据结构(反序列化),一般通过
JSON.stringify(obj)和JSON.parse(str)实现
const obj = { name: '张三', age: 18 } const json = JSON.stringify(obj) // '{"name":"张三","age":18}' console.log(json) console.log(JSON.parse(json)) // {name: '张三', age: 18}- 不支持序列化的内容
Symbol、BigInt和函数类型- 循环引用
undefined:作为属性会被忽略、作为数组元素会转换为null- 其他非可枚举类型:被隐藏的内部实现(属性描述符中
emuerable为false)。属性描述符被用来描述属性的元信息,告诉引擎这个属性的行为规则,具体操作见变量特性
- 安全整数范围:,在这个范围外的整数会发生精度丢失,结果不一定正确,因为整数是使用
String:字符串
let a = 'hello world';,字符串可以使用'、"、`包裹,`支持模板字符串写法,模板字符串可以使用${}包裹变量,比如let a = `${name}`;Boolean:布尔值,
let a = true, b = false;Undefined:未定义值
let a;Null:有意义的空值
let b = null;Symbol:唯一标识符
const s = Symbol("id");- 可以通过
s.description获取描述符"id" - 每次创建的
Symbol值都是唯一的,永远不相等 - 同时
Symbol作为属性键是不会参与遍历的,避免干扰普通对象的遍历和序列化逻辑,可以安全地隐藏 - 如果希望遍历
Symbol属性,可以通过Object.getOwnPropertySymbols(obj)获取到包含所有Symbol属性键的数组 - 如果需要在全局存储
Symbol,可以通过Symbol.for(key)创建,这个函数会根据给定的字符串key从运行时的Symbol注册表中查找,如果没有注册过,则创建一个放入注册表中。对应的,可以使用Symbol.keyFor(s)根据全局的Symbol返回键,未找到返回undefined
- 可以通过
Object:引用类型,比如数组
[]、函数function(){}、普通对象{}- 数据保存在堆内存(
Heap)中,并在栈内存(Stack)中保存一个引用指针 - 使用
{}直接创建的对象相当于直接使用new Object({})创建,是一种语法糖 - 引用类型的属性将按写的顺序逐个初始化,因此如果有属性依赖其他属性,那么属性的初始化顺序可能会对结果有影响
除
Object外,其他类型称为基本类型或原始值类型Primitive Types,数据会按值传递栈中的变量存储方式
- 现在不同引擎采用的存储方式不太相同,但可能有以下优化
NaN-Boxing:即NaN装箱,使用IEEE754双精度浮点数统一表示所有的基本类型,用于对齐处理方式。在IEEE754中,规定非数字NaN需要阶码全1,但没有规定尾数需要是多少,因此可以将此时的非0尾数进一步划分,将所有的其他非数字类型存入这52位尾数中tag pointer:在js中,引用地址一般为8B(64位),但一般不会用上全部的地址空间,这样就可以使用未使用空间存储其他信息,比如v8将最低位用于堆地址和立即数类型的判断
- 数据保存在堆内存(
基本类型在使用方法时,
js会临时创建对应的包装对象(Number、String、Boolean等),这些对象有扩展的方法,调用完方法后对象立即销毁
1.2.1. 类型判断
在js中主要有三种方法可以判断类型
typeof:返回类型字符串仅用于获取基本类型字符串,所有非可调用的引用类型都将获取到
object,函数的判断是规范中规定的特意处理(有[[Call]]属性的可调用对象返回function)无法区分
null和非function类型的object[[key]]这样的属性被称为内部槽,这些属性仅能被引擎处理,无法通过js直接定义和处理typeof 123 // "number" typeof 'abc' // "string" typeof true // "boolean" typeof undefined // "undefined" typeof Symbol() // "symbol" typeof 10n // "bigint" typeof function(){} // "function" typeof class A {} // "function" typeof {} // "object" typeof [] // "object" typeof null // "object" (历史遗留问题,null读取到类型标记和对象类型一致)
instanceof:判断对象是否是某个构造函数的实例,返回布尔值会沿着构造函数的原型链查找,直到找到
null,看能否匹配跨窗口或
iframe判断时永远是false,因为不同的全局环境有不同的构造函数无法判断基本类型,对于基本类型永远返回
false[] instanceof Array // true [] instanceof Object // true new Date() instanceof Date // true new Date() instanceof Object // true
Object.prototype.toString():返回对象类型的字符串表示,最精准的判断方法返回字符串表示,不受跨窗口和
iframe的影响准确获取对象的类型,且对基本类型也有效果
实际上获取的是
[Symbol.toStringTag]属性的值,这个值在ES6新增用于类型判断对于早于
ES6不支持的[Symbol.toStringTag]属性的类型,有特殊的标签对应:Array、Function、Error、Boolean、Number、String、Date、RegExp对于自己定义的类型,默认为
[object Object],可以通过设置[Symbol.toStringTag]属性来改变实际上因为这个方法挂载在原型链上,在未定义类的
toString函数情况下,对象直接使用toString输出的就是[object Object][Symbol.toStringTag]的中括号表明这个属性的键是Symbol类型,需要通过中括号才能访问到,这个键是全局的,可以直接使用
Object.prototype.toString.call(123) // "[object Number]" Object.prototype.toString.call('abc') // "[object String]" Object.prototype.toString.call(true) // "[object Boolean]" Object.prototype.toString.call(undefined) // "[object Undefined]" Object.prototype.toString.call(null) // "[object Null]" Object.prototype.toString.call([]) // "[object Array]" Object.prototype.toString.call({}) // "[object Object]" Object.prototype.toString.call(new Date()) // "[object Date]" Object.prototype.toString.call(/abc/) // "[object RegExp]" Object.prototype.toString.call(() => {}) // "[object Function]" // 自定义 Symbol.toStringTag class ValidatorClass { // get 函数设置值,当然也可以使用 this[Symbol.toStringTag] 设置值 get [Symbol.toStringTag]() { return "Validator"; } } Object.prototype.toString.call(new ValidatorClass()); // "[object Validator]"
1.2.2. 类型转换
js作为一门弱类型语言,意味着你可以经常使用与预期类型不同的类型的值,由js为你把它转换为正确的类型。js为此定义了一些强制转换规则对象参与运算时会考虑进行原始类型转换,依次调用
[Symbol.toPrimitive]()、valueOf()和toString()方法- 其中
valueOf()和toString()谁先调用依赖期望返回的类型,如果是字符串,则优先调用toString();如果是数字类型,则优先调用valueOf();其余情况除了Date对象外,其他对象都先调用valueOf() [Symbol.toPrimitive](hint):方法获取到一个字符串,表示转换的提示,可以是"string"、"number"、"default",根据不同提示可以选择返回不同值valueOf():意为返回原始类型的值,默认返回自身,但在转换时不应该返回对象类型,对象类型的返回值会被忽略,接下来会继续考虑toString()toString():返回字符串类型的值
// valueOf() 被忽略的例子 // 默认返回自身 {} + [] // 被忽略后使用 toString(),得到 "[object Object]" + "" console.log({} + []) // 如果直接{}+[]在控制台会发现是 0 // 使用 {} 开头被解析为 {}; +[] // Number([]) 实际上是 Number('') 结果是 0- 其中
可以自己调用包装对象函数进行类型转换,比如
Number('123')js中有一元加法运算,比如+'123'实际上就是Number('123')。BigInt为了和asm.js兼容,不支持单目加法运算,需要使用函数形式调用Number方法会先尝试获取到一个原始类型值- 对于
null和空字符串''会返回0 - 其他的
string会尝试将字符串转为数字,如果无法转换,则返回NaN undefined会返回NaNboolean会返回1或0- 其他对象期望获取到数字
number,进行原始类型转换
- 对于
算术运算
+时,如果一方是字符串,全部使用字符串运算,否则转换为数字进行运算,- * /中有字符串会得到NaN因此只会转换为数字进行运算宽松相等
==或<、>等比大小运算时,会触发类型转换- 字符串或
Boolean类型和数字比较:字符串或Boolean类型会转为数字进行数字比较 null和undefined相等- 对象也会转换为原始值计算,需要的类型依赖另一边的原始类型,如果两个都是对象不转换,根据引用是否相同判断
如果希望同时比较类型是否相等,需要使用
===严格相等- 字符串或
逻辑运算符的返回值:逻辑运算符会将操作数转换为布尔值再判断真假
- 除了
!返回布尔值外,其他逻辑运算符返回最后一个被判断的值,比如0 || 1返回1、0 && 'hello'返回0,而且返回的是原操作数的引用值
- 除了
BigInt向number类型的自动转换:在需要传入number类型时,在数据大小允许的情况下,允许BigInt数据类型自动转换为number类型进行运算,比如array.splice(0, 1n)
1.2.3. 变量特性
- 引用变量常见特性如下
属性可写性:
writable属性- 不可写的属性不能被修改
属性特性的设置方法
- 属性可写性、可枚举性和可配置性,也就是属性描述符都是通过
Object.defineProperty方法配置的
Object.defineProperty(person, 'age', { value: 25, // 属性值 writable: false, // 不可修改 enumerable: true, // 可枚举 configurable: false // 不可删除或重新定义 // 访问器属性,与 value 和 writable 描述符不能同时使用 // get() { return 25; }, // set 的调用者是属性所属的对象 person // set(value) { this.age = value; }, }); // 查看 Object.getOwnPropertyDescriptor(person, 'age');属性可枚举性:
enumerable属性- 不可见的属性不会被枚举到键,比如
for in和Object.keys() Object.getPropertyName方法和Object.getPropertySymbol方法不受影响- 使用反射
Reflect.OwnKeys获取也不受影响
- 不可见的属性不会被枚举到键,比如
属性可配置性:
configurable属性- 在为
true时,可以使用delete obj.prop删除属性,也可以修改属性修改它的类型(比如改为使用访问器get和set方法定义,访问器的使用见访问器属性) - 在为
false时,属性无法被删除、不能修改它的类型、也不能再使用Object.defineProperty配置
- 在为
- 对象可扩展性:
Object.proventExtensions(),阻止新增属性。可以通过Object.isExtensible()判断对象是否可扩展 - 对象密封性:
Object.seal(),禁止新增删除属性,并无法设置属性描述符,相当于configurable: false。可以通过Object.isSealed()判断对象是否密封 - 对象冻结性:
Object.freeze(),对象和属性完全只读,但深层嵌套属性仍可更改。可以通过Object.isFrozen()判断对象是否冻结`
| 方法 | 能新增属性? | 能删除属性? | 能修改属性值? | 能改属性描述符? | 属性configurable变为 | 属性writable变为 |
|---|---|---|---|---|---|---|
Object.preventExtensions | ❌ | ✅ | ✅ | ✅ | 不变 | 不变 |
Object.seal | ❌ | ❌ | ✅ | ❌ | false | 不变 |
Object.freeze | ❌ | ❌ | ❌ | ❌ | false | false |
可迭代性:要成为可迭代对象,对象需要实现
[Symbol.iterator]()方法,此方法返回一个迭代器对象- 可迭代对象可以使用
for...of循环 - 迭代器对象中必须实现
next()方法,方法返回一个对象,包含value和done属性,value为迭代器返回的值,done为迭代器是否结束,也就是迭代器结束需要返回{value: undefined, done: true} - 当然迭代器对象可以使用
function*生成器函数语法糖实现,生成器函数会在有yield时停下并- 将结果作为
value返回给使用的场景,同时设置done: false - 当没有更多迭代器
yield返回时返回{value: undefined, done: true}
- 将结果作为
生成器函数可以更好地实现状态机代码,更方便实现需要大量暂停和回复状态的迭代器模式,语法更简洁
const obj = { a: 1, b: 2, c: 3, [Symbol.iterator]() { const entries = Object.entries(this); let index = 0; return { next() { if (index < entries.length) { const [key, value] = entries[index++]; return { value: [key, value], done: false }; } else { return { done: true }; } } }; } }; // 生成器函数写法 const obj = { a: 1, b: 2, c: 3, *[Symbol.iterator]() { for (const key of Object.keys(this)) { yield [key, this[key]]; } } };- 可迭代对象可以使用
1.2.4. 数组
在
js中,数组可以存储任意类型的数据,并且可以随意扩大长度和删除元素- 数组实际上是一种对象,只是引擎内部做了一定的优化
- 有一些非负整数数字索引和一个表示数组长度的属性
length,length的值自动为最大的索引值加1
实际上数组结构可以通过对象实现,这样的对象被称为类数组对象
- 类数组对象:结构上类似数组,但不是
Array实例的对象,因为许多数组作用的方法和...解构语法只是读取特定数据,因此可以像一般数组一样使用,这些实例:- 有整数索引属性
- 有
length属性 - 无其他数组原型的方法,需要使用
Array.prototype.toString.call(arrayLike)才能调用这些方法 - 如果希望判断是不是数组,可以使用
Array.isArray()方法
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 }; console.log(arrayLike[0]); // 'a' console.log(arrayLike.length); // 3 console.log(Array.isArray(arrayLike)); // false // 作为数组处理 console.log(Array.prototype.join.call(arrayLike, '-')); // 'a-b-c' // 转为数组的方法 const array = [...arrayLike] const array1 = Array.from(arrayLike)- 类数组对象:结构上类似数组,但不是
js的数组不同于其他编程语言的数组,数组的元素可以是稀疏的,在引擎中会切换到哈希表存储,输出时会明显看到空位(empty slot)- 数组长度不会因为稀疏而改变,保持为最大的索引值加1
- 读取空位时会获取到
undefined - 数组方法对空位的处理:因为方法可能被稀疏数组使用,比如
[1,,3].forEach(callback),因此较久的方法考虑了空位的处理(跳过),新方法不处理空位(作为undefined),具体见数组方法与空位 for in和for of也会跳过空位
const a = [1, , 3]; // [1, <empty item>, 3] console.log(a.length); // 3
1.2.5. 函数
函数声明
function声明- 匿名函数作为变量赋值
- 立即执行函数
- 箭头函数,
ES6新加,不创建函数arguments、不能作为构造函数、不可new、不能继承(因此没有this、prototype)
// 函数声明 function func() {} // 匿名函数作为变量赋值 const func = function () {}; // 立即执行函数 (function () {})(); // 箭头函数 const func = () => {}; // 只有一个参数时,可以省略小括号 // 只有一行代码时,可以省略大括号,且无需写 return,默认返回 const func = a => 2 * a函数的
this除箭头函数外,函数创建的
this指向调用它的对象- 箭头函数不会创建
this,会随作用域不断向上一级查找,直到找到有this的作用域,因此像事件回调函数希望获取到节点信息,最好使用普通函数 - 通过
function定义的函数会挂载在window对象上,如果指明了window.fn,this指向window对象,如果未指明,则this在非严格模式会修改到全局对象 - 如果是
new执行的创建对象的构造函数,this指向新创建的对象 - 如果是通过对象绑定的函数,
this指向对象,比如obj.f()、f.bind(obj)()、f.call(obj)
改变指向
在
js中,bind()、call()、apply()都可以改变this的指向(在不使用箭头函数的情况下使用外部this的方法)bind(thisObj, ...arg):返回一个新函数,新函数的this指向绑定的对象- 新函数的前几个参数会固定为当前传入的参数列表中的参数,之后使用仅需要传入剩余的参数,也就是类似函数柯里化
- 使用
new时,指向会被修改为新创建的对象,此时this指向的修改相当于没做 - 对箭头函数无效,因为箭头函数不产生
this - 方法实际上通过原型继承
call(thisObj, ...arg):立即执行,并改变this的指向,arg是函数参数列表- 方法和下面的方法实现类似,只是参数列表传入方式不同
- 两个方法都不能对箭头函数(无效)和
new情况(一般报错,因为立即执行返回值一般不是构造函数)做处理 - 两个方法实现原理实际上是将
thisObj转换为对象,然后将函数挂载在对象上,最后调用对象上的函数,返回返回值,最后删除挂载
apply(thisObj, array):立即执行,并改变this的指向,主要用于传递数组值作为函数参数
callFunction.prototype.call = function (context) { // 基础类型转包装对象 if (context === undefined || context === null) { context = window } else if (typeof context === 'string') { context = new String(context) } else if (typeof context === 'number') { context = new Number(context) } else if (typeof context === 'boolean') { context = new Boolean(context) } // 保存原函数至指定对象的fn属性上 context.fn = this // 获取除第一个参数之后的所有参数 const args = Array.from(arguments).slice(1) // 通过指定对象的fn属性执行原函数并出入参数 const fnValue = context.fn(...args) delete context.fn // 从context中删除fn原函数 return fnValue }bindFunction.prototype.bind = function (context) { // 保存原函数 const ofn = this // 获取除第一个参数之后的所有参数 const args = Array.from(arguments).slice(1) function O() {} function fn() { // 第一个参数的判断是为了忽略使用new实例化函数时让this指向它自己,否则就指向这个context指定对象 // 第二个参数的处理做了参数合并, 就是 bind & fn 两个函数的参数合并 ofn.apply(this instanceof O ? this : context, args.concat(Array.from(arguments))) } O.prototype = this.prototype fn.prototype = new O() return fn }applyFunction.prototype.apply = function (context, arr) { // 基础类型转包装对象 if (context === undefined || context === null) { context = window } else if (typeof context === 'string') { context = new String(context) } else if (typeof context === 'number') { context = new Number(context) } else if (typeof context === 'boolean') { context = new Boolean(context) } // 非对象,非undefined,非null的值才会抛错 if (typeof arr !== 'object' && typeof arr !== 'undefined' && typeof arr !== 'null') throw new TypeError('CreateListFromArrayLike called on non-object') arr = Array.isArray(arr) && arr || [] // 非数组就赋值空数组 // 保存原函数至指定对象的fn属性上 context.fn = this // 通过指定对象的fn属性执行原函数并出入参数 const fnValue = context.fn(...arr) delete context.fn // 从context中删除fn原函数 return fnValue }:::
- 箭头函数不会创建
严格和非严格模式的区别
- 非严格模式下,如果不是对象调用,
this会指向全局对象window(浏览器)或Node的一个当前模块对象 - 严格模式下,
this指向不会修改,非对象调用且非箭头函数的this为undefined
- 非严格模式下,如果不是对象调用,
函数参数
- 非箭头函数中有
arguments类数组可迭代变量,包含了所有传入的参数- 在
arguments中有callee属性,对应函数本身,arguments.callee.toString()方法返回函数定义
- 在
- 所有函数支持
...args接受剩余参数,以...开头的传入参数是一个数组,包含剩下的所有参数
传入参数个数和函数定义的参数个数可以不一致,如果无剩余参数,其余的需要通过
arguments访问,如果无传入,参数为undefinedfunction sum(...args) { return args.reduce((a, b) => a + b); }- 函数的默认构建时的参数个数可以通过
length属性获取
- 非箭头函数中有
函数重载:
js中不支持函数函数重载,每次定义函数时,会覆盖之前的定义,但可以通过arguments参数实现类似重载的效果,判断参数个数和类型,实现不同的逻辑
1.2.6. 解构赋值
解构的核心是表达式左边的变量都能够对应上右边的表达式,会为左边变量赋值,赋值后的变量与右边变量无关
解构的变量和外部变量一样,变量名不要冲突
数组解构,变量会通过相同位置的数组元素赋值,实际上所有有迭代器方法的对象都可以进行数组解构
- 如果变量没有对应
[a, b] = [1],则b的值为undefined - 支持直接在解构内部通过
=提供默认值[a, b=1],解构成功会覆盖默认值 - 支持
[a, , b]=[1, 2, 3]跳过第二个值,按需导入 - 支持多维解构
[[a, b], c] = [[1, 2], [3, 4]] - 支持剩余参数
[a, ...rest] = [1, 2, 3]获取剩余的所有值,rest会外包数组rest = [2, 3]
let [a, b, c, d=5] = [1, 2]; console.log(a, b, c, d); [b, a] = [a, b]; console.log(a, b); let [aa, , ...rest] = [1, 2, 3, [4], [5, 6, 7]]; console.log(aa, rest);- 如果变量没有对应
对象解构
如果变量没有对应名称的属性,则返回
undefined支持修改别名
{a: b},将对象原有的属性a赋给新的变量b可以进行进一步解构赋值
{a: {b: c}},得到变量c支持直接在解构内部通过
=提供默认值{a=0, c:b=1},解构成功会覆盖默认值支持剩余参数
{a, ...rest} = {a: 1, b: 2, c: 3}获取剩余的所有值{b: 2, c: 3}let {a, b} = {a: 1, b: 2}; console.log(a, b); const { a: { b: c } } = { a: { b: 42 } }; console.log(c); let {name, hobby: {d, e, f}} = {name: '张三', hobby: {a: 1, b: 2, c: 3}} console.log(name, d, e, f); let {aa, bb, ...rest} = {a: 1, b: 2, c: 3} console.log(aa, bb, rest); let {a: [b1, c1], d1=1} = {a: [1, 2, 3]} console.log(b1, c1, d1);
1.3. 类
1.3.1. 原型链
- 原型链:继承和方法共享的实现原理,基于原型对象的继承使得不同构造函数的原型对象关联到一起,关联是链状的结构
每个对象都有一个内部属性
[[Prototype]],这个属性被称为隐式原型(也称为原型对象),指向对象的原型对象,但这种内部槽属性无法直接访问,而需要在每个实例上通过obj.__proto__属性访问或使用Object.getPrototypeOf(obj)方法访问推荐使用函数获取对象原型,而不是直接访问
__proto__属性,__proto__属性为兼容之前的浏览器而保留的属性每个函数都包含一个显式原型
prototype,通过此函数创建的对象实例的隐式原型指向此函数的显式原型function Person() {} let obj = new Person() console.log(obj.__proto__ === Person.prototype)每个函数也都是对象,有隐式原型
__proto__(也称为对象原型)指向Function.prototype,由于Function也是函数,所以Function的__proto__属性指向Function.prototype每个变量都拥有原型,也可以成为原型,原型通过
__proto__循环形成链,直到最后Object.prototype.__proto__为null- 实际上
Person.prototype也是一个对象,如果手动让Person.prototype = new Animal(),那么Person.prototype的__proto__属性指向Animal.prototype,这样就实现了继承 - 别忘了
constructor属性需要修改正确,不然person instanceof Person为false,是Animal对象
function Person() {} function Animal() {} let obj = new Person() console.log(obj.__proto__.__proto__ === Object.prototype) console.log(Object.prototype.__proto__ === null) Person.prototype = new Animal() Person.prototype.constructor = Person // 不推荐直接修改原型,可能有兼容性问题 // Person.prototype.__proto__ = new Animal() while (obj.__proto__) { console.log(obj.__proto__.constructor.name) obj = obj.__proto__ }- 实际上
当访问对象属性时,会先查找对象本身,如果找不到,就沿着原型链
__proto__向上查找,直到null- 因为查找是基于
__proto__属性递归向上的,如果为当前的构造函数Person.prototype.a添加属性,构造函数Person.a是无法访问到这个属性的,必须Person.prototype.a或在实例上才能获取到
原型方法通过实例调用,
this会指向实例对象- 因为查找是基于
可以使用
instanceof来检查构造函数的prototype是否出现在对象的原型链上function Person() {} let p = new Person() console.log(p instanceof Person)
1.3.2. 类和构造机制
es6增加了class关键字,在之前类声明是通过function构造函数完成的- 每一个原型对象都有一个
constructor属性,该属性指向该对象的构造函数Person.prototype.constructor == Personclass声明时可以指定constructor属性,如果不指定也会有默认的值,永远指向类本身,直接修改构造函数的prototype属性无效constructor属性只有一个值,因此每个类仅一个构造函数- 早期直接通过构造函数创建时,
constructor可以被后续修改,如果将对象原型完整地重新赋值时Person.prototype = {...},此属性会丢失,最好在完整赋值时添加constructor,指向构造函数
constructor可以用于判断对象类型是否相同
class Person { // 如果直接添加属性,不需要使用 let age = 18; // 静态属性 static type = 'Person'; // 构造函数 constructor(name) { this.name = name; // 实例属性 } // 原型方法 greet() { console.log(`Hello, I'm ${this.name}`); } // 静态方法 static greet() { console.log('Hello, I\'m a static method'); } } // 早期创建方式,构造函数 function Person(name) { this.name = name; // 方法会添加到实例上,因此每个实例都会创建一遍 this.greet = function() { console.log(`Hello, I'm ${this.name}`); } // return this; } Person.greet = function() { console.log('Hello, I\'m a static method'); } Person.type = 'Person'; // 挂载在原型上,就是所有实例共享,不需要多次创建 Person.prototype.greet = function() { console.log(`Hello, I'm ${this.name}`); }this指向
在
function声明类时最好不要直接调用Person()这样的构造函数,在非严格模式如果利用和返回this,会得到window对象,操作将影响全局作用域,比如原型链和函数this中的示例function Person() { this.name = 'Tom'; } Person(); console.log(window.name);私有属性:
ES6之前,私有属性一般都通过闭包实现ES6引入了WeakMap可以用于实现实例到变量的转换;Symbol类型可以作为唯一键,如果无法获取到键就无法访问到对应的值WeakMap和WeakSet只允许将可垃圾回收的值作为键,这些键要么是对象,要么是未注册的symbol,即使键仍在集合中,也可能被回收,详见js中的哈希结构ES2022才添加#,#name属性会被认为是私有的
// 闭包实现 function A(name) { let _name = name; return { getName() { return _name; }, setName(v) { _name = v; } }; } const _weakName = new WeakMap(); const _symName = Symbol("symName"); class Person { #hashName; // ES2022 私有字段 constructor(name) { // 分别初始化三种私有存储方式 _weakName.set(this, name + " (WeakMap)"); this[_symName] = name + " (Symbol)"; this.#hashName = name + " (#Private)"; } // Getter(访问不同私有属性) getWeakName() { return _weakName.get(this); } getSymbolName() { return this[_symName]; } getPrivateName() { return this.#hashName; } // Setter(修改不同私有属性) setWeakName(value) { _weakName.set(this, value); } setSymbolName(value) { this[_symName] = value; } setPrivateName(value) { this.#hashName = value; } }
- 每一个原型对象都有一个
new操作:- 创建一个空对象
- 将空对象的
__proto__指向函数或类的prototype - 构造函数的
this指向这个空对象 - 执行构造函数内部的代码
- 如果构造函数返回一个对象,则返回这个对象,否则返回创建的对象
虽然
new运算符支持在不添加参数时不带括号,比如new Foo等效new Foo(),但new运算符不带括号时优先级没有.高,因此new Foo.bar()会先执行Foo.bar获取静态方法,再执行new操作创建对象的三种方式
- 使用
new操作符,通过构造函数创建,比如可以使用内置构造函数(创建包装类型,如Number、String、Boolean、Array),原型对应构造函数 - 使用
{}直接得到一个对象,原型固定是Object.prototype - 使用
Object.create()创建对象,指定原型,不使用构造逻辑,还可以使用可选属性描述参数,类似Object.defineProperty
const obj = Object.create(Object.prototype, { x: { value: 10, writable: true, enumerable: true, configurable: true } }); console.log(obj.x); // 10属性覆盖
在初始化创建属性时,由于已经挂载了
prototype属性,因此看起来会存在使用原型属性的情况,实际上属性覆盖表明了属性的修改存在隔离- 我们都知道对于实例可以通过原型链使用上面的属性
- 对于普通属性来说,实例创建同名属性
name时,原型上的name不会被修改,之后根据优先级也不能通过obj.name被访问和修改,也就是属性遮蔽 - 对于通过
Object.defineProperty中的get和set定义的属性来说,实例赋值不会创建同名的属性,这个属性是通过原型链上的get获取的,修改会调用set方法值得注意的是,在控制台输出时,看起来好像当前的实例上有一个同名且是使用get方法获取到的属性,但这是可能是方便查看而展示的,如果使用
Object.getOwnPropertyDescriptor(o, 'a')会发现实例上没有这个属性,但在原型o.__proto__上使用是有的
function A () { this.a = 1 this.b = 1 } let a = 2 Object.defineProperty(A.prototype, 'a', { get () { return a }, set (val) { a = val } }) let o = new A() let b = 2 Object.defineProperty(A.prototype, 'b', { get () { return b }, set (val) { b = val } }) console.log(a) // 被修改为 1 console.log(b) // 保持原值 2 // 发现实例上有一个通过 get 得到的属性 a // 结果:A{ b:1 } 1 1 1 2 console.log(o, o.a, o.b, o.__proto__.a, o.__proto__.b) // undefined { enumerable: false, configurable: false, get: ƒ, set: ƒ } console.log(Object.getOwnPropertyDescriptor(o, 'a'), Object.getOwnPropertyDescriptor(o.__proto__, 'a'))使用
class定义且预声明了这个属性时,get和set方法定义的属性行为就和一般属性一样了,在实例对象和原型上有两个属性。如果是ts编译的话,目标版本高于es2021时编译结果会添加a的声明class A { a; // 声明后,默认有这个属性,初始值 undefined }深入
[[set]]:一个属性设置时,实际上调用了这个内部槽,对应obj.[[set]](key, value, receiver)- 具体逻辑见TC39文档,这些只是通用规范描述,在大多数情况下,
js引擎会通过内联和缓存等方法优化,在不违反规则的情况下,减少操作 - 在内部
[[set]]实际上调用了OrdinarySet(o, p, v, receiver),这个方法需要在原型链上查找,o是当前查找对象,p是属性名,v是属性值,receiver是当前赋值对象 OrdinarySet方法从当前对象开始查找,如果有对象描述符,就停下;如果__proto__为null也没有找到同名属性,则提供一个可写的属性描述符,将这个属性作为可写属性- 针对前置逻辑中获取到的属性描述符,判断是否可写(
writable为true或有setter) - 如果不可写放弃在
Receiver上添加值 - 如果是
setter属性,使用setter.call(receiver, value)调用这个方法 - 如果是普通属性(
IsDataDescriptor(desc)),在Receiver上尝试创建该值,如果Receiver上的已有get/set属性描述符(IsAccessorDescriptor(desc),避免被修改为普通属性,描述符冲突检查)或该属性不可写,取消修改,否则重新定义属性描述符,然后修改当前Receiver上的值
OrdinarySet方法参数非常类似Reflect.set(target, property, value, receiver)方法,在劫持之后使用Reflect.set实际上就是调用这个,也就是回到正常赋值流程- 具体逻辑见TC39文档,这些只是通用规范描述,在大多数情况下,
1.3.3. 继承
继承:由于变量会根据原型链向上访问,因此可以基于原型链设计继承关系
function Person() { this.name = 'Tom'; this.age = 18; } function Student() {} Student.prototype = new Person(); Student.prototype.constructor = Student;避免使用相同的对象作为父对象
- 如果两个原型对象
prototype指向相同的对象,在prototype或对象的__proto__上添加的属性,会添加到这两个原型对象构建的变量的__proto__中,被访问到,没有隔离
// 所有 prototype 都引用同一个变量 // 修改会共享 function Person() {} function Man() {} function Woman() {} Man.prototype = Person Man.prototype.constructor = Man Woman.prototype = Person Woman.prototype.constructor = Woman let man = new Man() let woman = new Woman() woman.__proto__.say = function () { console.log('hello world') } // Man 也会有say man.say()- 如果两个原型对象
1.3.4. get和set方法
get和set方法是js中的访问器属性语法,用于在读取和设置值时执行函数逻辑,等同于Object.definePropertyget在读取属性时执行函数set在设置属性时执行函数,必须有一个参数
const user = { firstName: "Tom", lastName: "Hanks", // getter get fullName() { return this.firstName + " " + this.lastName; }, // setter set fullName(value) { const [first, last] = value.split(" "); this.firstName = first; this.lastName = last; } };
1.3.5. Object实例
- 几乎所有声明的对象都是
Object的实例,所有的默认类声明的上一级prototype都是Object.prototype new Object()方法可以传入一个变量,如果是原始类型会得到一个封装类,如果是一个对象会得到它本身(不会拷贝)- 此方法也可以不加
new,直接调用
- 此方法也可以不加
Object原型链上的方法,这些方法可以直接在实例上调用,影响许多机制和运行行为
1.3.6. 代理和反射
1.4. 异常处理
1.4.1. 简单处理
throw抛出异常:在遇到预估会发生错误的情况时,使用throw抛出一个错误对象,避免错误的发生,使程序不会继续运行- 配合
new Error()使用,提供错误信息
function add(a, b) { if (!a || !b) throw new Error('参数不能为空') }- 配合
try...catch:捕获异常,确保之后的无关部分代码继续执行finally块:无论正常执行还是报错,离开try...catch代码块,甚至在try块和catch块中return,都会执行,如果有多次return,以第一次return返回的空间为准return时,会用一个新的引用记录数据,如果是基本类型,后续修改不会影响返回值,如果是引用类型,对属性修改会影响结果
try { add(1) } catch (error) { console.log('捕获异常') // 输出错误信息 console.log(error.message) } finally { console.log('finally代码块') } console.log('后续代码继续执行')浏览器错误调试
debugger:在代码中添加debugger行,浏览器会在对应行暂停代码执行,并进入调试模式,此时可以查看变量的值,并执行代码,直到结束调试模式,可用于快速查看异常原因,似乎会因为在回调中而不起作用
1.4.2. Error 对象
Error对象:Error对象是JavaScript中的通用错误对象,用于创建错误对象,并返回错误信息,构造函数:new Error(message[, fileName[, lineNumber]])message:错误信息,默认为空字符串fileName:错误出现的文件名,默认为出错代码所在的文件名lineNumber:错误出现的行号,默认为出错代码所在行号
Error对象的属性和方法:- 标准属性和方法
name:错误名称,默认为Errormessage:错误信息toString():返回错误信息
- 浏览器拓展的属性和方法,但为了兼容性应谨慎
stack:错误堆栈,返回错误出现的位置,现在一般都支持fileName:错误出现的文件名lineNumber:错误出现的行号columnNumber:错误出现的列号
- 标准属性和方法
1.4.3. 全局异常处理
window对象上有一个error事件,在出现错误并没有被捕获时,会触发- 一般用于处理资源加载错误,需要注意的是
onerror属性无法捕获到图片、脚本等资源加载错误(不冒泡),但error事件监听可以设置在捕获阶段触发 - 在跨域脚本出现错误时,为了安全,不会获取到错误信息,仅能得到为
e.message = "Script error" - 通过
e.error属性可以获取错误 Promise的错误不会触发error事件
跨域脚本
跨域脚本通过读取错误信息,可能能用于构造攻击,因此浏览器很早就隐藏了跨域脚本之间通过
error事件获取跨域脚本的错误详情,得到的都是Script error希望解决
Script error需要在script标签中添加crossorigin="anonymous"属性,并在服务器端设置响应头Access-Control-Allow-Origin当然不是所有的脚本都能进行前后端设置,可以在执行前对原生方法进行劫持,在原生方法外添加
catch,因为try...catch处理不会有这样的问题const nativeFetch = window.fetch window.fetch = function () { return nativeFetch.apply(this, arguments).catch(e => { console.log(e.message) throw e }) }因为原生方法有很多,全部劫持完不现实
- 一般用于处理资源加载错误,需要注意的是
Promise的错误处理:window上有一个unhandledrejection事件,在Promise对象已拒绝却未被catch函数错误处理时,会触发这个事件- 因为
Promise是链式的,如果在then或catch中返回了新的拒绝的对象,依旧会触发这个事件 - 事件似乎是在对应的同步代码和微任务执行之后触发的
- 因为
1.5. 异步请求
1.5.1. Promise
Promise是ES6引入的异步编程的新解决方案。语法上Promise是一个构造函数,用来封装异步操作并可以获取其成功或失败的结果Promise构造函数:Promise((resolve, reject) => {}):resolve和reject是两个函数resolve:异步操作成功时调用,并将异步操作的结果作为参数传递给resolve方法reject:异步操作失败时调用,并将异步操作的错误信息作为参数传递给reject方法
const promise = new Promise((resolve, reject) => { setTimeout(() => { resolve('成功') }, 2000) })
命运与生命周期
Promise对象可能有两种命运,已解决resolved或未解决unresolved- 未解决表明尝试解决或拒绝会对对象产生影响
- 已解决不代表已经完成,可能是等待另一个
Promise对象的结果,在这种情况下,外层的Promise的最终状态将跟随内部Promise对象的状态,获取到的结果是内部Promise对象得到的结果(吸收)
// Promise 对象也可以作为异步操作的结果传递给 resolve 方法 // 当前对象会被锁定,直到获取到新的 Promise 对象的返回结果 // 新的 Promise 对象操作结束时,当前对象会接收到结果,作为本对象的结果 const promise2 = new Promise((resolve) => { resolve(new Promise((_, reject) => { reject('失败') console.log('内部 Promise 拒绝') // 如果在内部直接使用外部的接收函数 // 因为初始化在外部 resolve(promise) 之前会先执行 // resolve('成功') })) })一个
Promise对象有三种状态:pending挂起(初始挂起状态)、fulfilled已兑现(操作成功)、rejected已拒绝(操作失败,可能由错误或其他原因导致)fulfiled和rejected状态将在Promise对象在pending状态时第一次被resolve或reject时发生- 之后代码会继续执行,但再次使用
resolve或reject将不会改变Promise对象的状态和结果 Promise对象实际上执行回调方法和吸收内部Promise对象结果状态都是通过微任务队列完成的,在内部对象已兑现之后,微任务队列会先执行内部对象的then和catch回调,之后让外部对象吸收内部对象状态结果,接下来再执行外部对象的链式回调
在浏览器中输出一个
Promise对象,可以发现有一个内部的[[PromiseState]]内部槽属性,表明Promise的状态,但此属性无法通过编程式的方法获取到
Promise.prototype.then方法:在所有Promise实例都可以调用,来自thenable接口,写法为then((val)=>{}, (err)=>{})Promise.prototype.catch方法:相当与then(null, (err)=>{})Promise.prototype.finally方法:避免重复写在fulfilled和rejected的状态的处理逻辑,finally(()=>{})不接受任何参数,在Promise被敲定(settled,兑现resolved或拒绝rejected)时执行链式调用
then、catch方法会返回新的Promise对象,finally方法会返回原始的Promise对象,因此可以进行链式调用then方法和catch方法中的回调函数返回值将作为新的Promise对象的结果值;如果在执行期间报错,将Error对象作为新的Promise对象结果值;如果不返回,新的Promise对象结果值将未定义undefined- 只有执行回调报错时,新的
Promise对象状态为已拒绝,剩下都为已完成,因此如果希望链式调用,回调必须返回值,并且判断好如何设置Promise对象状态,推荐链式调用时then方法在catch方法之前,避免捕获后无原始值
// 链式调用 promise.then((val)=> val + val) .then((val)=> val + val) .catch((err)=>{ console.log(err) }).then((val)=> { console.log(val) }, (err)=>{ console.log(err) }).finally(()=>{ console.log('finally') })
Promise的静态方法Promise.all(...promises):当所有的promise都成功时,返回一个成功的promise,否则返回带有第一个操作失败原因的已拒绝promisePromise.allSettled(...promises):返回一个promise,该promise在所有promise都完成时完成,并带有一个数组,该数组包含所有操作的promise结果,无论成功或失败Promise.any(...promises):返回带有第一个成功结果的成功promise,如果都失败,返回包含拒绝原因数组的已拒绝promisePromise.race(...promises):返回第一个返回成功结果或第一个失败原因的promisePromise.try(fn, ...args):尝试执行fn(...args)函数,如果fn是同步的,本身也将同步地返回值或抛出错误,如果fn是异步的,获得Promise对象,包含未来返回的结果(已兑现)或抛出的错误(已拒绝)Promise.resolve(value):返回一个已兑现的promise,并带有给定结果值,也就是将一般类型转换为promise。如果传入的是Promise对象,直接返回此对象Promise.reject(reason):返回一个已拒绝的promise,并带有给定拒绝原因
取消异步请求操作
Promise本身没有取消的一级协议,通常使用AbortController实现AbortController对象是一个用于"取消正在进行的异步操作"的对象- 许多异步操作的对象配置中允许直接传递
{signal: controller.signal},signal本身是一个事件目标,在AbortController.abort时触发,对这些有配置的异步操作对象而言,会停止内部操作 - 现在也可以
abortSignal.timeout(ms)设置一个超时触发 - 对于不支持的异步操作停止配置的异步操作对象,可以使用以下方法
function abortable(promise, signal) { return Promise.race([ promise, new Promise((_, reject) => signal.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')) )) ]); }- 许多异步操作的对象配置中允许直接传递
1.5.2. ajax
ajax:Asynchronous JavaScript And XML是浏览器提供的一种异步请求技术,用于向服务器发送请求,并获取数据,并且无刷新地更新页面中相关部分XMLHttpRequest:js自带的服务器请求类型初始化请求对象并使用
open(type, url, async, user, password)设置参数,包含请求方式、请求地址、是否异步、用户名、密码参数- 请求方式:对应
HTTP支持的请求方式,表明使用什么方式请求 - 请求地址:请求的资源地址
- 是否异步:是否异步请求,默认为
true,表示异步请求,false表示同步请求,会阻塞当前线程,直到请求完成 - 用户名和密码参数几乎不使用,用于
HTTP基本认证,浏览器会自动在响应头中包含Authorization: Basic base64(username:password),base64函数将返回字符串username:password的base64编码
现在大多数认证机制都用定义的认证头实现,基本认证将在请求中包含密码原文,非常不安全,实际上基本认证的用户名密码也可以写在
URL中,https://username:password@example.comlet xhr = new XMLHttpRequest(); xhr.open('GET', 'https://api.github.com/users/mzj'); // 通过添加认证头进行认证,比如JWT认证 xhr.setRequestHeader('Authorization', 'Bearer ' + token);- 请求方式:对应
设置跨域请求的
cookie行为,xhr.withCredentials = true,不设置的话sameSite: None的cookie也将不会发送Lax和Strict的cookie因为浏览器限制,无法被任何脚本请求跨域发送,Lax仅能在顶级导航变化时实现跨域发送- 同源请求会自动携带所有的
cookie
使用
onreadystatechange事件监听请求状态改变0对应UNSENT:请求已创建,但未调用open()方法1对应OPENED:请求已初始化,但未发送2对应HEADERS_RECEIVED:请求已发送,已接收响应头3对应LOADING:正在接收响应体4对应DONE:请求已完成,响应已就绪
在响应就绪
4时,通过responseText属性获取响应数据xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300 ) { console.log(xhr.responseText) // 如果是 xml 类型返回,可使用 dom 方法解析 console.log(xhr.responseXML) } else { console.log('请求失败') // 响应头 console.log(xhr.getAllResponseHeaders()) console.log(xhr.getResponseHeader('xhr')); // 响应文字 console.log(xhr.statusText) } } }调用
send()方法发送请求,在send()方法中可以传入请求体xhr.send() xhr.send(body)
::: 请求参数和请求体
有两种在请求时传输数据的方式,分别是请求参数
param和请求体body- 请求参数
param:放在URL里,一般有路径参数和查询参数两种,在请求路径中的参数比如/users/123中的123是请求参数;在最后的?之后的key=value对(多个用&连接)是查询参数 - 请求体
body:数据可以放在请求体里,一般情况下只有POST、PUT、PATCH、DELETE请求才会使用请求体,浏览器可能忽略GET的请求体,请求体的形式多样,因此需要在请求头Content-Type中指定数据格式
xhr.setRequestHeader('Content-Type', 'application/json') // json 类型 xhr.send(JSON.stringify({ name: '张三' })) xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') // 表单类型 xhr.send("user=admin&password=123456") xhr.setRequestHeader('Content-Type', 'multipart/form-data') // 文件上传或混合文本 const fileInput = document.getElementById('fileInput'); // 来自 <input type="file"> const formData = new FormData(); formData.append('username', 'Tom'); formData.append('file', fileInput.files[0]); xhr.send(formData) xhr.setRequestHeader('Content-Type', 'text/plain') // 纯文本 xhr.send('Tom') xhr.setRequestHeader('Content-Type', 'application/octet-stream') // 二进制数据 const file = document.querySelector('#fileInput').files[0]; xhr.send(file)大文件请求也可以使用
File.slice(start, end)方法来分片发送数据,此方法截取了文件的指定部分,并返回一个新的Blob对象- 请求参数
:::
除了原始的
XMLHttpRequest对象,js还有fetch方法,使用更加方便,返回值是Promise对象- 实际上写法和
JQuery的ajax方法基本一样,但返回值是Promise对象而不是JqXHR对象,只有当网络故障或其他阻止请求完成时对象才会被拒绝 then方法接收到一个Response对象,因为请求失败不会被拒绝,需要判断Response.ok和Response.status属性,可以使用response.json()方法获取响应体,还支持text()、blob()等方法解析
fetch(url, { method: 'POST', body: { a: 1, b: 2 } headers: { 'Content-Type': 'application/json' }, // 设置跨域请求的 cookie 行为,有三种选择,`omit`(不发送,默认值)、`same-origin`(同源发送)、`include`(强制发送) credentials: 'include' }).then(res => res.json() ) .then(data => console.log(data))取消请求
fetch请求可以取消,使用AbortController实现let controller = new AbortController(); const signal = controller.signal; // 传入 signal 参数 fetch(url, { signal }) .then((response) => { console.log("下载完成", response); }) .catch((err) => { console.error(`下载错误:${err.message}`); }); // 取消请求 cocontroller.abort();XMLHttpRequest请求可以直接使用abort()取消,这时readyStatus属性会变为0
- 实际上写法和
有其他库实现了请求功能,比如
JQuery和AxiosAxios一个简单的请求,可以直接使用
axios.请求名(url, body, config)或axios(config)完成get/delete/patch方法没有请求体body参数,因此参数为(url, config)
axios({ url: 'https:example.com/api/users/123', method: 'get', params: { name: 'Tom' }, headers: { Authorization: 'Bearer 123' }, withCredentials: true // 和 xhr 配置项名称相同,允许跨域请求携带 cookie }) // get/delete/patch 无请求体参数 axios.get('/api/users/123').then(res => console.log(res.data)); axios.post('/api/users', { name: 'Tom' }); axios.put('/api/users/123', { age: 25 }); axios.delete('/api/users/123'); axios.patch('/api/users/123', { age: 25 }, { headers: { Authorization: 'Bearer 123' } });config参数支持传入一个配置对象配置基本参数,全部配置说明见axios 文档
配置项 说明 url请求地址 method请求方法(默认 GET)baseURL基础路径(可统一设置) params查询参数(会自动序列化) data请求体数据 headers自定义请求头 timeout超时时间 responseType响应类型( json,blob,text等)withCredentials是否携带跨域凭证( cookie)signal用于传入 AboutController取消请求信号validateStatus自定义响应是否成功的函数,返回 true的请求被认为成功函数结果是一个
Promise对象- 可以直接使用
then((res)=>{})获得响应内容 - 可以使用
catch(err=>{})处理错误 - 可以使用
finally(()=>{})处理完成逻辑
axios.get('/api/users').then(res => { // 响应体 console.log(res.data) // 响应状态码 console.log(res.status) // 响应头 console.log(res.headers) // 响应状态信息 console.log(res.statusText) // 请求配置信息 console.log(res.config) // 请求的实例,浏览器中为 XMLHttpRequest 对象,Node.js 中为 http.ClientRequest 对象 console.log(res.request) }).catch(function (error) { if (error.response) { // 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围 console.log(error.response.data); console.log(error.response.status); console.log(error.response.headers); } else if (error.request) { // 请求已经成功发起,但没有收到响应,输出请求的实例 console.log(error.request); } else { // 发送请求时出了点问题 console.log('Error', error.message); } console.log(error.config); })- 可以直接使用
全局配置类:可以通过
axios.defaults修改全局配置,或者创建一个全局实例来管理这些信息// 全局配置 axios.defaults.baseURL = 'http://localhost:8080'; axios.defaults.timeout = 5000; axios.defaults.headers.common['Authorization'] = 'Bearer ' + token; // 全局实例 const api = axios.create({ baseURL: 'http://localhost:8080', timeout: 5000, headers: {Authorization: 'Bearer ' + token} }) // 自动使用配置,传入的 URL 会被拼接到 baseURL 后面 api.get('/api/users').then(res => { console.log(res) })拦截器:如果希望对请求或响应做全局处理,可以使用拦截器,拦截器可以挂载在实例或全局上
- 请求处理:请求正确设置时的处理
config=>config(返回值作为发送请求的配置) - 响应处理:响应正确处理
response=>any、响应出错处理error=>any,函数的返回值如果是普通值或非拒绝的Promise对象,都会提供给then函数,作为参数,如果报错或Promise.reject,才会作为参数,传递给catch函数
api.interceptors.request.use( config => { // 可以统一加 token const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } console.log('请求发送前', config); return config; }, error => { // 请求错误处理 return Promise.reject(error); } ); api.interceptors.response.use( response => { console.log('响应成功', response); return response.data; // 可以直接返回 data }, error => { console.error('响应错误', error); if (error.response?.status === 401) { // 可以统一处理未授权 console.log('跳转到登录页'); } return Promise.reject(error); } );- 请求处理:请求正确设置时的处理
JQuery$.ajax相比当时底层的XMLHttpRequest更灵活方便,可以完全控制请求的各个细节
$.ajax({ url: '/api/users', type: 'GET', // 响应体、状态描述字符串、JQuery 封装的 XHR 请求对象 // 状态描述字符串由 JQuery 生成 success(res, textStatus, jqXHR) { console.log(res) } // 请求对象、状态描述字符串、XHR 请求时或浏览器提供的错误信息 error(jqXHR, textStatus, errorThrown) { console.log(errorThrown) } },) // 支持固定类型的别名请求函数 // 简写时不支持传入错误处理函数和其他参数 $.get('/api/users', function(res) { console.log(res) })基本参数
参数名 含义 url请求地址 method/type请求方法( GET、POST、PUT、DELETE...)data请求参数(对象、字符串) dataType期望的响应类型( json、text、xml、html)contentType发送的数据类型(如 'application/json')success(res, )成功回调 error(xhr, status, err)失败回调 beforeSend(xhr)请求发送前触发 complete(xhr)请求完成(成功/失败)后触发 headers自定义请求头
1.5.3. CORS
- 浏览器同源策略:浏览器同源策略限制了页面的访问其他页面的资源,比如
iframe、script、ajax请求等- 源地址:由协议、网址域名、端口号组成
- 同源请求:请求所处页面源地址和服务器的目标源地址相同,否则是跨域请求
- 浏览器对跨域请求的限制:
DOM访问:源A的脚本不能读取源B的DOMCookie访问:源A不能访问源B的Cookieajax请求:源A能发送ajax请求,但是不能从源B获取响应结果- 跨域请求仅在浏览器端存在限制,服务器端不存在
- 即使跨域了请求也可以正常发出,但响应数据经过浏览器校验,不会交给页面,重点就在于如何符合校验规则
- 标签请求,比如
link、script、img,也可能跨域,不过浏览器对这些标签跨域没有严格的限制(可以通过CSP请求头或页面元信息添加限制),基本无影响
CORS介绍见计算机网络部分
JSONP:早期解决跨域问题的有效方法,但存在XSS威胁风险,前后端为了配合需要大幅修改,现在新项目基本不使用- 使用
script发送简单get请求,服务器直接响应一个符合js规范的代码 - 服务器,使用一个约定的函数名,将需要发送的数据
json作为一个函数的参数,比如返回callback({name: '张三', age: 18}) - 页面在需要请求的时候创建一个
script标签并挂载在body最后,执行会调用方法响应请求,函数名可以通过query参数传入
- 使用
JQuery封装了一个JSONP请求方法getJSON// callback参数对应函数名,由 JQuery 自动生成 // 服务器应该读取 callback,作为函数名 callback({}) $.getJSON('/api/users?callback=?', (res) => { console.log(res) })
服务器端解决跨域的方法
Nginx转发
location /api { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; if ($request_method = 'OPTIONS') { return 204; } proxy_pass http://www.example.org/; }- 服务器代理转发,比如
http-proxy-middleware,是非常流行的node.js中间件,用于express、next.js等许多服务器框架中
const express = require('express') const { createProxyMiddleware } = require('http-proxy-middleware') const app = express() // 创建代理中间件 // 将 http://127.0.0.1:3000/api/foo/bar 转发到 http://www.example.org/api/foo/bar app.use( createProxyMiddleware({ target: 'http://www.example.org', // 目标服务器地址 changeOrigin: true, // 是否改变请求源 pathRewrite: { '^/api': '' }, // 路径重写 pathFilter: '/api', }) ) app.listen(3000)
1.5.4. await/async
async可以作为函数修饰,表明此函数将返回一个Promise对象,对象获取到的结果由函数返回值决定await表达式可以将异步像同步一样处理- 必须出现在最外层或
async函数内部,配合async函数让异步保留,可以不阻塞主程序 - 右侧返回值必须是一个
Promise对象,等待执行完成后返回成功resolve后的结果 - 如果
Promise对象失败reject,await会抛出错误,需要使用try...catch处理
async function getUser() { // 直接等待请求完成,获取结果再继续 const res = await axios.get('/api/users') return res.data } const user = await getUser()- 必须出现在最外层或
1.5.5. WebSocket
WebSocket是一种用于在浏览器和Web服务器之间建立全双工的通信协议,允许浏览器和服务器之间进行双向通信,可以发送和接收数据,并且可以保持连接,直到显式关闭在
js中有WebSocket对象,用于创建一个WebSocket连接new WebSocket(url)创建一个WebSocket连接,参数为url,可以是ws://或wss://开头的urlWebSocket对象有如下属性:readyState:表示连接状态,有四种状态CONNECTING、OPEN、CLOSING、CLOSED,和XMLHttpRequest对象一样
- 方法:
send(data):发送数据ping():发送心跳包,在WebSocket连接中,如果长时间没有收到消息,可以发送心跳包,如果对方没有收到,则认为连接已断开,也可以使用send自定义心跳消息
- 事件
onopen:连接成功时触发onmessage:收到消息时触发onclose:连接关闭时触发onerror:连接错误时触发
const ws = new WebSocket('ws://localhost:8080') ws.onopen = function(e) { console.log('连接成功') } ws.onmessage = function(e) { console.log("收到消息", e.data) } ws.onclose = function(e) { console.log('连接关闭') } ws.onerror = function(e) { console.log('连接错误', e) } // 发送消息 if (ws.readyState === WebSocket.OPEN) { ws.send('hello') }也可以使用其他库,比如
Socket.IO实现const socket = io('ws://localhost:3000', { transports: ['websocket'], // 强制 WebSocket pingInterval: 25000, // 心跳间隔 25s(自定义) pingTimeout: 5000, // 超时 5s reconnection: true, // 自动重连 reconnectionAttempts: 5 // 最多重试 5 次 }); // 连接成功 socket.on('connect', () => { console.log('连接成功,心跳启动'); }); // 接收消息(包括 pong 响应) socket.on('message', (data) => { console.log('收到消息:', data); }); // 心跳断开检测(Socket.IO 自动处理) socket.on('disconnect', (reason) => { console.log('断开原因:', reason); // e.g., 'ping timeout' }); // 发送消息 function sendMessage(msg) { if (socket.connected) { socket.emit('message', msg); // emit 是 Socket.IO 的发送方法 } }
1.5.6. SSE
SSE(Server-Sent Events,服务器发送事件)是一种用于在浏览器和Web服务器之间建立单向的通信协议,允许服务器单向不断向浏览器发送数据,可用于实时推送消息,可以保持连接,直到显式关闭EventSource对象用于创建一个SSE连接,使用的是http请求,请求头中包含Accept: text/event-stream构造函数:
new EventSource(url, options),其中options可以包含一个参数withCredentials,表示是否允许cors,创建时默认打开连接EventSource对象有如下属性方法:readyState:表示连接状态,有四种状态CONNECTING(0)、OPEN(1)、CLOSED(2)url:SSE连接的urlwithCredentials:是否允许corsclose():关闭连接
EventSource对象有如下预定义事件:open:连接成功时触发message:收到消息时默认触发error:连接错误时触发,比如后端关闭了连接时,该事件不可取消
得益于
SSE使用text/event-stream内容协议支持自定义事件名,可以自定义事件类型并接收- 协议返回的数据支持多行文本,每次数据格式为
event: xxx\ndata: xxx\n\n,其中event表示事件名(可选),data表示数据,\n表示换行符,\n\n表示结束符 - 可以使用事件字段定义自定义事件,默认是
message - 当发现有事件字段时,
EventSource会触发一个名称和事件字段一致的事件,而不仅仅是默认的message事件
EventSourceconst source = new EventSource('http://localhost:8080/sse') es.addEventListener('update', (e) => { // 自定义 console.log('更新:', JSON.parse(e.data).time); }); es.addEventListener('message', (e) => { // 固定,默认 console.log('默认:', e.data); }); es.addEventListener('close', (e) => { console.log('关闭连接:', e) }) es.addEventListener('error', (e) => { console.log('错误:', e) }) es.addEventListener('end', (e) => { es.close() console.log('已关闭连接') })expressapp.get('/events', (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*') res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE') res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization') res.setHeader('Access-Control-Max-Age', '86400') res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' }); // 发送自定义事件 const a = setInterval(() => { res.write('event: update\ndata: ' + JSON.stringify({ time: Date.now() }) + '\n\n'); }, 2000); // 发送默认消息 const b = setInterval(() => { res.write('data: 默认消息\n\n'); }, 3000); setTimeout(() => { clearInterval(a); clearInterval(b); res.write('event: end\ndata: end\n\n'); res.end(); }, 9000); });- 协议返回的数据支持多行文本,每次数据格式为
1.6. 模块化
随着应用的复杂度越来越高,代码量和复杂度都会急速增加,会导致全局污染、依赖混乱、数据安全的问题,因此出现了模块化
模块化的核心思想就是模块之间是隔离的,通过导入和导出进行模块间的数据和功能的共享,现在流行的模块化方案有
CommonJS:Node.js服务器端在ES6之前广泛使用的方案- 导出:在文件内部有一个空对象,模块内部使用
module.exports、exports、this都指向它,最终导出的就是这个对象modeule.exports
exports是module.exports对象的初始引用,因此不应该直接赋值exports = value,应该通过exports.xxx挂载属性- 导入:使用
require函数导入模块,返回模块的导出对象,这个对象可以进行解构赋值,如果有重复声明冲突,需要{ name: alias }修改名称name为alias
index.jsconst { add, mul } = require('./js/mathUtils.js')mathUtils.jsfunction add(num1, num2) { return num1 + num2 } function mul(num1, num2) { return num1 * num2 } module.exports = { add, mul } // 也可以 // exports.add = add // exports.mul = mul实现原理
CommonJS的模块化实际上是基于外层函数包裹完成的,可以在模块文件中通过arguments.callee.toString()获取到包含模块内容的完整函数,实际上mathUtils.js文件内容对应的函数如下:function (exports, require, module, __filename, __dirname) { function add(num1, num2) { return num1 + num2 } function mul(num1, num2) { return num1 * num2 } module.exports = { add, mul } // 也可以 // exports.add = add // exports.mul = mul console.log(arguments.callee.toString()) }node会帮助处理模块函数,require函数会正确引入这些导出的内容在浏览器中不支持
CommonJS,require函数是不存在的,需要经过编译,可以使用browserify编译npm install -g browserify # 编译,将上方的index.js编译为浏览器可以导入的bundle.js文件 browserify app.js -o bundle.js # 可以在浏览器中导入 # <script type="text/javascript" src="bundle.js"></script>
- 导出:在文件内部有一个空对象,模块内部使用
ES6 Module:ES6标准提出的模块化标准,广泛用于浏览器页面导出
export:导出(暴露)方式有三种,分别暴露、统一暴露和默认暴露,三种方式可以出现在一个文件中// 分别暴露 export const add = (a, b) => a + b; export const minus = (a, b) => a - b; // 统一暴露 const mul = (a, b) => a * b; const div = (a, b) => a / b; export { mul, div }; // 默认暴露,默认暴露的属性会放入 default 属性中 const mod = (a, b) => a % b; export default mod导入
import:导入的形式也多种多样,可以静态导入,也可以动态导入import时,被导入文件全局操作(副作用)会被执行,可以什么都不导入import 'xxx',仅执行// 静态导入 // 针对非默认暴露,可以全导入或按需引入 // 实际上全导入的对象中有 default 属性对应默认暴露的属性 import * as math1 from './math.js'; // 由于 default 是标识符,所以无法这样导入 import { add, minus, mul, div } from './math.js'; // 针对默认暴露,获取到的math就是mod函数 import math2 from './math.js'; // 针对混合暴露,既有默认暴露,也有非默认暴露 // 必须先写默认暴露 import math3, { add as add1, minus as minus1 } from './math.js'; // 动态导入,result 就是一个全导入的对象,导入的内容和 math1 一致 // 在需要的时候导入,之后才能使用 const result = await import('./math.js'); // 包含所有暴露的属性 console.log(math1); console.log(result); // 获取默认暴露的属性,本身是 mod 函数 console.log(math2); console.log(math3); console.log(math1.default(10, 2)); // mod(10, 2) console.log(math2(10, 2)); // mod(10, 2) console.log(add(10, 2)); // add(10, 2)两种模块化导出的基本类型变量区别
在
CommonJS中,模块化导出的是原始变量的引用(赋值),也就是说基本数据类型的修改不会相互影响,引用类型内容的修改会相互影响let count = 1; function inc() { count++; } module.exports = { count, inc } // CommonJS 导入时 let { count, inc } = require('./module'); console.log(count); // 1 inc(); console.log(count); // 1在
ES6模块中,模块化导出的内容和原始数据指向的是同一块内存空间,在导出之后无法在外部直接修改(引用不可变,键不可扩展),相当于使用const声明,但在使用模块内部函数后,值可能被改变,这可能带来值的不确定性export let count = 1; export function inc() { count++; } // ES6 模块导入时 import { count, inc } from "./count.js"; console.log(count); // 1 inc(); console.log(count); // 值变化为 2
在
html中引入:ES6模块是浏览器支持的功能,可以直接引入模块文件,脚本类型为module,引入的内容声明不会污染全局作用域,不交出去不获取的变量就无法使用<script type="module" src="./math.js"></script>在
node.js中使用:需要创建package.json文件,并设置json内容{"type": "module"};也可以直接修改后缀为.mjs(module模块化特殊文件后缀, 对应CommonJS后缀为.cjs)
type属性用于决定node如何处理js文件,默认是commonjs,就是按CommonJS处理AMD:来自require.js的模块化规范环境配置requirejs.config({ baseUrl: "./js", paths: { // ./js/num.js num: "num", // ./js/add.js add: "add" } })使用// c 是 num 模块导出的变量容器对象 // 模块和变量对应,导入顺序和变量名称无关 define(["num"], function(c) { function add(a, b) { return a + b + c.num; } return { add } })引入依赖<script src="https://cdn.jsdelivr.net/npm/requirejs@2.3.7/require.min.js"></script>
:::
CMD:来自sea.js使用// c 是 num 模块导出的变量 // 模块和变量对应,导入和变量名称无关 define(function(require, exports, module) { const c = require("num.js") function add(a, b) { return a + b + c.num; } exports.add = add })引入依赖<script src="https://cdn.jsdelivr.net/npm/sea.js@2.3.0/sea.min.js"></script>
