运行机制

执行上下文

也可以叫js的运行环境,函数被调用时或全局脚本执行前才会创建,否则不会创建执行上下文,也就不会进行函数预编译。分【分类】全局执行上下文、函数执行上下文、Eval执行上下文。【哪些周期】执行上下文的周期分为创建阶段和执行阶段。执行上下文会被依次放入到执行栈中,调用完毕后出栈。

  • 创建阶段:创建词法环境;生成变量对象(VO,是js代码进入执行上下文时,js引擎在内存中建立的一个对象,用来存放当前执行环境中的变量),创建作用域链;确定this指向并绑定this。

  • 执行阶段:进行变量赋值;函数引用;执行代码。

  • 执行完后出栈等待被回收

    JS编译步骤:语法分析、预编译、解释执行

    Js是解释性语言,编译一行执行一行,编译之前存在预编译

    【预编译在哪个阶段】预编译:发生在在函数执行之前或者全部页面加载完,在当前环境的执行上下文的创建阶段进行。变量声明提升,函数整体提升。

    函数预编译:

  • 创建AO对象

  • 形参变量声明,使其作为AO的属性名,值赋为undefined

  • 实参的值赋值给形参

  • 函数声明,函数名做AO对象属性名,值赋予函数体

    预编译结束,执行代码(变量赋值和函数引用都在其中)

    全局预编译:

  • 建立GO对象

  • 变量声明作为属性名,值赋undefined

  • 函数名作为函数声明,值为函数体

    参考资料,结合实例理解过程

    https://juejin.cn/post/7030370931478364196

    结合闭包理解

    https://juejin.cn/post/6971727286856843295

js V8引擎垃圾回收机制

V8 引擎的垃圾回收机制主要通过分代回收、标记 - 清除、标记 - 压缩、增量标记等算法和策略来工作,以下是其详细工作原理:

内存分代

V8 将内存分为新生代和老生代两个区域。

新生代:用于存储存活时间较短的对象,通常是一些临时变量、短期使用的对象等。新生代内存空间相对较小,一般采用复制算法进行垃圾回收,分为 From 空间和 To 空间。

老生代:存放存活时间较长的对象,像全局变量、长期存在的对象等。老生代内存空间较大,回收算法相对复杂,主要采用标记 - 清除和标记 - 压缩算法。

新生代垃圾回收

  • Scavenge 算法(复制算法)
    • 标记阶段:从根对象(如全局对象、函数调用栈中的变量等)开始遍历,标记所有可达的对象,即正在被使用的对象。
    • 复制阶段:将 From 空间中标记的存活对象复制到 To 空间,并按照顺序排列,同时更新对象的引用地址。未被标记的对象则被视为垃圾,直接被回收。复制完成后,From 空间和 To 空间的角色互换,To 空间成为下一次垃圾回收的 From 空间,原来的 From 空间变为 To 空间。

老生代垃圾回收

  • 标记 - 清除算法
    • 标记阶段:同样从根对象开始,对整个老生代内存中的对象进行遍历,标记所有可达的存活对象。
    • 清除阶段:遍历完所有对象后,清除未被标记的对象,释放其占用的内存空间。但标记 - 清除算法会导致内存碎片化,即内存中会出现大量不连续的空闲空间。
  • 标记 - 压缩算法
    • 标记阶段:与标记 - 清除算法的标记阶段相同,标记出所有存活对象。
    • 压缩阶段:将存活对象向内存一端移动,使存活对象紧密排列,然后清除边界以外的内存空间,解决了内存碎片化问题。

增量标记与并发标记

  • 增量标记
    为了减少垃圾回收对主线程的阻塞时间,V8 采用增量标记技术。它将标记过程分成多个小步骤,穿插在主线程的执行过程中,每次执行一小段标记操作,然后暂停让主线程执行一段时间,再继续标记,如此循环,直到完成整个标记过程。
  • 并发标记
    V8 还支持并发标记,即垃圾回收线程与主线程同时运行,在主线程执行的同时,垃圾回收线程进行标记操作。但并发标记需要解决与主线程的同步问题,确保在标记过程中对象的状态不会被主线程的操作所干扰。

写屏障(Write Barrier)

在垃圾回收过程中,为了保证标记的准确性,V8 使用了写屏障技术。当对象的属性被修改时,写屏障会被触发,它会记录下这个修改操作,确保在垃圾回收时能够正确处理对象之间的引用关系,避免出现漏标或误标的情况。

参考资料

https://juejin.cn/post/6981588276356317214

作用域链

作用域:是指程序中定义变量的区域,是可访问变量、对象、函数的集合。分为全局作用域函数作用域。内层作用域可以访问外层作用域中的变量。

let和const是块级作用域。执行上下文和作用域的区别:执行上下文在运行时确定,随时可能改变,作用域在定义时就确定了,不会改变。

作用域链就是当函数或变量被调用时,取自由变量时,要到创建这个函数的作用域中取,取不到就到上一层作用域取,一直到全局作用域,还取不到就取undefined。

var、let、const区别?

  • var没有块作用域的概念,let、const都是块级作用域,只在当前代码块有效。

  • var会有变量提升,而let和const都存在暂时性死区,只能在声明后使用。

  • let、const不可以在当前作用域下重复声明相同的变量名称,会报错。

  • let和const有很多地方都是相同的,但是const是对常量进行定义,声明后内存地址不可以改变。(并不是绝对不可变的,不能修改指针但可以修改值且声明变量必须设置初始值。)

怎么理解闭包?闭包有什么用?有哪些应用?

闭包是指有权访问另一个函数作用域中变量的函数

创建的形式是在函数里创建一个函数,创建的函数可以访问到当前函数的局部变量此时就形成了闭包。

闭包的两个主要的作用是保存保护:

一方面,保存,由于内部函数保存了对外部函数的变量的引用,所以变量不会被垃圾回收

另一方面,保护,形成一个全新的私有作用域,内部的变量就是私有变量在外部无法引用,也不会影响到外部作用域的变量。我们可以通过闭包的特点来访问私有变量。

应用:需要记录函数内部变量的值的情况如回调函数,settimeout,防抖节流,函数柯里化。

参考资料,示例便于深入理解用途,结合执行上下文来看

https://juejin.cn/post/6971727286856843295

函数柯里化,有空了解

https://juejin.cn/post/6844903665308794888

this指向

this指向的规则包括new绑定、显示绑定、箭头函数绑定、隐式绑定、默认绑定。(去掉箭头函数绑定即是优先级顺序,因为箭头函数比较特殊)

  • new一个对象后this指向该实例对象

  • 箭头函数没有this,this指向箭头函数定义时所在作用域的this,即外层this,不受其他绑定方式影响;

  • 显示绑定,硬绑定call、apply、bind,this指向传入的参数中的对象

  • 隐式绑定,this绑定到上下文对象

  • 独立的函数被调用则适用默认绑定,this绑定到window(非严格模式)。

默认绑定(window)

需要注意:使用函数别名函数作为参数传递时使用的都是默认绑定,因此回调函数会丢失this绑定

普通全局函数:严格模式下,this指向undefined,非严格模式下,this指向window,this指向是动态的。

箭头函数

  • 无论是严格模式还是非严格模式下,全局箭头函数的this始终指向window。
  • 没有自己的this指向,都是指向函数所在作用域的this。
  • 不能使用new创建,没有argumens对象
  • 不能使用yield关键字 (用于生成器函数中,用于暂停和恢复函数的执行)
  • 不能被当做构造函数,无法通过call/apply/bind等改变this指向。

参考资料,包括箭头函数示例

https://new-developer.aliyun.com/article/1133805

call/apply/bind

三个方法都可以改变this指向,不同的是call的参数接收的是参数列表,apply的参数接收的是数组,call和apply都是立即调用,bind同样接收参数列表,但返回的是一个函数,需要手动调用。

原型链

什么是原型?什么是原型链?

js中每一个构造函数都有一个prototype属性,指向该函数的原型对象原型对象包含了可以由该构造函数创建的所有实例所共享的属性和方法。默认情况下,所有原型对象会自动获得一个constructor属性,指回与之关联的构造函数。通过构造函数创建一个新实例,可以在浏览器环境下通过__proto__属性访问对象的原型,也就是构造函数的原型。(在定义构造函数时,原型对象默认只会获得constructor属性,其他的所有方法都继承自Object。)

原型链:通过对象访问属性时会先从对象本身开始搜索,找到该名称则返回对应的值,没找到就会沿着指针进入原型对象,再在上面找,一直到找到object的原型对象上。Object原型对象用Object.prototype._proto_=null表示原型链的终点。这个搜索过程形成的一条链就叫原型链

继承

  • 原型链继承:将子类的原型指向父类的实例来实现继承。 缺点–不能向父类传参数,实例可以篡改父类的引用类型
  • 构造函数继承:子类构造函数中调用父类构造函数,可以父类传递参数。 缺点–继承不了父类原型对象的方法
  • 组合继承:构造函数继承结合原型链继承。缺点是父类构造函数被调用了两次。
  • 原型式继承:通过字面量对象或 Object.create() 创建一个新对象,使用一个对象来作为另一个对象的原型。
  • 寄生式继承:创建一个增强对象的函数,然后返回这个对象。
  • 寄生组合式继承:结合构造函数继承(属性)和寄生式继承(原型链优化)。
  • ES6 Class 继承:使用 class 和 extends 语法糖。

继承优缺点及详情,见:
http://www.duomangcoding.top/2023/02/24/js%E7%BB%A7%E6%89%BF/

new的过程/原理:

先创建一个空对象;

再把空对象的__proto__指向构造函数的原型对象;

把构造函数的this绑定到新对象上,调用函数;

判断结果是对象就直接返回结果,不是就返回创建的新的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function.prototype.myNew= function(constructor,...args){
//创建一个对象 ES5提供了Object.create方法,该方法可以创建一个对象,并让新对象的__proto__属性指向已经存在的对象。
// let o = Object.create(constructor.prototype);
//具体步骤:
//1.创建一个空对象

var o = {};
//2.把空对象上的__proto__属性指向constructor的原型对象
o.__proto__ = constructor.prototype
//3.把constructor的this绑定到了新建的对象上
let result = constructor.apply(o,args);
//4.判断调用构造函数的结果,是对象就返回,不是对象那就返回新对象
return result&&typeof(result == 'object')?result:o;

}

数据类型

基本数据类型

Number/String/Null/Undefined/Boolean/Symbol(ES6) /**Bigint(ES10)**【7种】

引用数据类型

object/(Array/Function/RegExp/Date )

基本数据类型和引用数据类型的根本区别:一个在栈中存储值,一个存储指针,是一个地址,指向堆中的对象。栈中存储基本类型值,也是代码执行的环境。

补充:js中数组和对象的区别:数组是一种特殊的对象,key值只能是数字。

怎么创建一个对象?

//字面量创建、new object创建、构造函数创建、Object.create()创建。

工厂模式、构造函数模式、原型模式、组合模式、动态原型模式、寄生构造函数模式。

判断数据类型的方法

1.typeof()

可以判断除null之外基本数据类型以及function

2.A instanceof B:

判断已知对象类型的方法。只适用于引用数据类型的判断,原理是判断左边原型链上是否有右边对象的原型。

3.根据对象的constructor判断:

可以判断数据的类型,实例也可通过construcor访问到它的构造函数

c.constructor === Array

4.Object.prototype.toString.call()(繁琐)

alert(Object.prototype.toString.call(a) === ‘[object String]’) ——-> true;

alert(Object.prototype.toString.call(b) === ‘[object Number]’) ——-> true;

alert(Object.prototype.toString.call(c) === ‘[object Array]’) ——-> true;

alert(Object.prototype.toString.call(d) === ‘[object Date]’) ——-> true;

alert(Object.prototype.toString.call(e) === ‘[object Function]’) ——-> true;

alert(Object.prototype.toString.call(f) === ‘[object Function]’) ——-> true;

5.jQuery.type()

如果对象是undefined或null,则返回相应的“undefined”或“null”。

1
2
3
4
5
6
7
8
jQuery.type( undefined ) === "undefined"

jQuery.type() === "undefined"

jQuery.type( window.notDefined ) === "undefined"

jQuery.type( null ) === "null"

如果对象有一个内部的[[Class]]和一个浏览器的内置对象的 [[Class]] 相同,我们返回相应的 [[Class]] 名字。 (有关此技术的更多细节。 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
jQuery.type( true ) === "boolean" 

jQuery.type( 3 ) === "number"

jQuery.type( "test" ) === "string"

jQuery.type( function(){} ) === "function"

jQuery.type( [] ) === "array"

jQuery.type( new Date() ) === "date"

jQuery.type( new Error() ) === "error"

jQuery.type( /test/ ) === "regexp"

判断数组的方法

Array.isArray() 、Object.prototype.toString.call()、obj instanceof Array

instanceof实现原理

判断左边原型链上是否有右边对象的原型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function myInstanceof(left, right) {
let L = left
while (true) {
if (L.__proto__ == null) {
return false
}
if (L.__proto__ == right.prototype) {
return true
}
L = L.__proto__
}
}
let a = [1, 2, 3, 4]
let b = {
a: 1
}
let res = myInstanceof(a, Object)
console.log(res)

类型转换规则

显示类型转换:

  • 转化为 Number 类型:Number() / parseFloat() / parseInt()
  • 转化为 String 类型:String() / toString()
  • 转化为 Boolean 类型: Boolean()

隐式类型转换:

+两边有一个是string类型,则其他类型转换为string

详细看https://juejin.cn/post/6956170676327677966#heading-12

undefined和null的区别

null表示”没有对象”,即该处不应该有值。

(1) 作为函数的参数,表示该函数的参数不是对象。

(2) 作为对象原型链的终点。

undefined表示”缺少值”,就是此处应该有一个值,但是还没有定义。

(1)变量被声明了,但没有赋值时,就等于undefined。

(2) 调用函数时,应该提供的参数没有提供,该参数等于undefined。

(3)对象没有赋值的属性,该属性的值为undefined。

(4)函数没有返回值时,默认返回undefined。

1
2
3
undefined == null // true

typeof null //object

null转换成数字是 0

undefined 转换成数字是 NaN

为什么typeof null 是object ?

不同的对象在底层的存储是用二进制表示的。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,如果二进制的前三位都为0,系统会判定是object类型。然而 null 表示为全零,其存储二进制正是000,所以系统会判定是object类型

0.1 + 0.2为什么不等于0.3

0.1+0.2的结果不是0.3,而是0.3000000000000000004,JS中两个数字相加时是以二进制形式进行的,当十进制小数的二进制表示的有限数字超过52位时,在JS里是不能精确储存的,这个时候就存在舍入误差

深拷贝与浅拷贝

对于对象的拷贝,浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果对象的属性值是基本类型,拷贝的就是基本类型的值,如果属性值是引用类型,拷贝的就是内存地址 ,所以对对象的引用类型属性进行修改,如果其中一个对象改变了这个地址,就会影响到另一个对象

深拷贝会递归地复制对象的所有层级,创建一个完全独立的新对象。

浅拷贝实现方式:object.assign slice concat Array.from 扩展运算符

深拷贝实现方式:Lodash.cloneDeep JSON.parse(JSON.stringfy)(只支持对象和数组,里面不能包括这两种类型之外的对象,并且会丢失undefined和null)

手写深拷贝

常用:使用JSON.parse(JSON.stringify(obj)) 序列化和反序列化

缺点是: 会忽略undefined、symbol、funciton

实现:递归+判断类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function deepClone(value,map= new WeakMap()) {
if (typeof value!== 'object' || value === null) {
return value;
}
// 我要判断 value 是对象还是数组 如果是对象 就产生对象 是数组就产生数组
let cloneObj = Array.isArray(value)?[]:{};
// 查看该对象是否已复制过,复制过直接返回避免自身引用导致栈溢出
if(map.get(value)) {
return map.get(value);
}
map.set(value,cloneObj);
for(let key in value){
if(value.hasOwnProperty(key)) {
cloneObj[key] = deepClone(value[key],map);
}
}
return cloneObj;
}

ES6数组相关方法

for in 、for of

for in 遍历key,for of 遍历value,for in 会遍历数组中所有可枚举属性,包括原型上的属性和方法,需要配合myobj.hasOwnProperty(key)判断key是否是实例上的属性,从而进行剔除,适合遍历对象的key。

for of是es6提出的,适合遍历数组,还可以遍历set/map/string的值。

Symbol数据类型有什么用

symbol是一种基本数据类型,表示独一无二的值,在之前,对象的键以字符串的形式存在,所以极易引发键名冲突问题,而Symbol的出现keyi正是解决了这个痛点。

作为对象属性的键,防止键名冲突。

1
2
3
let a = Symbol()
let obj = {}
obj[a] = "hello world"

比如Vue中使用symbol作为注入名可以避免潜在的冲突。

https://cn.vuejs.org/guide/components/provide-inject.html#working-with-symbol-keys

参考资料

https://juejin.cn/post/7143252808257503240

https://juejin.cn/post/6844903703242080263

map与weakMap

weakMap的值只能是对象,weakMap对对象是弱引用,如果对象没有在别的地方被引用,就很容易被垃圾回收。weakMap不支持迭代以及keys(),values(),entries方法,没办法获取到他所有的键值。

map与对象的区别是可以用复杂数据类型做key

应用:关联DOM节点:删除之后可以被垃圾回收,防止内存泄漏;利用弱映射,将内部属性设置为实例的弱引用对象,当实例删除时,私有属性也会随之消失,因此不会内存泄漏;数据缓存:当我们需要在不修改原有对象的情况下储存某些属性等,而又不想管理这些数据时

数组方法 forEach、map、filter、some、reduce的区别

  1. forEach没有返回值,直接改变原数组,map有返回值,适合做原始数据的映射。

  2. filter 里面写条件,生成一个每个选项都符合条件的新数组。

  3. some 判断数组中有没有至少一个符合条件,符合返回true,find的作用类似,但是返回的是符合条件的选项,要知道符合项的下标,就用findIndex

    var hasbig = potatos.some(potato => { return potato.weight > 100 })

  4. every判断数组中选项是否全部符合,返回boolean

  5. reduce 循环累加,接受一个回调函数作为参数,回调又接受4个参数

    reduce()方法返回的是最后一次调用回调函数的返回值;

var sum = weight.reduce((sum, w) => { return w + sum },0)

事件机制

事件委托(事件代理/DOM事件流)

DOM事件流是指从页面中接收事件的顺序。事件发生时会在元素节点与根节点之间按特定顺序传播,路径经过的所有节点都会收到该事件。

三个阶段:捕获阶段目标阶段冒泡阶段。默认情况只会触发冒泡阶段。

事件委托:通过事件捕获或事件冒泡把内层元素的事件绑定到外层,减少事件监听器的数量。回调函数通过target去找到事件发生的实际元素,就可以达到预期的效果。

事件冒泡由内向外,事件捕获由外向内。最外层是window。

冒泡和捕获的顺序:除了目标元素的处理顺序是按照注册顺序执行,其他都默认先由外到内捕获后由内到外冒泡。(火狐) 谷歌不管注册事件顺序。

element.addEventListener(type(事件类型), listener(回调), useCapture(true为捕获,false为冒泡,默认false))

阻止冒泡:回调中添加 event.cancelBubble(用于ie)或者event.stopPropagation() ;vue中添加事件修饰符@click.stop;小程序catchtap

参考资料

https://juejin.cn/post/6844904190280466440

事件循环

Js是一门单线程的语言,那么他的异步和多线程的实现就是靠eventloop事件循环机制实现的。大体由调用栈、消息队列和微任务队列组成。

一开始会从全局dom开始顺序执行,同步代码压入调用栈,执行完毕后出栈,遇到异步代码,(执行完毕后)将回调函数加入消息队列,当调用栈清空时查看消息队列,有则压入调用栈,直到清空消息队列,这就是一个tick。之后循环往复。遇到微任务像promise、mutationobserver、process.nexttick这样的异步操作会加入到微任务队列,查看消息队列之前会首先查看微任务队列。(微任务队列也叫任务队列,是针对ES6的promise提出的。遇到promise相关的比如then()会弹出调用栈后进入微任务队列。)

  • 宏任务主要包含:script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)

  • 微任务主要包含:Promise、MutaionObserver、process.nextTick(Node.js 环境)

参考视频:

https://www.bilibili.com/video/BV1kf4y1U7Ln

js应用

防抖节流

都是针对高频触发事件的解决方法。

区别:节流不管事件触发多频繁保证在一定时间内一定会执行一次函数。防抖是只在最后一次事件触发后才会执行一次函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//防抖
function debounce(fn,delay){
let timer = null;
return function(){
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this,arguments);
}, delay);
}
}
const debouncedSearch = debounce(searchSuggestions, 300);

//节流
function throttle(fn,delay){
let timer = null;
return function(){
if(timer){
return
}
else{
timer = setTimeout(()=>{
fn.apply(this,arguments)
timer = null;
},delay)
}
}
}
const throttledLoadMore = throttle(loadMoreData, 1000);

promise的作用与用法

  • Promise的作用:Promise是异步编程的一种解决方案,解决了异步多层嵌套回调的问题,让代码的可读性更高,更容易维护。

  • Promise使用:Promise是ES6提供的一个构造函数,可以使用Promise构造函数new一个实例,Promise构造函数接收一个函数作为参数,这个函数有两个参数,分别是两个函数 resolvereject,resolve将Promise的状态由等待变为成功,将异步操作的结果作为参数传递过去;reject则将状态由等待转变为失败,在异步操作失败时调用,将异步操作报出的错误作为参数传递过去。实例创建完成后,可以使用then方法分别指定成功或失败的回调函数,也可以使用catch捕获失败,then和catch最终返回的也是一个Promise,所以可以链式调用。

  • Promise有三个状态pendingresolvedrejected

  • 特点是对象的状态不会受外界影响,一旦状态改变就不会再发生变化,resolve的参数是then中回调函数的参数,reject中的参数是catch中的参数。Then和catch都会返回一个resolved状态的promise。

  • Promise的常见方法:
    Promise.prototype.then():
    用于指定 promise 成功后的处理函数。resolve()调用后then回调立即执行,并且其中传入的参数作为then回调函数的参数。

    Promise.prototype.catch():
    用于指定 promise 失败后的处理函数。

    Promise.prototype.finally():
    无论 promise 成功还是失败,都会执行的函数。回调函数不会接受参数。

    Promise.all() 方法接收一个可迭代对象(通常是数组)作为参数,该数组中的每个元素都是一个 Promise 对象。它会返回一个新的 Promise 对象,当数组中的所有 Promise 都成功时,新的 Promise 才会成功,并且会将所有 Promise 的结果按顺序组成一个数组作为新 Promise 的结果;如果数组中有任何一个 Promise 失败,新的 Promise 会立即失败,并将第一个失败的 Promise 的错误信息作为结果。

    Promise.race() 方法同样接收一个可迭代对象(通常是数组)作为参数,该数组中的每个元素都是一个 Promise 对象。它会返回一个新的 Promise 对象,当数组中的任何一个 Promise 率先改变状态(成功或失败)时,新的 Promise 就会以该 Promise 的结果作为自己 的结果。

    Promise.resolve()方法用于创建一个已成功的 Promise 对象,它接收一个参数作为 Promise 的结果。如果该方法的参数为一个Promise对象,Promise.resolve()将不做任何处理;如果参数thenable对象(即具有then方法),Promise.resolve()将该对象转为Promise对象并立即执行then方法;如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为fulfilled,其参数将会作为then方法中onResolved回调函数的参数,如果Promise.resolve方法不带参数,会直接返回一个fulfilled状态的 Promise 对象。需要注意的是,立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。

    Promise.reject()同样返回一个新的Promise对象,状态为rejected,无论传入任何参数都将作为reject()的参数

async关键字和await关键字的作用

在 JavaScript 中,async 和 await 是 ES2017 引入的用于处理异步操作的语法糖,它们建立在 Promise 的基础之上,使得异步代码看起来更像同步代码,从而提高了代码的可读性和可维护性。

async 关键字

async 关键字用于定义一个异步函数,异步函数总是返回一个 Promise 对象。如果函数内部没有显式地返回一个 Promise,JavaScript 会自动将函数的返回值包装在一个已解决(fulfilled)的 Promise 中。

await 关键字

await 关键字只能在 async 函数内部使用,它会暂停 async 函数的执行,直到所等待的 Promise 被解决(fulfilled)或被拒绝(rejected)。当 Promise 被解决时,await 表达式会返回 Promise 的结果;当 Promise 被拒绝时,会抛出一个错误,需要使用 try…catch 语句来捕获。
优点:代码可读性提升(让异步代码看起来更像同步代码)、错误处理更方便(try…catch 语句来捕获 await 表达式抛出的错误)

异步加载js的方式

deferasync,加在script标签里,defer表示延迟,async表示同步。

  • 当浏览器遇到async的script标签,马上异步请求该脚本资源,不会阻塞浏览器解析HTML。网络请求回来后,如果HTML还没解析完,就会暂停解析,先让js引擎执行代码,执行后再继续解析。(请求不阻塞解析,js先执行,会阻塞解析)其他脚本不会等待 async 脚本加载完成,同样,async 脚本也不会等待其他脚本。

  • defer会在请求脚本的时候解析HTML,一直到解析完毕再执行js代码

    (一直不会阻塞html解析,请求和解析同样可以同步,但要解析完才执行js)

    defer 特性除了告诉浏览器“不要阻塞页面”之外,还可以确保脚本执行的相对顺序。

async 的优先级高于defer

为什么script标签放在html文档的最后,link标签放在html文档开头?

  • script标签是从上到下边加载边解析执行,下载和解析会阻塞html解析,从而阻塞页面渲染,这是我们不想看到的,所以一般把script标签放在html文档的最后,或者给script标签加上defer,async参数,强制让script标签延迟加载。
  • 多个link是同时加载,先加载完的优先解析。link标签不会阻塞html解析,如果link标签放在dom之后,会导致浏览器发生回流重绘,这个开销是非常大的,所以我们一般把link标签放在html文档开头(head)中。

哪些操作会造成内存泄漏

  • 内存泄漏指任何对象在不再需要它之后仍然存在
  • setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄漏
  • 闭包、控制台日志、两个对象彼此引用且彼此保留

前端如何压缩图片

压缩的大概流程

  • 通过原生的input标签拿到要上传的图片文件
  • 将图片文件转化成img元素标签
  • 在canvas上压缩绘制该HTMLImageElement

核心步骤:

  1. 拿到转化后的img元素后,先取出该元素的宽高度,这个宽高度就是实际图片文件的宽高度。
  2. 然后定义一个最大限度的宽高度,如果超过这个限制宽高度,则进行等比例的缩放。
  3. 计算好将要压缩的尺寸后,创建canvas实例,设置canvas的宽高度为压缩计算后的尺寸,并将img绘制到上面
1
2
3
4
5
6
7
8
9
10
// 创建画布
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')

// 设置宽高度为等同于要压缩图片的尺寸
canvas.width = targetWidth
canvas.height = targetHeight
context.clearRect(0, 0, targetWidth, targetHeight)
//将img绘制到画布上
context.drawImage(img, 0, 0, targetWidth, targetHeight)

前端性能优化

加载方面的优化比如:

  • 减少网络请求次数:精灵图节流防抖
  • 提高加载速度:压缩文件体积(使用打包工具配置)、图片压缩静态资源cdn加速
  • 减少不必要加载: 缓存(HTTP缓存、浏览器本地缓存)图片延迟加载(data-src)

渲染方面的优化:

  • 减少渲染次数:缓存(Vue的keep-alive缓存 )、对DOM的操作合并,尽量避免重流的发生。
  • 提前渲染:ssr服务端渲染
  • 避免无用的渲染:懒加载(路由懒加载 模块懒加载)。
  • 避免渲染阻塞:css放在HTML的头部,js放在HTML的body底部。(css不会阻塞dom的解析和dom树生成,会阻塞页面渲染;js会阻塞dom解析和页面渲染,不会阻塞资源下载;浏览器遇到script标签且无defer、async属性会立即下载执行中断HTML的解析)

图片优化

  • 字体图标代替图片图标
  • webpack缓存
  • 图片延迟加载(data-src)
  • 图片压缩(image-webpack-loader)

前端模块化

发展历史:AMD(浏览器)/CMD/commonjs/ES6模块化(通用方案)

  • AMD:浏览器端专用,异步加载。依赖前置,提前执行。AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。代表性的是Require.js
  • CMD:依赖就近,延迟执行。需要时再require相应模块。代表性的是Sea.js
  • Commonjs:主要用在Nodejs ,运行时加载,加载的是一个对象,只有在脚本运行完才能生成,使用requeire引入是同步加载模块
  • ES6模块化可以说是浏览器和服务器通用的模块解决方案。导入导出使用import export,使用ES6模块默认开启use srict

ES6模块与commonjs模块的不同:

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
  • CommonJS 模块的require()同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段

参考资料:

AMD、CMD用法示例有

https://juejin.cn/post/6844903576309858318

阮一峰-模块化

https://es6.ruanyifeng.com/#docs/module-loader

简单说下axios,为啥用axios,axios和fetch区别

  1. 什么是axios:
  • axios是一个基于Promise的网络请求库,可以用于浏览器和node.js,它提供了API来执行HTTP请求。
  1. 优点/为什么用axios:
  • 易用性:它的API直观,使用起来很方便。
  • 兼容性:能兼容大多数浏览器,包括 IE11 等旧版本浏览器。
  • 功能丰富:支持请求和响应的拦截,转换数据格式,自动转换JSON数据,错误处理,取消请求等。
  • 基于Promise:支持ES6的Promise,使得异步处理更加优雅。
  1. axios和fetch的区别:
  • 数据转换:axios 会自动将请求体对象和响应数据序列化为 JSON 格式;fetch需要手动调用 response.json() 方法来将响应数据解析为 JSON 格式,请求体也需要手动序列化。
  • 错误处理:axios在请求失败时会抛出一个错误,而fetch不会,fetch只有在网络故障时才会失败,其他如404或500错误都会解析为成功的响应。
  • 拦截处理:axios有强大的拦截器功能,可以拦截请求和响应,设置统一请求体,并统一处理响应数据,fetch则需要手动实现拦截逻辑。

Ajax/XHR对象创建的过程

创建XHR对象 let request = new XMLHttpRequest()

设置请求参数request.open(methoAd,服务器接口地址)

发送请求 request.send() 如果是post请求要带上参数data

监听请求成功后的状态变化

函数柯里化/实现add(1)(2)(3)

函数柯里化指的是将能够接收多个参数的函数转化为接收单一参数的函数,并且返回接收余下参数和结果的新函数的技术。

好处是让函数参数的处理更加自由,可以用作工具函数。

举例来说,一个接收3个参数的普通函数,在进行柯里化后, 柯里化版本的函数接收一个参数并返回接收下一个参数的函数, 该函数返回一个接收第三个参数的函数。 最后一个函数在接收第三个参数后, 将之前接收到的三个参数应用于原普通函数中,并返回最终结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//普通函数
function add (...args) {
//求和
return args.reduce((a, b) => a + b)
}

function currying (fn) {
let args = []
return function temp (...newArgs) {
if (newArgs.length) {
args = [
...args,
...newArgs
]
return temp
} else {
let val = fn.apply(this, args)
args = [] //保证再次调用时清空
return val
}
}
}

let addCurry = currying(add)
console.log(addCurry(1)(2)(3)(4, 5)()) //15
console.log(addCurry(1)(2)(3, 4, 5)()) //15
console.log(addCurry(1)(2, 3, 4, 5)()) //15