23年复习

170 阅读37分钟

CSS

flex弹性布局

flex属性是flex-growflex-shrink 和 flex-basis的简写,默认值为0 1 auto。后两个属性可选。
flex-grow属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。
flex-shrink属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。
flex-basis属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。
flex-flow属性是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap
align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性 详见:www.ruanyifeng.com/blog/2015/0…

transitionanimation

transition:是过渡。用于简单动画效果,显示和隐藏,鼠标悬停缩放效果等等。 animation:是动画。用于复杂的动画效果,可以定义关键帧,控制动画每一步。

JS

原型

  • prototype : js通过构造函数来创建对象,每个构造函数内部都会一个原型prototype属性,它指向另外一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。

  • proto: 当使用构造函数创建一个实例对象后,可以通过__proto__访问到prototype属性。

  • constructor:实例对象通过这个属性可以访问到构造函数。

原型链

每个实例对象都有一个__proto__属性指向它的构造函数的原型对象,而这个原型对象也会有自己的原型对象,一层一层向上,直到顶级原型对象null,这样就形成了一个原型链。

闭包

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

垃圾回收

内存泄露:不再使用但是无法回收的内存空间。

闭包为什么会造成内存泄露:函数没有销毁,里面的变量还可触达,不敢回收内存。

闭包有一个隐蔽泄漏点:无法触达,但是还没被回收。

闭包会不会造成内存泄露

两种情况下闭包会造成泄露

  • 错误的持有本该被销毁的函数引用,会导致函数关联的词法环境无法销毁,造成内存泄露。

  • 当多个函数共享词法环境时,可能造成词法环境膨胀,就可能出现无法触达且无法销毁的内存,造成内存泄露。

手写instanceof

实例对象的隐式原型 = 构造函数的显式原型

function myInstanceof(left, right) {
    // 获取left的原型
    let proto = Object.getPrototypeOf(left);
    // 获取right构造函数中的原型对象
    let prototype = right.prototype;
    // 判断right构造函数中的prototype是否在left的原型链中
    while(true) {
        if(!proto) return false;
        if(proto === prototype) return true;
        // 如果没找到,继续在原型对象里的原型找
        proto = Object.getPrototypeOf(proto);
    }
}

new

当我们使用 new 关键字创建一个函数的实例时,实例对象的 [[Prototype]] 属性会指向该函数的 prototype 属性的值。这样,实例对象就可以访问函数原型上定义的属性和方法。

function myNew(fn,...args) {
    // 创建一个对象,让这个对象的原型_proto_指向构造函数fn的原型prototype上
    let obj = Object.create(fn.prototype);
    // 执行构造函数,并将obj绑定到this上
    let res = fn.apply(obj,args);
    // 判断构造函数执行后是否为对象,是则返回,不是则返回创建的对象
    return res instanceOf Object ? res : obj;
}

简而言之,new会创建一个实例对象,这个实例对象可以访问函数原型上的属性和方法。

reduce原理

用于迭代数组中每个元素

它接收两个参数,一个callback回调函数,一个累积器初始值
callback中接收4个参数:

  1. 累积器:用于累计回调函数中的返回值。
  2. 当前数组元素值
  3. 当前数组元素索引
  4. 原数组

如果设定了累积器初始值,则初始索引从0开始,否则从1开始。 如果设定了累积器初始值,则累积器取初始值,否则取数组第一个元素arr[0]。

function myReduce( arr, callback, initVal) {
    // 判断是否提累积器供初始值
    let total = initVal ? initVal : arr[0];
    // 设定累积器初始值,索引从0开始,否则从1
    const starIndex = initVal ? 0 : 1;
    // 循环执行callback
    for(let i = starIndex; i < arr.length; i++) {
        total = callback(total,arr[i],i,arr)
    }
    return total;
}

箭头函数和普通函数区别

普通函数:

  • 在普通函数中,this 的值取决于函数是如何被调用的。
  • 如果是作为对象的方法调用,this 将指向调用该方法的对象。
  • 如果是作为普通函数调用,this 将指向全局对象(在浏览器环境中通常是 window)。
  • 普通函数有一个 arguments 对象,它包含了函数调用时传递的参数。
  • 普通函数可以用作构造函数,通过 new 关键字创建实例。

箭头函数:

  • 箭头函数没有自己的 this 绑定,它继承自外部作用域。这通常被称为“词法作用域”。
  • 箭头函数的 this 是在函数定义时确定的,而不是在函数调用时。
  • 箭头函数没有自己的 arguments 对象,但可以使用剩余参数(rest parameters)来达到类似的效果。
  • 箭头函数不能被用作构造函数,不能通过 new 创建实例。因为箭头函数没有 prototype 属性,无法像普通构造函数那样创建具有原型链的对象。箭头函数没有自己的this指针,通过 call() 或 apply() 方法调用一个函数时,只能传递参数(不能绑定this)

SetMap 的区别

Set用于数据重组,Map用于数据储存。两种方法具有极快的查找速度。

Set:

  • 成员不能重复(可以用来数组去重)
  • 只存键值,没有键名,类似数组。
  • 可以遍历,方法有add,delete,has,clear

Map:

  • Map不允许键重复。
  • 本质上是键值对的集合,类似对象。
  • 可以遍历,和各种数据格式转换。

MapObject的区别

mapObject都是用键值对来存储数据,区别如下:

  • 键的类型Map 的键可以是任意数据类型(包括对象、函数、NaN 等),而 Object 的键只能是字符串或者 Symbol 类型。
  • 键值对的顺序Map中的键值对是按照插入的顺序存储的,而对象中的键值对则没有顺序。
  • 键值对的遍例Map 的键值对可以使用 for...of 进行遍历,而 Object 的键值对需要手动遍历键值对。
  • 继承关系Map 没有继承关系,而 Object 是所有对象的基类。

MapweakMap的区别

Map:

  • 键可以是任何类型。
  • 键跟内存地址绑定,只要内存地址不同,就视为两个键
  • 可以遍历。

weakMap

  • 键只能是对象(null除外)
  • 键是弱引用,如果键不再有其他引用,垃圾回收机制可以自动回收键值对。
  • 不可以遍历。

mapforeach

  • map 是创建一个新数组,有返回值,可以return出去。常用于不希望改变原数组情况。
  • foreach 没有返回值。常用于希望改变原数组情况。
  • 二者是否更改原来的数组,取决于数据类型,基本数据类型都不会改变,引用数据类型都会改变

数组方法和返回值

  • push()pop(),都是对数组尾部进行操作,对应 添加 和 删除
  • shift()unshift(),都是对数组头部进行操作,对应 删除 和 添加
  • 删除的,pop()shift() 都是返回删除的元素
  • 添加的,push()unshift() 都是返回数组长度
  • splice(开始删除的索引,从索引算要删除的个数,要替换的元素,要替换...),返回删除的元素组成的数组
  • --------------------以下为不改变原数组的分割线-------------
  • slice(开始截取的索引,结束的索引但是不包含该索引元素),创建新数组不改变原数组,返回包含截取的元素新数组,是浅拷贝
  • splice 和 slice 若索引为负数,如-1,则索引按 -1+arr.length 计算
  • every()所有元素是否都通过,返回布尔值
  • some()有一个通过,就返回true
  • filter()创建新数组不改变原数组返回符合包含过滤条件的元素新数组
  • toString(),数组转字符串[1,2,3]->'1,2,3'
  • join(),数组转字符串[1,2,3]->'1,2,3'。join('')数组转字符串[1,2,3]->'123'

事件监听机制

  • 捕获阶段:事件从文档根节点向目标元素传播,即从外向内。
  • 目标阶段:事件到达目标元素时,触发目标阶段。
  • 冒泡阶段:事件从目标元素向文档根节点传播,即从内向外。

通常使用冒泡阶段处理事件。addEventListener('click',myFun,false),第三个参数默认false冒泡阶段执行,true是捕获阶段执行。

Babel 的原理是什么?

  1. 解析 Parse: 将代码解析⽣成抽象语法树(AST),即词法分析与语法分析的过程;
  2. 转换 Transform: 对于 AST 进⾏变换⼀系列的操作,babel 接受得到 AST 并通过 babel-traverse 对其进行遍历,在此过程中进行添加、更新及移除等操作;
  3. ⽣成 Generate: 将变换后的 AST 再转换为JS 代码, 使用到的模块是 babel-generator。

ES6 代码转成 ES5 代码的实现思路是什么

ES6 转 ES5 目前行业标配是用 Babel,转换的大致流程如下:

  1. 解析:解析代码字符串,生成 AST;
  2. 转换:按一定的规则转换、修改 AST;
  3. 生成:将修改后的 AST 转换成普通代码。

如果不用工具,纯人工的话,就是使用或自己写各种polyfill

打印出 1 - 10000 中的对称数 如11,121,565

// [...Array(10000).keys()]生成 从0-10000的数组 [0,1,2,...,10000]
[...Array(10000).keys()].filter((x) => { 
    return x.toString().length > 1 && x === Number(x.toString().split('').reverse().join('')) 
})

如何取消重复请求

原生ajax取消请求

通过调用XMLHttpRequest对象实例的abort方法把请求给取消掉

axios取消请求

通过axiosCancelToken对象实例cancel方法把请求给取消掉。通过axios.CancelToken.source产生cancelTokencancel方法。调用cancel方法取消

通过axios请求拦截器取消重复请求

  1. 通过axios请求拦截器,在每次请求前把请求信息和请求的取消方法放到一个map对象当中,并且判断map对象当中是否已经存在该请求信息的请求,如果存在取消上传请求
  2. 通过axios的响应拦截器,在请求成功后在map对象当中,删除该请求信息的数据
  3. 通过axios的响应拦截器,在请求失败后在map对象当中,删除该请求信息的数据

XHR 和 Fetch

XHRFetch
监控请求进度✅监控请求进度❌
监控响应进度✅监控响应进度✅
service worker中不可用❌service worker中可用✅
控制cookie携带❌控制cookie携带✅
控制重定向❌控制重定向✅
流❌流✅
eventpromise
请求可取消✅请求可取消✅

文件上传,如果需要监控上传进度,需要使用XHR

问:axios和fetch的区别

axios: 是一个基于 promise 封装的网络请求库,它是基于 XHR 进行二次封装。

  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

fetch:是一个浏览器内置的 API,它是真实存在的,它是基于 promise 的。

umi-request:也是一个请求库,基于fetch

优点:

  • 符合关注分离,没有将输入、输出和用事件来跟踪的状态混杂在一个对象里
  • 更好更方便的写法
  • 更加底层,提供的API丰富(request, response)
  • 脱离了XHR,是ES规范里新的实现方式

缺点:

  • fetch只对网络请求报错,对400,500都当做成功的请求,需要封装去处理
  • fetch默认不会带cookie,需要添加配置项
  • fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了量的浪费
  • fetch没有办法原生监测请求的进度,而XHR可以

总结

  • 发送数据时,fetch()使用body属性,而Axios使用data属性
  • fetch()中的数据是字符串化的,JSON.stringify()
  • URL作为参数传递给fetch()。但是在Axios中,URL是在options对象中设置的

浏览器

浏览器跨域

浏览器发送不同源的跨域请求,服务端会收到请求并返回响应数据浏览器会进行校验,如果不通过则会跨域报错

CORS:是一套规则,帮助浏览器判断校验是否通过。浏览器校验响应数据的规则就是CORS规则

  • 只要服务器明确表示允许,则校验通过
  • 服务器明确表示拒绝或没有表示,则校验不通过
  • 使用CORS解决跨域,必须保证服务器是“自己人”

对简单请求的验证:简单请求时,请求头中会带有Origin: xxx,通知服务器当前源是什么,询问是否允许通过,服务器响应头会带有Access-Control-Allow-Origin: xxx

对预检请求的验证:浏览器会先发送一个请求方法OPTIONS的给服务器,没有请求体

请求头中会带有

  • Origin: xxx当前页面源,
  • Access-Control-Request-Method: POST当前请求的方法是什么,
  • Access-Control-Request-Headers: venderId,Content-Type改动了哪些请求头,

把这三个告诉服务器,服务器自行判断是否允许。

服务器响应头中会带

  • Access-Control-Allow-Origin: xxx允许通过的源
  • Access-Control-Request-Method: POST允许通过的请求的方法,
  • Access-Control-Request-Headers: venderId,Content-Type允许改动的请求头,
  • Access-Control-Max-Age: 86400告诉浏览器可缓存时间,86400毫秒内资源都不会变,不需要再预检请求了。这条不算校验规则。

预检请求通过后,才会发送真实请求(和简单请求一样)。

简单请求条件(同时满足)

请求方法是GET/POST/HEAD之一。

请求头是浏览器默认自带的请求头,没有自己增加或删除。

如果有Content-Type,必须是text/plainmutipart/form-dataapplication/x-www-from-urlencoded之一。

不满足以上任何一个条件的都是预检请求

Tips:XHR和Fetch都默认请求头不携带cookie,需要自行设置。如果浏览器简单或预检请求头携带cookie,而服务器响应头未设置Access-Control-Allow-Credentials:true携带身份凭证是否允许,浏览器仍视为跨域。对于携带身份凭证的请求,服务器不可Access-Control-Allow-Origin设置为*

Tips:在跨域访问时,js只能拿到一些最基本的响应头,如:Cache-Control,Content-Language,Content-Type,Expries,Last-Modified,Pragma。如需要其他头,则服务端需要手动设置允许客户端拿到的响应头白名单。如Access-Control-Expose-Headers: authorization, a, b

JSONP

很久很久以前,没有CORS,通过<script src="xxx.jsonp">实现跨域,也要求服务器是‘自己人’。 但是JSONP只能发送GET请求。容易产生安全隐患,恶意攻击者可能篡改callback=恶意函数造成XSS攻击。除非特殊原因,否则永远不要用JSONP。

运行模式:JS会准备一个callback回调函数function callback(res){},服务器响应结果是一个函数调用callback({data:xx}),响应后会运行该函数,并传递数据。

nginx代理

当需要ajax请求不是自己的服务器时,可以使用代理服务器,就是在请求目标前加一个中间层,用自己的服务器作为代理,代理服务器去请求目标服务器,返回的数据再根据情况是使用CORS还是JSONP

image.png

浏览器缓存机制

缓存位置

  • Service Worker:运行在浏览器背后的独立线程,一般可以用来实现缓存功能。传输协议必须为 HTTPS。先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件。用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。
  • Memory Cache内存中的缓存。包含的是当前中页面中已经抓取到的资源,如页面上已经下载的样式、脚本、图片等。关闭 浏览器标签 页面,内存中的缓存也就被释放了。内存缓存中有一块重要的缓存资源是preloader相关指令(例如<link rel="prefetch">)下载的资源。众所周知preloader的相关指令已经是页面优化的常见手段之一,它可以一边解析js/css文件,一边网络请求下一个资源。
  • Disk Cache存储在硬盘中的缓存,读取速度慢点。绝大部分的缓存都来自 Disk Cache
  • Push Cache推送缓存是 HTTP/2 中的内容。当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。

缓存策略

浏览器先去查浏览器缓存,没有再去请求服务器。都是通过设置http-header来实现的

  • 强缓存:通过设置Expires过期时间,和Cache-Control控制缓存的方式,指定缓存最大存活时间等,返回200状态码。

  • 协商缓存:设置Last-Modified最近一次修改时间,和Etag资源生成的唯一标识符。浏览器再次请求资源将时间传给服务器,若资源未改变,返回304状态码。

从输入URL到渲染发生了什么

  1. URL地址解析:判断输入的是一个合法的URL还是一个待搜索的关键词,并且根据你输入的内容进行自动完成、字符编码等操作
  2. DNS解析域名DNS是域名和系统相互映射的一个分布式数据库,解析器和域名服务器组成,通过域名可以找到相对应的ip地址。因为ip地址很难记住,所以用户输入域名,而ip地址才是真正的表示网站的位置,所以要把域名转化为ip地址。域名解析就是要获取到域名对应的ip地址的过程。
    • 先找浏览器有没有DNS缓存(之前有访问记录),如果有则返回ip
    • 如果没有,则寻找本地的host文件,看看有没有域名记录,如果有则返回ip
    • 都没有,会向本地DNS服务器请求,如果还没有,继续向上级DNS服务器请求,直到根服务器
  3. 建立TCP连接:三次握手
    • 第一次握手:客户端发送 SYN 数据包给服务端
    • 第二次握手:服务端收到客户端的数据包,返回 SYN/ACK 数据包给客户端
    • 第三次握手:客户端收到服务端的返回后,发送 ACK 数据包给服务端
  4. 发起http请求:拿到返回数据后会开始对资源进行解析。
  5. 接下面的浏览器渲染原理

浏览器渲染原理

  • 解析html:生成DOM树和CSSOM树。浏览器在开始解析前,会启动一个预解析的线程,率先下载 HTML 中的外部 CSS 文件和 外部的 JS 文件。

  • 计算样式:让每一个DOM节点都得到最终样式。这一过程很多预设值会变成绝对值,比如red会变成rgb(255,0,0);相对单位会变成绝对单位,比如em会变成px得到带样式的DOM树。

  • 布局:计算出每一个DOM节点的几何信息,布局树和DOM树不一样,不是一一对应。比如display:none的节点没有几何信息,因此不会生成到布局树;

  • 分层:为了提高后续渲染效率,根据策略把页面分成图层,产生绘制指令。分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。

  • 绘制:通过GPU把每个层单独进行绘制,单独绘制后产生合成指令交给分块。完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。

  • 分块:得到很多小块。

  • 光栅化:对每个小块进行光栅化,优先光栅化靠近屏幕的小块。

  • :把光栅化后的小块画出来。

绘制后开始合成图层,最终渲染到页面上。
更改元素的几何属性会触发重排
更改元素的绘制属性变化会触发重绘

为什么 transform 的效率高?

因为 transform 既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个「draw」阶段

由于 draw 阶段在合成线程中所以 transform 的变化几乎不会影响渲染主线程。反之,渲染主线程无论如何忙碌,也不会影响 transform 的变化。

断开TCP链接为什么要四次挥手?

  1. 完成数据传输后客户端会发起断开链接的请求,向服务器发送FIN,进入等待阶段。
  2. 服务器收到后得知要断开链接,先返回ACK给客户端,告诉客户端我知道了。
  3. 服务器确认可以断开链接时,再发送一个FIN给客户端。
  4. 客户端收到后,发送ACK给服务器。

浏览器事件循环

事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。

在 Chrome 的源码中,它开启一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。

过去把消息队列简单分为宏队列和微队列,这种说法目前已无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。

根据 W3C 官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。

HTTPS协议中间人攻击是什么?

指攻击者通过与客户端和服务器同时建立连接作为客户端和服务器的桥梁,处理双方的数据,整个会话期间的内容几乎是完全被攻击者控制的。攻击者可以拦截双方的会话并且插入新的数据内容

中间人攻击的过程:

  1. 服务器向客户端发送公钥。
  2. 攻击者截获公钥,保留在自己手上,然后攻击者自己生成一个伪造的公钥,发给客户端。
  3. 客户端收到伪造的公钥后,假公钥加密随机码key,发给服务器。
  4. 攻击者获得加密的随机码key,用对应的假私钥解密获得随机码key。
  5. 同时生成假的加密随机码key,发给服务器。
  6. 服务器用私钥解密获得假随机码key。
  7. 服务器用假随机码key加密传输信息。

为了防止中间人攻击,可以采取以下措施:

  1. 使用可信任的证书:确保服务器使用的数字证书是由可信任的证书颁发机构签发的,以防止攻击者伪造证书。
  2. 强制使用HTTPS:通过强制使用HTTPS来保护通信内容,避免使用不安全的HTTP协议。
  3. 安全验证机制:使用双向认证,要求服务器验证客户端的身份,以确保通信双方的身份合法。
  4. 公钥固定:在客户端中预先存储服务器的公钥,以防止攻击者替换公钥。
  5. 定期更换证书:定期更换服务器的数字证书,以减少攻击者伪造证书的机会。

如何实现浏览器内多个标签页之间的通信?

  • localStorage

  • postMessage:如果能够获得对应标签页的引用,就可以使用 postMessage 方法,进行通信。

  • websocket:因为 websocket 协议可以实现服务器推送,所以服务器就可以用来当做这个中介者。标签页通过向服务器发送数据,然后由服务器向其他标签页推送转发。

  • ShareWorker:shareWorker 会在页面存在的生命周期内创建一个唯一的线程,并且开启多个页面也只会使用同一个线程。这个时候共享线程就可以充当中介者的角色。标签页间通过共享一个线程,然后通过这个共享的线程来实现数据的交换。

点击刷新按钮或者按 F5、按 Ctrl+F5 (强制刷新)、地址栏回车有什么区别?

  • 点击刷新按钮或者按 F5:浏览器直接对本地的缓存文件过期,但是会带上 If-Modifed-Since,If-None-Match,这就意味着服务器会对文件检查新鲜度,返回结果可能是 304,也有可能是200
  • 用户按 Ctrl+F5(强制刷新):浏览器不仅会对本地文件过期,而且不会带上 If-Modifed-Since,If-None-Match,相当于之前从来没有请求过,返回结果是 200
  • 地址栏回车: 浏览器发起请求,按照正常流程,本地检查是否过期,然后服务器检查新鲜度,最后返回内容。

XSSCSRF

  • XSS:跨站脚本注入。
  • CSRF:跨站请求伪造。攻击者冒充用户发起恶意请求。利用用户登录态,获取cookie,在不登出的情况下访问危险网站。

CommonJS、AMD、CMD、ESModules

CommonJS:node.js提出的标准

  • 每个文件都是一个模块
  • 每个模块都有单独的作用域
  • 约定使用module.exports导出,require导入
  • 以同步的模式加载模块,不适用浏览器端

AMD:异步模块规范,适合浏览器端 require.js

  • 约定使用 define(模块名字,数组:用来声明模块依赖项,函数:参数与前面依赖项一一对应) 函数来定义,函数有三个参数
define('module1', ['jquery', './module2'], function($, module2){
    return {}
})
  • require 函数来加载函数
  • AMD需要使用大量 define require函数代码,使用起来相对比较复杂
  • 模块划分比较细的话,在同一个JS文件下请求次数会比较多,页面效率低下
  • 只能是模块化路上的一个过渡

CMD:和AMD同期淘宝推出的标准 Sea.js

  • 类似CommonJS
  • 使用上和require.js差不多

ES Modules

特性
  • 自动采用严格模式,等同于‘use strict’
  • 每个模块都有独立的作用域
  • ESM是通过CORS(跨域)来请求外部JS模块的,请求的地址需要支持CORS
  • ESM模块会延迟执行脚本,等同于加了defer,所以会出现console.log执行顺序和引入顺序是不同的情况
导出和导入

import时需要填写完整路径,例如.js扩展名不可省略

动态导入模块

// 传入导入模块的路径,返回一个promise,会自动执行then当中的回调函数
import('./modules.js').then(function(module) {
    console.log(module)
})

import 和 require

  • import是ESM的语法。异步加载,不会阻塞后续代码执行。import 语句会在代码解析和编译阶段执行
  • require是CommonJs语法。同步加载的,会阻塞后续代码执行。模块的加载和执行都是在程序运行时发生。

Grunt、 Gulp 自动化构建

都是微内核。

  • Grunt:插件生态非常完善,可以自动化的完成任何想要做的事情。但工作过程是基于临时文件实现的,所以构建速度较慢。例如对Sass文件进行构建,先对文件进行编译操作,之后添加自有属性的前缀,再压缩代码。每一步都会有磁盘读写操作。每一步执行后写入临时文件,下一步再读取。处理环节越多,文件读写次数越多。大型项目文件多,构建自然很慢。
  • Gulp:遵循CommonJS模块规则。高效、易用,插件也很完善。工作过程基于node的流式操作和内存缓存实现。对文件的处理环节都是在内存中完成的,相对于磁盘读写自然快很多。默认支持同时执行多个任务,效率自然大大提高。使用方式相比于Grunt更加直观易懂

核心思想是“流”,三个核心概念:读取流,转换流,写入流*。希望实现一个构建管道的概念,这样的话在后续扩展插件有一个统一的方式。它通过将数据流传递到各种插件中来进行转换。 通过Gulp提供的src方法创建读取流,借助插件提供的转换流实现加工,再通过Gulp提供的dest方法创建写入流。

Rollup

遵循ESM规则。支持多种输出格式,包括CommonJS、AMD、UMD、ES6等。仅仅是一个ESM的打包器,充分利用ESM各种特性高效打包,比webpack小巧的多。内置tree-shaking优化,自动过滤掉未引用的代码,减小打包后文件的体积。Rollup 的目标是产生更小、更快、更高效的代码,因此在构建 js 库时非常有用。

Rollup并没有webpack中的HMR这种热更新等高级的功能,但是可以使用插件去扩展功能,插件是唯一的扩展方式

image.png

Vite

  • 更快的开发服务器冷启动:基于浏览器原生 ES 模块化,启动前使用Esbuild预构建。从而实现“按需编译、按需加载”的特性。可以快速响应页面请求,无需等待整个项目打包完成
  • 热更新:使用了浏览器原生的模块热更新技术,只请求和更新变化的模块,而不是整个页面。
  • 真正的按需编译:基于浏览器原生 ES 模块化,只编译当前文件及其依赖,而不是整个项目。这在大型项目中可以显著提高构建速度。
  • 内置Rollup,还支持各种模块规范,如 CommonJS、ESM、AMD
  • 支持 Rollup 的插件系统。可以扩展和定制 Vite 的功能。

vite和webpack的区别

  1. 构建原理
    webpack是通过入口文件递归依赖构建。
    vite是用浏览器原生支持的 ES Module(ESM)特性,以模块为单位进行开发。基于esbulid预构建依赖

  2. 打包速度vite大于webpack
    Webpack 的打包速度相对较慢,尤其在大型项目中,因为它需要对整个项目进行扫描和分析,而且还需要通过各种插件和加载器来实现各种功能,因此构建时间往往会比较长。

    Vite 的打包速度非常快,因为它不需要对整个项目进行扫描和分析,在浏览器中使用原生 ES 模块的方式加载文件,开发模式下,使用可以<script type='module'>加载模块,直接在浏览器中运行未经构建的源码,而无需提前进行打包。因此构建时间往往比 Webpack 快数倍。

  3. 热更新
    webpack:模块以及模块依赖的模块需重新编译
    vite:浏览器重新请求该模块即可,它只会重新加载变化的模块,而不是整个页面。

  4. webpack的生态优于vite: 插件多 适合大项目

webpack

  • 模块打包器,支持 CommonJS、AMD、ES6 等多种模块化规范。将零散的模块代码,打包到一个或多个js文件当中,bundle.js是webpack打包产物是一个立即执行函数。默认打包后默认放在网站根目录下,可以设publicPath网站根目录路径,如publicPath: 'dist/'
  • 提供加载器loader和插件plugin来处理各种资源文件(js,css,图片等)。
  • 代码拆分Code Splitting,将代码文件拆分成较小的文件,其中每个文件可能包含多个模块。这样做可以在初始加载时减少数据量,但仍然需要一次性加载所需的文件。
  • 开箱即用的方案:dev serverHMRsourceMap等。

webpack工作原理

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  3. 确定入口:webpack根据配置找到所有的入口文件
  4. 依赖分析:根据资源入口里出现的import或require语句来解析推断出来资源依赖的模块,分别递归解析每个模块对应的依赖,最后形成一个所有依赖关系的依赖树
  5. 完成模块编译webpack递归依赖树,找到每个节点对应的依赖文件,通过配置文件中的rules属性找到对应的加载器loader,将文件进行处理转换转成js。
  6. 生成 Chunk::将模块组装成一个或多个 Chunk,把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
  7. 生成bundle将生成的 Chunk 输出为静态资源文件也就是生成bundle。可以根据配置生成单个文件或多个文件。
  8. 生成bundle过程插件优化:生成 Bundle 的过程中会通过pulugin优化,包括代码压缩、去重、提取公共模块等。这些优化措施旨在减小文件体积、提高加载速度。
  9. 输出:将最终生成的 Bundle 输出到指定的目录中。输出的文件名和路径等信息也可以在配置文件中进行设置。从而实现整个项目的打包。

loader机制是webpack的核心

loader:模块加载器

对于打包过程中有环境兼容问题的,可以通过loader进行编译转换。
编译转化类:

  • css-loader:样式加载器,把css文件打包成js模块
  • style-loader:以 style 标签的形式把打包好的css追加到页面里

文件操作类:

  • file-loader:图片、字体等资源文件加载器,先把文件拷贝到输出目录,bundle.js将访问路径向外导出(输出目录的路径作为当前模块返回值返回)。
  • url-loader:可以将文件直接转化成Data URL形式。 不需要拷贝文件到输出目录,适合体积小的文件,否则打包结果会大。可设置limit,限制文件大小内的使用url-loader,超过则使用file-loader。

优化点:
- 小文件可以使用url-loader,转化成Data URLs,减少请求次数
- 大文件可以使用file-loader,单独提取存放,减少打包体积,提高加载速度

代码检查类:

  • eslint-loader:代码校验加载
  • babel-loader:用于处理es新特性的兼容,需要配置插件preset-env

image.png

Data URLs:用URL直接代表文件

data:text/html;charset=UTF-8,<h1>html content</h1>

image.png

loader工作原理:管道特性 处理资源加载

  • 资源文件输入到输出的一个转换
  • 管道特性:同一个资源依次使用多个loader

loader都需要去导出一个函数,函数就是对我们加载资源的一个处理过程。
输入:资源文件内容
过程:可以直接入其他loader,依次处理
输出:加工后的结果,必须是js代码。

// markdown-loader.js
const marked = require('marked');
module.exports = source => {
    const html = marked(source);
 // return `module.exports = ${JSON.stringify(html)}`
    return `export default ${JSON.stringify(html)}`
}

plugin:解决除了资源加载以外的自动化工作

  • clean-webpack-plugin:自动清除打包前的dist目录,即清除上一次打包结果。
  • copy-webpack-plugin:拷贝不需要打包的静态资源文件到dist目录。
  • terser-webpack-plugin:压缩打包输出的代码,减小体积。
  • html-webpack-plugin:自动生成打包结果bundle.js的html,放到dist目录下。确保路径引用正常。 html中bundler引用是注入进去的,不需要手动硬编码。
  • definePlugin(自有):为代码注入全局成员的,比如定义环境变量

image.png

plugin工作原理:钩子机制,往webpack生命周期中的钩子函数上挂载任务函数

plugin机制其实是钩子机制,为了便于webpackkuozhan,给每一个事件都埋下了一个钩子,开发插件时可以根据不同的节点,挂载不同任务,来扩展webpack的能力

  • 插件必须是一个函数,或者包含apply()方法的一个对象。一般把插件定义一个类,类里定义apply方法
  • 使用:通过定义的类,构建一个实例去使用。
// 清除bundle.js里没用的注释,会在webpack启动时自动调用
// compiler:webpack核心对象,包含此次构建的配置信息,通过这个对象去注册钩子函数
// emit钩子:即将往输出目录输出文件时执行。
class MyPlugin {
    apply(compiler) {
        console.log('MyPlugin 启动');
        // 通过compiler里的hooks属性,来访问emit钩子,通过tap()方法注册钩子函数
        // tap接收两个参数,tap(插件名称,挂载到钩子上的函数)
        compiler.hooks.emit.tap('MyPlugin', compilation => {
            // compilation:可以理解为此次打包的上下文,打包的结果都会放到这个对象中
            // assets:获取即将写入目录的资源文件信息
            for(const name in compilation.assets) {
                if(name.endsWith('.js')) {
                    const contents = compilation.assets[name].source();
                    const withoutComments = contents.repalce(/\/\*\*+\*\//g, '');
                    compilation.assets[name] = {
                        source: ()=>withoutComments,
                        size: ()=>withoutComments.length
                    }
                }
            }
        })
    }
}

webpack dev server: 集成了自动编译,和自动刷新浏览器,将打包结果暂时存放在内存中,没有写入dist,减少磁盘读写次数,提升构建效率

webpack自动编译打包:watch工作模式
页面自动刷新:Browsersync插件自动监听编译,自动刷新浏览器

  • 自动编译打包
  • 自动刷新浏览器:问题,导致页面 状态丢失
  • 支持配置代理服务
  • 页面不刷新,模块也能更新,使用HMR 模块热更新

source-map:便于调试代码,有12种模式

  • 开发模式建议选择:cheap-module-eval-source-map
  • 生产模式建议选择:不选择source-map,会暴露源代码,其次可以选择nosource-source-map

HMR(Hot Module Replacement)模块热更新,已集成在web dev server中

运行webpack-dev-serve --hot 需要手动处理模块热更新的逻辑,不能开箱即用。
HMR可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。 HMR的核心就是客户端从服务端拉取更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 web dev server 与客户端之间维护了一个 Websocket,当本地资源发生变化时WDS 会向客户端推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新

webpack生产环境下优化

  • definePlugin(自有):为代码注入全局成员的,比如定义环境变量,定义base url
  • tree-shaking:去掉未引用的代码,生产模式下自动开启

tree-shaking

在 Webpack 中启用 Tree shaking 需要满足以下条件:

  1. 使用 ESM 模块(即使用 importexport)。
  2. 配置 Webpack 时,确保在 mode 选项中设置为 'production',以开启相关优化。
  3. package.json 文件中,确保 "sideEffects" 字段设置好有副作用的文件,不会被shaking(没有副作用:一个函数会、或者可能会对函数外部变量产生影响的行为。)

一些代码或模块可能会有副作用,例如修改全局变量、产生网络请求等。在这种情况下,确保正确配置 "sideEffects" 字段,以避免误删有副作用的代码。

// 所有文件都有副作用,全都不可 tree-shaking
{
 "sideEffects": true
}
 
// 没有文件有副作用,全都可以 tree-shaking,即告知 webpack,它可以安全地删除未用到的 export。
{
 "sideEffects": false
}
 
// 除了数组中包含的文件外有副作用,所有其他文件都可以 tree-shaking,但会保留符合数组中条件的文件
{
 "sideEffects": [
   "*.css",
   "*.less"
 ]

不是配置项,是一组功能搭配使用后的效果

  • usedExports:只导出外部使用的成员,负责标记「枯树叶」。
  • concatenateModules:合并输出模块函数(也叫Scope Hoisting作用域提升),尽可能将所有模块合并到一个函数中,既提升了输出效率,又减小了代码体积。
  • minimize:开启代码压缩功能,负责把枯树叶「摇」下来。
// 集中配置webpack优化功能
optimization: {
    usedExports: true, // 只导出外部使用的成员,   负责标记「枯树叶」
    concatenateModules: true, // 合并输出模块函数 Scope Hoisting,尽可能将
    minimize: true, // 开启代码压缩功能,         负责把枯树叶「摇」下来
}

tree-shaking&babel-loader不生效问题:

最新版本的babel-loader并不会导致tree-shaking失效, 失效原因是以下:
tree-shaking的前提是使用ES Modules,然而webpack在打包前会根据配置把不同的资源模块交给loader处理,最后再将所有loader处理后的结果,打包到一起。那么为了转化ES新特性,很多时候会选择引入babel-loader,老版本babel-loader会将ES Modules转化成Common JS,可以通过配置属性module:false不开启ESM转化的插件来解决

image.png

code splitting:代码拆分

可以按需打包, 不用担心所有文件打包到一起,包体积会大的问题;
先打包应用初次运行时必须加载的模块,然后其他模块会单独存放,等到应用实际需要这个模块时,再异步加载;
从而实现增量加载或叫渐进式加载;
不用担心文件太碎,或文件太大,这两个极端的问题。

  • 多入口打包:适用多页应用程序,一个页面对应一个打包入口。
  • 动态导入:单页应用,模块会被自动分包

webpack提升构建速度?

构建速度和打包体积是有关系的,体积小了,打包速度也快了。

  • 升级webpack版本
  • 配置持久化缓存,cache-loader,减少重复构建
  • tree-shaking,去掉未引用的代码
  • 合理的使用loader、plugin
  • 使用includeexclude选项来排除不必要的目录,只对需要处理的目录使用对应的Loader
  • 开启多线程打包,thread-loaderhappyPack,加速编译
  • 代码分割,code spliting,初次运行只打包必须加载的模块,其他模块按需加载
  • 将一些不经常变动的库使用DllPlugin,资源打到一起,并通过DLLReferencePlugin进行预编译
  • mode 分开发环境 和 生产环境两套不同的配置,生产环境可以不开启source-map

webpack减小打包体积?

  • tree-shaking,去掉未引用的代码
  • 开启代码压缩 minimize,使用 terser-webpack-plugin
  • 减小模块体积,图片提前压缩
  • 分割代码,只加载用户需要的部分,splitChunks
  • 使用 CDN 引入三方库
  • 减小 polyfill 的体积,core-js 的按需引入特性。

解释一下Webpack的文件指纹(file fingerprint)和缓存(caching)机制

  • 文件指纹:是在构建生成静态资源过程中为文件生成唯一的标识符。在 Webpack 中,文件指纹通常用于生成输出文件的名称,在输出文件名中引入文件指纹,可以有效解决浏览器缓存问题,确保用户在访问网站时能够获取到最新的文件版本。
  • 缓存:缓存机制是指浏览器在加载页面时,会将静态资源(如 JS、CSS、图片等)保存在本地,以便下次加载相同资源时可以直接使用缓存副本,从而提高网页加载速度。缓存机制分为强缓存和协商缓存两种方式。

Webpack的Resolve模块解析是什么?请解释resolve.modules、resolve.alias和resolve.extensions的作用。

Webpack的Resolve模块解析是用于解析模块路径的配置选项。它可以帮助Webpack正确地确定模块的位置。

  • resolve.modules用于指定模块的搜索路径。当Webpack在解析导入语句时,它会按照指定的顺序依次查找这些路径来确定模块的位置。默认情况下,Webpack会在当前工作目录和node_modules文件夹中查找。
  • resolve.alias用于创建模块的路径别名。通过配置别名,可以让Webpack在导入模块时使用更简短的路径。这对于减少代码中的冗余路径非常有用。
  • resolve.extensions用于指定可以省略的文件扩展名。当导入模块时没有指定文件扩展名时,Webpack会按照指定的顺序依次尝试添加扩展名来解析模块。这样可以让我们在导入模块时省略掉繁琐的扩展名,提高开发效率。

防抖&节流

防抖

只执行最后一次。操作时不执行,最后一次操作结束后等待n秒后,再执行。

  • 输入框输入实时校验:连续的输入事件的最后⼀次执行。
  • 表单提交:只执行最后一次。
  • 窗口大小变化:为了减少重排重绘
function debounce(fn, delay) {
    let timer;
    return function() {
        // 但凡有操作进来,都清楚之前的,重新定时
        if(timer) clearTimeout(timer);
        timer = setTimeout(()=>{
            fn.apply(this, arguments);
        },delay)
    }
}

节流

有规律的执行。到时间必须执行一次。如:

  • 滚动加载
  • 拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动
  • 缩放场景:监控浏览器 resize
  • 动画场景:避免短时间内多次触发动画引起性能问题
function throttle(fn, wait) {
    let time;
    return function {
        if(time) return;
        time = setTimeout(()=>{
            fn.apply(this, arguments);
            time = null;
        }, wait)
    }
}
或
function throttle(fn, wait) {
    let old = 0;
    return function() {
        let new = new Date().valueOf();
        if(new - old >= wait) {
            fn.apply(this, arguments);
            old = new;
        }
    }
}

deferasync

都是立即下载脚本,执行时机和顺序有差异,同一个script上同时有defer 和 async,会按async执行

  • defer:立即下载脚本,等HTML解析完成后,再按引入顺序执行。依赖文档结构可使用
  • async:立即下载脚本,下载完成后立即执行(可能中断HTML解析),先后顺序不一定。不依赖文档结构可使用

for ... infor ... of

  • for in:遍历key,查找原型链,用于普通对象
  • for of:遍历value,不查找原型链,用于数组,类数组,字符串,Set,Map

apply()call()bind()

都是用来改变函数执行上下文,改变 this 指向 apply 和 call 都会立即执行,bind返回一个函数,需要手动调用

哪些情况会导致内存泄露

  • 引用却未使用的对象
  • 闭包
  • 全局对象
  • 循环引用
  • 定时器和事件监听器

数字和字符串相互转换

TS

typeinterface

interface

  • 主要用来定义对象结构,包含属性、方法、继承等。
  • 可以多次声明同名接口,会自动合并同名接口
  • 可以继承,使用 extends
  • 可以间接使用 in 关键字遍历对象的键(KeyOf 操作)

type

  • 主要用来定义其他类型,基本类型、联合类型|、交叉类型&、交集类型,差集类型。
  • 不可多次声明,同名type会报错
  • 不可继承,但可以通过交叉类型 & 实现类似继承效果
  • 可以直接使用 in 关键字遍历对象的键(KeyOf 操作)

any unknow never 区别

any 类型表示任何类型,即 TypeScript 编译器对该值不进行类型检查,慎用,类型安全问题。
unknown 类型表示未知类型,需要显式的类型检查或类型断言来使用。

let value: unknown = 42; 
// 类型检查或类型断言 
if (typeof value === 'number') { 
    let result: number = value + 10; 
} 
// 类型断言 
let result: number = (value as number) + 10;

never 类型表示永远不存在的类型。用于标识不会正常返回值的函数和不可到达的代码块。如函数抛出异常、死循环等情况,这些函数不会正常返回值。

function throwError(message: string): never { 
    throw new Error(message); 
}

设计模式

单例模式:

保证一个类只有一个实例,并提供一个访问它的全局访问点。 如:模态框组件,Vuex全局的 Store

工厂模式:

用来创建对象,根据不同的参数返回不同的对象实例。 根据抽象程度的不同可以分为:简单工厂、工厂方法、抽象工厂。 如:工厂里有很多种颜色的鞋子,只需要告知要那种颜色鞋子就可以输出,不需要关注输出过程。

装饰器模式:

在不改变对象原型的基础上,对其进行包装扩展。 如:vue中的计算属性computed,还有高阶组件。

策略模式:

定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。

观察者/发布订阅模式:

定义了对象间一种一对多关系,当目标对象状态发生改变时,所有依赖它的对象都会得到通知。 如:Vue2响应式源码,EventBus。

React

Redux

redux 的三个原则:单一数据源、状态是只读的、使用纯函数来执行状态更改。

  • store:Redux 应用只有一个单一的 store,管理的state的容器,维持应用的 state
  • action:是一个普通的js对象,强制使用action更新,清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的。用来描述状态更改
  • reducer:是一个纯函数,将传入的stateaction结合起来生成一个新的state用来更新状态
  • middleware:允许在actionreducer 之间添加额外的逻辑,用于处理异步操作和副作用

React 中 setState 什么时候是同步的,什么时候是异步的?

  1. React 控制的事件处理程序,以及生命周期函数调用setState 是异步更新 state
  2. React 控制之外的事件中调用 setState 是同步更新的。比如原生js 绑定的事件setTimeout/setInterval 等。

即时通讯的方法

  • websocket
  • setInterval:定时任务询问
  • http2持续连接

跨标签页通信

  • indexDB
  • localstorage

性能优化(输入URL到渲染的每一步都可以进行优化)

业务通用优化手段

  • vue3中可以使用defineAsyncComponent工具来异步加载组件。
  • 优化老旧代码,将callback()改成async/await
  • 通过控制台去看接口响应时间,尽量控制在300ms以内,若无法缩短,考虑是否可以使用异步请求。
  • 手动替换体积大的三方库,如moment.js 替换成 day.js。
  • 对图片进行压缩,选择适合的格式,使用懒加载。
  • 静态文件采用CDN。使用DNS预解析CDN域名<link rel='dns-prefetc' href='cdn.com'
  • 合理使用防抖和节流,避免重排重绘。
  • 页面销毁前清除定时器,防止内存泄露。
  • 使用performance工具来查看性能和内存。
  • 使用HTTP缓存,如强缓存、协商缓存

小程序优化

  • 合理的分包subpackages,可以根据用户行为预加载分包,提前加载首屏资源,提高页面加载速度。
  • 不要频繁调用微信内置api,比如getSystemInfo,首页瀑布流中每个商品组件都调用了一次getSystemInfo,影响性能。
  • 启用初始渲染缓存,让逻辑层和视图层并行运行,直接把视图层的data初始值展示出来,可以预先展示一个骨架屏,可以展示固定不变的模块。

构建工具方面

  • 抽取公共包:使用DLLPlugin,将三方库(day.js等)和不会改变的资源打到一起,并通过DLLReferencePlugin进行预编译;
  • code-spliting:代码分割,配置 splitChunks将代码分块,只加载必要部分
  • webpack5 模块联邦,可抽取公共代码,进行共享,使业务代码打包体积减小,加载更快。