前端面试题整理

415 阅读1小时+

html

如何理解标签的语义化

  • 代码结构清晰,便于开发和维护
  • 便于SEO,搜索引擎会根据不同的标签赋予不同的权重
  • 能够根据语义渲染网页

什么是行内元素,什么是块元素,两者的区别?

(1)行内元素:span、a、b、strong、img、br、input、select、textarea、big

(2)块元素:div、h1-h6、p、ul、li、ol、dl、dt、hr、table、td、tr、thead、th

(3)行内元素与块元素区别

  • 行内元素无宽高,宽高根据文本内容的变化而改变,但可设置行高line-height,margin上下无效、左右有效,padding上下无效、左右有效

  • 块级元素都是在新的一行开始排列,各个块级独占一行,垂直方向排列,而行内元素和其它元素在同一水平线排列

  • 块元素可以包含行内元素,但是行内元素里面不可包括块元素

什么是重排,什么是重绘,重排和重绘有啥区别?

  • 重排(回流):当DOM的变化影响了元素的几何信息(元素的尺寸和位置),导致浏览器需要重新计算元素的几何属性,将其放在正确的位置上,这个过程叫做重排
  • 如何触发重排? (1)添加/删除可见的DOM元素

(2)元素的尺寸发生改变,比如边距、填充、宽度和高度

  • 重绘:当元素的外观发生改变时,但是没有改变元素的布局,比如box-show、color、background这些属性,这个过程叫做重绘

  • 区别:重排是改变元素的尺寸和位置,而重绘是改变元素的外观

  • 怎么减少重排? (1)缓存需要修改的DOM元素 (2)分离读写操作 (3)样式集中修改 (4)尽量修改position为absolute/fixed的元素,对其他元素影响不大 (5)使用GPU加速,translate使用3D变化

  • 为什么要transform不会导致重排、重绘? 因为transform属于合成属性,使用transform会创建一个合成层,导致动画在一个独立的层中渲染,当元素的内容不发生改变,就没必要进行重新渲染,浏览器通过重新复合来创建动画帧,所以不会引起重绘和重排

src与href的区别?

  • src是source的缩写,用于替换当前文档。href是建立起当前文档与引用资源之间的关系。
  • src指向外部资源的位置,当浏览器解析到该元素时,会暂停对其他资源的加载、编译与执行,这就是为什么把js脚本放在底部而不是头部。
  • href指向网络资源所在的位置,建立起当前元素与当前文档的连接,使用href会并行下载资源,不会影响对当前文档的处理,所以加载建议使用link,而不是@import。link在页面载入时同时加载,@import需要页面网页完全载入以后加载。

script标签中defer和async的区别?

  • 如果没有defer和async,浏览器加载到对应的js脚本就会立即执行,它不会等待后续文档的加载,这样会阻塞后续文档的加载。
  • defer和async都是异步解析js文件,不会阻塞页面的解析。
  • 多个defer按文档的加载顺序执行,多个async不确定执行的顺序。
  • async后续文档的加载和执行是是并行进行的(异步),defer后续文档的加载是异步的,只加载不执行,等到js解析完毕,DOMContentLoaded事件触发之前执行。

html有哪些内置对象(面试被问过)

  • window对象
  • location对象
  • navigator对象
  • history对象
  • URL对象
  • console对象
  • event对象
  • document对象
  • form对象

HTML5有哪些更新?

  • 增加语义化标签:nav、article、section等。
  • 增加音视频标签:audio、video。
  • 数据存储:localStorage、sessionStorage。
  • canvas(画布)、websocket(通信协议)、GeoLocation(地理位置)。
  • input新增属性:autofocus、required、placeholder、autocomplete等。
  • historyAPI:go、back、forward、pushState。
  • 移除纯表现的元素:basefont,big,center,font, s,strike,tt,u、frame,frameset,noframes。

css

css选择器以及优先级

  • 标签选择器、伪元素选择器:1;
  • 类选择器、伪类选择器、属性选择器:10;
  • id 选择器:100;
  • 内联样式:1000;
  • !important优先级最高
  • 优先级相同后定义的生效
  • 继承得到的优先级最低
  • *(通用元素选择器)、+(相邻同胞选择器)、>(后代选择器)的优先级为0
  • 内联样式>内部样式>外部样式>浏览器用户自定义样式>浏览器默认样式

单行和多行文本溢出

  • 单行
overflow:hidden;
text-overflow:ellipsis;
white-spce: noWrap;
  • 多行
overflow:hidden;
text-overflow:ellipsis;
display:--webkit-box;
--webkit-box-orient:vertical;
--webkit-line-camp:3;

什么是BFC?

  • 块级格式化上下文,是一个环境,里边的元素修改不会影响外面的环境,元素在垂直方向进行排列
  • 布局规则:box是css布局的基本单位,元素的类型和display属性决定这个box的类型
  • 创建BFC的方式: 浮动元素 display设为inline-block或者position:absolute
  • 作用:(1) 防止margin重叠 (2)清除浮动 (3) 自适应多栏布局

盒模型

  • 盒模型包括两种:IE盒模型和标准的盒模型,两者的内容计算方式不同 (1)IE:width = content + border + padding (2)标准: width = content

  • 盒模型允许我们在其它元素和周围元素边框之间放置元素

双飞翼布局和圣杯布局?

  • 为什么要使用双飞翼和圣杯布局?

    是为了解决两边顶宽,中间元素自适应布局,中间栏要优先渲染的问题

  • 双飞翼布局

    (1) 多用了一个div,放置内容,利用了子div的margin-left和margin-right这两个属性

    (2)实现:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>双飞翼布局</title>
  <style>
    .col {
      float: left;
    }

    #main {
      width: 100%;
      background: #000;
      height: 400px;
    }

    #main-wrap {
      margin: 0 190px 0 190px;
    }

    #left {
      width: 190px;
      background: red;
      margin-left: -100%;
      height: 400px;
    }

    #right {
      width: 190px;
      background: orange;
      margin-left: -190px;
      height: 400px;
    }
  </style>
</head>

<body>
  <!-- 为了中间的div不被遮挡,直接在中间div里面放置了一个子div,利用margin-left和margin-right为左右两栏留出位置 -->
  <div id="container">
    <div id="main" class="col">
      <div id="main-wrap">
        #main
      </div>

    </div>
    <div id="left" class="col">#left</div>
    <div id="right" class="col">#right</div>
  </div>

</body>

</html>

  • 圣杯布局 (1)利用的是padding属性,float以及position (2)实现
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>圣杯布局</title>
  <style>
    .container {
      border: 1px solid yellow;
      padding: 0 300px;
    }

    .container div {
      float: left;
      position: relative;
    }

    .main {
      width: 100%;
      height: 400px;
      background: orange;
    }

    .left {
      width: 300px;
      height: 400px;
      margin-left: -100%;
      background: pink;
      left: -300px;
    }

    .right {
      width: 300px;
      height: 400px;
      margin-left: -300px;
      background: blue;
      right: -300px;
    }
  </style>
</head>

<body>
  <div class="container">
    <div class="main">main</div>
    <div class="left">left</div>
    <div class="right">right</div>

  </div>
</body>

</html>

垂直居中、水平居中和垂直水平居中?

  • text-align只对行内元素有效,是继承属性,需要在父元素设置 参考链接:juejin.cn/post/691611…

flex布局

  • 又称弹性盒布局,它可以解决元素的居中问题,自动弹性收缩,可以适配不同的屏幕

  • 设置flex后,position、float、vertical-align都将失效,行内元素也可以设置属性inline-flex

  • 采用flex布局的元素,称为flex容器,它的所有元素称为flex项目

  • 容器的属性

    (1)flex-direction:决定主轴的方向,row-沿水平方向,起点在左边,row-reverse- 起点在右边,column-沿垂直方向,起点在上边,column-reverse-起点在下边

    (2)flex-wrap:决定元素换不换行,wrap-换行,nowrap-不换行,wrap-reverse-换行,第一行在下边

    (3)flex-flow:前两个的组合,flex-direction|flex-wrap

    (4)justify-content:水平方向的属性,flex-start-左对齐,flex-end-右对齐,center-居中对齐,space-between-两端对齐,space-around-每个项目两侧的间距相等

    (5) align-items:垂直方向属性,flex-start-交叉轴起点对齐,flex-end-交叉轴终点对齐,center-交叉轴中点对齐,baseline-项目中第一行文字的基线对齐,stretch-如果项目未设置高度或设为auto,将占满整个容器的高度

    (6)align-content: 定义了多根基线的对齐方式,一根基线不起作用,flex-start-交叉轴起点对齐,flex-end-交叉轴终点对齐,center-交叉轴中点对齐,space-between-交叉轴两端对齐,space-around-交叉轴到两侧的距离相等,stretch(默认值)-轴线占满整个交叉轴

  • 项目的属性

    (1)order:决定元素的排列顺序,越小排的越靠前

    (2)flex-grow: 定义项目的放大比例,默认为1,如果都为1,则不发生变化,如果一个项目为2,则这个项目是其他项目的两倍

    (3)flex-shrink:定义项目的缩小比例,默认为1,空间不足,将会缩小,如果一个为0,则它不缩小,其他的缩小

    (4)flex-basis:分配多余空间,项目占据的主轴空间,浏览器根据这个属性计算是否有多余的空间,默认为auto,也可以是length

    (5)flex:flex-grow|flex-shrink|flex-basis的组合

    (6)align-self:允许单个项目与其他项目不一样的对齐方式,默认auto,auto | flex-start | flex-end | center | baseline | stretch

flex:1代表什么(面试问过)

  • 元素占据容器的剩余空间,是flex-grow、flex-shrink、flex-basis的缩写。默认值为0 1 auto
  • flex:auto与flex:1的区别是伸缩时是否将元素尺寸纳入考虑,auto会考虑

怎么做响应式布局?(面试问过)

  • 媒体查询:使用@media 查询,可以针对不同的媒体类型定义不同的样式
  • 百分比:当浏览器的宽度或者高度发生变化时,通过百分比单位,使得浏览器中的组件的宽高随着浏览器的变化而变化,从而实现响应式的效果。
  • vw/vh:相对于视图窗口的宽高,1vw 表示视图宽度的百分之一。
  • rem:rem 是相对于根元素 html 的 font-size 属性,默认情况下浏览器的字体大小为 16px
  • 响应式布局从以下几个方面思考:弹性盒子(图片、表格、视频)和媒体查询技术。使用百分比创建流式布局句的弹性 ui,同时使用媒体查询限制元素的尺寸和内容变更范围。使用相对单位使得内容自适应调节。选择断点,针对不同的断点实现不同布局和内容展示。

js

js数据类型

  • 包括八种:Undefined、Null、Boolean、String、Number、Object(Function、Array)、Symbol、BigInt
  • Symbol和BigInt是ES6新增的数据类型,Symbol是为了定义对象的唯一属性名
  • BigInt可以用来表示任意大小的整数

数据类型的判断

  • typeof:可以判断值类型和函数,不能判断Null和Object,判断Null会返回Object,null是一个空对象指针

  • instanceof:能判断对象类型,不能判断基本数据类型,内部运行机制是判断在原型链中能否找到类型的原型

  • object.prototype.toString.call():能判断所有的数据类型

  • 判断是否为数组:

    (1)Array.isArray(arr) //true

    (2)object.prototype.toString.call() //能判断所有的数据类型

    (3)array instanceof Array

    (4)arr._proto_ = Array.prototype //true

闭包

  • 闭包是指有权访问另一个函数作用域的变量的函数,当函数可以记住和访问当前所在的词法作用域时,就产生了闭包
  • 闭包的用处? (1) 私有化变量 (2)模拟块级作用域 (3)模块化 (4)能够访问函数所在的词法作用域,不让其被回收
  • 闭包的劣势: 闭包中的变量不会被js的垃圾回收机制回收,会一直存在于内存中,容易造成内存泄漏,最好不用

eventloop(面试问过)

  • JS是单线程的,为了防止一个函数执行时间过长阻塞后面的代码,会将同步代码压入执行栈中,依次执行,将异步代码推入异步队列。异步队列又分为宏任务队列和微任务队列,宏任务执行的执行时间长,所以微任务的优先级大于宏任务
  • 先同步后异步先微后宏

事件循环.png

  • 执行顺序:

事件循环.png (1)一开始整个脚本作为一个宏任务执行

(2)执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列

(3)当前宏任务执行完毕后出队,检查微任务列表,直至全部微任务执行完

(4)执行浏览器的UI渲染工作

(5)检查是否有web worker任务,有就执行

(6)执行完一轮宏任务,继续2,直至宏任务和微任务列表为空
  • 宏任务:script、setTimeOut、setIntervel、setImmediate、I/O、UI Rendering

  • 微任务: promise.then()/promise.catch()、mutationObserver、fetch API、Node的process.nextTick、V8的垃圾回收过程

作用域与作用域链

  • 全局作用域、函数作用域、块作用域
  • 作用域:规定了如何查找变量,也就是执行代码对变量的访问权限,如果当前作用域不能查到这个变量,就一层层地向上找,直到找到全局作用域也没有找到,就宣布放弃,这种层级关系叫做作用域链

执行上下文

  • 在执行js代码之前,需要先解析代码,创建一个全局上下文的执行环境,先将代码中所有的函数声明、变量都拿出来,变量赋值为undefined,函数先声明好再使用。
  • 函数上下文:变量定义、函数声明、this、arguments
  • 全局上下文:变量定义、函数声明

this

  • 函数调用:如果一个函数不是一个对象的属性,直接作为函数来调用的话就指向window
  • 方法调用:如果一个函数作为对象的方法来调用时,那个this指向调用函数的对象
  • 构造函数:this指向new出来的对象
  • call、apply、bind调用方式:apply接收的是数组,call接受参数列表,bind方法通过传入一个对象,返回一个this绑定对象的新函数,这个函数除了使用new的时候this指向会改变,其它情况不变
  • 箭头函数:由于箭头函数没有this,所以箭头函数的this取决于定义时的环境

继承

  • 原型链继承:以原型链的方式实现继承,缺点是在包含有引用数据类型的时候,这个对象会被所有的实例对象所共享,造成修改混乱的情况,子类型不能向超类型传递参数
  • 构造函数继承:这种方法是通过子类型的函数中调用超类型的构造函数实现,解决了子类型不能向超类型传递参数的问题,但是构造函数不能复用,超类型的方法子类型不能访问
  • 组合继承:是上边两种方法的组合,通过构造函数来实现类型的属性的继承,通过把子类型的原型设置为超类型的实例来实现方法的继承,解决了单独使用中的问题,但是由于以超类型的实例作为子类型的原型,调用了两次构造函数,所以会多一些不必要的属性
  • 原型继承:基于现有的对象,创建一个对象,方法是通过传入一个对象,来返回该对象的原型,这种方法只是对简单对象的继承,缺点和原型链继承相同,ES5的object.create()就是利用原型链继承实现
  • 寄生式继承:思路是创建一个继承封装的函数,通过传入一个对象,是复制了一份当前对象的副本,然后对对象进行扩展,最后返回这个对象,只是简单对象的继承,如果这个对象不是自定义类型时,无法实现函数的复用
  • 寄生式组合继承:组合继承使用超类型的实例作为子类型的原型,添加了不必要的原型属性,寄生式继承是用超类型的副本当做子类型的原型,避免了创建不必要的属性

原型和原型链(面试问过)

image.png

  • JavaScript是通过构造函数来创建对象的,每个构造函数内部都有一个prototype属性,属性值是一个对象,这个属性上包含了构造函数中所有的实例属性和方法。
  • 如果从一个对象中查找属性,如果对象内部不存在这个属性,就会去原型上找,原型对象又会有自己的原型,于是就一直查找下去,这就叫做原型链。原型链的终点是object.prototype,这就是新建一个对象能够使用tostring的原因。

事件冒泡、事件委托

  • 事件冒泡:如果在一个对象上触发事件,如果此对象上绑定了事件,就触发这个事件,如果没有,就由父对象触发这个事件,这个过程叫做事件冒泡
  • 事件委托:是在事件冒泡的基础上建立,事件冒泡传至父节点,父节点可以通过事件找到目标节点,因此可以把事件绑定到父节点上,由父节点统一处理一个或多个监听函数,这个方式叫做事件委托

跨域(面试问过)

  • 由于浏览器的同源策略,如果端口、域名、协议三者任何一个不同,就属于跨域,这是为了防止xss攻击、csrf的攻击
  • vue中的ProxyTable解决跨域,是利用了webpack的http-proxy-middleware,提供了一个跨域的代理中转服务器服务

image.png

  • cors解决跨域,cors翻译过来就是跨域资源共享,它允许浏览器向跨源服务器发出XMLHttpRequest请求,需要后端配置请求头
Access-Control-Allow-origin:* 代表接受所有域名的请求,也可以指定域名
Access-Control-Allow-Credentials:true 是否允许发送cookie
Access-Control-Expose-Headers: FooBar   // 指定返回其他字段的值
Content-Type: text/html; charset=utf-8   // 表示文档类型
  • Nginx反向代理: (1)反向代理:相当于找中介租房子,客户端发出请求时,我们就可以将请求发送给代理服务器,由代理服务器向真正的服务器请求数据,代理服务器和真正的服务器就相当于一个服务器,再发给客户端,整个过程客户端是无感知的,隐去了服务器的ip,暴露出来的是代理服务器的地址 (2)正向代理:在中国并不能访问Google,但是美国可以,你可以将你的IP变成美国的,那么你就可以访问Google了,正向代理位于客户端和服务器之间,客户端向服务器请求资源,它要先向代理服务器发送请求,由代理服务器指向目标服务器,代理服务器拿到数据发送给客户端,典型代表就是VPN,我们电脑接入vpn后,ip就变成服务器的公网ip,我们就可以请求所需的资源了
  • 正向代理代理的是客户端,反向代理代理的是服务器
  • jsonp解决跨域:是由于script、img、link标签中的src属性不受同源的限制,所以可以用来解决跨域,不过它只能发送get请求,不能发送post请求,请求完之后使用callback拿到返回结果,不会返回状态码
  • postMessage解决跨域:是h5的message里面的api,postMessage()允许来自不同源的脚本采用异步的方法进行通信,可以实现跨文本档、跨窗口、跨域的通信
  • websocket解决跨域:websocket最大的特点就是服务端可以向客户端发送数据它可以传输二进制和文本数据、数据格式比较轻便、对http有良好的兼容性、不受同源策略的限制、标识符是ws服务器网址就是url
  • nodejs中间件也可以解决跨域:当然针对后端是用node写的情况
var express =require('express');
var proxy = require('http-proxy-middleware') app.use('/',proxy({target:'https://www.baidu.com',changeOrigin:true})); 
app.listen(3000);

[]==![]返回结果?(面试问过)

  • 考察的是隐式类型转换
  • 返回true,!将后面的操作数转为bool值,任何对象转为bool值为true,取反为false,两边为对象和布尔,所以转为数字进行比较,[]经过toPrimitive为'',转为数字0,两边都是0,所以返回true
  • {}==!{},!将后面的操作数转为bool值,任何对象转为bool值为true,取反为false,两边为bool和对象,所以转为数字进行比较。Number({})为NAN,NAN与任何值比较,都为false

隐式类型比较规则

  • 同一类型直接进行比较
  • 两者都为简单类型,转为数字后进行比较
  • 简单类型和引用类型进行比较,需要将对象转为原始类型再进行比较,对象先调用toPromitive方法,再调用valueof方法
  • 两者都为引用类型,比较是否指向同一个对象
  • null和undefined相等
  • 任何值与NAN比较,都返回false

js延迟加载的方式

  • JS延迟加载会提高页面的加载速度
  • defer属性、async属性、动态地创建DOM、setTimeOut()让JS最后加载

cookie和session的区别

  • 浏览器的本地存储主要包括cookie、webStorage和IndexDB,webStorage又包括sessionStorage和localStorage
  • 这些都是保存在浏览器端,同源
  • cookie与session的区别 (1)cookie保存的数据不能超过4K,sessionStorage和localStorage可以达到5M (2)sessionStorage保存的数据在会话窗口关闭前有效 (3)localStorage存储的数据始终有效,即使窗口关闭也有效 (4)cookie只在设置的过期时间之前有效,不管浏览器窗口是否关闭 (5)sessionStorage不在不同的浏览器窗口之间共享,localStorage和cookie在同源的窗口之间,数据可以共享

什么会造成内存泄漏

  • 闭包会造成内存泄漏
  • 没有清理的DOM元素会引起内存泄漏,定时器未清理/回调函数
  • 意外的全局变量
  • 子元素存在引用导致内存泄漏

深拷贝、浅拷贝(实现原理)

  • 基础类型存储在栈中,引用类型存储在堆中,指向堆中实际对象的引用。
  • 浅拷贝只拷贝一层,引用类型拷贝的是内存地址,修改对象会影响另一个对象,共用一块内存,只复制指向某个对象的指针
  • 深拷贝开辟出新的栈,两个对象属性完全相同,指向不同的内存地址,一个对象不会影响另一个对象
  • 浅拷贝:object.assign、Array.prototype.slice()、Array.prototype.concat()、...扩展运算符
  • 深拷贝:JSON.stringfy()弊端是不能拷贝undefined symbol函数,lodash的._cloneDeep、手写循环递归函数、jquery.extend()

function deepClone(obj, hash = new WeakMap()) {
    // 处理 null/undefined
    if (obj === null || typeof obj === 'undefined') {
        return obj;
    }

    // 处理 Date 类型
    if (obj instanceof Date) {
        return new Date(obj);
    }

    // 处理 RegExp 类型
    if (obj instanceof RegExp) {
        return new RegExp(obj);
    }

    // 处理基本类型(字符串、数字、布尔、Symbol 等),直接返回
    if (typeof obj !== 'object') {
        return obj;
    }

    // 处理循环引用:如果当前对象已拷贝过,直接返回缓存的拷贝结果
    if (hash.get(obj)) {
        return hash.get(obj);
    }

    // 创建与原对象构造函数一致的新对象(数组会创建 [],普通对象创建 {})
    let cloneObj = new obj.constructor();
    // 将原对象和新对象存入缓存,防止循环引用
    hash.set(obj, cloneObj);

    // 遍历对象的所有自有属性(包括 Symbol 键)
    const keys = Reflect.ownKeys(obj); // 替代 for...in,支持 Symbol 键
    for (const key of keys) {
        // 只处理自有属性(排除原型链上的属性)
        if (obj.hasOwnProperty(key)) {
            // 修复:给新对象的对应 key 赋值,而非覆盖整个 cloneObj
            cloneObj[key] = deepClone(obj[key], hash);
        }
    }

    return cloneObj;
}

promise(面试问过)

  • promise是异步编程的解决方法,promise是一个对象,可以获取异步操作的消息,它是一种承诺,承诺一段时间后会给你一个结果,它有三种状态:pending(等待)、fulfiled(成功)、rejected(失败),状态一旦改变,就不会更改,创建一个promise对象,它会立即执行
  • 给一个数组无论成不成功都会返回结果,除了 race 还有 allsettled。
  • promise 优点:避免回调地狱、错误处理:提供统一的错误处理方式,状态凝固性:一旦状态发生变化,不会再变。
  • promise 缺点:无法取消:一旦新建 promise 就会立即执行,无法取消。错误不暴露:如果不设置回调函数,promise 的错误不会反应到外部。进度不可知:在 pending 状态时,无法得知进展到哪一个阶段
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
function MyPromise(fn) {
// 保存初始化状态
var self = this;
// 初始化状态 this.state = PENDING;
// 用于保存 resolve 或者 rejected 传入的值
this.value = null;
// 用于保存 resolve 的回调函数
this.resolvedCallbacks = [];
// 用于保存 reject 的回调函数
this.rejectedCallbacks = [];
// 状态转变为 resolved 方法
function resolve(value) {
// 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变
if (value instanceof MyPromise) {
return value.then(resolve, reject);
}
// 保证代码的执行顺序为本轮事件循环的末尾
setTimeout(() => {
// 只有状态为 pending 时才能转变,
if (self.state === PENDING) {
// 修改状态 self.state = RESOLVED;
// 设置传入的值 self.value = value;
// 执行回调函数
self.resolvedCallbacks.forEach(callback => {
callback(value); });
}
}, 0);
} 
// 状态转变为 rejected 方法 
function reject(value) { 
// 保证代码的执行顺序为本轮事件循环的末尾
setTimeout(() => { 
// 只有状态为 pending 时才能转变 
if(self.state === PENDING) { 
// 修改状态
self.state = REJECTED; 
// 设置传入的值 
self.value = value; 
// 执行回调函数
self.rejectedCallbacks.forEach(callback => { callback(value); }); 
} 
}, 0); 
} 
// 将两个方法传入函数执行 
try { 
fn(resolve, reject);
}catch (e) { 
// 遇到错误时,捕获错误,执行 reject 函数 
reject(e); 
} 
} 
MyPromise.prototype.then = function(onResolved, onRejected) { 
//首先判断两个参数是否为函数类型,因为这两个参数是可选参数 
onResolved = typeof onResolved === "function" ? onResolved : function(value) {
return value; 
}; 
onRejected = typeof onRejected === "function" ? onRejected : function(error) { 
throw error; 
}; 
//如果是等待状态,则将函数加入对应列表中 
if (this.state === PENDING) {
this.resolvedCallbacks.push(onResolved);
this.rejectedCallbacks.push(onRejected); 
} 
// 如果状态已经凝固,则直接执行对应状态的函数 
if (this.state === RESOLVED) {
onResolved(this.value); 
} 
if (this.state === REJECTED) {
onRejected(this.value);
} 
};

虚拟DOM

  • 虚拟DOM实质是一个JavaScript对象,是对真实DOM一层抽象

  • 浏览器频繁地操作DOM,开销非常大,会产生性能问题,在patch的过程中尽可能将差异更新到DOM中,就不会产生性能很差的情况

  • 为什么操作DOM的开销比较大?

    (1)DOM操作会引起重绘和重排,导致性能开销比较大

    (2)DOM树的实现模块和js模块的跨模块通信增加了成本

  • 为什么虚拟DOM性能更好? (1)虚拟DOM是相对于浏览器渲染的真实DOM而言的,每次查询DOM都要遍历整个DOM树,如果建立一个对应的虚拟DOM对象(js对象),以对象嵌套的形式表示DOM以及层级结构,这样的话DOM操作就变成js对象的增删改查,这样查询js对象的属性比查询DOM的开销小

ajax

  • ajax是一种异步通信方式,从服务端获取数据,来达到页面局部刷新的效果
  • 过程: (1)创建XmlHttpRequest对象 (2)调用open方法传入三个参数,请求方式、url、是否是异步请求(true/false) (3)监听onReadyState事件,当readyState返回4时,返回responseText (4)调用send传递参数

数组方法

  • push():向数组的末尾添加元素,相当于向栈顶添加元素,push进去一个元素之后,打印出来的数组是个length

  • pop():从数组的末尾弹出元素,返回被弹出的元素

  • shift():删除数组中的第一个元素,返回被删除的元素

  • unshift():向数组开头添加元素

  • map():数组的每一项传入函数,返回函数构成结果的数组

  • forEach():无返回值

  • every():对数组的每一项传入函数,如果每一项函数都返回true,方法返回true

  • some():对数组的每一项都传入函数,如果有一项返回true,则这个方法返回true

  • filter():对数组的每一项传入函数,函数返回true的会组成数组返回

  • find():返回第一个匹配的元素

  • findIndex():返回第一个匹配元素的索引

  • join():不改变原数组,以特定的符号分开

  • slice():创建一个包含原有数组中一个或多个元素的新数组,接受两个参数,开始索引和结束索引,如果没有结束索引,则返回开始到最后一个位置,不包含结束位置

  • splice():可以替换、插入、删除,删除:开始位置,要删除的元素数量 插入:开始位置、0(要删除元素个数)、要插入的元素 替换:开始位置、要删除的元素数量、替换的元素

  • reduce():四个参数:归并值、当前项、当前索引、数组自身

  • concat():连接数组,会把数组每一项添加至结果数组,不是数组加到最后面,symbol.isConcatSpreable可以阻止打平数组

  • sort():正序排列

  • reverse():倒序

  • includes():接受两个参数要查找的元素和起始位置,返回true/false

  • indexOf():正序找,返回第一个找到元素的位置

  • lastIndexOf():倒序找,返回找到元素的位置

浏览器垃圾回收机制

  • 标记清除:当变量进入环境时,就给其标记为进入环境,离开环境时标记为离开环境,垃圾回收机制会给存储在内存中的变量都给加上标记,去掉环境中的变量和被环境中变量引用的变量,之后还有标记的就会被垃圾回收机制回收
  • 引入计数:当声明了一个变量并将一个引用类型赋值给该变量时,这个变量的引用次数就为1,如果该变量的值变成了另外一个,这个变量的引用次数就会减1,当引用次数变为0时,说明这个变量没有其他地方在使用,就可以被回收掉,释放内存空间,相互引用会占用大量的内存空间,可能会造成循环引用,如果不及时回收,会造成内存泄漏

ES6新特性

  • 模板字符串
  • 变量声明:let和const
  • 函数:箭头函数、迭代器和for...of
  • class类
  • export和import
  • promise async await
  • 解构赋值
  • set和map数据结构

call、apply、bind区别

  • call、apply、bind都是Function.prototype的一个方法,是由JS的内部引擎实现的,所以每个方法都有这几个默认的方法
  • call和apply、bind的区别? (1)接受的参数不同,call、bind是依次传参,apply只有两个参数,第一个this指向的对象,第二个可以接受数组、类数组或者带下标的集合 (2)call、apply都是直接调用函数,而bind返回的仍然是一个函数,要想执行,需要加() (3)call和apply对比
//apply
let f1 = function(a,b) {
    console.log(a + b);
}
let f2 = function(a,b,c) {
    console.log(a,b,c);
}
f2.apply(f1,[1,2])//1,2,undefined
//执行过程:
(1)先执行f1,并不是输出结果,只是实例化f1,没有返回值,结果undefined
(2)f2执行,此时的this指向window,参数为[1,2],解析后为1,2,undefined
(3)执行后输出1,2,undefined

//call
let f1 = function(a,b) {
    console.log(a + b);
}
let f2 = function(a,b,c) {
    console.log(a,b,c);
}
//传入一个数组,相当于只传了一个参
f2.call(f1,[1,2]);//[1,2],undefined,undefined
//传入两个参
f2.call(f1,1,2);//1,2,undefined
//bind
let  person ={
    name:'小明',
    age:'22',
    sex:'男',
    hobby:'写代码'
    say:function(sex,hobby) {         
        console.log(this.name,this.age,sex,hobby)      
    }
}
let person2 = { name:'小红', age:'23', }
person.say.bind(person2,'男','学习')();

promise与async和await

  • async和await是解决异步回调的终极武器,和promise并不冲突,两者相辅相成
  • 执行async,返回的是promise对象
  • await相当于promise的then
  • try...catch相当于promise里面的catch

哪些对象有...,为什么可以使用...?

  • 可迭代对象,包括数组、类数组对象(包括字符串、arguments对象、NodeList)、set和map
  • 可迭代对象中有iterator接口,任何部署了iterator接口的对象都可以使用
  • 可迭代对象:有[symbol.iterator]属性,数组、字符串是可迭代对象,可迭代对象不一定是数组,iterator-为不同的数据结构提供统一的访问接口,核心是为了ES6的for...of

模块化的标准

CommonJS、AMD、CMD、ES6

  • 模块化的优点:避免命名冲突,更好地复用,按需加载,更高复用性和维护性。
  • commonJS:同步加载,适用于服务端编程,输出一个值的拷贝,运行时加载。
  • AMD/requireJS:异步加载模块,可并行加载多个模块,依赖前置,加载模块后直接执行,无法保证执行顺序。
  • CMD/SeaJS:异步,依赖就近,加载后直到调用才重新执行。
  • ES6:异步,输出的是值的引用,编译时输出,在代码静态解析时就会发生。

babel是怎么将let和const转换为ES5的var的?

  • let和var的区别:var定义的变量存在两种作用域,即全局作用域和函数作用域,let定义的变量是块级作用域,只在定义它的块级代码{}和子块中使用。
  • 无法直接替换的原因是:JS在ES5中缺少块级作用域。
  • Babel采取的方式是:若let处于全局作用域中,直接替换成var,若let处于块级作用域中,换个变量名,直接替换成var,前面加_
  • 若let处于循环语句中,闭包-手动转换闭包,将循环体写成立即执行函数。

Map和object的区别?

  • 键类型:Map的键值是任意类型,object只有字符串/symbol
  • 插入顺序:Map保持插入顺序,object按照属性的一定规则排列(先整数后小数再字符串最后symbol)
  • 迭代器:Map是可迭代对象,支持for...of...和forEach,object默认不可迭代,使用for...in或者方法循环((object.keys(o)、values、entries))
  • 原型:Map默认不会覆盖原型的值,object会
  • 操作方法:Map使用new创建,map.set('a',1),map.get('a'),map.delete('a'),长度使用map.size()获取,object使用点语法或者[],删除需要delete obj.a,长度需要object.keys(obj).length获取

柯里化

  • 返回一个从接收多个参数,变成只接收单一的参数,柯里化的函数不会立即执行,会把传入的参数通过闭包保存起来,直到最后求值的时候,之前的参数一次求值。
  • 应用:参数复用:正则匹配函数、XHR请求、提前返回:不同的环境执行不同的方法,延迟计算:记账

get和post的区别?

getpost
向服务端获取指定资源向服务器提交数据,数据放在请求体里
把参数放在URL中通过request.body传递参数
有长度限制2kb没有限制
只能进行URL编码支持多种编码方式
在浏览器回退时是无害的在浏览器回退时,会再次发送post请求
get请求会被浏览器主动cachepost不会缓存,除非主动设置
请求参数会被完整地保留在浏览器的历史记录里参数不会被保留

数据类型检测方法

  • typeof:判断基本数据类型,其中数组、对象、object、null都会被判断为object,其他的准确。
  • instanceof:通过判断在原型链中能否找到该类型的原型,只能判断引用类型。
//实现原理
function instanceof(left,right){
    let proto = object.getPrototypeOf(left);
    let prototype = right.prototype;
    while(true){
        if(!proto){
            return false;
        }
        if(proto !== prototype){
            return true;
        }
        //如果没有找到,就一直顺着原型链向上找
        proto = object.getPrototypeOf(proto);
    }
}
  • constructor:一是判断数据的类型,二是对象实例通过constructor来访问它的构造函数,如果改变了原型,则不能判断。
  • object.prototype.toString.call():通过object的原型的tostring方法来判断数据类型。

0.1+0.2为什么不等于0.3?

  • 计算器是用二进制存储数据的,0.1的二进制是0.0001100110011001100...(1100循环),0.2的二进制是:0.00110011001100...(1100循环)都是无限循环的数,加起来是0.3000000000004。
  • 解决方案: (1)toFixed(),保留几位小数 (2)先转化为整数,相加之后再转化为小数 (3)设置一个误差范围Number.EPSILON
function numberEpsilon(num1,num2){
    return Math.abs(num2 - num1) < Number.EPSILON;
}

箭头函数

  • 箭头函数不能new
  • new过程 (1)创建一个对象 (2)将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的prototype属性) (3)指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法) (4) 返回新的对象
  • 继承上一级的this,在定义时已经确定了。this指向不会改变。

js 怎么调用字符串‘a’函数?(面试问过)

  • eval(a + '()'),eval 可以执行参数字符串。字符串转为命令行执行可以通过 eval 函数。

forEach怎么跳出循环?

  • try...catch抛出异常
try {
  [1, 2, 3].forEach((item) => {
    if (item === 2) {
      throw new Error("BreakLoop");
    }
    console.log(item); // 输出 1
  });
} catch (e) {
  if (e.message !== "BreakLoop") throw e; // 忽略特定异常
}

图片的懒加载(面试问过)

image.png

  • 图片加载条件:img.offsetTop < window.innerHeight + document.body.scrollTop;
<div class="container">
     <img src="loading.gif"  data-src="pic.png">
     <img src="loading.gif"  data-src="pic.png">
     <img src="loading.gif"  data-src="pic.png">
     <img src="loading.gif"  data-src="pic.png">
     <img src="loading.gif"  data-src="pic.png">
     <img src="loading.gif"  data-src="pic.png">
</div>
<script>
var imgs = document.querySelectorAll('img');
function lozyLoad(){
		var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
		var winHeight= window.innerHeight;
		for(var i=0;i < imgs.length;i++){
			if(imgs[i].offsetTop < scrollTop + winHeight ){
				imgs[i].src = imgs[i].getAttribute('data-src');
			}
		}
	}
  window.onscroll = lozyLoad();
</script>

如何上拉加载,下拉刷新?

上拉加载

image.png

  • scrollTop:滚动视窗的高度距离window顶部的距离,它会随着往上滚动而不断增加,初始值是0,它是一个变化的值
  • clientHeight:它是一个定值,表示屏幕可视区域的高度;
  • scrollHeight:页面不能滚动时也是存在的,此时scrollHeight等于clientHeight。scrollHeight表示body所有元素的总长度(包括body元素自身的padding)
scrollTop + clientHeight >= scrollHeight

实现:

let clientHeight  = document.documentElement.clientHeight; //浏览器高度
let scrollHeight = document.body.scrollHeight;
let scrollTop = document.documentElement.scrollTop;
 
let distance = 50;  //距离视窗还用50的时候,开始触发;

if ((scrollTop + clientHeight) >= (scrollHeight - distance)) {
    console.log("开始加载数据");

下拉刷新

下拉刷新的本质是页面本身置于顶部时,用户下拉时需要触发的动作

关于下拉刷新的原生实现,主要分成三步:

  • 监听原生touchstart事件,记录其初始位置的值,e.touches[0].pageY
  • 监听原生touchmove事件,记录并计算当前滑动的位置值与初始位置值的差值,大于0表示向下拉动,并借助CSS3的translateY属性使元素跟随手势向下滑动对应的差值,同时也应设置一个允许滑动的最大值;
  • 监听原生touchend事件,若此时元素滑动达到最大值,则触发callback,同时将translateY重设为0,元素回到初始位置 Html结构如下:
<main>
    <p class="refreshText"></p >
    <ul id="refreshContainer">
        <li>111</li>
        <li>222</li>
        <li>333</li>
        <li>444</li>
        <li>555</li>
        ...
    </ul>
</main>

监听touchstart事件,记录初始的值

var _element = document.getElementById('refreshContainer'),
    _refreshText = document.querySelector('.refreshText'),
    _startPos = 0,  // 初始的值
    _transitionHeight = 0; // 移动的距离

_element.addEventListener('touchstart', function(e) {
    _startPos = e.touches[0].pageY; // 记录初始位置
    _element.style.position = 'relative';
    _element.style.transition = 'transform 0s';
}, false);

监听touchmove移动事件,记录滑动差值

_element.addEventListener('touchmove', function(e) {
    // e.touches[0].pageY 当前位置
    _transitionHeight = e.touches[0].pageY - _startPos; // 记录差值

    if (_transitionHeight > 0 && _transitionHeight < 60) { 
        _refreshText.innerText = '下拉刷新'; 
        _element.style.transform = 'translateY('+_transitionHeight+'px)';

        if (_transitionHeight > 55) {
            _refreshText.innerText = '释放更新';
        }
    }                
}, false);

最后,就是监听touchend离开的事件

_element.addEventListener('touchend', function(e) {
    _element.style.transition = 'transform 0.5s ease 1s';
    _element.style.transform = 'translateY(0px)';
    _refreshText.innerText = '更新中...';
    // todo...

}, false);
  • 当前手势滑动位置与初始位置差值大于零时,提示正在进行下拉刷新操作
  • 下拉到一定值时,显示松手释放后的操作提示
  • 下拉到达设定最大值松手时,执行回调,提示正在进行更新操作

Vue

MVVM的理解

MVVM是Model-view-viewModel的缩写,其中Model是数据模型层,view是视图层,viewModel是数据和视图之间的桥梁,数据绑定到viewModel自动渲染到页面,视图变化会通知viewModel更改数据

computed和watch的区别(面试问过)

  • computed是计算属性,是带缓存的watcher,有返回值,当依赖的属性没有发生变化时,会从缓存中读取,如果一个数据依赖于其他数据,可以使用computed
  • watch: 每次都执行函数,没有缓存,用于监听数据的变化,如果在数据变化时需要执行一些操作时,异步操作或者开销较大时,可以使用watch

wacth和watchEffect的区别?(面试问过)

  • watchEffect 接收一个函数作为参数,会初始化执行一次,在副作用发生期间追踪依赖,自动分析出侦听数据源;这个函数会立即执行,并响应式的追踪其依赖,并在依赖变化时重新运行。
import { ref, watchEffect } from "vue";

const count = ref(0);

watchEffect(() => {
  console.log(count.value);
});
  • watch 接收两个参数,要观察的数据源和一个回调函数,第三个为配置参数,immediate 和 deep(立即执行和深度监听),当数据源发生变化时,回调函数会被调用,与 watchEffect 不同的是,watch 允许访问前一个和当前的值。
import { ref, watch } from "vue";

const count = ref(0);

watch(count, (newValue, oldValue) => {
  console.log("The new count is: " + newValue);
  console.log("The old count was: " + oldValue);
});
  • watchEffect 适合没有特定数据源,或者需要观察多个数据源的场景,watch 适合需要访问前一个和当前值的场景。

v-if和v-show区别(面试问过)

  • v-if是动态的添加和删除DOM,v-show是通过display属性来控制元素的显示和隐藏
  • v-if切换有一个局部的编译/卸载过程,切换过程中会销毁和重建事件监听和子组件,v-show只是基于css切换
  • v-if编译惰性,只有第一次条件为真时才会编译,v-show在任何情况下,条件是否为真,都会被编译,缓存dom元素
  • v-if适合条件运行条件不大可能改变,v-show适合频繁的切换

history和hash(面试问过)

(1)location.protocol:协议,http

(2)location.hostname:主机名,127.0.0.1

(3)location.host:主机,127.0.0.1:8080

(4)location.port:端口,8080

(5)location.pathname:访问页面,a.html?b=10&c=20#

(6)location.search:搜索内容,?b=10&c=20

(7)location.hash:哈希值,/age

优点:

hash的改变会触发hashChange事件,能控制浏览器的前进与后退,兼容性比较好,hash值会出现在url中,不会包含在http请求中,不会引起页面的重新加载

缺点:url中带#号,不太美观,只能修改#后面的部分,所以只能设置与当前url同文档的url,有体积限制,不 能设置太长,每次只有url发生变化时,才会触发hashChange事件,url的变化不会发请求,不利于seo,因为传不到后端

  • history模式:onpopState事件触发,利用h5的history interface新增的pushState()和replaceState()方法,pushState()会改变url但是不会发送请求,replaceState()可以读取历史记录栈,还可以对浏览器的历史记录进行修改 优点:美观,pushState()可以设置与当前url同源的url,也可以设置与当前url一样的url,也会添加进栈中,可以设置title,可以通过stateObject添加任意数据类型的数据到记录中,浏览器的前进后退会触发onPopState事件,通过window.location.pathname可以控制页面变化

缺点:url的改变会发送http请求,根据pushState()来实现无刷新跳转,由此会重新请求服务器,前端的 URL 必须和实际向后端发起请求的 URL 一致,需要后端设置,如果url和后端配置的url不一致,则请求不到资源,会报404

父子组件通信方式(面试问过)

  • 父向子通信:子组件定义props,父组件通过v-bind进行值的绑定
  • 子向父通信:子组件通过$emit来触发自定义事件,父组件通过绑定监听器获取从子组件传递过来的参数,子组件设置ref,父组件通过设置子组件的ref来获取数据
  • 兄弟组件通信:eventbus、通过共同的祖辈parentparent和root来建立通信桥梁
  • 跨级通信:事件总线、vuex、provide与inject,祖先向子孙通信可以通过attrsattrs和listeners

vuex数据流转过程(面试问过)

  • vuex是状态管理模式,可以进行全局状态的管理
  • State:存储数据,相当于vue中的data
  • Mutations: 唯一可以改变State的方法就是通过提交Mutations里的方法
  • Actions: 可以异步处理请求,通过dispatch进行触发
  • Getters:相当于vue中的计算属性,可以处理数据
  • Modules: 对数据进行模块化

image.png 总结: (1)通过new Vuex.store()创建一个仓库state存储公共状态,state->vue Components渲染页面 (2)通过this.store.state.xxx获取属性,来进行渲染 (3)当需要修改数据的时候,需要遵从单向数据流,需要通过this.$store.dispatch触发Actions中的方法 (4)Actions中方法的触发需要接受一个对象,里面包括commit方法,用于触发Mutation (5)Mutations中的方法用来修改state中的数据,接受两个参数,state和需要传的参数 (6)Mutations方法执行完毕,state改变,由于vueX中的数据是响应式的,所以组件状态会发生改变

mutations和actions区别?(面试问过)

  • action 可以包含任何异步操作,action 提交的是 mutation context.commit('increment')
  • mutation 是修改 state 的唯一方式,action 用于复杂业务代码和异步请求
  • mutation 必须同步执行,action 可以异步操作
  • 视图更新时,先触发 actions,actions 触发 mutation
  • mutation 的参数是 state,action 的参数是 context,是 state 的父级

keep-alive(面试问过)

  • keep-alive可以实现组件的缓存,当组件进行切换时,不会卸载组件,它有两个生命周期,activated是当组件激活时使用,假如有两个组件Home.vue和About.vue,当切换到About时,created和activated会同时出发,当再次切换时,只会触发activated,created只会触发一次,deactivated在组件失活时触发,切回Home时,deactivated会触发
  • 有两个属性:include和exclude,接受字符串或正则表达式,include是满足条件缓存,exclude相反
  • exlude和include同时存在时,组件不会被缓存
  • max 来限制组件的最大数,缓存组件不能超过 max,最久没有被访问的缓存实例会被销毁,以便为新实例腾出空间。
  • keep-alive 的应用场景:查看表格某条数据的详情页,返回还是之前的状态(之前的筛选结果,页数等)。填写的表单内容路由跳转返回还在,比如 input、select 框,输入一堆东西,跳转回来不能清空让用户再填写一遍。

vue中如何做性能优化(面试问过)

编码优化:

  • keep-alive缓存组件
  • 防抖和节流
  • 路由懒加载、异步组件
  • key唯一
  • 事件代理 加载优化:
  • 按需加载
  • 图片懒加载

用户体验优化:

  • 骨架屏等
  • seo

常见的指令

v-model、v-bind、v-if、v-for、v-show、v-html、v-text、v-once

v-model

@input和value的语法糖,通过v-bind向输入框绑定一个值,在@input事件里,将用户输入的值赋值给输入框绑定的值

<template>
  <div class="container">
    <input v-bind:value="msg" @input="inputMethods" type="text"/>
    <div class="msg">{{msg}}</div>
  </div>
</template>
<script>
export default  {
  name: '',
  data(){
    return {
      msg: '你好'
    }
  }
  methods: {
    inputMethods(e){
      this.msg = e.target.value;


    }
  }
}
</script>

vue的双向绑定原理(面试问过)

  • 版本1:当Vue实例被创建时,Vue会遍历所有data数据的属性,通过object.defineProperty给它们转为getter/setter并在内部追踪相关依赖,当属性修改或访问时通知其变化。每个组件都有相应的watcher程序实例,它能在组件渲染的过程中将属性转为依赖,当setter被调用时,通知watcher重新计算,这样组件就能得到更新。
  • 版本 2:vue 采用数据劫持结合发布-订阅者模式,通过 object.defineProperty 来劫持各个属性的 setter、getter,在数据变化时发布消息给订阅者,触发相应的监听回调。 (1)需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 getter 和 setter,这样对属性赋值的话,就能触发 setter,就能监听到数据变化。 (2)compile 解析模版指令,将模版中的变量替换成数据,然后初始化渲染视图。并将每个指令的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变化,收到通知,更新视图。 (3)watcher 是 compile 和 observer 之间的桥梁,主要做的是<1>在自身实例化时往属性订阅器(dep)中加自己<2>自身有 update 方法<3>在属性 dep.notify()时,调用自身的 update()方法,触发 compile 的回调 (4)mvvm 作为数据绑定的入口,整合 compile、watcher 和 observe,通过 observe 来监听自己的 model 变化,通过 compile 来解析编译模版指令,利用 watcher 搭建起 observe 和 compile 的桥梁,达到数据变化-视图更新,视图变化(input)-model 更新.

nextTick

  • nextTick是Vue提供的一个全局API,它在下次DOM更新循环结束时延迟回调,在修改数据后调用nextTick,会在回调中获取更新后的DOM
  • Vue更新DOM是延迟执行的,只要监听到数据变化,Vue就会开启一个队列,缓冲所有的数据变更,假如一个watcher被触发多次,那么只会推入到队列一次,避免重复的数据进行计算和DOM操作,nextTick方法在队列中加入一个回调函数,该函数在DOM操作结束后才会调用
  • 主要利用了宏任务微任务那些,定义了一个异步方法,多次调用nextTick会将函数存入异步队列,之后用异步方法清除

父子组件生命周期的执行顺序(面试问过)

  • 父子组件生命周期执行顺序:父beforeCreate->父created->子beforeCreate->子created->子mounted->父mounted
  • 更新过程:父beforeUpdate->子beforeUpdate->子updated->父updated
  • 销毁过程: 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

生命周期(面试问过)

vue2

  • beforeCreate:Vue实例创建前,此时data和methods还没有初始化
  • created:Vue实例已经被创建,data有值,可以在这个阶段发请求,获取数据
  • beforeMount:Vue实例挂载前,此时还不能操作DOM
  • mounted:Vue实例已经被挂载到真实的DOM节点上,可以操作DOM
  • beforeUpdate:实例更新前,页面数据尚未发生改变
  • updated:实例已经更新完毕,data改变,组件被重新渲染
  • beforeDestroy:组件销毁前,都还能用
  • destroyed:Vue实例被销毁,data、methods这些都不能用了

image.png

vue3

  • setup相当于vue2的beforeCreate和created
  • onMounted(): 注册一个回调函数,在组件挂载完成后执行。这个钩子在服务器端渲染期间不会被调用
  • onUpdated():注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。父组件的更新钩子将在其子组件的更新钩子之后调用。不要在 updated 钩子中更改组件的状态,这可能会导致无限的更新循环
  • onUnmounted():注册一个回调函数,在组件实例被卸载之后调用。这个钩子在服务器端渲染期间不会被调用,可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接
  • onBeforeMount():注册一个钩子,在组件被挂载之前被调用。这个钩子在服务器端渲染期间不会被调用
  • onBeforeUpdate():注册一个钩子,在组件即将因为响应式状态变更而更新其 DOM 树之前调用。这个钩子在服务器端渲染期间不会被调用 -onBeforeUnmount():注册一个钩子,在组件实例被卸载之前调用。这个钩子在服务器端渲染期间不会被调用,组件实例还保留所有的功能 image.png

diff算法(面试问过)

  • 首先,对比节点自身,判断是否为同一节点,如果不为相同节点,则删除自身节点重建节点进行替换。
  • 如果为相同节点,则 patchVnode,判断对该节点的子节点如何处理,如果一方有子节点一方没有子节点,如果新的 children 没有子节点,则把旧节点移除掉
  • 如果都有子节点,则 updateChildren,判断对这些新老节点进行操作,匹配时,找到相同的子节点,最后递归子节点 答案 2:
  • 同级比较,再比较子节点
  • 先比较一方有子节点另一方没有子节点的情况(如果新的节点没有 children,则把旧的节点移除)
  • 比较都有子节点的情况(核心 diff)
  • 递归比较子节点
  • vue2 采用的双端比较算法,同时从新旧 children 的两端进行比较,借助 key 值找到可复用的节点,再进行相关操作。
  • vue3 采用的动态规划的最长递归子序列,借助 ivi 和 inferno 算法
  • 一个树的时间复杂度是o(n^3),Vue将其优化为O(n)

  • diff算法就是要保证最小量更新,所以v-for里面必须要有key,没有key会造成暴力复用,加key会减少操作DOM,只会比较同层的,不会跨层进行比较,只有是同一虚拟DOM节点才会进行比较

  • 旧前: oldChildren中所有未处理的第一个节点

  • 旧后:oldChildren中所有未处理的最后一个节点

  • 新前: newChildren中所有未处理的第一个节点

  • 新后: newChildren中所有未处理的最后一个节点

  • 新前与旧前:比较这两个节点是不是同一节点,如果是同一节点,由于位置相同,则不需要进行移动操作,只需要更新节点即可

  • 新后与旧后:对比这两个节点是不是同一节点,如果是同一节点,就将这两个节点对比更新视图,由于位置相同,也不需要移动节点的操作,只需要更新节点就行

  • 新前与旧后:对比这两个节点是不是同一节点,如果是同一节点,由于位置不同,需要进行节点的移动操作,要将新前移动到oldChildren的所有未处理节点的前面,才能满足新前之前的位置,如果不是同一节点,接着进行对比

  • 新后与旧前:对比这两个节点是不是同一节点,如果是同一节点,由于位置不同,需要进行节点的移动操作,需要把新后移动到oldChildren所有未处理节点的后面,同上道理

data为什么是函数

避免组件间的数据相互影响,同一个组件可能被复用多次创建不同的Vue实例,如果data是一个对象的话,用的就是同一个构造函数,所以为了保证数据的独立性,每个组件通过data函数返回一个对象来作为组件的状态

v-for中key的作用

  • 用来对比虚拟DOM中的每个节点是否是同一节点

  • Vue在patch的过程中判断两个节点是否是同一节点,就是通过key判断的,相同就复用,不相同就删除旧的创建新的,如果不添加key,组件就默认复用,不会删除节点添加新节点,只是会改变列表中的文本值,中间需要大量的DOM操作,非常耗费性能

vue组件渲染和更新过程

渲染组件时,通过Vue.extend()方法构建构造函数,进行实例化,最后调用$mount()进行挂载。更新组件时,通过patchNode,核心是diff算法

newVue之后发生了什么

  • initProxy:作用域代理,拦截组件内访问其他组件的数据
  • initLifeCycle:建立父子组件关系,在当前组件实例上添加一些属性和生命周期标识
  • initEventBus:对父组件传入的事件添加监听,事件是谁创建谁监听,子组件创建父组件监听
  • initRender:生命slots和createElement()等
  • initInjections:注入数据,初始化inject,适用于组件嵌套层级深的通信
  • initState:数据响应式,初始化状态,比如data、methods、props、computed、watch等
  • initProvide: 提供数据注入

defineProperty和proxy的区别?

  • defineProperty在vue初始化时,使用object.defineproperty将所有属性转为getter和setter,属性改变时,调用setter,添加或删除属性时,监测不到属性的变化,需要使用$set,无法监控数组下标和长度的变化
  • proxy直接代理整个对象而不是属性,只需要做一层代理就可以监听同级结构下所有属性的变化,包括新增和删除属性,可以监听数组的变化。

vue3相对于vue2的性能提升(面试问过)

  • 虚拟 DOM 重构,vue3 采用了基于 proxy 的响应式系统,替代了 Vue2 的 object.defineProperty,能很好的处理数组和 set 等数据结构
  • 组件渲染优化:vue3 引入了基于类型的优化策略,如 patchFlag,能更好的处理组件渲染
  • 更新机制优化:按需更新组件,避免不必要的 DOM 操作
  • 打包优化:通过插件机制提供更好的 tree-shaking,打包时去除未使用的代码,减少打包文件的大小
  • 运行时优化:vue3 引入 compositionAPI,允许开发者以更简洁的代码组织业务逻辑,避免不必要的组件渲染
  • 更好的 ssr:ssr 加载性能提升

vue3中还用object.defineProperty吗?(面试问过)

  • ref用的是object.defineProperty来对数据进行双向绑定,用于基础类型,proxy用于对象类型,Proxy可以监听对象上所有的属性访问和修改操作,而不仅仅是单个属性的访问和修改。

vue2和vue3的区别?(面试问过)

  • API 风格:vue3 采用 compositionAPI,面向函数编程,使用函数和组合来组合代码,更加灵活和强大。
  • 组件通信:vue2 通过 emit 和 props 来进行组件通信,vue3 用 defineEmits 和 defineProps 来定义 props 和事件,类型更加安全和清晰。
  • 生命周期钩子:vue3 去除 beforeCreated、created、beforeMount->onMount,mounted->onMounted,beforeUpdate->onBeforeUpdate,updated->onUpdated,beforeDestroy->onBeforeUnmount,destroyed->onUnMounted
  • ts 支持:vue3 默认支持 ts,vue2 需要额外的配置才能支持 ts
  • 响应式:vue2 响应式采用 object.defineProperty,使用 getter 和 setter 来进行数据劫持。vue3 使用 proxy 来代理对象,能够自动追踪其依赖,实现更高效的响应式更新。
  • vue2 只支持一个根节点,vue3 支持多个根节点,允许更灵活的模版结构,减少标签层级。

vue的打包工具有哪些?(面试问过)

Webpack

Webpack 是 Vue 项目中最常用的打包工具之一。它具有丰富的插件生态系统和强大的配置能力,适合复杂项目。Webpack 可以将项目中的各种资源(如 JavaScript、CSS、图片等)打包成一个或多个文件。其核心特点包括:

  • 模块化管理:将项目中的所有文件视为模块,便于管理依赖关系。
  • 插件系统:丰富的插件生态系统,能够实现代码分割、热更新等功能。
  • 高度配置化:通过配置文件(如 webpack.config.js)可以对打包过程进行详细控制。

Vite

Vite 是一个新兴的打包工具,特别适合现代前端开发。其核心特点包括:

  • 极速冷启动:利用浏览器的原生 ES 模块支持,提供快速的开发体验。
  • 即时热更新:通过 HMR(热模块替换),实现毫秒级的热更新。
  • 零配置:默认配置已足够强大,无需额外的配置文件。

Parcel

Parcel 以零配置著称,适合小型项目或快速原型开发。其核心特点包括:

  • 零配置:无需复杂的配置文件,适合快速开发和测试。
  • 快速构建:构建速度快,适合小型项目或快速迭代的需求。

vue的动态组件(面试问过)

  • 允许根据不同的条件或数据动态的渲染不同的组件
  • 使用is属性绑定组件名
//使用组件名
<template>
  <div>
    <component :is="currentComponent"></component>
  </div>
</template>
 
<script>
export default {
  data() {
    return {
      currentComponent: 'MyComponent'
    };
  },
  components: {
    MyComponent: {
      template: '<div>This is MyComponent</div>'
    }
  }
}
</script>
//使用构造函数
<template>
  <div>
    <component :is="currentComponent"></component>
  </div>
</template>
 
<script>
import MyComponent from './MyComponent.vue';
 
export default {
  data() {
    return {
      currentComponent: MyComponent
    };
  }
}
</script>

  • 使用v-bind绑定动态组件名或构造函数
<template>
  <div>
    <component :is="dynamicComponent"></component>
  </div>
</template>
 
<script>
import MyComponent from './MyComponent.vue';
import AnotherComponent from './AnotherComponent.vue';
 
export default {
  data() {
    return {
      componentType: 'MyComponent' // or 'AnotherComponent' based on your logic
    };
  },
  computed: {
    dynamicComponent() {
      switch (this.componentType) {
        case 'MyComponent': return MyComponent;
        case 'AnotherComponent': return AnotherComponent;
        default: return null; // or a default component if needed
      }
    }
  }
}
</script>
  • 使用keep-alive保存组件状态
<template>
  <div>
    <keep-alive>
      <component :is="currentComponent"></component>
    </keep-alive>
  </div>
</template>
  • 使用:key确保正确的销毁和创建过程
<template>
  <div>
    <component :is="currentComponent" :key="currentComponent"></component> 

vue的SSR

  • 白屏时间更短
  • 便于seo,搜索引擎抓取,便于爬虫
  • Vue 的 SSR(Server-Side Rendering,服务端渲染)是一种将 Vue 应用在服务器端渲染成HTML 字符串,然后发送到客户端的技术。与传统的客户端渲染(CSR)相比,SSR 可以提升首屏加载速度、改善 SEO(搜索引擎优化),并提供更好的用户体验。
  • 同构:代码既可以在客户端使用,也能在服务器端使用。(服务端首屏渲染和客户端激活问题)

image.png

如何做首屏幕渲染?(面试问过)

  • 减少网络请求,多次请求合并为一个,使用缓存,将静态资源保存至本地。
  • 图片优化:图片懒加载、使用适当的图片格式和算法,减少图片文件大小。使用雪碧图(精灵图)整合多张图片,减少 http 请求次数。
  • 使用骨架屏进行预渲染:对页面进行预渲染,提前将页面以骨架屏的形式展示给用户,提升用户体验。
  • 使用缓存:利用浏览器缓存,减少重复请求和加载时间。
  • 异步加载:使用异步加载的方式加载 js 文件,避免页面阻塞。
  • 代码优化:删除不必要的代码和注释,对代码进行优化,减少文件大小。
  • 推迟加载:将一些不必要的资源推迟加载,等页面加载完成后再加载这些资源。
  • 使用 CDN 加速:使用内容分发网络(CDN)来加速资源的加载,将资源分发到离用户最近的服务器上,减少网络延迟。

React

组件基础

react的事件机制

  • react并不是将事件监听绑定到真实DOM上,而是在document上监听所有的事件,事件发生冒泡到document上时,事件处理函数才会执行。
  • react冒泡到document上的事件不是原生事件,而是合成事件,阻止冒泡用event.preventDefault()方法。
  • 能够兼容所有的浏览器,更好的跨平台。原生事件中,监听一个事件,就会创建一个事件分配对象,如果创建的对象很多的话,会造成内存的浪费。对于合成事件,会创建一个事件分配池,来管理对象的创建和销毁,使用时复用,事件回调结束后,进行销毁。

react高阶组件、hooks、render props有什么区别,为什么迭代?

  • 高阶组件是通过传入一个组件或者其他参数(有需要的话),返回另一个组件,包裹组件的props容易与被包裹的组件重名,进而被覆盖。
  • props是react组件间通过值为prop的函数来共享代码的方式。数据共享,代码复用,无法在return外进行数据的访问。
  • hooks解决了props的层级嵌套过深和高阶组件的重名问题,但是只能在组件顶部使用。

对react-fiber的理解,解决了什么问题?

  • 由于react在渲染时,会递归比较虚拟DOM,同步更新,这个过程会占用浏览器资源。
  • 不让浏览器的渲染、绘制、布局、资源加载和事件响应和脚本执行一起执行,通过调度策略来分配资源。
  • fiber是一种协程,让cpu可中断,是一种控制流程的让出机制,让cpu在这段时间内可以干其他的事情。

react高阶组件是什么,和普通组件有什么区别,使用场景

  • 高阶组件接受一个组件作为参数,返回一个新的组件,是纯组件。优点是逻辑复用,不影响包裹组件的内部逻辑,缺点是传递给包裹组件的props容易和包裹后的组件重名,进而被覆盖。
  • 使用场景:可以用于权限控制。

react哪些方法会触发重新渲染?

  • 使用setState,state不发生改变或者为null时不会重新渲染。
  • 父组件重新渲染,即使传给子组件的props不发生改变也会重新渲染。
  • 重新渲染render时,会根据diff算法比较新旧dom树的差异,对新旧两棵树进行深度优先遍历,如果有差异放到一个对象中,遍历差异对象,根据对应的规则更新vnode。

状态组件和无状态组件?

  • 状态组件通常是类组件,可以使用生命周期函数,对state进行管理,可继承,可使用this
  • 无状态组件可以是类组件也可以是函数组件,不用使用this,不可以使用生命周期有更高的性能,专注于render。

受控组件和非受控组件?

  • 受控组件例如input、select通过onchange来改变组件的状态state,这种需要通过value或checked来改变组件状态的叫做受控组件,react官方推荐使用受控组件。流程:通过state来赋初始值,表单发生变化时,调用onchange事件,事件处理器拿到e之后来改变组件状态,setstate更新state,页面重新渲染。受控组件有多个的话,代码会显得臃肿。
  • 非受控组件,通过ref来获取表单数据,真实数据放在dom节点中,代码中同时存在react代码和非react代码,影响代码美观性,但是可以快速编写代码,减少代码量。

类组件和函数组件

  • 类组件主打的生命周期和继承,函数组件强调的是函数式编程,immutable、没有副作用,引入透明。
  • 类组件使用shouldComponentUpdate阻断渲染来提升性能,函数组件使用react.memo缓存来提升性能。
  • 函数组件轻便,迎合reacthooks,生命周期的概念也在淡化,组合大于继承,所以推荐使用函数组件。

数据管理

react setState调用原理

image.png

react setState调用之后发生什么?同步还是异步?

  • 调用setState后,react会将当前差异与组件的状态合并,以相对高效的方式重建react树并渲染页面。
  • react会自动计算新树和老树的差异,根据差异最小化重新渲染,按需更新,短时间触发多次setState会将差异压入栈中,合适的时机,批量更新state和视图。
  • 默认是异步的,如果是同步每执行一次setState就会重新渲染,造成性能浪费。react无法控制的如addEventListener、setTimeOut等同步更新
  • 如果更新了state,没有render,props和state不能保持一致性,会出现问题。

react组件的state和props区别?

  • props用于父组件向子组件传递数据,不可更改。
  • state是保存组件的状态,在constructor时初始化,只能通过this.setState更改,修改state会重新渲染页面。

生命周期

react的生命周期有哪些?(面试问过)

  • 有三个阶段:装载、更新、卸载
  • React的生命周期可以分为三个主要阶段:挂载(Mounting)、更新(Updating)、卸载(Unmounting)。

挂载阶段:

constructor()

getDerivedStateFromProps()

render()

componentDidMount()

更新阶段:

getDerivedStateFromProps()

shouldComponentUpdate()

render() getSnapshotBeforeUpdate()

componentDidUpdate()

卸载阶段:

componentWillUnmount()

react的hooks(面试问过)

  • 使函数组件具有类组件的能力
  • useState:在函数组件中添加局部状态和更新状态
  • useEffect:用于在组件渲染后执行副作用的操作,比如数据获取、订阅管理等,还可以清理资源,比如清除定时器
  • useContext:用于在函数组件中访问react上下文的值
  • useReducer:处理复杂的状态逻辑,使用reducer函数来更新状态
  • useCallback:用于记忆化回调函数,避免每次渲染时重新创建函数,提高性能
  • useMemo:缓存计算结果,避免在每次渲染时重新计算结果
  • useRef:用于在组件的生命周期中保持一个可变的引用值
  • useLayoutEffect:在所有的DOM变化之后调用,常用于测量布局并重新更新DOM
  • useImperativeHandle:用于父组件中直接调用子组件的方法或访问其DOM节点
  • hooks优势: 简化逻辑复用、关注分离、避免副作用

react的加载慢怎么解决(面试问过)

  • 使用代码分割,使用动态的import进行代码分割,只加载当前需要的组件或模块。
  • 优化组件渲染,使用React.memo来避免纯函数组件的重复渲染,或者使用shouldComponentUpdate或者pureComponent避免类组件的重复渲染,使用fragment减少不必要的dom元素,提高渲染效率,使用useCallback和useMemo缓存函数和变量,避免重新计算
  • 延迟非关键资源的加载比如懒加载图片和懒加载组件
  • 优化资源加载:压缩资源、利用cdn、缓存策略
  • 减少第三方库的使用
  • 使用性能分析工具
  • 服务端渲染或者预渲染
  • 减少重定向和dns查找时间

react性能优化在哪个生命周期,优化的原理是什么?

-shouldComponentUpdate来阻断渲染

组件通信

父子组件通信方式

  • 父向子通信:父组件通过props传递数据给子组件。
  • 子向父通信:props+回调

跨级组件通信方式

  • 使用props
  • 使用context,不管嵌套多深,都能使用。

如何解决props层级过深问题

  • context,相当于一个容器,组件间的状态共享
  • redux

组件的通信方式有哪些?

  • 父向子通信:props
  • 子向父通信:props+回调
  • 兄弟组件通信:找到共同的父节点,由父节点转发信息进行通信
  • 跨级组件通信:context
  • 发布订阅模式:通过event
  • 全局状态管理工具:redux

路由

react-router实现原理是什么?

  • hash,通过监听hashChange事件,改变hash通过location.hash = xxx
  • historyApi,通过history.pushState和replaceState将url压入堆栈,监听url的变化通过自定义方法实现

react-router路由有几种模式?

  • BrowserRouter
  • hashRouter

redux

redux解决了什么问题?

  • redux是用来管理数据状态和UI状态的应用工具,项目大时,只能通过props向下传递数据,由于react是单向数据流,一个model的变化引起另一个model的变化,视图变化时,又会引起model的变化,管理麻烦
  • redux解决了将redux的状态和UI绑定在一起,dispatch action时会自动更新页面。

redux的理解

  • 单一数据源
  • state 是只读的
  • 使用纯函数来执行修改

image.png React Components 是借书的用户, Action Creactor 是借书时说的话(借什么书), Store 是图书馆管理员,Reducer 是记录本(借什么书,还什么书,在哪儿,需要查一下), state 是书籍信息

const redux = require('redux');

const initialState = {
  counter: 0
}

// 创建reducer
const reducer = (state = initialState, action) => {
  switch (action.type) {
    case "INCREMENT":
      return {...state, counter: state.counter + 1};
    case "DECREMENT":
      return {...state, counter: state.counter - 1};
    case "ADD_NUMBER":
      return {...state, counter: state.counter + action.number}
    default: 
      return state;
  }
}

// 根据reducer创建store
const store = redux.createStore(reducer);

store.subscribe(() => {
  console.log(store.getState());
})

// 修改store中的state
store.dispatch({
  type: "INCREMENT"
})
// console.log(store.getState());

store.dispatch({
  type: "DECREMENT"
})
// console.log(store.getState());

store.dispatch({
  type: "ADD_NUMBER",
  number: 5
})
// console.log(store.getState());
  • createStore可以帮助创建 store
  • store.dispatch 帮助派发 action , action 会传递给 store
  • store.getState 这个方法可以帮助获取 store 里边所有的数据内容
  • store.subscrible 方法订阅 store 的改变,只要 store 发生改变, store.subscrible 这个函数接收的这个回调函数就会被执行

redux和vuex有什么区别

  • vuex改进了reducer和action,以mutation函数变化取代reducer,不需要switch,直接在mutation里改变state
  • vuex由于vue会重新渲染,无需调用重新渲染函数,生成新的state就行
  • vuex调用store的commit来提交mutation进行重新渲染

hooks

对react hooks的理解,实现原理?

  • react设计思想是函数,hooks是为了补全函数组件缺少的生命周期、对state的管理等

useState为什么要使用数组而不是对象?

  • 降低使用的复杂度,使用数组可以直接解构赋值,使用对象,如果多次使用需要别名

react hooks解决了什么问题?

  • 在组件间复用代码很难:通过props,高阶组件会使代码无法理解。provider、consumers与props结合会造成回调地狱。hooks将状态提取成公共函数,可以复用状态逻辑。
  • 难以理解的class:代码冗余
  • 复杂组件难以理解:生命周期函数包含不相关的逻辑,无法复用代码,不可将组件拆成更小的粒度。hooks将相关联的部分拆成更小的函数,便于复用

虚拟DOM

react diff算法原理是什么?

  • diff算法讨论的是虚拟dom发生变化时,生成dom更新补丁的方式。比较新旧dom树的差异,以最小成本完成视图更新。
  • 过程:真实dom转为虚拟dom,虚拟dom发生变化时,通过计算生成patch,patch是个结构化数据,包括节点的添加、更新、删除等。根据patch,更新视图。
  • 忽略跨层级比较,只比较同一层级的节点。
  • 只比较同class的组件,class名称相同,则认为为相似的树结构。父组件类型不同,就会重新渲染,这就是shouldComponentUpdate、pureComponent、React.memo能提高性能的原因。
  • 同一层级的节点,通过标记key进行比较。通过标记key,可以移动DOM,降低内耗。

react key是解决什么问题的,为什么要加key?

  • 用于追踪列表中哪些元素被修改、添加或移除的标识,减少不必要的重新渲染。尽量不要使用index作为key,key应当与具体的元素一一对应,不要在render时用随机数或其他操作作为key,比不加key更糟糕。

react的diff算法和vue有何不同?

  • diff算法包括触发更新-生成补丁-应用补丁
  • react的diff算法触发更新的时机在state变化和hooks之后,此时应用深度优先遍历,为了优化效率,采用组件、类、元素的比对。vue缺乏时间分片能力,但不意味着性能差,除动画外,可用防抖和节流提高性能。

其他

react和vue的异同

  • 同:都把注意力放在核心库,把全局状态管理和路由交给其他库;都用虚拟DOM来提升性能;都有自己的构建模版,快速搭建项目模板;都使用props来进行数据传递;都鼓励使用组件,提高复用性;
  • 异:vue支持双向绑定,react提倡单向数据流;vue能更快的比较出dom的差异进行渲染,react每当state改变时,子组件会全部渲染,可用shouldComponentUpdate和pureComponent来控制;支持跨平台vue-weex,react-react native;监听数据变化的原理不同,vue使用proxy,react通过比较引用,如果不优化,容易造成性能问题;

react为什么要用jsx?

  • react需要将组件转虚拟DOM树,xml在树结构上可读性强;
  • jsx像react.createElement的语法糖;

网络

http和https的区别

  • https需要申请CA证书,http则不需要
  • http的端口是80,https的端口是443
  • http是超文本传输协议,是明文传输,https是具有安全性的SSL加密传输协议
  • http工作在应用层,而https的安全传输机制工作在传输层

三次握手(面试问过)

  • 第一次握手:客户端发送SYN报文给服务端,发送完之后客户端处于SYN_SEND状态
  • 第二次握手:服务端接收SYN报文后发送SYN和ACK给客户端
  • 第三次握手:客户端收到SYN和ACK,给服务端发送ACK报文,客户端转为established状态,服务端接收到ACK报文,也转为established状态,此时双方建立连接

image.png

四次挥手

  • 第一次挥手:客户端发送一个FIN报文,报文中包含一个序列号,此时客户端处于FIN——WAIT1状态
  • 第二次挥手: 服务端收到FIN报文后,会给客户端发送ACK报文,并且把客户端的报文序列号+1作为ACK报文的序列值,表示已经收到客户端的报文了,此时服务端处于CLOSE_WAIT状态
  • 第三次挥手: 客户端收到FIN报文后,也会发送ACK报文作为应答,也会把服务端的序列号+1作为自己ACK的报文序列号,这时候客户端处于TIME_WAIT状态,需要等一会确认服务端收到自己的ACK报文之后才会进行CLOSED状态
  • 服务端收到ACK报文后,将状态置为CLOSED

get和post的区别

  • get在url中传参有限制,post没有
  • get在浏览器中回退不会再次请求,post会
  • get请求的参数会保存在浏览器的历史记录中,post不会
  • get请求的参数会暴露在地址栏上,不安全,post放在报文内部更安全
  • get一般用于查询信息,post用于提交信息执行某些修改操作
  • get请求会被浏览器缓存,post不会
  • get参数通过url传递,post放在request body中
  • get产生一个TCP数据包,post产生两个TCP数据包

http各个版本的区别

  • 1.1和1.0相比,1.1可以传输更多文件,1.1持久连接,引入了更多的缓存策略,新增put、options、head等请求方法
  • 2.0采用二进制格式,增加了多路复用、数据流、header压缩、服务端推送

输入url按下回车发生了什么(面试问过)

  • 在浏览器输入url,从里面解析出主机、协议、端口、路径等信息,构建http请求
  • 查看缓存,看本地是否有缓存,如果没有就发送http请求,如果有,就将缓存资源返回给浏览器进程
  • DNS解析(主机名到ip地址的转换服务):网络进程请求从DNS缓存中查找是否缓存过当前的域名信息,有就返回,没有则进行DNS解析查找端口和IP,默认是80,如果是https请求,还需要建立TLS连接
  • 等待TCP队列:Chrome机制是一个域名最多只能建立6个TCP链接,多了就排队
  • 建立TCP连接:三次握手建立双方的连接,建立数据传输
  • 发送http请求:浏览器向服务器发送请求行,包含请求方法、url、请求头等
  • 服务器接收到请求后,返回code码和协议版本
  • 断开TCP连接,数据传输完成,TCP通过四次挥手断开连接,如果http头部加上keep-alive就会一直保持连接
  • 浏览器渲染:包括构建DOM树、css样式计算、生成布局、页面分层、合成、栅格化、显示

http的缓存(面试问过)

  • 缓存位置有四种:service Worker(传输协议必须是https)、Memory Cache(内存缓存,浏览器关闭消失)、Disk cache(磁盘缓存,读取速度慢)和push Cache(http/2的内容)
  • 协商缓存:强制缓存失效后,浏览器向服务器再次发送请求,Etag优先级高于last-Modified
  • 强制缓存:不向服务端请求资源,直接从缓存中读取资源,强缓存可以通过设置Expires和Cache-control,expires缓存过期的时间,是HTTP/1的产物,修改时间后会导致缓存失败,cache control是HTTP/1.1的产物,用于控制网页缓存,可在请求头和响应头中设置,no-cache是否缓存需要协商缓存决定
  • 默认先使用强制缓存,强制缓存生效直接使用缓存,不生效使用协商缓存,协商缓存由服务器决定是否使用缓存,协商缓存失效,返回200,重新请求,重新返回资源和缓存标识,再次存入浏览器缓存中,生效返回304

image.png

udp和tcp

  • tcp向上提供面向连接的可靠服务,udp的连接不可靠
  • 对数据要求准确性高的话,可以选择TCP

七层模型

  • 物理层:SMTP协议、DNS
  • 数据链路层:PPP点对点协议、FDDI光纤分布式数据接口、PDN公用数据网
  • 网络层:ARP地域解析协议
  • 传输层:TCP(数据传输协议)、UDP(用户数据报协议)
  • 会话层:SMTP(简单邮件传输协议)、DNS
  • 表示层:Telnet(远程登录服务标准协议)、SNMP(网络管理协议)
  • 应用层:HTTP、TFTP(简单文本传输协议)、NFS(数据加密)、WAIS(广域信息查询系统)

dns 域名解析是递归还是迭代?从www.baidu.com到192.168.30的过程,本地缓存存的是文件还是其他?(面试问过)

  • 既可以是递归也可以是迭代,取决于请求的处理方式。
  • **递归解析是 DNS 服务器代替客户端完成整个解析过程,直到找到最终的 IP 地址返回给客户端。**流程:客户端发起请求后,本地服务器会向根域名服务器、顶级域名服务器、权威域名服务器,递归查询,最终查到 ip 地址。
  • **迭代解析中,dns 服务器不会向客户端去查询整个解析过程。相反:dns 服务器会逐步返回能够查询的下一个 dns 服务器的地址。**流程:客户端向 dns 服务器请求解析,如果 dns 服务器没有答案,它会返回指向下级 dns 服务器的地址,客户端再去查询这个服务器,直到找到最终的答案。
  • 递归解析是由 dns 服务器代替客户端去执行整个查询过程,直到返回最终的 ip 地址;迭代解析是客户端向 dns 服务器请求解析,如果服务器无法查到最终结果,会返回下级 dns 服务器的地址,由客户端继续查询;本地缓存存储,缓存在内存中,有 ttl(过期时间),超时后重新解析。
  • host 文件用来将主机名(域名)映射到 ip 地址,这个文件通常用来不依靠 dns 的情况下进行域名解析,在网络配置、调试或阻止特定网站时使用。(优先于 dns 解析)

前端工程化

git和svn的区别?

  • git是分布式的,svn是集中式的,因此不能在离线的情况下使用svn。
  • git的分支的指针指向某次提交,svn是复制版本库的一份完整的目录。git分支创建开销更小且分支上的变化不会影响其他人,而svn会影响其他人。
  • git存的是元数据,svn是文件。
  • git和svn的分支不同,svn会存在分支遗漏的情况,git可以在一个工作目录下进行分支的来回切换,很容易发现未被合并的分支。
  • git没有全局的版本号,而svn有。
  • svn的指令比git简单,学习成本低。
  • git内容的完整性要优于svn,git内容存储用的是SHA-1哈希算法。

git merge和git rebase的区别?(面试问过)

  • git merge会创建一个新的commit对象,然后两个分支以前的commit记录都指向这个新的commit记录,这种方法会保留之前每个分支的commit历史。
  • git rebase会找到两个分支共同commit的祖先记录,然后将提取当前分支之后的commit记录,然后将commit记录添加到目标分支的最新提交后面。经过这次合并后,两个分支合并后的commit记录就变成线性的了。

常用的git命令

git init 新建代码库
git add 添加指定文件到暂存区
git rm 删除工作区文件,并把这次删除放入暂存区
git commit -m [message] 提交暂存区到仓库区
git branch 列出所有分支
git checkout -b [branch] 新建一个分支,并切换到此分支
git status 显示变更文件的状态

webpack的构建流程?(面试问过)

  • 初始化参数,从配置文件和shell语句中读取与合并参数,得出最终的参数。
  • 开始编译:从上一步得到的参数初始化compile对象,加载所有的配置插件,执行run方法开始编译。
  • 确定入口:根据配置中entry找出入口文件。
  • 编译模块:从入口文件出发,调用所有的loader对模块进行编译,再找出模块依赖的模块,再递归步骤直到找出所有依赖的文件都经过了本步骤。
  • 完成模块编译:模块编译完成后,得到每个模块被编译的最终内容和依赖关系。
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个的包含多个模块的chunk,再把每个chunk转成一个文件加入到输出列表,这步是可以修改输出内容的最后机会。
  • 输出完成:确定好输出内容后,根据配置输出路径和文件名,把文件内容写入到文件系统。

怎么使用webpack优化打包流程?

  • 压缩代码:删除多余的代码,注释、简化代码的写法。可以利用webpack的uglifyPlugin和parallelUglifyPlugin来简化js代码。利用cssnano简化css。
  • 利用CDN加速:构建过程中,将静态资源路径修改为cdn上的路径。
  • Tree Shaking:将代码中永远不会用到的代码删除掉。可以在webpack启动时加--optimize- minimize实现。
  • code Spliting:将代码按照组件或路由分块,这样可以按需加载,又可以充分利用浏览器缓存。
  • 提取公共的第三方库:splitChunkPlugin来进行公共模块的抽取,利用浏览器的缓存来缓存长期无需变动的公共代码。

Babel的原理是什么?

  • 解析Parse:将代码解析成抽象语法树(AST),即语法解析和词法解析的过程。
  • 转换Transform:对AST的变换执行一系列的操作,babel接收到AST并通过babel-traverse进行遍历,在此过程中执行添加、更新及移除等操作。
  • 生成Generate:将变换的AST转为JS代码,使用到的模块是babel-generate

TypeScript

TS和JS的关系?

TS是JS的超集,它增加了静态类型和其他特性,TS代码可以编译成JS,可以在任何支持JS环境中运行。

ts中interface和type的区别?(面试问过)

  • interface只能定义对象类型,不能定义基础类型、联合类型和交叉类型。type可以定义基础类型、联合类型、交叉类型等更广泛的类型
  • 定义两个相同名称的interface会被合并,type会报编译错误
  • interface可以使用extends进行扩展,实现接口的继承。type可以用&进行类型合并
  • type使用类型别名时,可以使用typeof获取实例类型

ts中required、partial、omit、pick(面试问过)

  • required<T>:将类型中的属性变为必选
  • partial<T>:将类型中的属性变为可选
  • omit<T,K>:从类型T中去除一组属性K
  • pick<T,K>:从类型T中挑选一组属性K

ts中怎么使用泛型?

  • 泛型是一种创建可重用代码的工具,它允许在定义函数、类、或接口时使用占位符类型,用<>表示

场景题

单点登录

  • 淘宝、天猫都属于阿里旗下,当用户登录淘宝后,再打开天猫,系统便自动帮用户登录了天猫,这种现象就属于单点登录

同域名下的单点登录

cookiedomain属性设置为当前域的父域,并且父域的cookie会被子域所共享。path属性默认为web应用的上下文路径

利用 Cookie 的这个特点,没错,我们只需要将Cookiedomain属性设置为父域的域名(主域名),同时将 Cookiepath属性设置为根路径,将 Session ID(或 Token)保存到父域中。这样所有的子域应用就都可以访问到这个Cookie

不过这要求应用系统的域名需建立在一个共同的主域名之下,如 tieba.baidu.com 和 map.baidu.com,它们都建立在 baidu.com这个主域名之下,那么它们就可以通过这种方式来实现单点登录

不同域名下的单点登录(一)

如果是不同域的情况下,Cookie是不共享的,这里我们可以部署一个认证中心,用于专门处理登录请求的独立的 Web服务

用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将 token 写入 Cookie(注意这个 Cookie是认证中心的,应用系统是访问不到的)

应用系统检查当前请求有没有 Token,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心

由于这个操作会将认证中心的 Cookie 自动带过去,因此,认证中心能够根据 Cookie 知道用户是否已经登录过了

如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录

如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL,并在跳转前生成一个 Token,拼接在目标URL 的后面,回传给目标应用系统

应用系统拿到 Token之后,还需要向认证中心确认下 Token 的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将 Token写入Cookie,然后给本次访问放行。(注意这个 Cookie 是当前应用系统的)当用户再次访问当前应用系统时,就会自动带上这个 Token,应用系统验证 Token 发现用户已登录,于是就不会有认证中心什么事了

此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做法

不同域名下的单点登录(二)

可以选择将 Session ID (或 Token )保存到浏览器的 LocalStorage 中,让前端在每次向后端发送请求时,主动将LocalStorage的数据传递给服务端

这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将 Session ID(或 Token)放在响应体中传递给前端

单点登录完全可以在前端实现。前端拿到 Session ID(或 Token )后,除了将它写入自己的 LocalStorage 中之外,还可以通过特殊手段将它写入多个其他域下的 LocalStorage 中

如何实现免密登录(面试问过)

在用户第一次登录成功后,后端会返回一个 token,这个 token 的主要作用是识别用户身份。获取到 token 后,把 token 放入 session 中,下次请求中直接读取 token,在请求头上带上

  • 免密登录包括(短信验证码、邮箱验证码、生物识别、第三方 oauth(google、微信、github 等))

大文件上传(面试问过)

  • file 对象:一组文件,使用选择文件时,这些文件存储在 file 对象中。
  • blob 对象:二进制数据,常用来表示大型数据对象,file 对象是 blob 对象的一个子类。
  • formData:前端先将数据保存在 formData 中,才能传给后端。
  • 大文件上传核心是文件切割,file 对象的方法 slice,继承自 blob 对象,slice(start,end(bytes)) ###专业术语
  • 断点续传
  • 断开重连重传
  • 切片上传

方案

  • 前端切片 chunk 1024M 500k,要分成(1024 * 1024 / 500)个片
  • 切的片要传递给后端,切的片要取名:通过 hash(spark-md5,第一个切片和最后一个切片全部参与计算,中间剩余的切片分别在前面后面和中间取两个字节参与计算)或者 index
  • 后端组合切片

优化

  • 前端切片,主线程可能会卡顿,放到 web-worker 多线程里面去切,处理完后交给主线程发送,切片太多,浏览器一次性不能处理太多请求,chrome 默认的并发数只有 6 个,过多的请求不会提高上传进度,所以要限制前端请求个数。
  • 切片后,将 blob 存储到 IndexDB 中,下次用户进来后,看看是不是存在未完成上传的切片,有就继续上传
  • websocket,实时通知和请求序列的控制,wss
  • 断点续传,前端在上传前,将自己对应的 hash 告诉服务器,看看服务器有没有对应的文件,如果有,就直接返回,不需要重新执行上传分片。

设计

  • props、事件、状态
  • 拖拽上传、多文件选择
  • 通用化不同文件的上传,上传统一协议

手写题

深拷贝

// 浅拷贝可以用object.assign
        // ES6扩展运算符
        // 深拷贝
        const deepCopy = (obj) => {
          if (typeof obj !== 'object' || !obj) {
            return;
          }
          let newObj = Array.isArray(obj) ? [] : {};
          for (let key in newObj) {
            if (obj.hasOwnProperty(key)) {
              newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
            }
          }
          return newObj;
        }

数组去重

    // 数组去重
      const arr = [2, 32, 2, 5, 7];
      console.log([...new Set(arr)]);
      // 数组对象去重
      let arrObj = [
        { name: '小红', age: 18 },
        { name: '小名', age: 19 },
        { name: '小青', age: 20 },
        { name: '小白', age: 18 },
      ]
      let map = new Map();
      for (let item of arrObj) {
        if (!map.has(item.age)) {
          map.set(item.age, item);
        }
      }
      resArr = [...map.values()];
      console.log(resArr, '这是去重后的数组对象');
//使用set
function uniqueArray(arr){
	return [...new Set(arr)];
}

//使用filter
function uniqueArray(arr){
	//返回true,时间复杂度为o(n),不推荐使用
	return arr.filter((item,index) => arr.indexof(item) === index);
}

function uniqueArray(arr){
	let res = [];
	for(let i = 0; i < arr.length; i++){
		if(!res.includes(arr[i])){
			res.push(arr[i]);
		}
	}
	return res;
}
//使用reduce
function uniqueArray(arr){
	return arr.reduce((accumulator,current)=>{
		if(!accumulator.includes(current)){
			accumulator.push(current);
		}
		return accumulator;
	},[]);
}

数组扁平化

     // reduce本来就是一个递归
      const flatten = (arr) => {
        return arr.reduce(function (pre, cur) {
          return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
        }, arr)
      }
      const arrayMsg = [1, [1, 2, 3, 4, 5], 5];
      console.log(flatten(arrayMsg), '扁平化后的数组');

防抖和节流

// 防抖:触发事件n秒内只执行一次
      // 1.搜索框持续输入,最后一次输入完,才会发起请求
      // 2.手机号、邮件验证输入检测input、onchange事件
      // 3.窗口resize改变,窗口完成后,计算窗口大小,防止重复渲染
      const debounce = (fn, delay) => {
        let timer = null;
        return function (...args) {
          const context = this;
          clearTimeout(timer);
          setTimeout(() => {
            fn.apply(context, args);
          }, delay)
        }
      }
      // 节流:持续触发事件时,保证一段时间内触发一次
      // 1.懒加载、滚动加载、加载更多、或者监听滚动条的位置
      // 2.百度搜索联想功能
      // 3.防止高频点击提交,防止表单重复提交
      const throttle = (callback, limit) => {
        let waiting = false;
        return fuction(...args) => {
          if (!waiting) {
            callback.apply(this, args);
            waiting = true;
            setTimeout(() => {
              waiting = false;
            }, limit)
          }
        }
      }

apply、call、apply实现

      // call的实现 let和const只能用于块级作用域
      // call做了两件事:1.将this指向obj 2.fn方法执行
      var obj = { value: "jake" };
      function fn() {
        console.log(this.value);
      }
      fn.call(obj);
      // 改造函数
      var obj = {
        value: "jake",
        fn: function () {
          console.log(this.value, '我是改造后的fn');

        }
      }
      obj.fn();
      // 这样会多给obj添加一个fn属性,可以删掉
      obj.fn = fn;
      obj.fn();
      delete obj.fn;


      // call的实现,可以按照上边改造的来,必须把它挂到函数的原型上来,否则不能随处使用
      Function.prototype.myCall = function (context) {
        // 不是函数抛出异常
        if (typeof this !== "function") {
          throw new Error("Type Error");
        }
        // 取出参数
        let args = [...arguments].slice(1);
        // 声明执行结果变量
        let result = null;
        // context是否传入,不传入就是window
        context = context || window;
        // 改变this指向
        context.fn = this;
        // 执行调用的函数
        result = context.fn(...args);
        // 删除多余的this属性
        delete context.fn;
        // 返回执行结果
        return result;
      }
      // apply实现,同call差不多,但是apply接收数组,call接收的是参数列表

      Function.prototype.apply = function (context) {
        // 不是函数抛出异常
        if (typeof this !== "function") {
          throw new Error("Type Error");
        }
        // 用symbol来标识属性的唯一
        let params = symbol();
        context = context || window;
        let result = null;
        // 改变this指向
        context[params] = this;
        // 执行调用的函数
        if (argument[1]) {
          result = context.fn(...argument[1]);
        } else {
          result = context[params]();
        }
        delete context[params];
        return result;

      }
      // bind实现:bind返回的是一个函数
      Function.prototype.myBind = function (context) {
        // 不是函数抛出异常
        if (typeof this !== "function") {
          throw new Error("Type Error");
        }
        // 取出参数
        let args = [...arguments].slice(1);
        // this指向的是个函数
        const fn = this;
        return function fn() {
          return fn.apply(this instanceof Function ? this : context,
            // 当前arguments是指fn的参数
            context.concat(...arguments)
          );
        }
      }

实现一个new

     // new的实现
      // 首先创建一个空对象
      // 构造原型链,将空对象的__proto__设置为构造函数的prototype
      // 构造函数的this指向这个对象,为新对象添加属性
      // 判断返回值,要是引用类型,就返回引用类型的对象
      function myNew(context) {
        const obj = new Object();
        obj.__proto__ = context.prototype;
        const res = context.apply(obj,args);
        return  res instanceof Object=== 'object' ? res : obj;
      }


## 实现一个reduce
```js
      // 不考虑初始值
      Array.prototype.myReduce = function (cb) {
        // this是调用reduce方法的数组
        const arr = this;
        // 数组第一项
        let total = arr[0];
        for (let index = 1; index < arr.length; index++) {
          total = cb(total, arr[index], index, arr);
        }
        return total;
      }
      // 考虑初始值
      Array.prototype.myReduce = function (cb, initialValue) {
        // this是调用reduce方法的数组
        const arr = this;
        // 数组第一项
        let total = initialValue || arr[0];
        for (let index = initialValue ? 0 : 1; index < arr.length; index++) {
          total = cb(total, arr[index], index, arr);
        }
        return total;
      }

排序

  • 冒泡排序

      // 冒泡排序
      // 1.比较相邻的两个值,如果两个元素的大小关系不正确,则交换位置,一直重复
      function bubbleSort(arr) {
        let index = 0;
        //外层循环,控制趟数,每一次找到一个最大值
        for (let i = 0; i < arr.length; i++) {
          // 内层循环,控制比较的次数,并且判断两个数的大小
          for (let j = 0; j < array.length - i - 1; j++) {
            //  如果前面的数大,放到后面 则为 从小到大的冒泡排序
            if (arr[j] > arr[j + 1]) {
              index = arr[j];
              arr[j] = arr[j + 1];
              arr[j + 1] = index;

            }
          }
        }

      }
  • 选择排序

      // 选择排序
      // 原理:在未排序的序列中找到最小或者最大的元素,放置好位置,接着从剩下的元素中找到最小(最大)的元素
      // 升序
      function selectionSort(arr) {
        // 外层趟数
        for (let i = 0; i < array.length; i++) {
          let index = i;
          // 内层比较最小值和其他数据大小
          for (let j = i; j < array.length; j++) {
            // 找到最小值
            if (array[j] < array[index])
              index = j;
          }
          let temp = array[index];
          array[index] = array[i];
          array[i] = temp;
        }
        return arr;
      }
      let array = [1, 3, 2, 5, 7, 9, 0];
      console.log(selectionSort(array));
  • 快速排序

     // 二分查找,快排
      //  快速排序算法通过,设置一个分界值,把比分界值小的放到一边,比分界值大的数字放到另一边,一般把第一个数字作为分界值,
      // 这样排列好后,把左边的设置一个分界值,按照第一步排序
      // 右边的设置一个分界值,按照第一步排序
      // 假如有一个数组A[0]-A[N-1],取第一项为关键数据
      // 1.设置两个变量,i和j,i开始为1,j= N
      // 2.以第一个值作为关键数据,key=A[0]
      // 3.从j开始向前搜索,j--,找到第一个小于Key的值,A[j]与A[i]交换位置
      // 4.从i开始向后搜索,i++,找到第一个大于Key的值,A[i]与A[j]交换位置
      // 重复3、4步骤
      function quickSort(arr) {
        if (arr.length <= 1) {
          return arr;
        }
        const base = arr[0];
        let leftArray = [];
        let rightArray = [];
        for (let i = 1; i < arr.length; i++) {
          if (arr[i] <= base) {
            leftArray.push(arr[i]);
          } else {
            rightArray.push(arr[i]);
          }
        }
        return quickSort(leftArray).concat([base], quickSort(rightArray));
      }

      let array = [1, 3, 2, 5, 7, 9, 0];
      console.log(quickSort(array));

找连续数的最大和

 // 动态规划:将一个问题分成若干子问题,反复求解子问题,得到原问题的解决方案
  function maxSum(arr) {
    // 开始位置
    let pre = 0;
    // 保存和
    let maxSum = arr[0];
    arr.forEach(item => {
      pre = Math.max(pre + item, item);
      maxSum = Math.max(pre, maxSum);

    })
    return maxSum;
  }
  const nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
  console.log(maxSum(nums));

  

函数柯里化

function curry(fn){
	return function curried(...args){
		//如果参数够
		if(args.length >= fn.length){
			//直接执行函数
			return fn(...args);
		}else{
			//如果参数不够
			return function(...newArgs){
				return curried(...args,...newArgs);
			}
			
		}
	}
}

promise异步加载图片

function loadImg(url){
	return new Promise((resolve,reject) => {
		let img = new Image();
		img.onload = function(){
			resolve(img);
		}
		img.onerror = function(error){
			reject(error);
		}
		img.url = url;
	})
}

使用promise封装ajax请求

function ajaxRequest(url,method = 'GET',data = null){
	return new Promise((resolve,reject) => {
		let xhr = new XMLHttpRequest();
		xhr.open(method,url,true);
		if(method === 'POST'){
			xhr.setRequestHeader('Content-Type','application/x-www-urlencoded');
		}
		//xhr加载状态,判断是否请求成功
		xhr.onload = function(){
			if(xhr.status === 200) {
				resolve(this.responseText);
			} else {
				reject(new Error(this.statusText));


			}
		}
		//错误报错
		xhr.onerror = function(){
			reject(new Error('network error'));
		}
		//是否有数据返回
		if(data) {
			xhr.send(data);
		} else{
			xhr.send();
		}
	})
}

实现数组求和

//循环累加
function sumArray(arr) {
	let sum = 0;
	for(let i = 0; i < arr.length; i++) {
		sum += arr[i];
	}
	return sum;
}

//使用reduce
function sumArray(arr){
	return arr.reduce((accumulator,current) => accumulator + current,0)
}

实现promise.all

function myPromiseAll(promises){
	return new Promise(function(resolve,reject){
		if(!Array.isArray(promises)){
			throw TypeError('promises must be array');
		}
		let promisesLen = promises.length;
		let num = 0;
		let resArray = [];
		for(let i = 0;i < promisesLen; i++){
			Promise.resolve(promises[i]).then(value =>{
				num++;
				resArray[i] = value;
				if(num === promisesLen){
					return resolve(resArray);
				}
			},error=>{
				return reject(error)
			})
		}
	})
}

实现promise.race

function myPromiseRace(promises){
	return new Promise(function(resolve,reject){
		//谁先到谁先执行
		for(let i = 0; i < promises.length; i++){
			promises.then(resolve,reject);
		}
	})
}

实现eventEmitter

class EventEmitter{
	constructor(){
		this.events = {};
	}
	on(name,fn){
		if(!this.events[name]){
			this.events[name] = [];
		}
		this.events[name].push(fn);

	}
	emit(name,...args){
		if(!this.events[name]){
			return;
		}
		this.events[name].forEach((cb) => cb(this,args));

	}
	off(name,fn){
		if(!this.events[name]){
			return;
		}
		this.events[name] = this.events[name].filter((cb) => cb !== fn );
	}
	once(name,callback){
		const fn = (...args) => {
			callback(this,args);
			this.off(name,fn);
		}
		this.on(name,fn);
	}
}

实现斐波那契数列

//递归
function fibonacciRecursive(n){
	if(n = 1){
		return n;
	}
	return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2);

}
//使用动态规划
function fibonacciRecursive(n) {
	let dp = new Array(n + 1).fill(0);
	dp[0] = 0;
	dp[1] = 1;
	for(let i = 2; i <= n; i++) {
		dp[i] = dp[i - 1] + dp[i - 2];
	}
	return dp[n];
}
//使用迭代
function fibonacciRecursive(n) {
	let a = 0; b = 1; tmp;
	if(n === 0){
		return 0;
	}
	if(n === 1){
		return 1;
	}

	for(let i = 2; i <= n; i++){
		tmp = a + b;
		a = b;
		b = tmp;

	}
	return b;

}

字符串中出现的不重复字符的长度

//使用滑动窗口的方法
function lengthOfLongestSubstring(s){
	let start = 0;
	let maxLength = 0;
	//可见的字符
	let seen = new Set();
	for(let i = 0; i < s.length; i++){
		//如果当前字符已经在可见窗口,则移除
		while(seen.has(s[i])){
			seen.delete(s[i]);
			start++;
		}
		seen.add(s[i]);
		maxLength = Math.max(maxLength,i - start + 1);
	}
	return maxLength;


}

扁平化数据转化为树

const items = [  
	{ id: 1, name: 'Item 1', parentId: null },  
	{ id: 2, name: 'Item 1.1', parentId: 1 },  
	{ id: 3, name: 'Item 1.2', parentId: 1 },  
	{ id: 4, name: 'Item 2', parentId: null },  
	{ id: 5, name: 'Item 2.1', parentId: 4 },  
    // ... 更多的项目  
  ];

function flatToTree(items,parentId = null) {
	let children = items.filter((item) => item.parentId === parentId);
	if(!children.length){
		return null;
	}
	return children.map((node) => ({
		...node,
		children: flatToTree(items,node.id)
	}))
}
console.log(flatToTree(items));

//循环
function flatToTree(items,parentId = null) {
	let tree = [];
	for(let i in items) {
		//说明是子节点
		if(items[i].parentId === parentId){
			const children = flatToTree(items,items[i].id);
			if(children.length){
				items[i].children = children;
			}
			tree.push(items[i]);
		}
	}
	return tree;
}

循环打印红黄绿灯

function red(){
	console.log('red');
}
function yellow(){
	console.log('yellow');
}
function green(){
	console.log('green');
}
function taskRunner(timer,light){
	 new Promise((resolve,reject) =>{
		setTimeout(() => {
			if(light === 'red'){
				red();

			}else if(light === 'yellow'){
				yellow();
			}else{
				green();
			}
			
		}, timer);
		resolve();
	});
	const step = () => {
		taskRunner(3000,'red').then(()=>taskRunner(2000,'yellow')).then((1000,'green')).then(step);
	}
	step();
}

//async和await
const taskRunner = async() =>{
	await taskRunner(3000,'red');
	await taskRunner(2000,'yellow');
	await taskRunner(1000,'green');
	taskRunner();

}
taskRunner();

实现add(1)(2)(3)

function add(args) {
	let sum = args;
	function innerAdd(num) {
		sum += num;
		return innerAdd;
	}
	innerAdd.toString = function() {
		return sum;
	}
	return innerAdd;
}

setTimeOut实现setInterval

function mySetInterval(callback,delay){
	//初始调用
	callback();
	//递归调用setTimeout来模拟setInterval
	const intervalId = setTimeout(() => {
		clearTimeout(intervalId);
		mySetInterval(callback,delay);
		callback();
		
	}, delay);

}

括号匹配

function areParenthesesMatched(expression){
	//栈先入先出
	const stack = [];
	const map = { ')': '(', ']': '[', '}': '{' };
	for(let i = 0; i <= expression.length - 1; i++){
		let char = expression[i];
		if(char === '(' || char === '[' || char = '{'){
			stack.push(char);
		}else {
			let topElement = stack[stack.length - 1];
			if(topElement === char){
				stack.pop();
			}else {
				return false;
			}
		}
	}
	//栈为空,所有的都匹配了
	return stack.length = 0;
}

(5).add(3).minus(2)

(function(){
  Number.prototype.add = add;
  Number.prototype.minus = minus;
  function add(n){
	return this + n;
  }
  function minus(n){
	return this - n;
  }
})();

数据结构

两数之和

function twoSum(nums,target) {
	for(let i = 0; i < nums.length; i++){
		let targetIndex = nums.indexOf(target - nums[i]);
		if(targetIndex > -1 && targetIndex !== i) {
			return [i,targetIndex];
		}
	}
}

三数之和

function threeSum(nums,target){
	const result = [];
	nums.sort((a,b) => a - b);
	for(let i = 0; i < nums.length - 2; i++){
		//从剩余的元素中找两个元素
		//如果重复
		if(nums[i] === nums[i - 1] && i > 0 ){
			continue;
		}
		//双指针
		let left = i + 1;
		let right = nums.length - 1;
		while(left < right){
			let sum = nums[i] + nums[left] + nums[right];
			if(sum === target){
				result.push(nums[i],nums[left],nums[right]);
				//跳过重复元素
				while(left < right && nums[left] === nums[left + 1]){
					left++;
				}
				while(left < right && nums[right] === nums[right - 1]){
					right--;
				}
				left++;
				right--;
			}else if(sum > target){
				right--;
			}else {
				left++;
			}
		}
	}
	return result;

}

广度优先遍历树

//搜索根节点附近的节点
function breadthFirstDepth(root){
	if(!root){
		return;
	}
	//用队列存储
	const queue = [root];
	while(queue.length > 0){
	//从队列中取出第一个节点
		const childrenNode = queue.shift();
	//将第一个节点的子节点加入到任务队列
		for(const children of childrenNode.children){
			queue.push(children);
		}
		
	}
}

深度优先遍历树

//尽可能深的搜索树的分支
function depthFirstTree(root){
	if(!root){
		return;

	}
	for(let i = 0; i < root.children.length; i++){
		depthFirstTree(root.children[i]);
	}

}

二叉树中查找符合条件的节点

//二叉查找树,判断是否为前序遍历的结果
//前序遍历,左子树小于根节点,右子树大于根节点
function verifyPreOrder(preOrder){
	if(preOrder.length === 0){
		//空数组有效
		return true;
	}
	//根节点
	const rootVal = preOrder[0];
	//左子树
	let leftTree = [];
	//右子树
	let rightTree = [];
	for(let i = 0; i < preOrder.length; i++){
		if(preOrder[i] > rootVal){
			rightTree.push(preOrder[i]);
		}else if(preOrder[i] < rootVal){
			leftTree.push(preOrder[i]);
		}
	}
	return(
		verifyPreOrder(leftTree) && verifyPreOrder(rightTree) && rightTree.every(item => item > rootVal)
	);
}

买卖股票的最佳时机

function maxProfit(prices) {
	let minPrice = prices[0];
	let maxProfit = 0;
	for(let i = 0; i < prices.length; i++){
		if(prices[i] < minPrice){
			minPrice = prices[i];
		}
		if(price[i] - minPrice > maxProfit){
			maxProfit = price[i] - minPrice;
		}

	}
	return maxProfit;
}

输入一个字符串,找到第一个不重复字符的下标

function firstNonRepeat(str){
	//判断是否在hash表中出现
	const result = {};
	for(let i = 0; i < str.length; i++){
		if(result[str[i]]){
			//出现过加1次
			result[str[i]]++;
		}else {
			//出现1次
			result[str[i]] = 1;
		}
	}
	for(let i = 0; i < result.length; i++){
		if(result[i] === 1){
			return i;
		}
	}
	return -1;
}

输入一个字符串,打印出字符串的所有排列组合

function permute(str,memo = [],result = []){
	//如果memo的长度等于str的长度,说明找到一个完整的排列,将其加入到结果数组
	if(memo.length === str.length){
		result.push(memo.join(''));
		return;
	}
	for(let i = 0; i < str.length; i++){
		//如果已经memo中已经存在当前字符,跳过
		if(memo.includes(str[i])){
			continue;
		}
		//将当前字符添加到memo中
		memo.push(str[i]);
		//递归下去
		permute(str,memo,result);
		//回溯,将当前字符从memo中移除,尝试其他排列
		memo.pop();
	}
	return result;
}

最长递归子序列

function lengthOfLIS(nums){
	if(nums.length === 0){
		return 0;
	}
	//最短为1
	const dp = new Array(nums.length + 1).fill(1);
	for(let i = 1; i < nums.length; i++){
		for(let j = 0; j < i; j++){
			if(nums[i] > nums[j]){
				dp[i] = Math.max(dp[i],dp[j] + 1);
			}
		}

	}
	return Math.max(...dp);
}

滑动窗口最大值

function maxSlidingWindow(nums,k){
	if(nums.length === 0){
		return [];
	}
	//双端序列
	let deque = [];
	let result = [];
	//移除当前队列中超出窗口的元素
	for(let i = 0; i < deque.length; i++){
		if(deque.length && deque[0] < i - k + 1){
			deque.shift();
		} 
		//保持队列单调递增
		//队列头部存储的是最大值的索引
		//队列元素从大到小排列
		while(deque.length && deque[deque[deque.length - 1]] < nums[i]){
			deque.pop();
		}
		deque.push(i);
		//当窗口大小为k时,记录当前窗口的最大值
		if(i >= k - 1){
			result.push(nums[deque[0]]);
		}
	}
	return result;
	
}

判断一个数是不是质数,是返回 1,否则返回 0

//判断一个数是不是质数
function isPrime(num) {
  //1不是质数
  if (num === 1) {
    return false;
  }
  if (num === 2) {
    return true;
  }
  //除去偶数偶数不是质数
  if (num % 2 === 0) {
    return false;
  }
  //从3到根号i能否整除num
  for (let i = 3; i * i <= num; i = i + 2) {
    if (num % i === 0) {
      return false;
    }
  }
  return true;
}

给定两个数字,求两个数的最大公约数和最小公倍数

function gcd(a, b) {
  while (b !== 0) {
    //辗转相除法,不断的a%b,直到b为0,a就是最大公约数
    let temp = b;
    b = a % b;
    a = temp;
  }
  return a;
}
//最小公倍数 Math.abs(a * b) /gcd(a,b)
function lcm(a, b) {
  return Math.abs(a * b) / gcd(a, b);
}

分解质因数,打印出 90 = 2 * 3 * 3 *5

function primeFactors(n){
	let factors = [];
	//先除以2
	while(n % 2 === 0){
		factors.push(2);
		n / = 2;
	}

	//从3开始到根号n
	while(let i = 3; i * i <= n; i = i + 2){
		factors.push(i);
		n /= i;
	}
	//剩下的n是大于2的质数
	if(n > 2){
		factors.push(n);
	}
	return factors;
}
function printPrimeFactors(n){
	const factors = primeFactors(n);
	console.log(`${n} = ${factors.join('*')}`);
}

给定一个一元的硬币,有 1 分的,2 分的和 5 分的,求有多少种兑换方式,打印出每种兑换方式

  function exchangeCoins(){
    let count = 0;
    for(let i = 0; i <=100; i++){
      for(let j = 0; j <= 50; j++){
        for(let k = 0; k <= 20; k++){
          if(i + j * 2 + k * 5 === 100){
            count++;
            console.log(`1分的要${i}枚,2分的要${j}枚,5分的要${k}枚`)
          }
        }
      }
    }
    console.log(`总共有${count}种兑换方式`);
  }

杨辉三角,打印 10 行

//每一行都是左上角加上右上角的和
function generate(numRows) {
  let ret = [];
  for (let i = 0; i < numRows; i++) {
    let lineArr = new Array(i + 1).fill(1);
    if (i > 1) {
      for (let j = 1; j < i; j++) {
        lineArr[j] = ret[i - 1][j - 1] + ret[i - 1][j];
      }
    }
    ret.push(lineArr);
  }
  return ret;
}

连续正整数和,15 = 1 + 2 + 3 + 4 + 5,15 = 7 + 8, 15 = 4 + 5 + 6,16 的话就输出 NONE

  function findConsecutiveSum(n){
    //是否找到
    let found = false;
    //只能在1 - n / 2之间找
    for(let i = 1; i <= n / 2; i++){
      //初始值
      let sum = 0;
      //存放连续正整数的数组
      let sequence = [];
      //和不等于n,一直累加
      for(let j = i; j < n; j++){
        sum = sum + j;
        sequence.push(j);
        if(sum === n){
          found = true;
          console.log(sequence.join('+'));
        }
      }
    }
    if(!found){
      console.log('NONE');
    }

  }

打印出

####### ######### ############ ############## ################

let hashCount = 1;
for (let i = 0; i < 8; i++) {
  console.log("#".repeat(hashCount));
  hashCount = hashCount + 2;
}

有 n 个人围成一圈,顺序排号。从第一个人开始报数(从 1 到 3 报数),凡报到 3 的人退出圈子,问最后留下的是原来第几号。

function josephus(n,k){
  //初始化一个people数组
  let people = [];
  //1-n个人
  for(let i = 1; i <= n; i++){
    people.push(i);
  }
  //当有人时,一直移除是3的倍数的人
  let index = 0;
  while(people.length > 1){
    index = (index + k - 1) % people.length;
    people.splice(index,1);
  }
  return people[0];
}

零钱兑换(给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。)

/**
 * @param {number[]} coins
 * @param {number} amount
 * @return {number}
 */
var coinChange = function(coins, amount) {
    let dp = new Array(amount + 1).fill(1);
    dp[0] = 0;
    for(let i = 1; i <= amount; i++){
        for(let j = 0; j < coins.length; j++){
            if(coins[j] <= i){
                dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
            }
        }
    }
    return dp[amount] > amount ? -1 :dp[amount];

};

背包问题

  • 背包问题(Knapsack Problem)是一个经典的优化问题,通常分为两种类型:0/1 背包问题完全背包问题。这些问题的目的是在给定的重量限制下,选择物品,使得物品的总价值最大化。
//1个物品放一次0/1背包
function knapsack01(weights, values, capacity) {
    const n = weights.length;
    // 创建二维 dp 数组,dp[i][w] 表示前 i 个物品,容量为 w 时的最大价值
    const dp = Array.from({ length: n + 1 }, () => Array(capacity + 1).fill(0));

    // 填充 dp 数组
    for (let i = 1; i <= n; i++) {
        for (let w = 0; w <= capacity; w++) {
            if (weights[i - 1] <= w) {
                dp[i][w] = Math.max(dp[i - 1][w], dp[i - 1][w - weights[i - 1]] + values[i - 1]);
            } else {
                dp[i][w] = dp[i - 1][w];
            }
        }
    }

    // 返回最大价值
    return dp[n][capacity];
}

// 示例
const weights = [2, 3, 4, 5];
const values = [3, 4, 5, 6];
const capacity = 5;
console.log(knapsack01(weights, values, capacity)); // 输出最大价值

//一个物品放多次完全背包
function knapsackComplete(weights, values, capacity) {
    const dp = Array(capacity + 1).fill(0); // 创建一维 dp 数组,dp[w] 表示容量为 w 时的最大价值

    // 填充 dp 数组
    for (let i = 0; i < weights.length; i++) {
        for (let w = weights[i]; w <= capacity; w++) {
            dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
        }
    }

    // 返回最大价值
    return dp[capacity];
}

// 示例
const weights = [2, 3, 4, 5];
const values = [3, 4, 5, 6];
const capacity = 5;
console.log(knapsackComplete(weights, values, capacity)); // 输出最大价值