前端面试题

浏览器

浏览器工作原理
  1. 用户界面
  2. 网络
  3. UI后端
  4. 数据存储
  5. 浏览器引擎
  6. 渲染引擎
  7. js解释器
主流浏览器
主流浏览器:IE,Firefox,Safari,Google Chrome,Opera
四大内核:Trident,Gecko,webkit,Blink
介绍一下你对浏览器内核的理解
主要分为两部分:渲染引擎,JS引擎
渲染引擎:

负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入CSS等),以及计算网页的显示方式,然后会输出至显示器或打印机。浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要内核

JS引擎:

解析和执行javascript来实现网页的动态效果。最开始渲染引擎和JS引擎并没有区分的很明确,后来JS引擎越来越独立,内核就倾向于只指渲染引擎

计算机网络

OSI七层网络参考模型及TCP/IP模型

OSI 七层从下往上依次是:物理层数据链路层网络层传输层会话层表示层应用层

TCP/IP模型是参考OSI的七层网络模型,可以认为是OSI的简化版,已经成为事实国际标准

图片

一个页面从输入URL到页面加载显示完成,这个过程中都发生了什么?
  1. DNS解析
  2. 发起TCP连接
  3. 发送HTTP请求
  4. 服务器处理请求并返回HTTP报文
  5. 浏览器解析渲染页面
  6. 连接结束
DNS解析过程

浏览器缓存 -> 系统缓存(Hosts文件) -> 路由器缓存 -> ISP DNS 缓存 -> 根域名服务器

DNS负载均衡

访问网站的时候,每次响应的并非是同一个服务器(IP地址不同),一般大公司都有成百上千台服务器来支撑访问,DNS可以返回一个合适地机器的IP给用户,例如根据每台机器的负载量,该机器用户地理位置的距离等等,这个过程就是DNS负载均衡。

TCP连接

TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议

UDP(User Datagram Protocol 用户数据报协议)是 OSI(Open System Interconnection,开放式 系统互联) 参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务

UDP和TCP的区别

图片

Summary
  • TCP 向上层提供面向连接的可靠服务 ,UDP 向上层提供无连接不可靠服务
  • UDP 没有 TCP 传输可靠,但是可以在实时性要求高的地方有所作为
  • 对数据准确性要求高,速度可以相对较慢的,可以选用TCP
三次握手

图片

TCP连接是传输层的一个面向连接的,安全可靠的传输协议,三次握手机制是为了保证能建立一个安全、可靠的连接。

第一次握手:

客户端 —> 服务器

发送一个报文,报文中syn位置1,客户端接收到就知道客户端要向我发起连接

第二次握手:

服务器 —> 客户端

发送一个确认消息包,在这个消息包里面ACK位置1;经过以上两次握手后,对于客户端来说,已经知道了客户端既可以接收到服务器发送的请求,又可以向服务器发送消息。但此时的服务器还不知道客户端能否接收到自己的消息,因此有了第三次握手。

第三次握手:

客户端 —> 服务器

客户端向服务器发送一个ACK位置1的确认消息包

通过以上3次连接,客户端和服务端都彼此知道了,双方既可以收到对方的消息又可以向对方发送消息

四次挥手

图片

第一次挥手:

客户端 —> 服务端

客户端向服务端发送一个报文,报文的FIN位置1,表示想要与服务端断开连接

第二次挥手:

服务端 —> 客户端

服务端接收到FIN位置1的报文时,就知道了客户端想要断开连接;但此时服务端不一定可以做好断的准备,因此发送确认数据包,ACK位置1,表明客户端已经接收到客户端想要断开连接的消息

第三次挥手:

服务端 —> 客户端

当服务端准备好断开连接时,会向客户端发送一个FIN位置1的确认数据包,表明此时服务端可以与客户端断开连接

第四次挥手:

客户端 —> 服务端

客户端向服务端发送ACK位置1的消息确认包,表明已经收到了服务端的断开请求

经过这4次的挥手,不管是服务端和客户端都已经做好了断开连接的准备,此时就可以断开连接

HTTP请求
HTTP请求过程:
  1. DNS解析
  2. 建立TCP连接
  3. 发送HTTP请求
  4. 服务器响应HTTP请求
  5. 解析HTML代码并请求HTML代码中的资源(如js,css,图片等)
  6. 浏览器对页面进行渲染呈现给用户
HTTP请求原理:

HTTP协议是应用层的一种协议,是一种C/S架构服务,基于TCP/IP协议来通信,监听在TCP的80端口上,HTTP协议实现的是客户端可以向服务器获取web资源

HTTP 1.1 和 HTTP 2.0
http 1.1

持久连接

请求管道化

增加缓存处理(新的字段如cache-control)

增加Host字段、支持断点传输等在

http 2.0

二进制分帧

多路复用

头部压缩

服务器推送

HTTP和HTTPS的区别
  1. HTTPS 协议需要到 CA 申请证书,一般免费证书较少,因而需要一定费用
  2. HTTP 是超文本传输协议,信息是明文传输,HTTPS 则是具有安全性的 SSL 加密传输协议
  3. HTTP 和 HTTPS 使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443
  4. HTTP 的连接很简单,是无状态的;HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全
HTTPS的加密过程

img

常见的状态码有哪些?

1xx:请求处理中,请求已被接受,正在处理 (在HTTP升级为WebSocket的时候,如果服务器同意变更,就会发送状态码 101)

2xx:请求成功,请求被成功处理

3xx:重定向,完成请求必须进一步处理(301永久重定向 302临时重定向 304当协商缓存命中时会返回这个状态码)

4xx:客户端错误,请求不合法 (403禁止 404找不到 405请求方法不被服务器允许 )

5xx:服务器端错误,服务不可用等 (501客户端请求的功能还不支持 502服务器自身是正常的,访问出错错误未知 503服务器当前很忙 暂时无法响应服务)

HTTP缓存

浏览器 -> 网站

需要加载资源html, css , js , img…

第一次之后,利用缓存策略来进行资源的缓存 --> 加快网页打开速度

HTTP协商缓存

协商缓存是一种服务端缓存策略

服务器返回资源和资源标识,将资源保存在本地缓存;当客户端再次和服务器进行通信时,会将资源标识也一同传给服务器,服务器会进行判断浏览器返回的资源标识和服务器想要发送的资源标识是否相同,若相同则浏览器直接从本地缓存中找资源

资源标识

Last-Modified:资源上次修改的时间

Etag:资源对应的唯一字符串

HTTP强制缓存

客户端和服务器进行通信时,服务器觉得当前返回的数据是应该被缓存的,就会在响应头headers里面,增加一个Cache-Control,通过设置max-age来设置缓存资源的时间;max-age(s)没有过期,直接从缓存里拿对应的资源。

Session、Cookie的区别

session在服务器端,cookie在客户端(浏览器)

session默认被存储在服务器的一个文件里(不是内存)

session的运行依赖session id,而session id是存在cookie中的,也就是说,如果浏览器禁用了cookie,同时session也会失效(但是可以通过其他方式实现,比如在url中传递session_id

用户验证这种场合一般会用session

Web性能优化技术
  • DNS查询优化
  • 客户端缓存
  • 优化TCP连接
  • 避免重定向
  • 网络边缘的缓存
  • 条件缓存
  • 压缩和代码极简化
  • 图片优化

img

什么是XSS攻击?
XSS概念

XSS即Cross Site Scripting 中文名称为:跨站脚本攻击。XSS的重点不在于跨站点,而在于脚本的执行。

XSS的原理:

恶意攻击者在web页面中会插入一些恶意的script代码。当用户浏览该页面的时候,那么嵌入到web页面中的script代码会执行,因此会达到恶意攻击用户的目的。

XSS分类:
存储型

存储型 XSS 的攻击步骤:

  1. 攻击者将恶意代码提交到目标网站的数据库中。
  2. 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
  3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
反射型

反射型 XSS 的恶意代码存在 URL 里,被浏览器解析后执行,调用目标网站接口执行攻击者指定的操作

DOM型

DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞

XSS攻击的预防

httpOnly: 在 cookie 中设置 HttpOnly 属性后,js脚本将无法读取到 cookie 信息。

输入过滤: 一般是用于对于输入格式的检查,例如:邮箱,电话号码,用户名,密码……等,按照规定的格式输入。不仅仅是前端负责,后端也要做相同的过滤检查。因为攻击者完全可以绕过正常的输入流程,直接利用相关接口向服务器发送设置。

转义 HTML: 如果拼接 HTML 是必要的,就需要对于引号,尖括号,斜杠进行转义,但这还不是很完善.想对 HTML 模板各处插入点进行充分的转义,就需要采用合适的转义库

白名单: 对于显示富文本来说,不能通过上面的办法来转义所有字符,因为这样会把需要的格式也过滤掉。这种情况通常采用白名单过滤的办法,当然也可以通过黑名单过滤,但是考虑到需要过滤的标签和标签属性实在太多,更加推荐使用白名单的方式

CSRF攻击

跨站请求伪造(Cross-site request forgery),是一种挟制用户在当前已登录的web应用程序上执行非本意的操作的攻击方式。跟跨网站脚本相比,XSS利用的是用户对指定网站的信任,CSRF利用的是网站对用户网页浏览器的信任。

跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并进行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份认证的一个漏洞:简单的身份验证只能保证请求是发自某个用户的浏览器,却不能保证请求本身是用户资源发出的。

简而言之:网站过分相信用户

CSRF攻击的预防

验证码:强制用户必须与应用进行交互,才能完成最终请求。此种方式能很好的遏制 csrf,但是用户体验比较差。

Referer check:请求来源限制,此种方法成本最低,但是并不能保证 100% 有效,因为服务器并不是什么时候都能取到 Referer,而且低版本的浏览器存在伪造 Referer 的风险。

token:token 验证的 CSRF 防御机制是公认最合适的方案(具体可以查看本系列前端鉴权中对token有详细描述)若网站同时存在 XSS 漏洞的时候,这个方法也是空谈

CSS

BFS
什么是BFC

BFC(Block Formatting Context) 格式化上下文,是web页面中盒模型布局的css渲染模式,指一个独立的渲染区域或者说是一个隔离的独立容器。

形成BFC的条件
  1. 浮动元素,float除none以外的值
  2. 定位元素,position (absolute, fixed)
  3. display为以下的其中之一: inline-block, table-cell, table-caption, flex
  4. overflow为以下的其中之一:hidden,scroll,auto
BFC的特性
  1. 内部的Box会在垂直方向上一个接一个的放置
  2. 垂直方向上的距离由margin决定
  3. bfc的区域不会与float的元素区域重叠
  4. 计算bfc的高度时,浮动元素也参与计算
  5. bfc就是页面上的一个独立容器,容器里面的子元素不会影响外面
flex布局

flex属性值可以为1个,2个,3个以及关键字属性

一个值

如果flex的属性值只有一个,则如果是数值例如flex:1,则这个1表示flex-grow,此时flex-shrink和flex-basis都使用默认值,分别是1和auto。如果是长度值,例如flex:100px,则这个100px显然指flex-basis,因为3个缩写css属性中只有flex-basis的属性值是长度值。此时,flex-grow和flex-shrink都使用默认值,分别是0和1

两个值

如果flex的属性值有两个值,则第1个值一定指flex-grow,第2个值根据值的类型不同表示不同的CSS属性,具体规则如下: 如果第2个值是数值,例如flex: 1 2,则这个2表示flex-shrink,此时flex-basis使用默认值auto。 如果第2个值是长度值,例如flex: 1 100px,则这个100px指flex-basis,此时flex-shrink都使用默认值0

三个值

如果flex的属性值有三个值,则这3个值分别表示flex-growflex-shrinkflex-basisgrow是放大,shrink是收缩,而basis是基准

display : none / visibility : hidden / opacity : 0 的区别
display : none
  1. DOM结构:浏览器不会渲染display属性为none的元素,不占据空间;
  2. 事件监听:无法进行事件监听
  3. 性能:动态改变此属性时会引起重排,性能较差
  4. 继承:不会被子元素继承,毕竟子类也不会被渲染
  5. transition:transition不支持display
visibility : hidden
  1. DOM结构:元素被隐藏,但是会渲染不会消失,占据空间;
  2. 事件监听:无法进行DOM事件监听
  3. 性能:动态改变此属性时会引起重绘,性能较高
  4. 继承:会被子元素继承,子元素可以通过设置visibility : visible来取消隐藏
  5. transition:visibility会立即显示,隐藏时会延时
opacity : 0
  1. DOM结构:透明度为100%,元素隐藏,占据空间
  2. 事件监听:可以进行DOM事件监听
  3. 性能:提升为合成层,不会触发重绘,性能较高
  4. 继承:会被子元素继承且子元素不能通过opacity : 1来取消隐藏;
  5. transition: opacity 可以延时显示和隐藏
如何使一个盒子水平垂直居中
利用定位(常用方法,推荐)
利用 margin:auto
利用 display : flex;设置垂直水平都居中
利用 transform
如何实现圣杯(双飞翼)布局
如何垂直居中一个img标签
#container {
    display : table-cell;
    text-align: center;
    vertical-align : center;
}
用CSS做一个三角形
.triangle{
    width: 0;
    height: 0;
    border: 30px solid transparent;
    border-top-color: #ccc
}
CSS单位中px,em,rem和vh的区别
px

px像素(Pixel)相对长度单位,像素px是相对于显示器屏幕分辨率而言的

rem

rem是全部的长度都相对于根元素<html>元素。通常做法是给html元素设置一个字体大小,然后其他元素的长度单位就为rem

em

元素用em的话是相对于该元素的font-size

vw/vh

全称是 Viewport Width 和 Viewport Height,视窗的宽度和高度,相当于屏幕宽度和高度的 1%,不过,处理宽度的时候%单位更合适,处理高度的 话 vh 单位更好

移动端适配方案
1. rem适配方案

1rem长度等于html标签的font-size长度

flexible的实现

function setRemUnit(){
    var rem = docEl.clientWidth / 10
    docEl.style.fontSize = rem + 'px'
}
setRemUnit()
2. vw, vh 方案

vh、vw方案即将视觉视口宽度 window.innerWidth和视觉视口高度 window.innerHeight 等分为 100 份; 不过在工程化的今天,webpack解析css 的时候用postcss-loader 有个postcss-px-to-viewport能自动实现px到vw的转化

3. px为主,vx和vxxx(vw/vh/vmax/vmin)为辅,搭配一些flex(推荐)

之所以推荐使用此种方案,是由于我们要去考虑用户的需求,用户之所以去买大屏手机,不是为了看到更大的字,而是为了看到更多的内容,这样直接使用px是最明智的方案,使用vw,rem等布局手段无可厚非,但是,flex这种弹性布局大行其道的今天,如果如果还用这种传统的思维去想问题显然是有两个原因(个人认为px是最好的,可能有大佬,能用vw,或者rem写出精妙的布局,也说不准):

1、为了偷懒,不愿意去做每个手机的适

2、不愿意去学习新的布局方式,让flex等先进的布局和你擦肩而过

JavaScript

数据类型
基本数据类型:Number,String,Boolean,undefined,null, symbol
引用数据类型:Array,function,Object, Date,RegExp
判断基本数据类型 ===> typeof

typeof无法用来判断null

typeof 'seymoe'    // 'string'
typeof true        // 'boolean'
typeof 10          // 'number'
typeof Symbol()    // 'symbol'
typeof null        // 'object' 无法判定是否为 null
typeof undefined   // 'undefined'

typeof {}           // 'object'
typeof []           // 'object'
typeof(() => {})    // 'function'

instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上

console.log([] instanceof Array);  // true
console.log({} instanceof Object);   // true
判断引用数据类型 ===>Object.prototype.toString.call()
手写函数判断数据类型
function typeOf(object) {
    return Object.prototype.toString.call(object).slice(8, -1);
}

// console.log(Object.prototype.toString.call([]))
// console.log(Object.prototype.toString.call({}))
// console.log(Object.prototype.toString.call(99))

console.log(typeOf([]))   // Array
console.log(typeOf({}))    // Object
console.log(typeOf(99))  // Number
如何判断null
  1. ===
  2. Object.prototype.toString.call()
let re = null 
console.log(re === null);   //true
console.log(Object.prototype.toString.call(null));  // object Null
判断数组的方法

instanceof

instanceof 主要是用来判断某个实例是否属于某个对象

arr instanceof Array

Object.prototype.toString.call()

Object.prototype.toString.call(arr) // [object Array]

Array.isArray()

Array.isArray(arr) // true

constructor

arr.constructor  //Array
如何在JavaScript中比较两个对象
  1. 利用JSON.stringify将对象转化成字符串进行===比较
let obj1 = {
    name: "smy",
    age: 23,
    team:"HII"
}

let obj2 = {
    name: "smy",
    age: 23,
    team:"HII"
}

function isObjEqual(obj1,obj2) {
    return JSON.stringify(obj1) === JSON.stringify(obj2)
}

console.log(isObjEqual(obj1, obj2)) //true
  1. 逐个进行对象比较
function diff(obj1,obj2) {
    let ty1 = obj1 instanceof Object;
    let ty2 = obj2 instanceof Object;
    if (!ty1 || !ty2) {
        return false;
    }
    if (Object.keys(obj1).length !== Object.keys(obj2).length) {
        return false;
    }
    for (let key in obj1) {
        let key1 = obj1[key] instanceof Object;
        let key2 = obj2[key] instanceof Object;
        if (key1 && key2) {
            diff(obj1[key],obj2[key])
        } else if (obj1[key] !== obj2[key]) {
            return false;
        }
    }
    return true;
}
检测一个空对象

Object.getOwnPropertyName 获取到对象中的属性名,存到一个数组中

Object.keys(obj) 获取给定对象的所有可枚举属性的字符串数组

hasOwnProperty检测属性是否存在对象实例中(可枚举属性),如果存在则返回true,不存在则返回false

(1)通过Object.keys()方法获取键,长度为空

let obj = {
    name: "smy",
    age: 23,
    team:"HII"
};
console.log(Object.keys(newObj).length ===0);

(2)通过JSON.stringify转化成json字符串

let obj = {}
let b = JSON.stringify(obj) 
console.log(b === '{}')  // true

(3)通过对象的方法Object.getOwnPropertyNames

const res = Object.getOwnPropertyNames(obj);
console.log(res.length === 0); // true 说明是空对象

(4)通过for循环来判断

function test(obj) {
        for (let key in obj) {
          return false;
        }
        return true;
      }
伪数组转换成真数组

方法I:遍历添加到一个新数组中

var newArr = [];           // 先创建空数组
for(var i = 0; i < arr.length; i++){  // 循环遍历伪数组
    newArr.push(arr[i]);;    // 取出伪数组的数据,逐个放在真数组中
}

newArr.push("hello");
console.log(newArr);   // hello

方法II:arr.push.apply(arr,伪数组)

let newArr = []
newArr.push.apply(newArr,arr)

方法III:Array.from()

let newArr = Array.from(arr)

方法IV:使用slice方法,利用Array原型对象的slice方法配合apply,将slice中的this指向伪数组

let newArr = Array.prototype.slice.apply(arr)
undefined

undefined既是一个原始数据类型,也是一个原始值数据

undefined全局对象上的一个属性 window.undefined

不可写

window.undefined = 1
console.log(window.undefined)  // undefined

不可配置

delete window.undefined
console.log(window.undefined)

不可枚举

for(var k in window){
    if(k === undefined){
        console.log(k)
    }
}

不可重新定义

Object.defineProperty(window,'undefined')

undefined不是保留字或关键字

// 定义全局变量 
var undefined = 1;
      console.log(undefined);   // undefined

// 定义局部变量
 function test() {
  var undefined = 1;
  console.log(undefined); // 1
  }
  test();

void()表达式返回值为undefined

 var a, b, c;
 a = void ((b = 1), (c = 1));
 console.log(a, b, c);
数组的常见方法
graph LR
A[数组常见的方法] ---> B[操作方法]
A ---> C[排序方法]
A ---> D[转换方法]
A ---> E[迭代方法]

数组基本操作可以归纳为 增、删、改、查,需要留意的是哪些方法会对原数组产生影响,哪些方法不会

操作方法

下面前三种是对原数组产生影响的增添方法,第四种则不会对原数组产生影响

push()

方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度

unshift()

在数组开头添加任意多个值,然后返回新的数组长度

splice()

传入三个参数,分别是开始位置、0(要删除的元素数量)、插入的元素,返回空数组

let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 0, "yellow", "orange")
console.log(colors) // red,yellow,orange,green,blue
console.log(removed) // []

concat()

首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组

let colors = ["red", "green", "blue"];
let colors2 = colors.concat("yellow", ["black", "brown"]);
console.log(colors); // ["red", "green","blue"]
console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]

下面三种都会影响原数组,最后一项不影响原数组:

pop()

方法用于删除数组的最后一项,同时减少数组的length 值,返回被删除的项

let colors = ["red", "green"]
let item = colors.pop(); // 取得最后一项
console.log(item) // green
console.log(colors.length) // 1

shift()

shift()方法用于删除数组的第一项,同时减少数组的length 值,返回被删除的项

let colors = ["red", "green"]
let item = colors.shift(); // 取得第一项
console.log(item) // red
console.log(colors.length) // 1

splice()

传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组

let colors = ["red", "green", "blue"];
let removed = colors.splice(0,1); // 删除第一项
console.log(colors); // green,blue
console.log(removed); // red,只有一个元素的数组

slice()

slice() 用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组

let colors = ["red", "green", "blue", "yellow", "purple"];
let colors2 = colors.slice(1);
let colors3 = colors.slice(1, 4);
console.log(colors)   // red,green,blue,yellow,purple
concole.log(colors2); // green,blue,yellow,purple
concole.log(colors3); // green,blue,yellow

即修改原来数组的内容,常用splice

splice()

传入三个参数,分别是开始位置,要删除元素的数量,要插入的任意多个元素,返回删除元素的数组,对原数组产生影响

let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
console.log(colors); // red,red,purple,blue
console.log(removed); // green,只有一个元素的数组

即查找元素,返回元素坐标或者元素值

indexOf()

返回要查找的元素在数组中的位置,如果没找到则返回-1

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.indexOf(4) // 3

includes()

返回要查找的元素在数组中的位置,找到返回true,否则false

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.includes(4) // true

find()

返回第一个匹配的元素

let person = [
    {
        name: "smy",
        age:22,
    },
    {
        name: "wy",
        age:19
    }
]
let result = person.find((item, index, arr) => item.age > 20)
console.log(result)
排序方法

数组有两个方法可以用来对元素重新排序

reverse()

顾名思义,将数组元素方向排列

let values = [1, 2, 3, 4, 5];
values.reverse();
alert(values); // 5,4,3,2,1

sort()

sort()方法接受一个比较函数,用于判断哪个值应该排在前面

function compare(value1, value2) {
    if (value1 < value2) {
        return -1;
    } else if (value1 > value2) {
        return 1;
    } else {
        return 0;
    }
}
let values = [0, 1, 5, 10, 15];
values.sort(compare);
alert(values); // 0,1,5,10,15
转换方法

常见的转换方法有:

join()

join() 方法接收一个参数,即字符串分隔符,返回包含所有项的字符串

let colors = ["red", "green", "blue"];
alert(colors.join(",")); // red,green,blue
alert(colors.join("||")); // red||green||blue
迭代方法

常用来迭代数组的方法(都不改变原数组)有如下:

some()

some()方法测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个Boolean类型的值

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let result = numbers.some((item, index, arry) => item > 2)
console.log(result)

every()

对数组每一项都运行传入的函数,如果对每一项函数都返回 true ,则这个方法返回 true

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let result = numbers.every((item, index, array) => item > 2)
console.log(result)  //false

forEach()

对数组每一项都运行传入的函数,没有返回值

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.forEach((item, index, array) => {
    // 执行某些操作
});

filter()

对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let result = numbers.filter((item, index, array) => item > 2)
console.log(result)  

map()

对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let result = numbers.map((item, index, array) => item * 2)
console.log(result) 
什么是伪数组?如何转换成真数组?
伪数组:
  1. 具有length属性
  2. 按索引方式存储数据
  3. 不具有数组的push/pop等方法
伪数组转化成真数组:
  1. Array.from() ----> ES6新增方法
let lis = document.querySelectorAll("li");
      Array.from(lis).forEach((item) => {
        console.log(item);
      });
  1. [].slice.call(eleArr) 或则 Array.prototype.slice.call(eleArr)
[].slice.call(lis).forEach((item) => {
        console.log(item);
      });
reduce方法
字符串的常用方法
graph LR
A[字符串的常用方法] --->B[操作方法]
A---> C[转换方法]
A ---> D[模板匹配方法]
操作方法

我们也可将字符串常用的操作方法归纳为增、删、改、查

这里增的意思并不是说直接增添内容,而是创建字符串的一个副本,再进行操作

除了常用+以及${}进行字符串拼接之外,还可通过concat

concat

用于将一个或多个字符串拼接成一个新字符串

let stringValue = "hello ";
let result = stringValue.concat("world");
console.log(result); // "hello world"
console.log(stringValue); // "hello"

这里的删的意思并不是说删除原字符串的内容,而是创建字符串的一个副本,再进行操作

常见的有:

  • slice()
  • substr()
  • substring()

这三个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数

let stringValue = "hello world";
console.log(stringValue.slice(3)); // "lo world"
console.log(stringValue.substring(3)); // "lo world"
console.log(stringValue.substr(3)); // "lo world"
console.log(stringValue.slice(3, 7)); // "lo w"
console.log(stringValue.substring(3,7)); // "lo w"
console.log(stringValue.substr(3, 7)); // "lo worl"

这里改的意思也不是改变原字符串,而是创建字符串的一个副本,再进行操作

常见的有:

trim()、trimLeft()、trimRight()

删除前、后或前后所有空格符,再返回新的字符串

let stringValue = " hello world ";
let trimmedStringValue = stringValue.trim();
console.log(stringValue); // " hello world "
console.log(trimmedStringValue); // "hello world"
repeat()

接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果

let stringValue = "na ";
let copyResult = stringValue.repeat(2) // na na 
padEnd()

复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件

let stringValue = "foo";
console.log(stringValue.padStart(6)); // " foo"
console.log(stringValue.padStart(9, ".")); // "......foo"
toLowerCase()、 toUpperCase()

大小写转化

let stringValue = "hello world";
console.log(stringValue.toUpperCase()); // "HELLO WORLD"
console.log(stringValue.toLowerCase()); // "hello world"

除了通过索引的方式获取字符串的值,还可通过:

  • chatAt()
  • indexOf()
  • startWith()
  • includes()
charAt()

返回给定索引位置的字符,由传给方法的整数参数指定

let message = "abcde";
console.log(message.charAt(2)); // "c"
indexOf()

从字符串开头去搜索传入的字符串,并返回位置(如果没找到,则返回 -1 )

let stringValue = "hello world";
console.log(stringValue.indexOf("o")); // 4
startWith()、includes()
let message = "foobarbaz";
console.log(message.startsWith("foo")); // true
console.log(message.startsWith("bar")); // false
console.log(message.includes("bar")); // true
console.log(message.includes("qux")); // false
转换方法
split

把字符串按照指定的分割符,拆分成数组中的每一项

let str = "12+23+34"
let arr = str.split("+") // [12,23,34]
模板匹配方法

针对正则表达式,字符串设计了几个方法:

  • match()
  • search()
  • replace()
match()

接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp对象,返回数组

let text = "cat, bat, sat, fat";
let pattern = /.at/;  //.匹配一切
let matches = text.match(pattern);
console.log(matches[0]); // "cat"
search()

接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp对象,找到则返回匹配索引,否则返回 -1

let text = "cat, bat, sat, fat";
let pos = text.search(/at/);
console.log(pos); // 1
replace()

接收两个参数,第一个参数为匹配的内容,第二个参数为替换的元素(可用函数)

let text = "cat, bat, sat, fat";
let result = text.replace("at", "ond");
console.log(result); // "cond, bat, sat, fat"
类型转换机制
graph LR
A[类型转换机制]----> B[概述]
A---->C[显示转换]
A---->D[隐式转换]
显示转换

显示转换,即我们很清楚可以看到这里发生了类型的转变,常见的方法有:

  • Number()
  • parseInt()
  • String()
  • Boolean()

Number() 将任意类型的值转化为数值

图片

Number(324) // 324

// 字符串:如果可以被解析为数值,则转换为相应的数值
Number('324') // 324

// 字符串:如果不可以被解析为数值,返回 NaN
Number('324abc') // NaN

// 空字符串转为0
Number('') // 0

// 布尔值:true 转成 1,false 转成 0
Number(true) // 1
Number(false) // 0

// undefined:转成 NaN
Number(undefined) // NaN

// null:转成0
Number(null) // 0

// 对象:通常转换成NaN(除了只包含单个数值的数组)
Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([5]) // 5

从上面可以看到,Number转换的时候是很严格的,只要有一个字符无法转成数值,整个字符串就会被转为NaN

parseInt()

parseInt相比Number,就没那么严格了,parseInt函数逐个解析字符,遇到不能转换的字符就停下来

parseInt('32a3') //32
String()

可以将任意类型的值转化成字符串

给出转换规则图:

图片

// 数值:转为相应的字符串
String(1) // "1"

//字符串:转换后还是原来的值
String("a") // "a"

//布尔值:true转为字符串"true",false转为字符串"false"
String(true) // "true"

//undefined:转为字符串"undefined"
String(undefined) // "undefined"

//null:转为字符串"null"
String(null) // "null"

//对象
String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"
Boolean()

可以将任意类型的值转为布尔值,转换规则如下:

图片

Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false
Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true
隐式转换

我们这里可以归纳为两种情况发生隐式转换的场景:

  • 比较运算(==!=><)、ifwhile需要布尔值地方
  • 算术运算(+-*/%

除了上面的场景,还要求运算符两边的操作数不是同一类型

自动转换为布尔值

在需要布尔值的地方,就会将非布尔值的参数自动转为布尔值,系统内部会调用Boolean函数

可以得出个小结:

  • undefined
  • null
  • false
  • +0
  • -0
  • NaN
  • “”

除了上面几种会被转化成false,其他都换被转化成true

自动转换成字符串

遇到预期为字符串的地方,就会将非字符串的值自动转为字符串

具体规则是:先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串

常发生在+运算中,一旦存在字符串,则会进行字符串拼接操作

'5' + 1 // '51'
'5' + true // "5true"
'5' + false // "5false"
'5' + {} // "5[object Object]"
'5' + [] // "5"
'5' + function (){} // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"
自动转换成数值

除了+有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值

'5' - '2' // 3
'5' * '2' // 10
true - 1  // 0
false - 1 // -1
'1' - 1   // 0
'5' * []    // 0
false / '5' // 0
'abc' - 1   // NaN
null + 1 // 1
undefined + 1 // NaN
null`转为数值时,值为`0` 。`undefined`转为数值时,值为`NaN
JSON.parse() (JSON字符串转JS对象)

只有在‘’包裹“”情况下,才可以完成转换

const json = '{"name":"smy","age":23,"team":"HII"}'
let js = JSON.parse(json);
console.log(js)

缺点:

当json变成使用“”包裹‘’时,会发生报错

const str = "{'name':'smy','age':23,'team':'HII'}"
let json = JSON.parse(str);
console.log(str)
查询字符串 && JS对象 && JSON字符串转化
栈内存与堆内存

JavaScript 中的变量分为基本类型和引用类型

基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问,并由系统自动分配和自动释放。 这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间。 JavaScript 中的 Boolean、Null、Undefined、Number、String、Symbol 都是基本类型

引用类型(如对象、数组、函数等)是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript 不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用。 JavaScript 中的 Object、Array、Function、RegExp、Date 是引用类型。

栈/堆内存空间

深拷贝和浅拷贝
graph LR
A[浅拷贝与深拷贝]---->B[数据类型存储]
A---->C[浅拷贝]
A---->D[深拷贝]
A---->E[区别]
数据类型存储

JavaScript中存在两大数据类型:

  • 基本类型
  • 引用类型

基本类型数据保存在在栈内存中

引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中

浅拷贝

浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝

如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址

即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址

下面简单实现一个浅拷贝

function shalowClone(obj){
    const newObj = {};
    for(let prop in obj){
        if(obj.hasOwnProperty(prop)){
             newObj[prop] = obj[prop]
        }
    }
    return newObj;
}

JavaScript中,存在浅拷贝的现象有:

  • Object.assign
  • Array.prototype.slice(), Array.prototype.concat()
  • 使用拓展运算符实现的复制
Object.assign()
// object.assign()
var obj = {
    age: 18,
    nature: ['smart', 'good'],
    names: {
        name1: 'fx',
        name2: 'xka'
    },
    love: function () {
        console.log('fx is a great girl')
    }
}
let newObj = Object.assign({}, obj);
slice()

slice(0) 返回一个从索引0开始的新数组

const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.slice(0)
console.log(fxArrs) //  ["One", "Two", "Three"]
concat()
const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.concat()
console.log(fxArrs) //  ["One", "Two", "Three"]
扩展运算符
const fxArr = ["One", "Two", "Three"]
const fxArrs = [...fxArr]
console.log(fxArrs) //  ["One", "Two", "Three"]
深拷贝

深拷贝开辟一个新的栈,两个对象属性完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性

常见的深拷贝方式有:

  • _.cloneDeep()
  • jQuery.extend()
  • JSON.stringify()
  • 手写循环递归
_.cloneDeep()
const _ = require("lodash")
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1)
console.log(obj1.b.f === obj2.b.f);// false
jQuery.extend()
const $ = require('jquery');
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false
JSON.stringify()
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = JSON.parse(JSON.stringify(obj1));

但是这种方式存在弊端,会忽略undefinedsymbol函数

循环递归
function deepClone(newObj,obj) {
    for (let key in obj) {
        if (obj[key] instanceof Array) {
            newObj[key] = [];
            deepClone(newObj[key],obj[key])
        } else if (obj[key] instanceof Object) {
            newObj[key] = {};
            deepClone(newObj[key],obj[key])
        } else {
            newObj[key] = obj[key]
        }
    }
    return newObj
}
可迭代的对象

iteration:Array String Map Set Arguments TypeArray -----> 可使用 for…of 遍历

迭代器对象:可以通过for…of 的方式进行遍历

遍历和迭代有什么区别?

迭代:从目标源依次逐个抽取的方式来提取数据

目标源:1.有序的;2. 连续的

Set和Map的区别
SET

成员不能重复

只有键值没有名

可以遍历

Set 属性和方法 : size() add() delete() has() clear()

Map

本质上是键值对的集合,类似集合

可以遍历

可以跟各种数据格式转换

Map属性和方法:map.set() map.get()

Map可以用for…of 遍历,而不能使用for…in进行遍历

数组中的 forEach 和map 的区别?
相同点:

都是循环遍历数组中的每一项

不同点:

map方法返回一个新数组,数组中的元素未原始数组调用函数处理后的值

map方法不会对空数组进行检测,map方法不会改变原始数组

forEach方法用来调用数组的每个元素,将元素传给回调函数

forEach对于空数组是不会调用回调函数的。无论arr是不是空数组,forEach返回的都是undefined

原型

JavaScript 是一种通过原型实现继承的语言与别的高级语言是有区别的,像 java,C#是通
过类型决定继承关系的,JavaScript 是的动态的弱类型语言,总之可以认为 JavaScript 中所有 都是对象,在JavaScript中,原型也是一个对象,通过原型可以实现对象的属性继承,

JavaScript的对象中都包含了一个” prototype”内部属性,这个属性所对应的就是该对象的原型。

原型链

当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去 prototype 里找这个属性,这个 prototype 又会有自己的 prototype,于是就这样一直找下去, 也就是我们平时所说的原型链的概念

构造函数、实例、原型对象三者之间的关系
JavaScript的成员查找机制
  • 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性
  • 如果没有就查找它的原型(也就是__proto__指向的prototype原型对象)
  • 如果还没有就查找原型对象的原型(Object的原型对象)
  • 以此类推一直找到Object为止(null)
JS中的继承

JS中的六种继承方法:原型链继承,借用构造函数继承,组合继承,型式继承,寄生式继承,寄生组合式继承

原型链继承

JavaScript 实现继承的基本思想:通过原型将一个引用类型继承另一个引用 类型的属性和方法

function Animal() {
    this.color = ['black', 'pink', 'white'];
}
Animal.prototype.getColor = function () {
    return this.color;
}

function Dog() {
}
Dog.prototype = new Animal();
let dog1 = new Dog();
console.log(dog1.color);
原型链继承存在的问题:
  • 问题1:原型中包含的引用类型属性将被所有实例共享;
  • 问题2:子类在实例化的时候不能给父类构造函数传参
构造函数继承

JavaScript 实现继承的基本思想:在子类构造 函数内部调用超类型构造函数。通过使用 apply()和 call()方法可以在新创建的子类对象上执 行构造函数

function Animal(name) {
    this.name = "Dog"
    this.getName = function () {
        return this.name;
    }
}
function Dog(name) {
    Animal.call(this, name);
}
Dog.prototype = new Animal();
let dog1 = new Dog();
console.log(dog1.name);
解决:

借用构造函数实现继承解决了原型链继承的 2 个问题:引用类型共享问题以及传参问题。但是由于方法必须定义在构造函数中,所以会导致每次创建子类实例都会创建一遍方法。

class实现继承
class Animal{
    constructor(name){
        this.name = name;
    }
    getName() {
        return this.name;
    }
}

class Dog extends Animal{
    constructor(name, age) {
        super(name);
        this.age = age;
    }
}
new操作符具体干了什么?
  1. 创建一个空对象,并且this变量引入该对象,同时还继承了函数的原型
  2. 设置原型链,空对象指向构造函数的原型对象
  3. 执行函数体,修改构造函数this指针指向空对象,并执行函数体
  4. 判断返回值,返回对象就用该对象,没有的话就创建一个对象
function objectFactory() {
    var obj = new Object()  // 创建一个空对象
    Constructor = [].shift.call(arguments) // 设置原型链
    obj.__proto__ = Constructor.prototype 
    var ret = Constructor.apply(obj, arguments) // 设置this指向传入的arguments实例化对象
    return typeof ret  === 'object' ? ret || obj:obj // 判断返回值,返回是对象立即返回若不是则新建对象返回
}
this

this是JavaScript的一个关键字

当前环境执行期上下文对象的一个属性

this在不同的环境、不同作用下,表现是不同的

this的指向问题
  1. 以函数形式调用的时候,this永远都是window
  2. 以方法的形式调用,this是调用方法的对象
  3. 以构造函数的形式调用时,this是实例化对象
  4. 使用call和apply调用时,this是指定的那个对象
  5. 箭头函数:箭头函数的this就是内部箭头函数的this,如果没有就是window
  6. 特殊情况:通常意义上this指针指向为最后调用它的对象。这里需要注意的一点就是如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例
Promise

异步的问题以同步的方式执行

Generator&iterator
循环 & 遍历 & 迭代

循环:语言层面上的语法 -> 重复执行一段程序的方案

遍历:业务层面上的做法 -> 观察或者获取集合中的元素的一种做法

迭代:实现层面上的概念 -> 实现遍历的底层方案其实就是迭代

可迭代对象

原型对象prototype中具有Symbol.iterator,迭代器对象

for...in语句以任意顺序迭代对象的可枚举属性

for...of语句遍历可迭代对象定义要迭代的数据

生成器 -> 返回迭代器
function* generator(arr) {
        for (let v in arr) {
          yield v;
        }
      }
const res = generator(arr);
console.log(res.next()); //{value: '0', done: false}
console.log(res.next()); // {value: '1', done: false}
console.log(res.next()); // {value: '2', done: false}
console.log(res.next()); // {value: undefined, done: true}

手写迭代器

      function generator(arr) {
        let nextIndex = 0;
        return {
          next() {
            return nextIndex < arr.length
              ? { value: arr[nextIndex++], done: false }
              : { value: undefined, done: true };
          },
        };
      }
      const res = generator(arr);
      console.log(res.next());
      console.log(res.next());
      console.log(res.next());
      console.log(res.next());
JavaScript的垃圾回收机制
什么是JavaScript的垃圾回收

JS 的垃圾回收机制是为了以防内存泄漏,内存泄漏的含义就是当已经不需要某块内存时这块内存还存在着,垃圾回收机制就是间歇的不定期的寻找到不再使用的变量,并释放掉它们所指向的内存

JavaScript中常见的垃圾回收方式:标记清除

工作原理:是当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其 标记为“离开环境”。

标记“离开环境”的就回收内存

代码题

深拷贝
cloneDeep
const _ = require('lodash');
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);  // false
JSON.stringify
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj2);
递归
function deepClone(newObj,obj){
    for (let key in obj) {
        if (obj[key] instanceof Array) {
            newObj[key] = []
            deepClone(newObj[key],obj[key]) 
        } else if (obj[key] instanceof Object) {
            newObj[key] = {}
            deepClone(newObj[key],obj[key])
        } else {
            newObj[key] = obj[key]
        }
    }
    return newObj
}
箭头函数如何使用arguments
let func = (...rest)=>{
    console.log(rest)
}
判断数据类型
function typeOf(obj) {
    return Object.prototype.toString.call(obj).slice(8, -1);
}
数组去重
indexOf方法
function deDulp(arr) {
    let newArr = []
    arr.forEach(item => {
        if (newArr.indexOf(item) === -1) {
            newArr.push(item)
        }
    })
    return newArr
}
sort方法
function deDulp(arr) {
    let newArr = [arr[0]]
    arr.sort((a, b) => a - b)
    for (let i = 1; i < arr.length; i++){
        if(arr[i] !== arr[i-1]) newArr.push(arr[i])
    }
    return newArr
}
includes方法
function deDulp(arr) {
    let newArr = []
    arr.forEach(item => {
        if(!newArr.includes(item)) newArr.push(item)
    })
    return newArr
}
ES6 set数据结构
function deDulp(arr) {
    return arr.forEach(item => [...new Set(item)])
}
数组扁平化的实现
递归

实现方法:

  1. 如果碰到的是数组,则继续调用flat()递归
  2. 如果碰到的不是数组,则将其放入到result中
function deepFlatten(arr) {
    let result = [];
    for (let i = 0; i < arr.length; i++){
        if (Array.isArray(arr[i])) {
            result = result.concat(deepFlatten(arr[i]));
        } else {
            result.push(arr[i]);
        }
    }
    return result;
}
reduce

箭头函数写法

function flatten(arr){
   return arr.reduce((acc,curr) => acc.concat(Array.isArray(curr)? flatten(curr):curr) ,[])
}
ES6扩展运算符
function deepFlatten(arr) {
    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}
数组嵌套的层数
function deepFlat(arr) {
    let count = 0;
    while (arr.some((item) => Array.isArray(item))) {
        arr = [].concat(...arr);
        count++;
    }
    return count;
}
解析URL
请写一段 JS 程序提取 URL 中的各个 GET 参数(参数名和参数个数不确定),将 其按key-value 形式返回到一个 json 结构中,如{a: “1”, b: “2”, c: “”, d: “xxx”, e: undefined}
function serilizeUrl(url) {
    let urlObject = {}
    let reg = /\?/
    if (reg.test(url)) {  // 匹配url是否有?
        let urlString = url.substring(url.indexOf("?")+1)
        let urlArray = urlString.split("&")

        for (let i = 0; i < urlArray.length; i++){
            let item = urlArray[i].split("=") // [a,1] [b,2] [c,] [d,xxx] [e]
            urlObject[item[0]] = item[1] 
        }
        return urlObject
    }
    return null
}
驼峰转换
function shiftT(foo) {
    let arr = foo.split("-")     // ['get','element','by','id']
    for (let i = 1; i < arr.length; i++){
        arr[i] = arr[i].charAt(0).toUpperCase() + arr[i].substring(1)
    }
    return arr.join("")
}
数组中元素出现的个数是否相同
 function ifSame(arr) {
     let map = {}
     for (let num of arr) {
         if (!map[num]) map[num] = 1;
         else map[num]++
     }
     let count = Object.values(map)
     count = count.sort((a, b) => a - b)
     for (let i = 1; i < count.length; i++){
         if(count[i] === count[i-1]) return true
     }
     return false
 }
找出字符串中出现次数最多的字母
function find(str) {
    let obj = {};
    for (let i = 0; i < str.length; i++){
        if(!obj[str[i]]) obj[str[i]] = 1;
        else obj[str[i]]++;
        // console.log(str[i])  // h,e,l,l,o
    }

    // 比较大小
    let s = str.charAt(0)    // 假设出现次数最大的是字符串中的第一个字符
    let max = obj[s];
    for(let key in obj){
        if(key === s) continue
        if(obj[key] > max){
            max = obj[key]
            s = key
        }
    }
    return [s,max]
}
手动实现数组的reverse方法
function rever(arr) {
    let i = 0;
    let j= arr.length - 1;
    while(i<=j){
        let temp = arr[i]
        arr[i] = arr[j]
        arr[j] = temp
        i++
        j--
    }
    return arr
}
防抖

重新计时

假如一个程序约定在规定时间后触发,那么在规定的时间内,如果再次触发此事件,那么要重新计时,这样做是为了保证事件里面的代码只执行1次。

防抖的实现
let debounce = (fn, wait) => {
    let timer = null;
    return function () {
        clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(this,arguments)
        },wait)
    }
}
节流

控制次数

事件触发后,约定时间内,代码只执行一次。

节流的实现
const throttle = (func, delay) => {
    let timer = null;
    return function () {
        if (timer) {
            return;
        }
        timer = setTimeout(() => {
            func.apply(this, arguments);
            timer = null;
        },delay)
    }
}
继承
原型链继承

将另一个构造函数的原型对象设置成一个构造函数的实例化对象

function Animal() {
    this.color = ['white','brown']
}
Animal.prototype.getColor = function () {
    return this.color
}
function Dog() { }
Dog.prototype = new Animal()

let dog1 = new Dog()
dog1.color.push('black')

let dog2 = new Dog()
console.log(dog1.getColor())

原型链继承存在的问题:

  1. 原型中包含的引用类型属性将被所有实例共享
  2. 子类在实例化的时候不能给父类构造函数传参
构造函数继承
function Animal(name) {
    this.name = name
    this.getName = function () {
        return this.name
    }
}
function Dog(name) {
    Animal.call(this,name)
}
Dog.prototype = new Animal()

借用构造函数实现继承解决了原型链继承的 2 个问题:

  • 引用类型共享问题以及传参问题
  • 但是由于方法必须定义在构造函数中,所以会导致每次创建子类实例都会创建一遍方法
组合式继承

组合继承结合了原型链和盗用构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性

function Animal(name) {
    this.name = name
    this.color = ['black','while']
}
Animal.prototype.getName = function () {
    return this.name
}
function Dog(name,age) {
    Animal.call(this, name)
    this.age = age
}
Dog.prototype = new Animal()
Dog.prototype.constructor = Dog   // 原型对象constructor指向构造函数

let dog1 = new Dog('haha', 1)   
console.log(dog1)  // Dog { name: 'haha', color: [ 'black', 'while' ], age: 1 }

组合继承已经相对完善了,但还是存在问题,它的问题就是调用了 2 次父类构造函数

  • 第一次是在 new Animal()
  • 第二次是在 Animal.call() 这里
寄生式组合继承

基于组合继承的代码改成最简单的寄生式组合继承

function Animal(name) {
    this.name = name
    this.color = ['black','white']
}
function Dog(name,age) {
    Animal.call(this, name)
    this.age = age
}
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog
let dog1 = new Dog('xixi',2)
console.log(dog1)
class实现继承

使用 class 构造一个父类,使用 class 构造一个子类,并使用 extends 实现继承,super 指向父类的原型对象

class Animal{
    constructor(name){
        this.name = name
    }
    getName() {
        return this.name
    }
}

class Dog extends Animal{
    constructor(name, age) {
        super(name)
        this.age = age
    }
}
数组方法
实现forEach方法
Array.prototype.forEach2 = function(callback,thisArgs) {
    if (this === null) {
        throw new Error("this is null or not defined")
    }
    if (typeof callback !== 'function') {
        throw new Error(callback+"is not a function")
    }

    let o = Object(this)  // this表示数组
    let len = o.length >>> 0   // 无符号右移
    let k = 0
    while (k < len) {
        if (k in o) {
           callback.call(thisArgs,o[k],k,o) 
        }
        k++
    }
}

o.length >>> 0 是什么操作?就是无符号右移 0 位,那有什么意义嘛?就是为了保证转换后的值为正整数

其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型

实现map方法

根据callback函数,返回一个新的数组

Array.prototype.map2 = function(callback,thisArgs) {
    if (this === null) {
        throw new Error("this is null or not defined")
    }
    if (typeof callback !== 'function') {
        throw new Error(callback +"is not a function")
    }
    const o = Object(this)      // this表示数组
    let len = o.length >>> 0     // 无符号右移
    let k = 0
    let res = []
    while (k < len) {
        if (k in o) {
            res[k] = callback.call(thisArgs,o[k],k,o)
        }
        k++
    }
    return res  
}

map返回结果是一个新的数组,因此最后return res;map的参数中,最后一个形参表示的是数组本身,因此形参值为o

实现some方法

数组的some方法,只要数组中有一个值满足条件则返回true;否则返回false

Array.prototype.some2 = function(callback,thisArgs) {
    if (this === null) {
        throw new Error("this is null or not defined")
    }
    if (typeof callback !== 'function') {
        throw new Error(callback+"is not a function")
    }
    const o = Object(this)  // this表示数组
    const len = o.length >>> 0  // 无符号右移
    let k = 0
    while (k < len) {
        if (k in o) {
            if (callback.call(thisArgs,o[k],k,o)) return true
        }
        k++
    }
    return false
}
实现filter方法

数组的filter方法:将满足条件的值放入新数组中,并返回

Array.prototype.filter2 = function(callback,thisArgs) {
    if (this === null) {
        throw new Error("this is null or not defined")
    }
    if (typeof callback !== 'function') {
        throw new Error(callback+"is not a function")
    }
    const o = Object(this)   // this表示数组
    const len = o.length >>> 0
    let k = 0 , res = []
    while (k < len) {
        if (k in o) {
            if (callback.call(thisArgs,o[k],k,o)) {
                res.push(o[k])
            }
        }
        k++
    }
    return res
}
实现函数原型方法
实现bind方法
实现new关键字

new 运算符用来创建用户自定义的对象类型的实例或者具有构造函数的内置对象的实例

  • 创建一个空对象,并且this变量引入该对象,同时还继承了函数的原型
  • 设置原型链,空对象指向构造函数的原型对象
  • 执行函数体,修改构造函数this指针指向空对象 并执行函数体
  • 判断返回值,返回对象就用该对象 没有的话就创建一个对象
function objectFactory() {
    var obj = new Object()   // 创建一个空对象
    Constructor = [].shift.call(arguments)  
    obj._proto_ = Constructor.prototype  // 设置原型链
    var ret = Constructor.apply(this, arguments)  // 修改构造函数this指针指向空对象 并执行函数体

    return typeof ret === 'object' ? ret || obj:obj // 判断返回值 返回对象就用该对象,没有的话就创建一个对象
}
实现instanceof关键字

instanceof 就是判断构造函数的 prototype 属性是否出现在实例的原型链上

function instanceOf(left,right) {
    let proto = left._proto_
    while (true) {
        if (proto === null) return false
        if (proto === right.prototype) return true
        let proto = proto._proto_
    }
}
Promise
手写实现Promise.race()方法

Promise.race会返回一个由所有可迭代实例中第一个 fulfilled 或 rejected 的实例包装后的新实例

Promise.race = function(promiseArr) {
    return new Promise((resolve, reject) => {
        promiseArr.forEach(p => {
            Promise.resolve(p).then(val => {
                resolve(val)
            }, err => {
                rejecte(err)
            })
        })
    })
}
手写快排

分治思想

function quickSort(arr) {
    if (arr.length <= 1)    return arr;
    const pivot = arr[arr.length - 1];
    let leftArr = [];
    let rightArr = [];
    for (let el of arr.slice(0, arr.length - 1)) {
        el < pivot ? leftArr.push(el) : rightArr.push(el);
    }
    return [...quickSort(leftArr), pivot, ...quickSort(rightArr)]; 
}
树形结构化数据的结构化
将扁平化的数组整理成树形结构
const data = [
    {
        id: 2,
        pid: 0,
        path: "/course",
        name: "Course",
        title:"课程管理"
    },
    {
        id: 3,
        name: "CourseOperate",
        path: "operate",
        linke: "/course/operate",
        pid: 2,
        title:"课程操作"
    },
    {
        id: 4,
        name: "CourseInfoData",
        path: "info_data",
        link:"/course/operate/info_data",
        pid: 3,
        title:"课程数据"
    },
    {
        id: 5,
        name: "CourseAdd",
        path: "add",
        link: "/course/add",
        pid:2,
        title:"增加课程"
    },
    {
        id: 6,
        pid: 0,
        path: '/student',
        name: "Student",
        title: "学生管理",
    },
    {
        id: 7,
        name:"StudentOperate",
        path: "operate",
        link: "/student/operate",
        pid: 6,
        title:"学生操作"
    },
    {
        id: 8,
        name: "StudentAdd",
        path: "add",
        link: "/student/add",
        pid: 6,
        title:"增加学生"
    }
]

转换后的数据格式:

const data = [
    {
        id: 2,
        pid: 0,
        path: "/course",
        name: "Course",
        title:"课程管理",
        children:[
            {
                id: 3,
                name: "CourseOperate",
                path: "operate",
                linke: "/course/operate",
                pid: 2,
                title:"课程操作",
                children:[
                     {
                        id: 4,
                        name: "CourseInfoData",
                        path: "info_data",
                        link:"/course/operate/info_data",
                        pid: 3,
                        title:"课程数据"
                    },
                ]
            },
             {
                id: 5,
                name: "CourseAdd",
                path: "add",
                link: "/course/add",
                pid:2,
                title:"增加课程"
            },
        ]
    },
    {
        id: 6,
        pid: 0,
        path: '/student',
        name: "Student",
        title: "学生管理",
        children:[
             {
                id: 7,
                name:"StudentOperate",
                path: "operate",
                link: "/student/operate",
                pid: 6,
                title:"学生操作"
             },
              {
                id: 8,
                name: "StudentAdd",
                path: "add",
                link: "/student/add",
                pid: 6,
                title:"增加学生"
             }
        ]
    },
]

转换思路:

  • 区分parentschildren元素
  • 遍历寻找c.pid === p.id的元素
  • 递归寻找children中的c.pid === p.id
function formDataTree(data) {
    // 分离父子结构
    let parents = data.filter(p => p.pid === 0)
    let children = data.filter(c => c.pid !== 0)

    let dataToTree = (parents, children) => {
        parents.map(p => {
            children.map((c,i) => {
                if (p.id === c.pid) {
                    // 处理第一层
                    if (p.children) {
                        p.children.push(c)
                    } else {
                        p.children = [c]
                    }
                    // 递归处理
                    let _children = JSON.parse(JSON.stringify(children))    // 深拷贝
                    _children.splice(i,1)     // 将c此时的parents从children中去除
                    dataToTree([c], _children)
                }
            })
        })
    }

    // 处理结构
    dataToTree(parents, children)
    return parents
}

第二种解法:非扁平化

function formDataTree(data) {
    const _data = JSON.parse(JSON.stringify(data))
    return _data.filter(p => {
        const _arr = _data.filter(c => c.pid === p.id)
        if (_arr.length) p.children = _arr
        return p.pid === 0   //  返回值是最外层对象
    })
}
将树形结构转换成数组
function treeToData(tree) {
    if (!Array.isArray(tree)) throw new Error("请不要传入空树")
    if (!tree.length) return tree

    return tree.reduce((sum, item) => {
        if (!item.children || !item.children.length) {
            sum.push(item)
        } else {
            // 递归children
            const mid = treeToData(item.children)
            delete item.children
            sum.push(item,...mid)
        }
        return sum
    },[])
}
实现一个树形结构进行过滤的函数,其中树形结构的格式如下:
tree = [
    { name: 'A' },
    {
        name: 'B',
        children: [
            { name: "A" },
            {name:"AA",children:[...]}
        ]
    },
    {name:'C'}
]

输出:

// 1. 假设我输入的str为A则过滤后返回的结果为
[
    { name: "A"},
    {
        name: 'B', children: [name : 'bcde']
        {name:'A'}
    ]}
]

// 2. 假设我输入的str为AA 则过滤后返回的结果为
[
    {
        name: 'B', children: [
        {name:'AA',children:[...]}
    ]}
]

解题思路:

function transTree(data,filterName) {
    if (!data) return []

    // 存储返回数据
    let treeData = []

    data.filter(item => {
        const _item = JSON.parse(JSON.stringify(item))

        if (_item.name === filterName) treeData.push(_item)

        const children = transTree(_item.children, filterName)

        if (children.length) {
            _item.children = children
            treeData.push(_item)
        }
    })

    return treeData
}
二叉树
二叉树的前序遍历
var preorderTraversal = function(root) {
    const res = [];
    const preorder = (root) => {
        if(!root) return;
        res.push(root.val);
        preorder(root.left);
        preorder(root.right);
    }
    preorder(root);
    return res;
};
二叉树的中序遍历
var inorderTraversal = function(root) {
    const res = [];
    const inorder = (root) =>{
        if(!root) return;
        inorder(root.left);
        res.push(root.val);
        inorder(root.right);
    }
    inorder(root)
    return res;
};
二叉树的后序遍历
var postorderTraversal = function(root) {
    const res = [];
    const postorder = (root) =>{
        if(!root) return;
        postorder(root.left);
        postorder(root.right);
        res.push(root.val);
    }
    postorder(root);
    return res;
};
二叉树的层序遍历
var levelOrder = function(root) {
    if(!root) return [];
    const queue = [root];
    const levels = [];
    while(queue.length){
        const len = queue.length;
        const currLevel = [];
        for(let i=0;i<len;i++){
            const current = queue.shift();
            if(current.left){
                queue.push(current.left);
            }
            if(current.right){
                queue.push(current.right);
            }
            currLevel.push(current.val);
        }
        levels.push(currLevel);
    }
    return levels;
};
二叉树的最大深度
二叉树的最大宽度

Git

Git是什么?

git,是一个分布式版本控制软件,最初目的是为更好地管理Linux内核开发而设计

分布式版本控制系统的客户端并不只提取最新版本的文件快照,而是把代码仓库完整地镜像下来。这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复

所以在实现团队协作的时候,只要有一台电脑充当服务器的角色,其他每个人都从这个“服务器”仓库clone一份到自己的电脑上,并且各自把各自的提交推送到服务器仓库里,也从服务器仓库中拉取别人的提交

Git的状态

文件状态对应的,不同状态的文件在Git中处于不同的工作区域,主要分成了四部分:

  • 工作区:相当于本地写代码的区域,如 git clone 一个项目到本地,相当于本地克隆了远程仓库项目的一个副本
  • 暂存区:暂存区是一个文件,保存了下次将提交的文件列表信息,一般在 Git 仓库目录中
  • 本地仓库:提交更新,找到暂存区域的文件,将快照永久性存储到 Git 本地仓库
  • 远程仓库:远程的仓库,如 github

图片

Git命令
  • add
  • commit
  • push
  • pull
  • clone
  • checkout
git工作中使用场景

两个分支master和dev

项目开始执行流程

查看所有分支 git branch -a

克隆地址 git clone 地址

拉取线上master最新代码 git pull origin master

切换到开发分支 git checkout dev

合并master本地分支 git merge master

开始开发

结束开发

查看当前文件更改状态 git status

把所有更改代码放到缓存区 git add .

查看当前文件更改状态 git status

缓存区内容添加到仓库中 git commit -m ‘本次更改注释’

把代码传到gitLab上 git push origin dev

若代码到达上线标准则合并代码到master,切换分支到master: git checkout master

拉取master最新分支: git pull origin master

合并分支代码到master git push origin master

代码上线后,用tag标签标记发布结点(命名规则:prod_+版本+上线日期) git tag -a prod_V2.1.8_20211009

git rebase & git merge

Ajax

Ajax的实现流程是怎样的?
  1. 创建XMLHTTPRequest对象,也就是创建一个异步调用对象
  2. 创建一个新的HTTP请求,并指定该HTTP请求的方法、URL及验证信息
  3. 设置响应HTTP请求状态变化的函数
  4. 发送HTTP请求
  5. 获取异步调用返回的数据
  6. 使用JavaScript和DOM实现局部刷新
Ajax接收到的数据类型有哪些,数据如何处理?
接收到的数据类型:

JS对象,JSON字符串

如何处理数据:
  1. 字符串转对象

(1) eval()

const str = '{"name":"smy","age":23,"team":"HII"}'
console.log(eval('(' + str + ')'))

(2) JSON.parse()

JSON.parse()与eval()区别

eval() 方法不会检查给的字符串是否符合JSON的格式

如果给定的字符串中存在js代码eval()也会一并执行

  1. 对象转字符串

JSON.stringify()

Get 和 Post 的区别
区别:
  1. GET使用URL或Cookie传参,而POST将数据放在Body中
  2. GET的URL会有长度上的限制,而POST的数据则可以非常大
  3. POST比GET安全,因为数据在地址栏不可见
最本质的区别

GET通常用于从服务器上获取数据,而POST通常用于向服务器上传递数据

GET/POST使用场景

使用post方法:

  1. 请求的结果具有持续性的作用,例如:数据库内添加新的数据行
  2. 若使用get方法,则表单上收集的数据可能让URL过长
  3. 要传送的数据不是采用ASCII编码

使用GET方法:

  1. 请求是为了查找资源,HTML表单数据仅用来搜索
  2. 请求结果无持续性的副作用
  3. 收集的数据及htm表单内的输入字段名称的总长不超过1024个字符
什么是跨域请求

所谓跨域请求就是指:当发起请求的域与该请求指向的资源所在的域不一致。这里所有的域就是协议、域名和端口号的合计,同域就是协议、域名和端口号均相同,任何一个不同都是跨域。

哪些是跨域请求
同源策略

同源策略是浏览器的核心安全策略,为了防御来自非法的攻击,同源是指浏览器的域名、端口和协议都相同。

为什么需要跨域请求

开发中经常需要调用第三方的服务接口(mock server, fake api) 随着专业化分工的出现很多专业的信息服务提供商为前端开发者提供各类接口,这种情况下就需要进行跨域请求,这类前端接口服务很多是采用的cors方法来解决跨域问题的。

还有一类情况是在前后端分离的项目中,前端后端分属于不同的服务跨域问题在采用这种架构的时候就存在。而且现在很多项目都采用这种前后分离的方式,这类项目很多会采用反向代理的方式来解决跨域问题。

跨域请求解决方案
修改浏览器的安全设置(不推荐)
JSONP

JSONP是JSON的一种使用模式,可用于解决主流浏览器的跨域数据访问问题

原理:利用script标签的src属性不受同源策略限制。因为所有的src属性和href属性都不受同源策略限制,可以请求第三方服务器数据内容。

步骤:

(1) 创建一个script标签

(2) script的src属性设置接口地址

(3) 接口参数,必须要带一个自定义函数名 不然后台无法返回数据

(4) 通过定义函数名去接收后台返回数据

<script>
      window.callback = function (data) {
        console.log("data", data);
      };
    </script>
    <script src="http://localhost:8000/jsonp.js"></script>
callback({
    name:"smy"
})
跨域资源共享CORS(Cross-Origin Resource Sharing)

CORS是新的W3C标准

原理:服务器设置Access-Control-Allow-OriginHTTP响应头之后,浏览器将会允许跨域请求

限制:浏览器需要支持HTML5,可以支持POST, PUT等方法兼容ie9以上需要后台设置

Access-Control-Allow-Origin: *  // 允许所有域名访问
Access-Control-Allow-Origin: HTTP://a.com //只允许所有域名访问

相比jsonp的优缺点:

CORS与JSONP对比来说优势比较明显,jsonp只支持GET方式局限性很多,而且jsonp并不符合处理业务的正常流程。采用CORS的方式,前端编码与正常非跨域请求没什么不同。目前很多服务上都采用CORS的方式来解决跨域问题

iframe(不推荐)
反向代理

既然不能跨域请求,那么我们不跨域就可以了。通过在请求到达服务前部署一个服务,将接口请求进行转发,这就是反向代理。通过一定的转发规则可以将前端的请求转发到其他的服务。以Nginx为例

server {
    listen 9999
    server_name localhost
    # 将所有localhost:9999/api 为开头的请求进行转发
        location ^~/api{
        proxy_pass http://localhost:3000;
    }
}

通过反向代理我们将前端后端项目统一通过反向代理来提供对外的服务,这样在前端看上去就跟不存在跨域一样

反向代理麻烦之处就在于原对Nginx等反向代理服务的配置,在目前前后端分离的项目中很多都是采用这种方式。

axios是什么?

axios是一个基于promise的http库,可以用在浏览器和node.js中

axios有什么特性?
  1. 从浏览器中创建XMLHttpRequests
  2. 从node.js创建http请求
  3. 支持Promise API
  4. 拦截请求和响应
  5. 转换请求数据和响应数据
  6. 取消请求
  7. 自动转换JSON数据
  8. 客户端支持防御XSRF

并发并行同步异步

并发&并行

表示计算机能够同时执行多项任务,计算机如何做到“并发”有许多不同的形式;

并发

比如:对于一个单核处理器,计算机可以通过分配时间片的方式;让一个任务运行一段事件,然后切换另一个任务再运行一段时间,不同的任务会这样交替往复的一直执行下去。这个过程也被称作是进程或者线程的上下文切换(context switching)

并行

对于多核处理器,我们可以在不同的核心上真正并行地执行任务,而不用通过分配时间片的方式运行,这种情况被称为并行(parallelism)

同步&异步
同步

同步代表必须等到前一个任务执行完毕之后,才能进行下一个任务;因此在同步中并没有并发并行的概念

异步

不同的任务之间并不会相互等待,在执行任务A时,并不会等到A执行结束再执行B。一个典型实现异步的方式则是通过多线程编程,可以创建多个线程并启动他们,在多核的环境下,每个线程就会被分配到独立的核心上运行,实现真正的并行;

JavaScript中的并发

JavaScript本身并没有多线程的概念,不同通过它的函数回调(function callback)机制,依然能够做到单线程的“并发”。比如:可以通过fetch()同时访问多个网络资源,执行fetch函数时,线程并不会等待而是继续向下执行,当返回请求结果后,才会显示。

**注意:**虽然主程序和回调函数看似是同时进行的,但他们依然只存在于一个主线程中

**异步任务:**WebSocket,setTimeout,setInterval,XMLHttpRequest,fetch,Promise,async function,queueMicrotask

多线程编程 vs (单线程)异步编程

**异步编程:**I/O密集的应用程序,比如web应用就会经常执行网络操作,数据库访问

**多线程编程:**计算量密集的应用,比如视频图像处理,科学计算等等

ES6模块化

模块化的好处

模块化可以避免命名冲突的问题

大家都遵守同样的模块化规范写代码,降低了沟通的成本,极大方便了各个模块之间的相互调用

只需关心当前模块本身的功能开发,需要其他模块的支持时,在模块内调用目标模块即可

ES6模块化的规范定义

每个js文件都是一个独立的模块

导出模块用export关键字

导入其他模块用import关键字(import 和 export 只能在代码的最顶部,不能写在代码内

Promise

Promise是异步编程的一种解决方案,将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数,要是为了解决异步处理回调地狱(也就是循环嵌套的问题)而产生的

Promise构造函数包含一个参数和一个带有resolve和reject两个参数的回调。在回调中执行一些操作,如果一切都正常,则用resolve,否则调用reject。对于已经实例化过的Promise对象可以调用Promise.then() 方法,传递resolve和reject方法作为回调。then() 方法接受两个参数:onResolve 和 onReject,分别代表当前Promise对象在成功或失败时

回调地狱

JS中或node中,都大量的使用了回调函数进行异步操作,而异步操作什么时候返回结果是不可控的,如果我们希望几个异步请求按照顺序来执行,那么就需要将这些异步操作嵌套起来,嵌套的层数特别多,就会形成回调地狱 或者叫做 横向金字塔

手写一个promise
let p = new Promise((resolve, reject) => {
    if (操作失败) {
        reject(err);
    } else {
        resolve(data);
    }
})
p.then(res => {
    console.log(res);
}).catch(err => {
    console.log(err);
})
Promise构造函数是同步执行还是异步执行,那么then方法呢?

promise构造函数是同步执行,then方法是异步执行

Promise中reject和catch处理上有什么区别

reject是用来抛出异常,catch是用来处理异常

reject是Promise的方法,而catch是Promise实例方法

reject后的东西,一定会进入then中的第二个回调,如果then中没有第二个回调,则进入catch

网络异常(如断网),会直接进入catch而不会进入then的第二个回调

promise的三种状态

最初状态,pending(等待)此时promise的结果是undefined

当resolve(value) 调用时,达到最终的状态之一,fulfilled此时可以获取结果value

当reject(value)调用时,达到最终的状态之一,rejected此时可以获取错误信息

达到最终的fulfilled或者rejected结果时,promise的状态就不会再改变了

then方法的链式调用

前一个then里面返回的字符串,会被下一个then方法接收到

前一个then里面返回的promise对象,并且调用resolve的时候传递了数据,数据会被下一个then接收到

前一个then里面如果没有调用resolve,则后续的then不会接收到任何值

import fs from "fs";

function readFiles(path) {
    return new Promise((resolve, reject) => {
        fs.readFile(path,'utf-8' ,(err, data) => {
            err ? reject(err) : resolve(data);
       })  
    })
}

let p1 = readFiles('./file/a.txt');
let p2 = readFiles("./file/b.txt")
let p3 = readFiles("./file/c.txt");

p1.then(r1 => {
    console.log(r1);
    return p2;
}).then(r2 => {
    console.log(r2);
    return p3;
}).then(r3 => {
    console.log(r3);
})
Promise的方法
graph LR
A[Promise] --->B[promise.all]
A ---> C[promise.race]
A ---> D[promise.any]
Promise.all

Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值

失败的时候返回最先被reject失败状态的值

let p1 = Promise.resolve('aaa')
let p2 = Promise.resolve('bbb')
let p3 = Promise.reject('ccc')
let p4 = Promise.resolve('ddd')
Promise.all([p1, p2, p3, p4]).then(res => {
    console.log(res); //返回数组
}).catch(err => {
    console.log(err);
})
ccc

成功的时候返回的是一个结果数组

let p1 = Promise.resolve('aaa')
let p2 = Promise.resolve('bbb')
let p3 = Promise.resolve('ccc')
let p4 = Promise.resolve('ddd')
Promise.all([p1, p2, p3, p4]).then(res => {
    console.log(res); //返回数组
}).catch(err => {
    console.log(err);
})
[ 'aaa', 'bbb', 'ccc', 'ddd' ]
Promise.race

Promise.race是赛跑的意思,也就是说Promise.race([p1, p2, p3,p4])里面的结果哪个获取的快,就返回哪个结果,不管结果本身是成功还是失败

let p1 = Promise.resolve('aaa')
let p2 = Promise.resolve('bbb')
let p3 = Promise.resolve('ccc')
let p4 = Promise.resolve('ddd')
Promise.race([p1, p2, p3, p4]).then(res => {
    console.log(res);
}).catch(err => {
    console.log(err);
})
Promise.any

Promise.any() 接收一个Promise可迭代对象,只要其中的一个 promise 成功,就返回那个已经成功的promise

async和await

await和async关键词能够将异步请求的结果以返回值的方式返回给我们

async返回一个Promise,如果Promise没有返回,会自动将它包装在一个promise并带有它的值的resolve中

await运算符用于等待promise,只能在async块中使用;关键字await使JavaScript等待直到promise返回结果。需要注意的是,它只是让async功能块等待,而不是整个程序执行

使用async和await主要是为了避免使用Promise.then链式表达式

graph LR
A[async三种情况]--->B[async + promise对象]
A--->C[async + num]
A--->D[async + 返回值是promise对象的函数]
执行async函数返回的都是promise对象
async function test1() {
    return 1;
}
function test2() {
    return Promise.resolve(2);
}

const result1 = test1();
const result2 = test2();

console.log("result1", result1);
console.log("result2", result2);
promise.then 成功的情况对应await
async function test3() {
    const p3 = Promise.resolve(3);
    p3.then((res) => {
        console.log("res" + res);
    })
    const data = await p3;
    console.log("data" + data);
}
 test3()
await num 可以自动封装成 promise.resolve(num)
async function test4() {
    const data4 = await 4;   // Promise.resolve(4)
    console.log("data4", data4);
}
test4();
await 后面可以跟async函数
async function test1() {
    return 1;
}

async function test5() {
    const data5 = await test1(); //test1()的返回值是一个promise对象
    console.log("data5", data cvbx5);
}
test5(); //输出结果是1
Promise.catch的异常情况对应try…catch
async function test6() {
    const p6 = Promise.reject(6);
    try {
        const data6 = await p6;
    } catch (e) {
        console.log("error", e);
    }
}
test6();
异步函数顺序执行问题

(1)

async function test1() {
    console.log('test1 begin'); //2 
    const result = await test2(); //3 先执行右边test2() ---> test2
    console.log("result",result); //4 test2()函数没有return 所以返回值是undefined
    console.log("test1 end"); // 5
}

async function test2(){
    console.log("test2");
}

console.log("script begin"); //1
test1(); //2
console.log("script end"); //3

分析

await ===> Promise.then() 是一个微任务,执行完await之后,直接跳出async函数,执行其他代码,其他代码执行完毕后,再回到async函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中。

结果

script begin
test1 begin
test2
script end
result undefined
test1 end

(2)

console.log('script start')

async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()

setTimeout(function() {
console.log('setTimeout')
}, 0)

new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})

console.log('script end')

分析

执行代码,输出script start

执行async1(),会调用async2(),然后输出async2 end,此时将会保留async1函数的上下文,然后跳出async1函数。

遇到setTimeout,产生一个宏任务

执行Promise,输出Promise。遇到then,产生第一个微任务

继续执行代码,输出script end

代码逻辑执行完毕(当前宏任务执行完毕),开始执行当前宏任务产生的微任务队列,输出promise1,该微任务遇到then,产生一个新的微任务

执行产生的微任务,输出promise2,当前微任务队列执行完毕。执行权回到async1

执行await,实际上会产生一个promise返回,即

let promise_ = new Promise((resolve,reject){ resolve(undefined)})

结果

script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout
事件循环Event Loop

JavaScript是一门单线程的语言,它的同步和异步操作是通过event loop来实现的

所有同步的任务都会被放到主线程上执行,形成一个执行栈call stack,将宏放到Task queue中,将微任务放到Microtask Queue中;当call stack中为空时,event loop去Queue中检查,先检查Microtask Queue再检查Task Queue;若callback queue为空则继续检查;当callback queue不为空时,将首部的任务取出放到call stack中执行;如此循环往复。

宏任务与微任务

微任务执行时机比宏任务早

宏任务:setTimeout, setInterval, DOM事件,AJAX请求 (script, setTimeout, setInterval, setImmediate, I/O, UI rendering)

微任务:Promise, async/await (promise, Object.observe, MutationObserversda)

Event Loop面试题
console.log(1)

setTimeout(function() {
  console.log(2)
}, 0)

const p = new Promise((resolve, reject) => {
  resolve(1000)
})
p.then(data => {
  console.log(data)
})

console.log(3)

结果

1
3
1000
2

2

console.log(1)
setTimeout(function() {
  console.log(2)
  new Promise(function(resolve) {
    console.log(3)
    resolve()
  }).then(function() {
    console.log(4)
  })
})

new Promise(function(resolve) {
  console.log(5)
  resolve()
}).then(function() {
  console.log(6)
})
setTimeout(function() {
  console.log(7)
  new Promise(function(resolve) {
    console.log(8)
    resolve()
  }).then(function() {
    console.log(9)
  })
})
console.log(10)

result

1
5
10
6
2
3
4
7
8
9

3

  console.log(1)

  setTimeout(function() {
    console.log(2)
  }, 0)

  const p = new Promise((resolve, reject) => {
    console.log(3)
    resolve(1000) // 标记为成功
    console.log(4)
  })

  p.then(data => {
    console.log(data)
  })

  console.log(5)

result

1
3
4
5
1000
2

4

  new Promise((resolve, reject) => {
    resolve(1)

    new Promise((resolve, reject) => {
      resolve(2)
    }).then(data => {
      console.log(data)
    })

  }).then(data => {
    console.log(data)
  })

  console.log(3)

result

analyse: 嵌套Promise先执行里面的

3
2
1

5

setTimeout(() => {
  console.log(1)
}, 0)
new Promise((resolve, reject) => {
  console.log(2)
  resolve('p1')

  new Promise((resolve, reject) => {
    console.log(3)
    setTimeout(() => {
      resolve('setTimeout2')
      console.log(4)
    }, 0)
    resolve('p2')
  }).then(data => {
    console.log(data)
  })

  setTimeout(() => {
    resolve('setTimeout1')
    console.log(5)
  }, 0)
}).then(data => {
  console.log(data)
})
console.log(6)

result

从上到下执行,setTimeout算宏任务,进到任务队列中排队;嵌套关系,先执行嵌套里面的

2
3
6
p2
p1
1
4
5

6

<script>
    console.log(1);
    async function fnOne() {
      console.log(2);
      await fnTwo(); // 右结合先执行右侧的代码, 然后等待
      console.log(3);
    }
    async function fnTwo() {
      console.log(4);
    }
    fnOne();
    setTimeout(() => {
      console.log(5);
    }, 2000);
    let p = new Promise((resolve, reject) => { // new Promise()里的函数体会马上执行所有代码
      console.log(6);
      resolve();
      console.log(7);
    })
    setTimeout(() => {
      console.log(8)
    }, 0)
    p.then(() => {
      console.log(9);
    })
    console.log(10);
  </script>
 <script>
    console.log(11);
    setTimeout(() => {
      console.log(12);
      let p = new Promise((resolve) => {
        resolve(13);
      })
      p.then(res => {
        console.log(res);
      })
      console.log(15);
    }, 0)
    console.log(14);
  </script>

分析:

第一次循环: 1 2 6 7 10 宏任务:script 定时器2 定时器0 微任务:3 9

第一次循环结束:1 2 6 7 10 3 9

第二次循环(script) :11 14 宏任务 定时器2 定时器0 定时器0 微任务:

第三次循环(第一个定时器0): 8

第四次循环(第二个定时器0):12 15 13

第五次循环(定时器2):5

result

1 2 6 7 10 3 9 11 14 8 12 15 13 5

注意:

  1. 函数不调用不执行
  2. await === then 属于微任务

前端工程化

什么是前端工程化?

前端工程化是指:在企业级的前端项目开发中,把前端开发所需的工具、技术、流程、经验等进行规范化、标准化

前端工程化的特点

模块化(js的模块化、css的模块化、资源的模块化)

组件化(复用现有的UI结构、样式、行为)

规范化(目录结构的划分、编码规范化、接口规范化、文档规划范、Git分支管理)

自动化(自动化构建、自动部署、自动化测试)

Babel的原理是什么

Babel的主要工作是对代码进行转译(解决兼容,解析执行一部分代码)

let a = 1 + 1       =>  var a = 2

转译分为三阶段:

  • 解析(Parse),将代码解析生成抽象语法树AST,也就是词法分析与语法分析的过程
  • 转换(Transform),对语法树进行变换方面的一系列操作。通过babel-traverse, 进行遍历并作添加、更新、删除等操作
  • 生成(Generate),通过babel-generator 将变换后的AST转换为js代码

Webpack

什么是webpack

webpack是前端项目工程化的具体解决方案

它提供了友好的前端模块化开发支持,以及代码压缩混淆、处理浏览器端JavaScript的兼容性、性能优化等强大的功能。

webpack的优点

让程序员把工作的重心放到具体功能的实现上,提高了前端开发效率和项目的可维护性。

webpack 的构建流程是什么?
  • 初始化参数:从shell参数和配置文件合并参数,得出最终的参数
  • 开始编译:从上一步获得的参数初始化compiler对象,加载所有的loader插件,通过run方法执行编译
  • 确定入口:根据配置文件的entry找出所有入口文件
  • 编译模块:从入口文件开始,调用所有配置的loader对模块进行翻译成complication,然后递归所有依赖的模块,然后重新编译。得到每个模块翻译后的最终内容以及他们之间的依赖关系
  • 输出资源:根据入口和模块的依赖关系,组装成一个个包含多个模块的chunk,然后将chunk转换成一个独立的文件加入输出列表,这是可以修改输出内容的最后机会
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内容写入文件系统上。
webpack的热更新原理
  1. 基本定义:webpack的热更新又称热替换(Hot Module Replacement),缩写为HMR,这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
  2. 核心定义

HMR的核心就是客户端从服务器拉取更新后的文件,实际上WDS与浏览器之间维护了一个websocket,当本地资源发生变化时,WDS会向浏览器推送更新,并带上构建时的hash,让客户端与上一次资源进行对比

客户端对比出差异后会向WDS发起Ajax请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息向WDS发起jsonp请求获取该chunk的增量更新

后续的部分由HotModulePlugin来完成,提供了相关API以供开发者针对自身场景进行处理,像react-hot-loader和vue-loader都是借助这些API实现HMR

如何利用webpack来优化前端性能
  • 压缩代码,uglifyJsPlugin压缩js代码,mini-css-extract-plugin压缩css代码
  • 利用CDN加速,将引用的静态资源修改为CDN上对应的路径,可以利用webpack对于output参数和loader的publicpath参数来修改资源路径
  • 删除死代码tree shaking,css需要使用Purify-Css
  • 提取公共代码,webpack4移除了CommonsChunkPlugin, 用optimization.splitChunksoptimization.runtimeChunk来代替

Vue面试题

Vue的优缺点
优势
  1. vue具有两大特点:响应式编程、组件化
  2. vue的优势:轻量级框架、简单易学、双向数据绑定、组件化、数据和结构的分离、虚拟DOM、运行速度快
  3. vue是单页面应用,使页面局部刷新,不用每次跳转页面都要请求所有数据和dom,这样大大加快了访问速度和提升用户体验,而且他的第三方ui库很多节省开发时间
缺点:
  1. Vue不缺入门教程,可是很缺乏高阶教程与文档
  2. Vue不支持IE8
  3. 生态环境不如react和angular
  4. 社区不大
用Vue实现样式绑定,可以用class或者内联样式,最少写出2个?
<!-- 第一种绑定class -->
<div :class="['classA','classB']"></div>

<!-- 第二种绑定class -->
<div :class="{'classA' : true, 'classB' : false}"></div>

<!-- 第一种绑定style -->
<div :style="{fontSize : '16px', color : 'red' }"></div>

<!-- 第二种绑定style -->
<div :style="[{fontSize : '16px' , color : 'red' }]"></div>
Vue的路由实现

通过vue-router的钩子函数来实现的

vue-router 有几种导航钩子?
  1. 全局守卫 router.beforeEach
  2. 全局解析守卫:router.beforeResolve
  3. 全局后置钩子: router.afterEach
  4. 路由独享的守卫:beforeEnter
  5. 组件内的守卫:beforeRouteEnter / beforeRouteUpdate(2.2新增) / beforeRouteLeave
Vue-router的钩子函数有哪些?

关于vue-router中的钩子函数主要分为三类:

  1. 全局钩子函数beforeEach

beforeEach的三个参数分别是:

to - router 即将进入的路由对象

from - 当前导航即将离开的路由

next - function,进行管道中的一个钩子,如果执行完了,则导航的状态就是confirmed否则为FALSE,则终止导航

  1. 单独路由独享组件

beforeEnter

  1. 组件内钩子

beforeRouterEnter

beforeRouterUpdate

beforeRouterLeave

Vue.route 和 Vue.router 有什么区别?

$router 为$VueRouter实例, 是路由操作对象,只写对象,想要导航到不同URL,则使用router.push方法

$route为当前router跳转对象,路由信息对象,只读对象,里面可以获取name,path,query,params等

Vue中父子组件创建和销毁的执行顺序
加载渲染过程
父beforeCreate -> 父 created -> 父beforeMount -> 子 beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
子组件更新过程
父 beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
父组件更新过程
父 beforeUpdate -> 父updated
销毁过程
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
前端路由和后端路由的区别
什么是路由?

路由是根据不同的url地址展示不同的内容或页面;

前端路由

很重要的一点是页面不刷新,前端路由就是把不同路由对应不同的内容或页面的任务交给前端来做,每跳转到不同的url都是使用前端的锚点路由

优点
  1. 用户体验好,和后台网速没有关系,不需要每次都从服务器全部获取,快速展现给用户
  2. 可以在浏览器中输入指定想要访问的url路由地址
  3. 实现了前后端的分离,方便开发
缺点
  1. 使用浏览器的前进,后退键的时候会重新发送请求,没有合理地利用缓存
  2. 单页面无法记住之前滚动的位置,无法在前进,后退的时候记住滚动的位置
后端路由

浏览器在地址栏中切换不同的url时,每次都向后台服务器发出请求,服务器响应请求,在后台拼接HTML文件传给前端显示,返回不同的页面,意味着浏览器会刷新页面,网速慢得话说不定屏幕全白再有新内容。后端路由得另一个极大得问题就是前后端不分离

优点:
  1. 分担了前端的压力,HTML和数据的拼接都是由服务器完成
缺点:
  1. 当项目十分庞大时,加大了服务器端的压力,同时在浏览器段不能输入制定的url路径进行指定模块的访问。另一个就是如果当前网速过慢,那将会延迟页面的加载,对用户体验不是很友好
$refs 和 $el的用法
ref有三种用法:
  1. ref加在普通的元素上,用this.$ref.name 获取到的是dom元素
  2. ref加在子组件上,用this.$ref.name 获取到的是组件实例,可以使用组件的所有方法
  3. 如何利用 v-for 和 ref 获取一组数组或者dom 节点
<ul>
    <li v-for = "item in people" ref = "refContent">{{ item }}</li>
</ul>

<script>
data:{
    people:['smy', 'lje', 'wy', 'zsy', 'zx']
},
created : function(){
    this.$nextTick(()=>{
    console.log(this.$refs.reContent)
})
}
</script>
vm.$el

获取Vue实例关联的DOM元素;

比如说想要获取自定义组件tabControl,并获取它的OffsetTop,就需要先获取该组件

在组件内设置属性ref = ‘一个名称(tabControl2)’,然后this.$refs.tabControl2, 就拿到了该组件

获取OffsetTop,组件不是DOM元素,是没有OffsetTop的,无法通过OffsetTop来获取,就需要通过$el来获取组件中的DOM元素:

this.tabOffsetTop = this.$refs.tabControl2.$el.offsetTop
vue的常用修饰符

.stop - 用来阻止事件冒泡

.prvent - 阻止默认行为

.once - 事件处理程序只执行一次

keydown.enter - 识别用户是否按了回车键

keydown.esc - 识别用户是否按了esc键

vue中v-if与v-show的区别以及使用场景
区别
  1. 手段:v-if是通过控制dom节点的存在与否来控制元素的显示和隐藏;v-show是通过设置DOM元素的display样式,block为显示,none为隐藏;
  2. 编译过程:v-if 切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;
  3. 编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译;v-show是在任何条件下(首次条件是否为真)都被编译,然后被缓存,而且DOM元素保留
  4. 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
使用场景

基于以上区别,因此,如果需要非常频繁的切换,则使用v-show较好;如果在运行时条件很少改变,则使用v-if较好

v-if和v-for为什么避免一起用

v-if和v-for一起使用,v-for的优先级要高于v-if,先循环再控制显示隐藏

  • 为了过滤一个列表中的项目(比如v-for = "user in users" v-if = "user.isActive")。在这种情况下,请将users替换为一个计算属性(比如activeUsers),让其返回过滤后的列表。
  • 为了避免渲染本应该被隐藏的列表(比如v-for = "user in users" v-if = "shouldShowUsers")。这种情况下,请将v-if移动至容器元素上(比如ulol
Vue中data中变量的数据值发生改变,界面没有跟着更新,是什么原因(Vue数据双向绑定失效)

在data对象里面没有属性和值,getter/setter 函数无法监听到属性值的数据变化,就会导致这样的问题发生。

解决办法:this.$set(obj,key,value)
Vue组件如何进行传值

父向子 -> props定义变量 -> 父在使用组件用属性给props变量传值

子向父 -> $emit触发父的事件 -> 父在使用组件用@自定义事件名 = 父的方法(子把值带出来)

兄弟组件传值:

  1. 引入第三方new Vue定义为eventBus
import Vue from 'vue'
 const eventBus = new Vue()
 export default eventBus
  1. 在组件中created中订阅方法eventBus.$on(‘自定义事件名’,methods中的方法名)
<script>
import eventBus from "./event-bus"
methods:{
    add(){
        eventBus.$emit('addItem',this.title)
    }
}
</script>
  1. 在另一个兄弟组件中的methods中写函数,在函数中发布eventBus订阅的方法eventBus.$emit(‘自定义事件名’)
<script>
import eventBus from "./event-bus"
export default{
    methods:{
        handleAddTitle(title){
            console.log('title',title)
        }
    }
    mounted(){
        eventBus.$on('addItem',this.handleAddTitle)
    }
}
</script>
  1. 在组件的template中绑定事件(比如click)
Vue组件data为什么必须是函数
  • 每个组件都是vue的实例

  • 组件共享data属性,当data的值是同一个引用类型的值时,改变其中一个会影响其他

  • 组建中的data写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份data,就会造成一个变了全都会变得结果。

什么是虚拟DOM?

Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实的DOM上。

真实DOM映射到虚拟DOM例子

真实DOM

<ul id='list'>
    <li class='item'>Item 1</li>
  <li class='item'>Item 2</li>
  <li class='item'>Item 3</li>
</ul>

虚拟DOM

var element = {
        tagName: 'ul', // 节点标签名
        props: { // DOM的属性,用一个对象存储键值对
            id: 'list'
        },
        children: [ // 该节点的子节点
          {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
          {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
          {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
        ]
    }
为什么需要虚拟DOM
跨平台

由于虚拟DOM是以JavaScript对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Node等

我们可以将DOM对比操作放在JS层,提高效率。 因为DOM操作的执行速度远不如Javascript的运算速度快,因此,把大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。

提高渲染性能

虚拟DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新

Diff算法

当新旧虚拟DOM发生变化时,利用diff算法来进行比较

Vue中的diff算法
  1. 只比较同一层,不跨层比较
  2. 当出现标签名不同时,直接删掉,不继续往下进行比较
  3. 标签名相同且key相同,则认为是相同结点,不进行深度比较

这样大大提升了性能,当有n个结点时,只进行了n次比较,时间复杂度为O(n)

Vue中的key
  • key会用在虚拟DOM算法(diff算法)中,用来辨别新旧节点
  • 不带key的时候会最大限度减少元素的变动,尽可能用相同元素(就地复用)
  • 带key的时候,会基于相同的key进行排列(相同的复用)
  • 带key还能触发过渡效果,以及触发组件的生命周期
Vue2.x 响应式原理(数据双向绑定原理)

Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

如何实现数据双向绑定
 let obj = {};
      Object.defineProperty(obj, "name", {
        set(newVal) {
          // 当我们设置name属性的时候自动调用的函数
          // 并且属性最新的值会被当成实参传入进来
          console.log("对象名字属性的新值为", newVal);
        },
        get() {
          // 当我们访问name属性的时候自动调用的方法
          // 并且get函数的返回值就是你拿到的值
          console.log("你刚刚访问了obj的name属性");
          return "pink";
        },
      });
解决getset的联动问题

使用中间变量

      let obj = {};
      let _name = "smy";
      Object.defineProperty(obj, "name", {
        set(newVal) {
          console.log("对象名字属性的新值为", newVal);
          _name = newVal;
        },
        get() {
          console.log("你刚刚访问了obj的name属性");
          return _name;
        },
      });
对象的劫持转化(将对象中每一个属性都设置成响应式)
// 可以访问到每个变量的属性
      let obj = {
        name: "smy",
        age: 23,
        team: "HII",
        location: "Shanghai",
      };

      Object.keys(obj).forEach((key) => {
        console.log(key, obj[key]);
        // 单独的函数 变成响应式
        observe(obj, key, obj[key]); // obj[key] 表示对象中的value
      });

      function observe(obj, key, value) {
        Object.defineProperty(obj, key, {
          set(newValue) {
            console.log("obj的属性被修改了");
            value = newValue;
          },
          get() {
            console.log("obj的属性被访问了");
            return value;
          },
        });
      }
响应式总结
  • 所谓的响应式其实就是拦截数据的访问和设置,插入一些我们自己想要做的事情
  • 在JavaScript中能实现响应式拦截的方法有两种,Object.defineProperty方法和Proxy对象代理
  • vue2.x中的data配置项,只要放到了data里的数据,不管层级多深不管最终会不会用到这个数据都会进行递归响应式处理,所以要求我们非必要,尽量不要添加太多的冗余数据在data中
  • 需要了解vue3.x中,解决了2中对于数据响应式处理的无端性能消耗,使用的手段是Proxy劫持对象整体 + 惰性处理(用到了才进行响应式转换)
Object.definePropertyProxy的区别
Proxy的优势:
  • Proxy可以直接监听对象而非属性
  • Proxy可以直接监听数组的变化
  • Proxy有多达13种拦截方法
  • Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改
  • Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利
Object.defineProperty的优势
  • 兼容性好,支持IE9,而Proxy存在浏览器兼容性问题,无法用polyfill磨平
Vuex是什么

vuex是专门为vue.js提供的一种状态管理模式,它采用的是集中式储存和管理所有组件的状态和数据,方便使用

Vue的5个核心属性是什么?

state/mutations/actions/getters/modules

  1. state

state为单一状态树,类似于data,在这里定义所有组件都可以访问的变量

  1. mutations

更改 store 中 state 状态的唯一方法就是提交 mutation,修改的语法是: this.$store.commit(‘变量名’), mutations里面放同步操作

  1. actions

action 可以提交mutation,在 action 中可以执行 store.commit,而且 action 中可以有任
何的异步操作。在页面中如果我们要嗲用这个 action,则需要执行 store.dispatch

  1. getters

getter 有点类似 vue.js 的计算属性,当我们需要从 store 的 state 中派生出一些状态,那
么我们就需要使用 getter,getter 会接收 state 作为第一个参数,而且 getter 的返回值会根据 它的依赖被缓存起来,只有 getter 中的依赖值(state 中的某个需要派生状态的值)发生改变 的时候才会被重新计算

  1. modules

module 其实只是解决了当 state 中很复杂臃肿的时候,module 可以将 store 分割成模 块,每个模块中拥有自己的 state、mutation、action 和 getter

Vuex的出现解决了什么问题

主要解决了两个问题:

  1. 多个组件依赖于同一状态时,对于多层嵌套的组件的传参将会非常繁琐
  2. 来自不同组件的行为需要变更同一状态
Vuex 的Mutation 和Action 之间的区别是什么

Mutations:专注于state的修改,理论上是修改state值的唯一途径。必须同步执行;

Actions:业务代码,异步请求

简述Vuex的数据传递流程
  1. 通过new Vuex.Store()创建一个仓库state是公共的状态,state—>components渲染页面

  2. 在组件内部通过this.$store.state属性来调用公共状态重的state,进行页面的渲染

  3. 当组件需要修改数据的时候,必须遵循单向数据流,通过this.$store.dispatch来触发actions中的方法

  4. actions中的每个方法都会接受一个对象 这个对象里面有一个commit方法,用来触发mutations里面的方法

  5. mutations里面的方法用来修改state中的数据 mutations里面的方法都会接收到2个参数 一个是store中的state,另外一个是需要传递到参数

  6. 当mutations中的方法执行完毕后state会发生改变,因为vuex的数据是响应式的 所以组件的状态也会发生改变

Vue怎么实现跨域
  1. 使用vue-cli脚手架搭建项目时,proxyTable解决跨域问题

​ 打开config/index.js 在proxyTable中添加如下代码:

proxyTable:{
    '/api': { //使用"/api"来代替"http://f.apiplus.c" 
    target: 'http://f.apiplus.cn', //源地址
    changeOrigin: true, //改变源
    pathRewrite: { '^/api': 'http://f.apiplus.cn' //路径重写
}
  1. 使用CORS(跨域资源共享)

(1) 前端设置,vue设置axios允许跨域携带cookie(默认是不带cookie)

axios.defaults.withCredentials = true;

(2) 后端设置:

  • 跨域请求后的响应头中设置
  • Access-Control-Allow-Origin //为发起请求的主机地址
  • Access-Control-Allow-Credentials, 当它被设置为true时,允许跨域带cookie,但此时Access-Control-Allow-Origin 不能为通配符*
  • Access-Control-Allow-Headers,设置跨域请求允许的请求头
  • Access-Control-Allow-Methods,允许跨域请求允许的请求方式
Vue的nextTick的原理是什么?
  1. 为什么需要nextTick

Vue是异步修改DOM的并且不鼓励开发者直接接触DOM,但有时候业务需要必须对数据更改——刷新后的DOM会做相应的处理,这时候就可以使用Vue.nextTick(callback)这个api了

this.$nextTick(()=>{
  // 这里的函数会等DOM异步更新完成后再执行
})
  1. 理解原理前的准备

首先需要知道事件循环中宏任务和微任务这两个概念:

(1)常见的宏任务有:script, setTimeout, setInterval, setImmediate, I/O, UI rendering

(2)常见的微任务有:process.nextTick(nodejs), Promise.then(), MutationObserver

  1. 理解nextTick的原理

正是vue通过异步队列控制DOM更新和nextTick回调函数先后执行的方式

Vue的生命周期

生命周期:vue实例从创建到销毁的整个过程

钩子函数是vue内置的函数,它随着组件的生命周期阶段自动执行。钩子函数有:初始化,挂载,更新和销毁

ajax请求一般用在哪个生命周期

组件创建完毕后,可以在created 生命周期函数中发起Ajax 请求,从而初始化 data 数据

生命周期的过程
初始化
  1. new Vue() - Vue实例化(组件也是一个小的Vue实例)
  2. Init Events & Lifecycle - 初始化事件和生命周期函数
  3. beforeCreate - 生命周期钩子函数被执行
  4. Init injections & reactivity - Vue内部添加data和methods等
  5. created - 生命周期钩子函数被执行,实例创建
  6. 接下来是编译模板阶段 - 开始分析
  7. Has el option? -是否有el选项-检查要挂到哪里

​ 没有,调用$mount()方法

​ 有,继续检查template选项

Question

Vue实例从创建到编译模板执行了哪些钩子函数? beforeCreate / created

created函数触发能获取data? 能获取data,不能获取真实DOM

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9od1DYx3-1645105523018)(https://cn.vuejs.org/images/lifecycle.png)]

挂载
  1. template选项检查

​ 有 - 编译template返回render渲染函数

​ 无 - 编译el选项对应标签作为template(要渲染的模板)

  1. 虚拟DOM挂载成真实DOM之前
  2. beforeMount - 生命周期钩子函数被执行
  3. Create … - 把虚拟DOM和渲染的数据一并挂到真实DOM上
  4. 真实DOM挂载完毕
  5. mounted - 生命周期钩子函数被执行
Question

Vue实例从创建到显示都经历了哪些钩子函数?

beforeCreate / created / beforeMount / mounted

created函数里,能获取真实DOM吗? 不能获取真实DOM

在什么钩子函数里可以获取真实DOM? mounted

更新
  1. 当data里数据改变,更新DOM之前
  2. beforeUpdated - 生命周期钩子函数被执行
  3. Virtual DOM… - 虚拟DOM重新渲染,打补丁到真实DOM
  4. updated - 生命周期钩子函数被执行
  5. 当有data数据改变 - 重复这个循环
Question

什么时候执行updated钩子函数?

当数据发生变化并更新页面后

在哪里可以获取更新后的DOM

在updated钩子函数里

销毁
  1. 当$destroy()被调用 - 比如组件DOM被移除(例v-if)
  2. beforeDestroy - 生命周期钩子函数被执行
  3. 拆卸数据监视器、子组件和事件侦听器
  4. 实例销毁后,最后触发一个钩子函数
  5. destroyed - 生命周期钩子函数被执行
Question

一般在beforeDestroy/destroyed里做什么?

手动消除计时器/定时器/全局事件

异步组件

组件按需渲染,当需要某个组件显示时,再引入异步组件

在组件注册时候,通过箭头函数的形式来进行组件的引入

<script>
components:{
    TestL: ()=> import("./componnet/Test")
}
</script>

通过这种引入方式来实现按需引入

Vue2.0 和 Vue3.0 的区别
  1. 响应式原理api的变化

Vue2响应式原理采用的是defineProperty,而vue3选用的是proxy。这两者前者是修改对象属性的权限标签,后者是代理整个对象。性能上proxy会更加优秀

  1. diff算法,渲染算法的改变

Vue3优化diff算法。不再像vue2那样比对所有dom,而采用了block tree的做法。此外重新渲染的算法里也做了改进,利用了闭包来进行缓存。这使得vue3的速度比vue2快了6倍

  1. 建立数据data

这里就是Vue2与Vue3 最大的区别 — Vue2使用选项类型API(Options API)对比Vue3合成型API(Composition API) 旧的选项型API在代码里分割了不同的属性(properties):data,computed属性,methods,等等。新的合成型API能让我们用方法(function)来分割,相比于旧的API使用属性来分组,这样代码会更加简便和整洁。

Vue2.0 兼容IE哪个版本以上吗?

不支持ie8及以下,部分兼容ie9,完全兼容10以上,因为vue的响应式原理是基于es5的Object.defineProperty(),而这个方法不支持ie8及以下

Vue中mixin

将vue项目中可以重复使用的函数或方法,保存到mixin中,可以实现代码复现的功能

<script>
import mixin from "./mixin"
export default {
    mixins : [mixin]
}
</script>

使用mixin的好处:

更加方便去维护代码

Vue2和Vue3生命周期的区别
Vue3的生命周期函数
<script>
beforeCreate(){

}
created(){

}
beforeMount(){

}
mounted(){

}
beforeUpdate(){

}
beforeUnmount(){

}
unmounted(){

}
</script>
Vue项目优化的解决方案有哪些?
  • 使用mini-css-extract-plugin插件抽离css
  • 配置optimization把公共的js代码抽离出来
  • 通过webpack处理文件压缩
  • 不打包框架、库文件,通过cdn的方式引入
  • 小图片使用base64
  • 配置项目文件懒加载
  • UI库按需加载
  • 开启Gzip压缩
说说你对 SPA 单页面的理解,它的优缺点分别是什么?

单页web应用(简称为SPA)是一种特殊的Web应用。它将所有的活动局限于一个web页面中,仅在该web页面初始化时加载相应的HTML、JavaScript和CSS,一旦页面加载完成了,SPA不会因为用户的操作而进行页面的重新加载或跳转。取而代之的是利用JavaScript动态的变换HTML的内容,从而实现UI与用户的交互。由于避免了页面的重新加载,SPA可以提供较为流畅的用户体验。得益于ajax,我们可以实现无跳转刷新,又多亏了浏览器的history机制,我们用hash的变化从而可以实现推动界面变化。从而模拟客户端的单页面切换效果。

SPA优缺点

优点

  • 无刷新界面,给用户体验原生的应用感觉
  • 节省原生(android & ios) app 开发成本
  • 提高开发效率,无需每次安装更新包
  • 容易借助其他知名平台更有利于营销和推广
  • 符合web2.0的趋势

缺点

  • 效果和性能确实和原生的有较大差距
  • 各个浏览器的版本兼容性不一样
  • 业务随着代码量增加而增加,不利于首屏优化
  • 某些平台对hash有偏见,有些甚至不支持pushstate
  • 不利于搜索引擎抓取

设计模式

MVVM

什么是MVVM模式?

第一个M是Model模型层,负责数据的存储和业务逻辑;第二个V是view视图层,负责展示model中的数据;第三个VM是view model视图模型层,负责连接model模型层和view视图层,具有数据双向绑定,自动更新视图的功能

MVC

什么是MVC模式?

Model-View-Controller 模型-视图-控制器,Controller指的是页面业务逻辑,使用MVC的目的是将M和V的代码分离。MVC是单向通信,也就是View和Model,必须通过Controller来承上启下。

MVP

MVP指的是Model-View-Presenter,但MVP中的View并不能直接使用Model,而是通过为Presenter提供接口,让Presenter去更新Model,再通过观察者模式更新View。

项目介绍

项目简介:

移动端IT资讯分享平台,类似于CSDN,博客园; 主要的功能有:标签页切换,频道管理、文章详情、关注功能、点赞功能、评论功能、搜索功能、登录功能、个人中心、编辑资料、自动客服

技术栈

vuex 存储用户登录时的token,refreshToken 在浏览器的localStorage中设置缓存

vue-router实现路由懒加载

axios 实现接口的三层封装,axios请求拦截器,axios响应拦截器

vant 组件库进行页面搭建

socket.io-client 实现客服功能

amfe-flexible 移动端适配

项目难点
对axios进行二次封装

目的:设置基地址,调用接口时就可以直接导入

axios请求拦截器

场景:

在发起请求之前,最后对要发送的请求配置对象进行修改

例如:如果本地有token,携带在请求头给后台

所有api接口里以后暂时不用自己携带Headers+Token了

service.interceptors.request.use(config => {
    return config;
}, error => {
  Promise.reject(error);
});
axios响应拦截器

在响应回来后,马上执行响应拦截器函数

例如:判断是否错误401,统一进行权限判断

axios.interceptors.response.use(function(response)){
       //对响应数据做点什么
       return response;
},function(error){
      // 对响应错误做点什么
    return Promise.reject(error)
}
频道列表切换

通过监听activeId的变化来实现;给activeId设置一个监听器,当他发生变化时,重新调用接口发起新的请求

上拉加载更多

原理

  1. 检测何时触底
  2. 触底了,发请求,获取下一页的数据
  3. 旧数据、新数据合并
检测何时触底(原生)
window.onscroll = function(){
        // 变量scrollTop是滚动条滚动时,距离顶部的距离
        var scrollTop = document.documentElement.scrollTop||document.body.scrollTop;
        // 变量windowHeight是可视区的高度
        var windowHeight = document.documentElement.clientHeight || document.body.clientHeight;
        // 变量scrollHeight是滚动条的总高度
        var scrollHeight = document.documentElement.scrollHeight||document.body.scrollHeight;
               // 滚动条到底部的条件
            if(scrollTop+windowHeight==scrollHeight){           
                console.log("距顶部"+scrollTop+"可视区高度"+windowHeight+"滚动条总高度"+scrollHeight);
             }   
        }

window添加onscroll滚动事件

滚动条距离顶部的距离

document.documentElement.scrollTop

可视区的高度

document.documentElement.clientHeight

滚动条总高度

document.documentElement.scrollHeight

Vant组件库

v-model是否处于加载状态,加载过程中不触发load事件

finished是否已加载完成,加载完成后不再触发load事件

offset滚动条与底部距离小于 offset 时触发load事件

下拉刷新

原理

  1. 拽动一个盒子

touchstart(触摸开始(手指放在触摸屏上)) + touchmove(拖动(手指在触摸屏上移动))

transform: translateY()

  1. 拽出一定距离

​ 发请求,拿新数据 / 替换之前的旧数据

频道管理-切换频道

添加点击事件获取频道的activeId,将所点击频道的activeId赋值给页面组件中activeId

自动回复
http协议复习

http组成 = 请求报文 + 响应报文

HTTP消息由请求行、请求头部、空行及请求体4个部分组成

HTTP响应消息由状态行、响应头部、空行和响应体4个部分组成

http协议的缺点

服务器只能被动地响应客户端的请求,无法主动给客户端发送消息。一次请求才能对应一次响应

即时通信-WebSocket

HTML5出了一个新的协议叫WebSocket,可以在一个TCP/lP链接上,实现即时通信效果

需要前端支持 + 需要后端支持

Summary

什么是即时通信?

服务器能主动给客户端发送消息,不局限于一次请求一次响应

ajax能否实现即时通信?

只能模拟,使用轮询方式,但是消耗资源

如何实现即时通信?

前端采用websocket协议,也是新出的一个类

后端也要提供websocket接口地址,建立连接

通过socket.io包来实现

后端里的socket.io

io()函数连接socket服务器

如果代码部署到了线上服务器,这个localhost要换成线上的ip地址

因为这个网页请求到本地浏览器上查看,你要还是localhost那不是请求本地呢吗?

路由懒加载

实现方式

然后通过Webpack编译打包后,会把每个路由组件的代码分割成一个js文件,初始化时不会加载这些js文件,只当激活路由组件才会去加载对应的js文件。 在这里先不管Webpack是怎么按路由组件分割代码,只管在Webpack编译后,怎么实现按需加载对应的路由组件js文件

在路由的配置文件中,将原来直接引入组件的方式,换成箭头函数的方式

const home = () => import('@/views/Home/home.vue')

简单来讲就是,通过import()引用的子模块会被单独分离出来,打包成一个单独的文件(打包出来的文件被称为chunk )

项目通过webpack打包时会进行资源整合,也就是会把项目中的JS、CSS等文件按照一定的规则进行合并,已达到减少资源请求的目的

路由全局守卫

用户只有在登录后,才可以访问页面

方法

1.路由前置守卫判断

router.beforeEach((to,from,next) => {
    // 有token,不能去登录页
    // 无token,需要用户“权限”才需要去登录页
    if(store.state.token.length < 0 && to.path === '/login'){
        //证明token已经登陆了
        next(false)
    }else{
        next()
    }
})
组件缓存

避免组件频繁地被创建和销毁的过程,因此使用keep-alive对组件进行缓存

图片懒加载
遇到的问题
封装的自定义聚焦指令

只有在点击一次的时候自定义指令会生效,第二次点击同一个

原因:

inserted()函数,只会触发一次

如何解决
封装的自定义聚焦指令

将Vue.directive中封装的自定义指令放到updated这个属性中

token的续签

token过期401,用refresh_token无感知的刷新一个新的token回来,替换旧的token的同时,继续上一次未完成的请求,用户体验好

步骤:

  1. 定义刷新token的接口方法
  2. 在响应拦截器401处,调用重新请求token的接口,同步给vuex和本地
refreshToken过期
  1. 清空本地的token和refreshToken(在请求拦截器里面)
  2. 强制跳转到登录页
如何处理浏览器的断网情况
断网事件offline和连网事件online

浏览器有两个事件:“online” 和 “offline”. 这两个事件会在浏览器在online mode和offline mode之间切换时,由页面的<body>发射出去

事件会按照以下顺序冒泡:document.body -> document -> window。

事件是不能去取消的(开发者在代码上不能手动变为online或者offline,开发时使用开发者工具可以)

注册上下线事件的几种方式

最最建议window+addEventListener的组合

  • 通过window或document或document.body和addEventListener(Chrome80仅window有效)
  • 为document或document.body的.ononline或.onoffline属性设置一个js函数。(注意,使用window.ononline和window.onoffline会有兼容性的问题)
  • 也可以通过标签注册事件<body ononline="onlineCb" onoffline="offlineCb"></body>

acm输入输出模式

1.一行输入,两个参数,中间是空格
1 5
10 20
while(line = readline()) {
    let arr = line.split(" ").map(item => parseInt(item));
    print(add(arr[0], arr[1]));
}
2. 第一行是输入的行数,后面是测试数字,中间以空格隔开
2
1 5 
10 20
let len = parseInt(readline())
for(let i=0;i<len;i++){
    let arr = readline().split(' ').map(item => parseInt(item))
    print(add(arr[0],arr[1]))
}
3.
1 5
10 20
0 0
while(line = readline()){
    let arr = line.split(' ').map(item => parseInt(item))
    if(arr[0] === 0 && arr[1] ===0) break
    print(add(arr[0],arr[1]))
}
4.
// input
4 1 2 3 4
5 1 2 3 4 5
0
//output
10
15
while(line = readline()){
    let arr = line.split(' ').map(item => parseInt(item))
    let len = arr[0]
    if(arr[0] === 0) break
    let sum = 0
    for(let i=1;i<=len;i++){
        sum += arr[i]
    }
    print(sum)
}
5.
2
4 1 2 3 4
5 1 2 3 4 5
10
15
let count = parseInt(readline())
for(let i=0;i<count;i++){
    let arr = readline().split(' ').map(item => parseInt(item))
    let len = arr[0]
    let sum = 0
    for(let i=1;i<=len;i++){
        sum += arr[i]
    }
    print(sum)
}
6.
// input
4 1 2 3 4
5 1 2 3 4 5
// output
10
15
while(line = readline()){
    let arr = line.split(' ').map(item => parseInt(item))
    let len = arr[0]
    let sum = 0
    for(let i=1;i<=len;i++){
        sum += arr[i]
    }
    print(sum)
}
7.
//input
1 2 3
4 5
0 0 0 0 0
// output
6
9
0
while(line = readline()){
    let arr = line.split(' ').map(item => parseInt(item))
    let sum = 0
    for(let i=0;i<arr.length;i++){
        sum += arr[i]
    }
    print(sum)
}
8.
// input
5
c d a bb e
// output
a bb c d e
let line = readline()
let arr = readline().split(' ')
arr.sort()
print(arr.join(' '))
9.
// input
a c bb
f dddd
nowcoder
// output
a bb c
dddd f
nowcoder
function format(arr){
    return arr.sort().join(' ')
}

while(line = readline()){
    let arr = line.split(' ')
    print(format(arr))
}
10.
// input
a,c,bb
f,dddd
nowcoder
// output
a,bb,c
dddd,f
nowcoder
while(line = readline()){
    let arr = line.split(',').sort().join(',')
    print(arr)
}

笔经

shopee提前批
Array 实例的方法不包括
copyWithin
length
reverse
toString

length是属性不是方法

以下描述正确的是

A 线程可提高程序的并发执行,可提高系统效率

B 一个线程可创建多个线程

C 系统支持线程和用户级线程,都需要内核的支持完成切换

D 线程间可使用信号量来实现同步

Correct : ABD

Cache-Control 被用于在http请求和响应中,通过指定指令来实现缓存机制。以下哪个说法是错的?

A 如果指定max-age 值为0,那么每次请求都需要重新发到服务器

B 如果设置了no-cache, 表示每次请求,缓存会将此请求发到服务器验证是否过期,若未过期,则缓存才使用本地缓存副本

C public指令表明响应可以被任何中间人缓存,即时是通常不可缓存的内容

D must-revalidate 指令表明即时未超过max-age , 也必须与服务器重新校验之后才能使用缓存

说出一下代码的执行顺序
setImmediate(() => {
  console.log(1);
  Promise.resolve().then(() => {
    console.log(2);
  });
}, 0);
new Promise((resolve) => {
  console.log(3);
  resolve();
}).then(() => {
  console.log(4);
  process.nextTick(() => {
    console.log(5)
  });
  setTimeout(() => {
    console.log(6);
  }, 0);
}).then(() => {
  console.log(7);
});
console.log(8);

output

3 8 4 7 5 1 2 6
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值