前端面试题

一、HTTP

1.GET和POST的请求的区别

区别GETPOST
幂等性
应用场景用于对服务器资源不会产生影响的场景(比如请求一个网页的资源等)用于对服务器资源会产生影响的情景(比如注册用户等)
是否缓存
传参方式查询字符串传参请求体传参
安全性将参数放入url中向服务器发送,不安全在请求体中,安全
请求长度浏览器对于url长度有限制,会受到影响在请求体中,不会收到浏览器影响
参数类型ASCII字符文件、图片等
幂等性:指一个请求方法执行一次和多次的效果完全相同

2.POST和PUT的请求的区别

区别POSTPUT
作用创建数据更新数据
为什么POST请求会发送两次?
  • 第一次请求为options预检请求,状态码为204

作用1:询问服务器是否支持修改的请求头,如果服务器支持,则在第二次中发送真正的请求
作用2: 检测服务器是否为同源请求,是否支持跨域

  • 第二次请求为真正的POST请求

3.常见的HTTP请求头和响应头

HTTP Request Header定义
Accept浏览器能够处理的内容类型
Accept-Charset浏览器能够显示的字符集
Accept-Encoding浏览器能够处理的压缩编码
Accept-Language浏览器当前设置的语言
Connection浏览器与服务器之间连接的类型
Cookie浏览器当前页面设置的任何Cookie
Host当前发出请求的页面所在的域
Referer当前发出请求的页面的URL
User-Agent浏览器的用户代理字符串
HTTP Responses Header定义
Date表示消息发送的时间,时间的描述格式由rfc822定义
server服务器名称
Connection浏览器与服务器之间连接的类型
Cache-Control控制HTTP缓存
content-type表示后面的文档属于什么MIME类型
Content-Type定义
application/x-www-form-urlencoded浏览器原生form表单
multipart/form-data表单上传文件
application/json服务器消息主体是序列化后的 JSON 字符串
text/xml提交 XML 格式的数据

4.状态码304

为什么会有304?

服务器为了提高网站访问速度,对之前访问的部分页面指定缓存机制。当客户端再次请求页面时,服务器会判断请求的页面是否已被缓存,若已经被缓存则返回304,此时客户端将调用缓存内容。

状态码304不应该被认为是一种错误,而是对客户端有缓存情况下服务端的一种响应。

产生较多304状态码的原因是什么?
  • 页面更新周期长或者长时间未更新
  • 纯静态页面或强制生成静态HTML
304状态码过多会造成什么问题?
  • 网站快照停止
  • 收录减少
  • 权重下降

5.常见的HTTP请求方法

方法作用
GET向服务器获取数据
POST向服务器发送数据
PUT修改数据
PATCH用于对资源进行部分修改
DELETE删除指定数据

  http请求过程:

1.对www.abc.com这个网址进行DNS域名解析,得到对应的IP地址

2.根据这个IP,找到对应的服务器,发起TCP的三次握手

3.建立TCP连接后发起HTTP请求

4.服务器响应HTTP请求,浏览器得到html代码

5.浏览器解析html代码,并请求html代码中的资源(如js、css、图片等)(先得到html 代码,才能去找这些资源)

6.浏览器对页面进行渲染呈现给用户

7.服务器关闭关闭TCP连接

浏览器输入一个地址。到页面展示中间经历了哪些东西?

#这个问题前端面试基本上百分百问的。测试的话,基础的功能面试可能不会问。自动化的话基本上也会问的。

1、浏览器输入url。先解析url地址是否合法

2、浏览器检查是否有ip缓存(游览器缓存-系统缓存-路由器缓存)。如果有,直接显示。如果没有,跳到第三步。

3、在发送http请求前,需要域名解析(DNS解析),解析获取对应过的ip地址。

4、浏览器向服务器发起tcp链接,与游览器简历tcp三次握手

5、握手成功后,游览器向服务器发送http请求,请求数据包

6、服务器收到处理的请求,将数据返回至浏览器

7、浏览器收到http响应。

8、浏览器解析响应。如果响应可以缓存,则存入缓存

9、游览器发送请求获取嵌入在HTML中的资源(html,css,JavaScript,图片,音乐等),对于未知类型,会弹出对话框

10、游览器发送异步请求

11、页面全部渲染结束。

http和https的区别:https=http+ssl;http是超文本传输协议,信息是明文传输。https则是具有安全性的ssl加密传输协议。

6.Cookie

Cookie是最早被提出来的本地存储方式,在此之前,服务端是无法判断网络中的两个请求是否是同一用户发起的,为解决这个问题,Cookie就出现了。Cookie的大小只有4kb,它是一种纯文本文件,每次发起HTTP请求都会携带Cookie

特性

  • Cookie一旦创建成功,就无法修改
  • Cookie是无法跨域的
  • 每个域名下Cookie的数量不能超过20个,每个Cookie的大小不能超过4kb
  • 存在安全问题,一旦被拦截,即可获得session的所有信息
  • Cookie在请求一个新的页面的时候都会被发送出去

如何解决无法跨域问题?

  • 使用Nginx反向代理
  • 在一个站点登陆之后,往其他网站写Cookie。服务端的Session存储到一个节点,Cookie存储sessionId

应用场景

  • 和session结合使用,将sessionId存储到Cookie中,每次发送请求都会携带这个sessionId,以便于服务端识别和响应
  • 可以用来统计页面的点击次数

7.LocalStorage

LocalStorage是HTML5新引入的特性,由于有的时候我们存储的信息较大,Cookie就不能满足我们的需求,这时候LocalStorage就派上用场了

优点

  • LocalStorage能存储5MB的信息
  • LocalStorage能够持久化存储数据,数据不会随着页面的关闭而消失,除非手动清除
  • 仅存储在本地,发起HTTP请求的时候不会被携带

缺点

  • 存在兼容性问题,IE8以下版本浏览器不支持
  • 如果浏览器设置为隐私模式,我们将无法获取到LocalStorage
  • 受到同源策略的限制,即端口、协议、主机地址有任何一个不相同,都不会访问

常用API

API注释
localStorage.setItem(key, value)保存数据到 localStorage
localStorage.getItem(key)从 localStorage 获取数据
localStorage.removeItem(key)从 localStorage 删除key对应的数据
localStorage.clear()从 localStorage 删除所有保存的数据
localStorage.key(index)获取某个索引的Key

应用场景

  • 一些网站配置个人设置的时候,比如肤色、字体等会将数据保存在LocalStorage中
  • 保存一些不经常变动的个人信息或用户浏览信息

8.SessionStorage

SessionStorage和LocalStorage都是在HTML5才提出来的存储方案,SessionStorage 主要用于临时保存同一窗口(或标签页)的数据,刷新页面时不会删除,关闭窗口或标签页之后将会删除这些数据

SessionStorage与LocalStorage对比

  • SessionStorage和LocalStorage都在本地进行数据存储
  • SessionStorage也有同源策略的限制,但是SessionStorage有一条更加严格的限制,SessionStorage只有在同一浏览器的同一窗口下才能够共享
  • LocalStorage和SessionStorage都不能被爬虫爬取

常用API

API注释
sessionStorage.setItem(key, value)保存数据到 sessionStorage
sessionStorage.getItem(key)从 sessionStorage获取数据
sessionStorage.removeItem(key)从 sessionStorage删除key对应的数据
sessionStorage.clear()从 sessionStorage删除所有保存的数据
sessionStorage.key(index)获取某个索引的Key

应用场景

  • 由于SessionStorage具有时效性,所以可以用来存储一些网站的游客登录的信息,还有临时的浏览记录的信息。当关闭网站之后,这些信息也就随之消除了

9.Cookie、LocalStorage、SessionStorage区别

CookieLocalStorageSessionStorage
实最开始是服务器端用于记录用户状态的一种方式,由服务器设置,在客户端存储,然后每次发起同源请求时,发送给服务器端。cookie 最多能存储 4 k 数据,它的生存时间由 expires 属性指定,并且 cookie 只能被同源的页面访问共享html5 提供的一种浏览器本地存储的方法,它一般也能够存储 5M 或者更大的数据。它和 sessionStorage 不同的是,除非手动删除它,否则它不会失效,并且 localStorage 也只能被同源页面所访问共享html5 提供的一种浏览器本地存储的方法,它借鉴了服务器端 session 的概念,代表的是一次会话中所保存的数据。它一般能够存储 5M 或者更大的数据,它在当前窗口关闭后就失效了,并且 sessionStorage 只能被同一个窗口的同源页面所访问共享

二、CSS

1.常见CSS选择器

注意事项
  • !important声明的样式的优先级最高
  • 如果优先级一致,则最后出现的样式生效
  • 继承得到的样式的优先级最低
  • 样式来源不同时,优先级顺序为:内联样式 > 内部样式 > 外部样式 > 浏览器用户自定义样式 > 浏览器默认样式

2.display属性值及其作用

属性值作用
none元素不显示,并且会从文档流中移除
block块元素类型。默认宽度为父元素宽度,可设置宽高,换行显示
inline行内元素类型。默认宽度为内容宽度,不可设置宽高,同行显示
inline-block行内块元素类型。默认宽度为内容宽度,可以设置宽高,同行显示
list-item像块类型元素一样显示,并添加样式列表标记
table此元素会作为块级表格来显示
inherit规定应该从父元素继承display属性的值

3.block、inline和inline-block的区别

区别blockinlineinline-block
独占一行
width
height
margin水平方向有效
padding

4.行内元素和块级元素的区别

行内元素:span,img,input

块级元素:h1-h6,div,p,header

区别行内元素块级元素
宽高无效有效
padding有效有效
margin水平方向有效有效
自动换行不可以可以
多个元素排列默认从左到右默认从上到下

5.隐藏元素的方法

方法说明
display: none;渲染树不会包含该渲染对象,因此该元素不会在页面中占据位置,也不会响应绑定的监听事件
visibility: hidden;元素在页面中仍占据空间,但是不会响应绑定的监听事件
opacity: 0;透明度设置为0,来隐藏元素。元素在页面中仍然占据空间,并且能够响应元素绑定的监听事件
position: absolute;通过使用绝对定位将元素移除可视区域内,以此来实现元素的隐藏
z-index: -10;使用其余元素遮盖当前元素实现隐藏
clip/clip-path使用元素裁剪的方法来实现元素的隐藏,这种方法下,元素仍在页面中占据位置,但是不会响应绑定的监听事件
transform: scale(0,0)将元素缩放为 0,来实现元素的隐藏。这种方法下,元素仍在页面中占据位置,但是不会响应绑定的监听事件

6.transition和animation的区别

transitionanimation
过渡属性,强调过渡,需要触发事件来实现过渡效果。动画属性,不需要触发事件,可自己执行,并且可以循环

7.伪元素和伪类的区别和作用

伪元素伪类
在元素前后插入额外的元素或样式,插入的元素没有子文档中生成,它们只在外部显示可见将特殊的效果添加到特定的选择器上。它是在现有元素上添加类别,并不会产生新的元素
css3中伪元素在书写是使用双冒号::,比如::before冒号:用于伪类,比如:hover
伪类是通过在元素选择器上加入伪类改变元素的状态,而伪元素通过对元素的操作来改变元素

8.盒模型

盒模型由四个部分组成,分别是margin、border、padding、content

标准盒模型和IE盒模型的区别在于:在设置width和height时,所对应的范围不同

  • 标准盒模型的width和height属性的范围只包含了content
  • IE盒模型的width和height属性的范围包含了border、padding和content

可以通过修改元素的box-sizing属性来改变元素的盒模型:

  • box-sizing: content-box 表示标准盒模型(默认值)
  • box-sizing: border-box 表示IE盒模型(怪异盒模型)

9.实现单行、多行文本溢出隐藏

单行文本溢出
overflow: hidden;           // 溢出隐藏
text-overflow: ellipsis;    // 溢出部分使用省略号显示
white-space: nowrap;        // 规定段落中的文本不可换行
多行文本溢出
overflow: hidden;            // 溢出隐藏
text-overflow: ellipsis;     // 溢出用省略号显示
display:-webkit-box;         // 作为弹性伸缩盒子模型显示。
-webkit-box-orient:vertical; // 设置伸缩盒子的子元素排列方式:从上到下垂直排列
-webkit-line-clamp:3;        // 显示的行数

10.实现水平垂直居中

利用绝对定位(一)
.parent {
    position: relative;
}
.child {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%)
}
利用绝对定位(二):适用于已知盒子宽高
.parent {
    position: relative;
}
.child {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    margin: auto;
}
利用绝对定位(三):适用于已知盒子宽高
.parent {
    position: relative;
}
.child {
    position: absolute;
    top: 50%;
    left: 50%;
    margin-top: -50px;     /* 自身 height 的一半 */
    margin-left: -50px;    /* 自身 width 的一半 */
}
flex布局
.parent {
    display: flex;
    justify-content: center;
    align-items: center;
}

11.浮动

非IE浏览器下,容器不设置高度且子元素浮动时,容器高度不能被撑开。此时,内容会溢出到容器外面影响布局

浮动的工作原理

  • 浮动元素脱离文档流,不占据空间(引起“高度塌陷”)
  • 浮动元素碰到包含它的边框或者其它浮动元素的边框停留

浮动元素可以左右移动,知道遇到另一个浮动元素或者遇到它外边缘的包含框。浮动框不属于文档流中的普通流,但元素浮动之后,不会影响块级元素的布局,只会影响内联元素的布局。此时文档流中的普通流就会表现得该浮动框不存在一样的布局模式。当包含框的高度小于浮动框的时候,此时就会出现“高度塌陷”

浮动元素引起的问题

  • 父元素的高度无法撑开,影响与父元素同级的元素
  • 与浮动元素同级的非浮动元素会跟随其后
  • 若浮动的元素不是第一个元素,则该元素之前的元素元素也要浮动,否则会影响页面的显示结构

清除浮动的方式

  • 给父级元素设置高度
  • 最后一个浮动元素之后添加一个空div标签,并添加clear: both样式
  • 包含浮动元素的父级元素添加overflow: hiddenoverflow: auto样式
  • 使用 ::after 伪元素
  • 使用clear属性清除浮动

12.块格式化上下文(Block Formatting Context,BFC)

BFC: 块格式化上下文(Block Formatting Context,BFC)是Web页面的可视化CSS渲染的一部分,是布局过程中生成块级盒子的区域,也是浮动元素与其他元素的交互限定区域。

通俗的讲,BFC是一个独立的环境布局,可以理解为一个容器,在这个容器中按照一定的规则进行物品摆放,并且不会影响其他环境中的物品。如果一个元素符合触发BFC的条件,则BFC中的元素布局不受外部影响。

BFC的创建条件

  • 根元素:body
  • 元素设置浮动:float除none以外的值
  • 元素设置绝对定位:position设置为absolute或fixed
  • display设置为inline-block、table-cell、table-caption、flex等
  • overflow设置为hidden、auto、scroll

BFC的特点

  • 垂直方向上,自上而下排列,和文档流的排列方式一致
  • 在BFC中上下相邻的的两个容器的margin会重叠
  • 计算BFC的高度时,需要计算浮动元素的高度
  • BFC区域不会与浮动的容器发生重叠
  • BFC是独立的容器,容器内部元素不会影响外部元素
  • 每个元素的margin-left值和容器的border-left相接处

BFC的作用

  • 解决margin的重叠问题:由于BFC是一个独立的区域,内部的元素和外部的元素互不影响,将两个元素变为两个BFC,就解决了margin重叠的问题
  • 解决高度塌陷的问题:对子元素设置浮动后,父元素会发生高度塌陷,即height变为0。只需将父元素变成一个BFC即可,常用的办法是给父元素设置overflow:hidden
  • 创建自适应两栏布局:左边的宽度固定,右边的宽度自适应。左侧设置float:left,右侧设置overflow: hidden。这样右边就触发了BFC,BFC的区域不会与浮动元素发生重叠,所以两侧就不会发生重叠,实现了自适应两栏布局

13.margin重叠问题

两个块级元素的上外边距和下外边距可能会合并(折叠)为一个外边距,其大小会取其中外边距值大的那个,这种行为就是外边距折叠。需要注意的是, 浮动的元素和绝对定位这种脱离文档流的元素的外边距不会折叠。重叠只会出现在 垂直方向

计算原则

  • 如果两者都是正数,取较大的那个
  • 如果一正一负,取正值减去负值后的绝对值
  • 都是负值是,用0减去两个中绝对值大的那个

解决办法

对于折叠的情况,主要有两种: 兄弟之间重叠父子之间重叠

兄弟间折叠:

  • 底部元素变为行内盒子:display: inline-block
  • 底部元素设置浮动:float
  • 底部元素的position的值为absolute/fixed

父子间的折叠:

  • 父元素加入:overflow: hidden
  • 父元素添加透明边框:border: 1px solid transparent
  • 子元素变为行内盒子:display: inline-block
  • 子元素加入浮动属性或定位

14.CSS选择符有哪些,哪些属性可以继承

CSS选择符:
    通配(*)
    id选择器(#)
    类选择器(.)
    标签选择器(div、p、h1...)
    相邻选择器(+)
    后代选择器(ul li)
    子元素选择器( > )
    属性选择器(a[href])
    
CSS属性哪些可以继承:
        文字系列:font-size、color、line-height、text-align...
***不可继承属性:border、padding、margin...

15.用CSS画一个三角形

用边框画(border),例如:
{
        width: 0;
        height: 0;

        border-left:100px solid transparent;
        border-right:100px solid transparent;
        border-top:100px solid transparent;
        border-bottom:100px solid #ccc;
}

16.display: none;与visibility: hidden;的区别

1. 占用位置的区别
display: none;                 是不占用位置的
visibility: hidden;   虽然隐藏了,但是占用位置

2. 重绘和回流的问题

visibility: hidden; 、 display: none;  产生重绘
display: none;     还会产生一次回流

产生回流一定会造成重绘,但是重绘不一定会造成回流。

产生回流的情况:改变元素的位置(left、top...)、显示隐藏元素....
产生重绘的情况:样式改变、换皮肤

三、JavaScript

1.数据类型(8种)

基本数据类型引用数据类型
Undefined、Null、Boolean、String、Number、Symbol(ES6)、BigInt(ES6)Object(包括数组、函数、对象等)
  • Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题
  • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

2.堆内存与栈内存

在操作系统中,内存被分为栈区和堆区,栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。

在数据结构中,栈中数据的存取方式为先进后出。堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。

数据存储方式

  • 基本数据类型的数据直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储
  • 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

img

3.数据类型检测

优缺点typeofinstanceofconstructorObject.prototype.toString.call
优点使用简单能检测出引用类型数据基本能检测所有的类型(除了null和undefined)检测出所有的类型
缺点只能检测出除null外的基本数据类型和引用数据类型中的function不能检测出基本类型,且不能跨iframeconstructor易被修改,也不能跨iframeIE6下,undefined和null均为Object

4.判断数组的方式

  • Object.prototype.toString.call([1, 2, 3]) // [object Array]
  • 通过ES6的Array.isArray([1, 2, 3])做判断 // true or false
  • [1, 2, 3] instanceof Array // true or false
  • Array.prototype.isPrototypeOf([1, 2, 3]) // true or false
  • 通过原型链去判断:[1, 2, 3].__proto__ === Array.prototype

5.Undefined与Null

  • Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null
  • undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化
  • undefined 在 JavaScript 中不是一个保留字,这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0
  • typeof null 的返回值为object,且undefined == null返回true

1. 作者在设计js的都是先设计的null(为什么设计了null:最初设计js的时候借鉴了java的语言)
2. null会被隐式转换成0,很不容易发现错误。
3. 先有null后有undefined,出来undefined是为了填补之前的坑。

具体区别:JavaScript的最初版本是这样区分的:null是一个表示"无"的对象(空对象指针),转为数值时为0;undefined是一个表示"无"的原始值,转为数值时为NaN。

6.this的指向问题

this 是一个在运行时才进行绑定的引用,在不同的情况下它可能会被绑定不同的对象

this 永远指向最后调用它的那个对象

  • 函数调用模式:当一个函数不是一个对象的属性,直接作为函数来调用时,this指向全局对象
  • 方法调用模式:当一个函数作为一个对象的方法来调用时,this指向这个对象
  • 构造器调用模式:如果一个函数使用new调用时,函数在执行前会创建一个新的对象,this就指向这个新的对象
如何改变this的指向?
  • 使用ES6箭头函数,箭头函数不绑定this,箭头函数的this使用指向函数定义时的this
  • 在函数内部定义一个变量_this保存this
  • 使用 apply、call、bind
  • new 实例化一个对象
this绑定的优先级:new绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级

7.apply、bind和call

apply、bind和call都可以改变this的指向

apply(thisArg[, argsArray])

apply() 方法调用一个具有给定  this 值的函数,以及以一个数组(或一个 类数组对象)的形式提供的参数

thisArg: 在函数运行时使用的 this 值。请注意,this 可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装

argsArray: 可选。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 函数。如果该参数的值为 null 或 undefined,则表示不需要传入任何参数

返回值:调用有指定 this 值和参数的函数的结果

const numbers = [1, 3, 2, 5, 7, 4]
const max = Math.max.apply(null, numbers)   // 7
const min = Math.max.apply(null, numbers)   // 1

call(thisArg[, arg1[, arg2[, ...]]])

call() 方法使用一个指定的  this 值和单独给出的一个或多个参数来调用一个函数
该方法的语法和作用与  apply() 方法类似,只有一个区别,就是  call() 方法接受的是 一个参数列表,而  apply() 方法接受的是 一个包含多个参数的数组

thisArg: 在函数运行时使用的 this 值。请注意,this 可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装

arg1, arg2, ...: 指定的参数列表

返回值:调用有指定 this 值和参数的函数的结果

bind(thisArg[, arg1[, arg2[, ...]]])

bind() 方法创建一个新的函数,在  bind() 被调用时,这个新函数的  this 被指定为  bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用(bind 是创建一个新的函数,我们必须要手动去调用)

thisArg: 调用绑定函数时作为 this 参数传递给目标函数的值。如果使用new运算符构造绑定函数,则忽略该值。当使用 bind 在 setTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。如果 bind 函数的参数列表为空,或者thisArgnullundefined,执行作用域的 this 将被视为新函数的 thisArg

arg1, arg2, ...: 指定的参数列表

返回值:返回一个原函数的拷贝,并拥有指定的 this 值和初始参数

8.伪数组(类数组)

一个拥有length属性和若干索引属性的对象可以被成为类数组对象,类数组对象和数组类似,但不能调用数组的方法
常见的类数组对象:arguments和DOM方法的返回结果,还有 一个函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数

9.类数组如何转换为数组

  • 通过call方法调用数组的slice方法
Array.prototype.slice.call(arrayLike);
  • 通过call方法调用数组的splice方法
Array.prototype.splice.call(arrayLike, 0);
  • 通过apply调用数组的concat方法
Array.prototype.concat.apply([], arrayLike);
  • 通过Array.from方法
Array.from(arrayLike);
  • 通过展开运算符
const array = [...arrayLike]

10.如何遍历类数组

arguments是一个对象,它的属性是从 0 开始依次递增的数字,还有 calleelength等属性,与数组相似;但是它却没有数组常见的方法属性,如 forEachreduce等,所以叫它们类数组
  • 使用call或apply方法
function sum() {
    Array.prototype.forEach.call(arguements, a => { console.log(a) })
}

function sum() {
    Array.prototype.forEach.apply(arguements, [a => { console.log(a)] })
}
  • 使用Array.from方法将类数组转化成数组
function sum() {
    const args = Array.from(arguements)
    args.forEach(a => { console.log(a) })
}
  • 使用展开运算符将类数组转成数组
function sum() {
    const args = [...arguements]
    args.forEach(a => { console.log(a) })
}

11.for...in与for...of

区别for...infor...of
遍历对象对象的键名,会遍历整个原型链,性能差对象的键值,只遍历当前对象
遍历数组返回数组中所有可枚举属性,包括原型链上的属性只返回对应数组的下标对应的属性值
for...in循环主要是为了遍历对象,不适用于遍历数组,for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象

12.ajax

AJAX Ajax 即“AsynchronousJavascriptAndXML”(异步 JavaScript 和 XML),是指一种创建交互式网页应用的网页开发技术。它是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。传统的网页(不使用 Ajax)如果需要更新内容,必须重载整个网页页面。其缺点如下:
  • 本身是针对MVC编程,不符合前端MVVM的浪潮
  • 基于原生XHR开发,XHR本身的架构不清晰
  • 不符合关注分离(Separation of Concerns)的原则
  • 配置和调用方式非常混乱,而且基于事件的异步模型不友好

ajax是一种能够实现局部网页刷新的技术,可以使网页异步刷新。

ajax的实现主要包括四个步骤:

 (1)创建核心对象XMLhttpRequest;

 (2)利用open方法打开与服务器的连接;

 (3)利用send方法发送请求;("POST"请求时,还需额外设置请求头)

 (4)监听服务器响应,接收返回值。

Ajax和后台的交互:封装好的Ajax的几个参数:url:发送请求的地址;type:发送请求的方式(post,get等,默认get);async: 同步异步请求,默认true所有请求均为异步请求。timeout : 超时时间设置,单位毫秒;data:要求为Object或String类型的参数,发送到服务器的数据等;

Ajax的实现流程:

  1. 创建XMLHttpRequest对象,也就是创建一个异步调用对象.
  2. 创建一个新的HTTP请求,并指定该HTTP请求的方法、URL及验证信息.
  3. 设置响应HTTP请求状态变化的函数.
  4. 发送HTTP请求.
  5. 获取异步调用返回的数据.

使用JavaScript和DOM实现局部刷新.

13.fetch

Fetch fetch号称是AJAX的替代品,是在ES6出现的,使用了ES6中的promise对象。Fetch是基于promise设计的。Fetch的代码结构比起ajax简单多。 fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象
优点缺点
语法简洁,更加语义化fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
基于标准 Promise 实现,支持 async/awaitfetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: 'include'})
更加底层,提供的API丰富(request, response)fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
脱离了XHR,是ES规范里新的实现方式fetch没有办法原生监测请求的进度,而XHR可以

14.axios

Axios 是一种基于Promise封装的HTTP客户端
  • 浏览器端发起XMLHttpRequests请求
  • node端发起http请求
  • 支持Promise API
  • 监听请求和返回
  • 对请求和返回进行转化
  • 取消请求
  • 自动转换json数据
  • 客户端支持抵御XSRF攻击

15.数组的遍历方法

方法改变原数组特点
forEach无返回值
map返回新数组,可链式调用
filter过滤数组,返回包含符合条件的元素的数组,可链式调用
for...offor...of遍历具有Iterator迭代器的对象的属性,返回的是数组的元素、对象的属性值,不能遍历普通的obj对象,将异步循环变成同步循环
every遍历的数组里的元素全部符合条件时,返回true
some遍历的数组里的元素至少有一个符合条件时,返回true
find返回第一个符合条件的值
findIndex返回第一个返回条件的值的索引值
reduce对数组正序操作
reduceRight对数组逆序操作

16.深拷贝

深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象(新旧对象不共享同一块内存),且 修改新对象不会影响原来的对象(深拷贝采用了在堆内存中申请新的空间来存储数据,这样每个可以避免指针悬挂)

实现方式如下:

JSON.parse(JSON.stringify())

这也是利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。 这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,因为这两者基于JSON.stringify和JSON.parse处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了

lodash的_.cloneDeep

需要安装lodash

jQuery.extend()

手写递归循环

递归方法实现深度克隆原理: 遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝
function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return obj // 如果是null或者undefined我就不进行拷贝操作
  if (obj instanceof Date) return new Date(obj)
  if (obj instanceof RegExp) return new RegExp(obj)
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
  if (typeof obj !== "object") return obj
  // 是对象的话就要进行深拷贝
  if (hash.get(obj)) return hash.get(obj)
  let cloneObj = new obj.constructor()
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj)
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash)
    }
  }
  return cloneObj
}

17.浅拷贝

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

实现方式如下:

Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象
let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' }
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }

lodash的_.clone

需要安装lodash

展开运算符

展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同
let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }

Array.prototype.concat()

let arr = [1, 3, {
    username: 'kobe'
}];
let arr2 = arr.concat();    
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]

Array.prototype.slice()

let arr = [1, 3, {
    username: ' kobe'
}];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]

18.赋值与深/浅拷贝的区别

对于引用数据类型
赋值深拷贝浅拷贝
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
和原数据是否指向同一对象第一层数据为基本数据类型且修改基本类型数据时原数据中包含子对象且修改子对象时
赋值改变会使原数据一起改变改变会使原数据一起改变
深拷贝改变不会使原数据一起改变改变不会使原数据一起改变
浅拷贝改变不会使原数据一起改变改变会使原数据一起改变

19.forEach如何跳出循环

forEach是不能通过 break或者 return来实现跳出循环的,forEach的回调函数形成了一个作用域,在里面使用 return并不会跳出,只会被当做 continue

实现方法:try...catch

function getItemById(arr, id) {
    var item = null;
    try {
        arr.forEach(function (curItem, i) {
            if (curItem.id == id) {
                item = curItem;
                throw Error();
            }
        })
    } catch (e) {}
    return item;
}

20.闭包

闭包是指有权访问另一个函数作用域中的变量的 函数
闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成

用途

  • 使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量
  • 使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收

使用场景

  • return 回一个函数
  • 函数作为参数
  • 自动执行函数
  • 循环赋值
  • 回调函数
  • 节流防抖
  • 函数柯里化

执行过程

  1. 形成私有上下文
  2. 进栈执行
  3. 开始一系列操作
  4. 初始化作用域链(两头<当前作用域,上级作用域>)
  5. 初始化this
  6. 初始化arguments
  7. 赋值形参
  8. 变量提升
  9. 代码执行
  10. 正常情况下,代码执行完成之后,私有上下文出栈被回收。但是遇到特殊情况,如果当前私有上下文执行完成之后中的某个东西被执行上下文以外的东西占用,则当前私有上下文就不会出栈释放,也就是形成了不被销毁的上下文,闭包

注意事项

容易导致内存泄漏。闭包会携带包含其它的函数作用域,因此会比其他函数占用更多的内存。过度使用闭包会导致内存占用过多,所以要谨慎使用闭包。

21.执行上下文

执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行

执行上下文有三种类型

  • 全局执行上下文:任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文
  • 函数执行上下文: 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤
  • eval函数执行上下文:执行在 eval 函数内部的代码也会有它属于自己的执行上下文

22.执行上下文栈

JavaScript引擎使用执行上下文栈来管理执行上下文
当JavaScript执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文
let a = 'Hello World!';

function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}

function second() {
  console.log('Inside second function');
}

first();
console.log('Inside Global Execution Context');

img

当上述代码在浏览器加载时,JavaScript 引擎创建了一个全局执行上下文并把它压入当前执行栈。当遇到  first() 函数调用时,JavaScript 引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部。
当从  first() 函数内部调用  second() 函数时,JavaScript 引擎为  second() 函数创建了一个新的执行上下文并把它压入当前执行栈的顶部。当  second() 函数执行完毕,它的执行上下文会从当前栈弹出,并且控制流程到达下一个执行上下文,即  first() 函数的执行上下文。
当  first() 执行完毕,它的执行上下文从栈弹出,控制流程到达全局执行上下文。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文

23.执行上下文的三个阶段

创建阶段 → 执行阶段 → 回收阶段

创建阶段

  1. this绑定
  2. 在全局执行上下文中,this指向全局对象(window对象)
  3. 在函数执行上下文中,this指向取决于函数如何调用。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined
  4. 创建词法环境组件
  5. 词法环境是一种有标识符——变量映射的数据结构,标识符是指变量/函数名,变量是对实际对象或原始数据的引用
  6. 词法环境的内部有两个组件:环境记录器:用来储存变量个函数声明的实际位置,外部环境的引用:可以访问父级作用域
  7. 创建变量环境组件
  8. 变量环境也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系

执行阶段

在这阶段,执行变量赋值、代码执行。如果 Javascript 引擎在源代码中声明的实际位置找不到变量的值,那么将为其分配 undefined 值

回收阶段

执行上下文出栈等待虚拟机回收执行上下文

24.作用域Scope

作用域是在运行时代码中的某些特定部分中变量、函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。 作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说 作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

全局作用域

  • 直接写在script标签的JS代码,都在全局作用域。在全局作用域下声明的变量叫做全局变量(在块级外部定义的变量)
  • 所有末定义直接赋值的变量自动声明为拥有全局作用域
  • 全局变量在全局的任何位置下都可以使用;全局作用域中无法访问到局部作用域的中的变量
  • 全局作用域在页面打开的时候创建,在页面关闭时销毁
  • 所有 window 对象的属性拥有全局作用域
var和function命令声明的全局变量和函数是window对象的属性和方法
let命令、const命令、class命令声明的全局变量,不属于window对象的属性

值得注意的是,块语句(大括号之间的语句,如if语句、switch语句、for循环语句、while语句)不会创建一个新的作用域,在块语句中定义的变量将保留在它们存在的作用域中

函数作用域(局部作用域)

  • 调用函数时会创建函数作用域,函数执行完毕之后,作用域销毁。每调用一次函数就会创建一个新的函数作用域,他们之间是相互独立的
  • 在函数作用域中可以访问全局变量,在全局作用域中一般情况下无法访问函数内的变量(可以通过闭包访问)
  • 在函数作用域中操作一个变量时,它会先在自身作用域内寻找,如果有就直接使用,如果没有就向上一级作用域中寻找,知道找到全局作用域中。如果全局作用域中仍未找到,则报错

块级作用域

块级作用域可通过新增命令let和const声明,所声明的变量在指定的块级作用域外无法被访问,块级作用域在如下情况被创建:

  • 在一个函数内部
  • 在一个代码块(由一对花括号包裹)内部

let 声明的语法与 var 的语法一致。基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。块级作用域有以下几个特点:

  • 声明变量不会提升到代码块顶部
  • 禁止重复声明

25.作用域链

在某个作用域内访问一个变量时,会先在当前作用域内寻找,如果没有找到,则去上一级作用域内寻找,以此类推。这样的变量作用域访问的链式结构,被称为作用域链

作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。

26.预解析(变量提升)

JS 引擎在执行一段代码的时候,会按照下面的步骤进行工作

  • 把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值
  • 吧函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用
  • 先提升function,再提升var声明的变量
区别
  • JavaScript 代码执行前引擎会先进行预编译,预编译期间会将变量声明与函数声明提升至其对应作用域的最顶端,函数内声明的变量只会提升至该函数作用域最顶层。当函数内部定义的一个变量与外部相同时,那么函数体内的这个变量就会被上升到最顶端
  • 函数提升只会提升函数声明式写法,函数表达式的写法不存在函数提升
  • 函数提升的优先级大于变量提升的优先级,即函数提升在变量提升之上

27.内存泄露

内存泄露是指由于疏忽或错误造成程序未能释放已经不再使用的内存

内存泄露的原因有以下几种:

  • 意外的全局变量:由于使用为声明的变量,而意外的创建了一个变量,这个变量将一直留在内存中无法被回收
  • 被遗忘的定时器或回调函数:设置了setInterval定时器而忘记销毁,如果循环函数有对外部的引用的话,这个变量将一直被留在内存中无法被回收
  • 脱离DOM的引用:获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收
  • 闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中

28.函数式编程的优缺点

优点缺点
降低维护成本过渡包装会导致性能开销
代码的复用性更强资源占用更强
组合起来更加优雅为了实现迭代,可能会掉入递归陷阱

29.纯函数

纯函数是对给定的输入返还相同的输出的函数,并且要求所有的数据都是不可变的

特性

  • 函数内部传入指定的值,就会返回唯一确定的值
  • 不会造成超出作用域的变化,例如修改全局变量或引用传递的参数

优势

  • 通过纯函数可以产生可测试的代码
  • 不依赖外部环境计算,不会产生副作用,复用性高
  • 可读性高,不管是不是纯函数,都会有一个语义化的名称,便于阅读
  • 符合模块化概念及单一职责原则

30.高阶函数

高阶函数是指使用其它函数作为参数、或者返回一个函数作为返回值的函数

常见的高阶函数

  • Array.prototype.map
  • Array.prototype.filter
  • Array.prototype.forEach
  • Array.prototype.reduce

31.函数柯里化

柯里化(Currying)又叫函数的部分求值,是把 接受多个参数的函数变换成 接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术

优点

  • 参数复用:需要输入多个参数,最终只需输入一个,其余通过arguments对象获取
  • 提前确认:避免重复判断某一条件是否符合,不符合则return
  • 延迟运行:避免重复执行程序,等真正需要结果的时候再执行
function curry(fn, args) {
    args = args || []
    var arity = fn.length

    return function() {
        var _args = Array.prototype.slice.call(arguments)
        Array.prototype.unshift.call(_args, ...args)
        _args = _args.concat(args)

        if (_args.length < arity) {
            return currying.call(null, fn, _args)
        }

        return fn.apply(null, _args)
    }
}

32.箭头函数

ES6中允许使用“箭头”( =>) 来定义函数。箭头函数相当于匿名函数,并且简化了函数定义

特点

  • 箭头函数不绑定this,箭头函数里的this永远指向定义箭头函数时所处的作用域
  • 箭头函数的this永远不会变,call、apply、bind也无法改变
  • 箭头函数只能声明成匿名函数,但可以通过表达式的方式让箭头函数具名
  • 箭头函数没有原型prototype
  • 因为this的指向问题,箭头函数不能作为构造函数使用
  • 箭头函数没有 arguments 在箭头函数内部访问这个变量访问的是外部环境的arguments, 可以使用 ...代替

33.Promise

ES6新增的一种异步编程的解决方案,比传统的回调函数和事件更加的合理和强大。通过 Promise可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。 Promise可以解决异步的问题,但不能说Promise是异步的

特点

  1. 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:
  2. pending——进行中
  3. resolved——已成功
  4. rejected——已失败
  5. 一旦状态改变,就不会再发生变化,任何时候都可以得到这个结果。Promise对象状态的改变只有两种可能:
  6. pending——resolved
  7. pending——rejected
  8. Promise内部发生错误,不会影响到外部程序的执行。
  9. Promise一旦执行则无法取消:
  10. 一旦创建就会立即执行,无法中途取消(缺点1
  11. 如果不设置回调函数,Promise内部抛出的错误将不会反应到外部(缺点2
  12. 当处于pending状态时,无法得知目前进展到哪一阶段,即无法预测是刚刚开始还是即将完成(缺点3

用法

创建Promise实例时,必须传入一个函数作为参数:

new Promise(() => {})

该函数可以接收另外两个由JavaScript引擎提供的函数,resolvereject:

  • resolve——将Promise对象的状态从pending变为resolved,将异步操作的结果作为参数传递出去
  • reject——将Promise对象的状态从pending变为rejected,将异步操作报出的错误作为参数传递出去
const promise = new Promise((resolve, reject) => {
    if (true) resolve('value')
    else reject('error')
})

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数:

promise.then(value => {
    console.log(value)  // 'value'
}, error => {
    console.log(error)  // 'error'
})

then方法只有一个函数参数时,此时为resolved状态的回调方法:

promise.then(value => {
    // 只有状态为resolved时才能调用,如果返回的是rejected状态,则报错 Uncaught (in promise) error
    console.log(value)
})
只有当 promise的状态变为 resolved或者 rejected时, then方法才会被调用

Promise新建后就会立即执行,并且调用resolvereject后不会终结 Promise的参数函数的执行。

let promise = new Promise(resolve => {
    console.log('1')
    resolve()
    console.log('2')
})
promise.then(resolved => {
    console.log('3')
})
console.log('3')

resolve返回的是另外一个Promise实例:

const p1 = new Promise((_, reject) => {
  setTimeout(() => reject('error'), 3000);
})
const p2 = new Promise(resolve => {
  setTimeout(() => resolve(p1), 1000);
})
p2.then(
  result => console.log(result),
  error => console.log(error) // error
)
上面代码中, p1是一个  Promise,3 秒之后变为 rejectedp2的状态在 1 秒之后改变, resolve方法返回的是 p1。由于 p2返回的是另一个 Promise,导致 p2自己的状态无效了,由 p1的状态决定 p2的状态。所以,后面的 then语句都变成针对后者( p1)。又过了 2 秒, p1变为 rejected,导致触发 catch方法指定的回调函数。可以理解成 p2.then 实际上是 p1.then
resolve返回的是另一个Promise实例的时候,当前Promise实例的状态会根据返回的Promise实例的状态来决定

常用API

Promise.resolve()

有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用,且实例状态为resolve:

Promise.resolve('foo')
// 等价于
return new Promise(resolve => resolve('foo'))

Promise.resolve()的参数有以下几种情况:

  • 参数是一个Promise实例:
const promise = new Promise(resolve => {
    resolve('resolve')
})
let p = Promise.resolve(promise)
// p 相当于
let p = new Promise(resolve => {
    resolve(promise)
})
console.log(p === promise)  // true
  • 参数是一个thenable对象:

thenable对象指的是具有then方法的对象,Promise.resolve()会将这个对象转为Promise对象,然后立即执行thenable对象的then方法

const thenable = {
    then(resolve, reject) {
        resolve('resolved')
    }
}
const p1 = Promise.resolve(thenable)
p1.then(res => {
    console.log(res)    // 'resolved'
})
上面代码中, thenable对象的 then()方法执行后,对象 p1的状态就变为 resolved,从而立即执行最后那个 then()方法指定的回调函数,输出'resolved'
  • 参数不是具有then()方法的对象,或者根本不是对象
const promise = Promise.resolve({name: 'James'})
promise.then(res => {
    console.log(res)    // {name: 'James'}
})
当参数是不含有 then()方法的对象,或者根本不是对象时,会直接返回该参数
  • 不带有任何参数
const promise = Promise.resolve()
promise.then(res => {
    console.log(res)    // undefined
})
Promise.resolve()方法允许调用时不带参数,直接返回一个 resolved状态的  Promise 对象,传参为 undefined

Promise.reject()

Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected

const promise = Promise.reject('Error')
// 等价于
const promise = new Promise((resolve, reject) => {
    reject('Error')
})

Promise.all()

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例

const p1 = new Promise((resolve, reject) => {})
const p1 = new Promise((resolve, reject) => {})
const p1 = new Promise((resolve, reject) => {})
const promise = Promise.all([p1, p2, p3])
promise.then(result => {}, error => {})

面代码中,Promise.all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。p的状态由p1p2p3决定,分成两种情况:

  • 只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数
  • 只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数
const number = 35

const p1 = new Promise((resolve, reject) => {
  if (number >= 10) resolve('p1 success!')
  else reject('p1 failed!')
})
const p2 = new Promise((resolve, reject) => {
  if (number >= 20) resolve('p2 success!')
  else reject('p2 failed!')
})
const p3 = new Promise((resolve, reject) => {
  if (number >= 30) resolve('p3 success!')
  else reject('p3 failed!')
})
const promise = Promise.all([p1, p2, p3]).then(res => {
  console.log(res)  // 当number为35时,res值为[ 'p1 success!', 'p2 success!', 'p3 success!' ]
}, error => {
  console.log(error)    // 当number为25时,p3会返回rejected,promise状态会变成rejected,error值为p3 failed!
})
如果作为参数的  Promise 实例,自己定义了 catch方法,那么它一旦被 rejected,并不会触发 Promise.all()catch方法
const p1 = new Promise(resolve => {
  resolve("hello");
}).then(result => result).catch(e => e);

const p2 = new Promise(() => {
  throw new Error("报错了");
}).then(result => result).catch(e => e); // p2实际上是catch返回的promise实例

Promise.all([p1, p2]).then(result => console.log(result)).catch(e => console.log(e));

34.EventLoop

为了解决单任务执行过长的问题和处理高优先级的任务,所以需要将任务划
  • 为了解决单个任务执行时间过长,把js任务分为同步任务和异步任务,同步任务直接执行,异步任务放入任务队列等待执行
  • 为了解决异步队列中等待任务的执行优先级的问题,把异步任务分为微任务和宏任务,同步任务执行完后,就先执行微任务

同步和异步

我们知道了浏览器页面是由任务队列和事件循环系统来驱动的,但是队列要一个一个执行,如果某个任务(http请求)是个耗时任务,那浏览器总不能一直卡着,所以为了防止主线程阻塞,就将任务分为同步任务和异步任务
  • 同步任务:就是任务一个一个执行,如果某个任务执行时间过长,后面的任务只能一直等下去
  • 异步任务:就是进程在执行某个任务时,该任务需要等一段时间才能返回,这时候就把这个任务放到专门处理异步任务的任务队列中去,执行栈则继续往下执行,不会因为这个任务而阻塞

微任务和宏任务

JS执行时,V8会创建一个全局执行上下文,在创建上下文的同时, V8也会在内部创建一个微任务队列

有微任务队列,自然就有宏任务队列,宏任务队列中的每一个任务则都称为宏任务,在当前宏任务执行过程中,如果有新的微任务产生,就添加到微任务队列中

  • 微任务包括:promise.then()、queueMicrotask()、MutationObserver(监听DOM)、node 中的 process.nextTick等
  • 宏任务包括:渲染事件、请求、script、setTimeout、setInterval、Node中的setImmediate、I/O 等

367e4062e66b2c2512768749e533393.jpg

事件循环

任务进栈到出栈的循环。即一个宏任务,所有微任务,渲染;一个宏任务,所有微任务,渲染.....

循环过程

  1. 所有同步任务都在主线程上依次进行,形成一个执行栈,异步任务进入到一个任务队列中
  2. 当执行栈中任务执行完后,再去检查微任务队列中的微任务是否完成,有就继续执行,如果微任务过程中又产生新的微任务,就添加到微任务队列末尾继续执行,直到所有微任务全部执行完毕
  3. 微任务执行完后,再到任务队列检查是否有宏任务,有就取出最先进入队列的宏任务压入执行栈中执行其同步代码
  4. 然后回到第2步执行该宏任务中的微任务,如此反复,直到宏任务也执行完,如此循环
<script>
    setTimeout(function () {
        console.log('setTimeout')
    }, 0)
    new Promise(function (resolve) {
        console.log('promise1')
        for( let i = 0; i < 1000; i++ ) {
            i === 999 && resolve()
        }
        console.log('promise2')
    }).then(function ()  {
        console.log('promise3')
    })
    console.log('script')
</script>
输出结果: promise1 ->  promise2 ->  script ->  promise3 ->  setTimeout
  • script是宏任务,先执行里面的微任务
  • 遇到宏任务setTimeout放到异步处理模块
  • 继续执行promise,打印promise1
  • 遇到for循环,执行,遇到resolve()回调,回调属于微任务,放到微任务队列
  • 继续执行,打印promise2
  • 继续执行,打印script
  • 执行栈内任务执行完毕,取出微任务队列中的任务
  • 执行promise的then回调,打印promise3
  • 所有微任务执行完毕,去任务队列中取出下一个宏任务
  • 执行setTimeout,打印setTimeout

35.async/await

async声明function是一个异步函数,返回一个 promise对象,可以使用 then 方法添加回调函数。
async函数内部 return语句返回的值,会成为 then方法回调函数的参数。
如果async函数没有返回值 async函数返回一个undefined的promise对象
async function test() {
    return 'test'
}
console.log(test)   // [AsyncFunction: test] async函数是[`AsyncFunction`]构造函数的实例
console.log(test()) // Promise { 'test' }

// async返回的是一个promise对象
test().then(res => {
    console.log(res)    // 'test'
})
await 操作符只能在异步函数 async function 内部使用
如果一个 Promise 被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果,也就是说它会阻塞后面的代码,等待 Promise 对象结果;如果等待的不是 Promise 对象,则返回该值本身
async function test() {
  return new Promise((resolve)=>{
    setTimeout(() => {
        resolve('test 1000');
    }, 1000);
  })
}
function fn() {
  return 'fn';
}

async function next() {
    let res0 = await fn(),
        res1 = await test(),
        res2 = await fn();
    console.log(res0);
    console.log(res1);
    console.log(res2);
}
next(); // 1s 后才打印出结果 为什么呢 就是因为 res1在等待promise的结果 阻塞了后面代码。

错误处理

如果 await后面的异步操作出错,那么等同于 async函数返回的 Promise 对象被 reject
async function test() {
  await Promise.reject('错误了')
}

test().then(res=>{
  console.log('success',res)
},err=>{
  console.log('err ',err)   // err 错误了
})
防止出错的方法,也是将其放在 try...catch代码块之中
async function test() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error('错误了');
    });
  } catch(e) {
      console.log('err', e)
  }
  return await('成功了');
}
多个 await命令后面的异步操作,如果不存在继发关系(即互不依赖),最好让它们同时触发
let foo = await getFoo();
let bar = await getBar();
// 上面这样写法 getFoo完成以后,才会执行getBar

// 同时触发写法 ↓

// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

优点

async/await的优势在于处理由多个Promise组成的 then 链,在之前的Promise文章中提过用then处理回调地狱的问题,async/await相当于对promise的进一步优化。 假设一个业务,分多个步骤,且每个步骤都是异步的,而且依赖上个步骤的执行结果
// 假设表单提交前要通过俩个校验接口

async function check(ms) { // 模仿异步 
  return new Promise((resolve)=>{
    setTimeout(() => {
        resolve(`check ${ms}`);
    }, ms);
  })
}
function check1() {
  console.log('check1');
  return check(1000);
}
function check2() {
  console.log('check2');
  return check(2000);
}

// -------------promise------------
function submit() {
  console.log('submit');
  // 经过俩个校验 多级关联 promise传值嵌套较深
  check1().then(res1=>{
    check2(res1).then(res2=>{
       /*
        * 提交请求
        */
    })
  })
}
submit();

// -------------async/await-----------
async function asyncAwaitSubmit() {
    let res1 = await check1(),
        res2 = await check2(res1);
        console.log(res1, res2);
        /*
        * 提交请求
        */
}

36.防抖

当事件触发时,相应的函数不会立即触发,而是等待一段时间;
当事件连续触发时,函数的触发等待时间会被不断重置(推迟)。

通俗的讲,防抖就是,每次触发事件时,在一段时间后才真正响应这个事件,具体应用如下:

  • 输入框中频繁输入内容,如果输入框改变一次就发送一次请求的话,会对服务器造成很大的压力,所以我们希望在连续输入的时候不发送请求,直到用户输入完或者一段时间没有继续输入的话才发送请求;
  • 频繁点击按钮触发事件(恶意的行为)
  • 用户缩放浏览器时频繁触发resize事件
  • 王者荣耀回城

如何实现防抖函数

function debounce(callback, time) {
    let timer
    return function() {
        clearTimeout(timer)
        let args = arguments
        timer = setTimeout(() => {
            callback.apply(this, args)
        }, time)
    }
}

37.节流

如果事件被频繁出发,那么节流函数会按照一定的频率来执行函数;
不管中间触发了多少次,执行函数的 频率总是固定的

说白了节流就是在间隔一段时间执行一次,具体应用如下:

  • 王者荣耀冷却中的技能无法再次释放;
  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断;
  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)。

如何实现节流函数

function throttle(func, delay) {
    let timer;
    return function() {
        let args = arguments;
        if (!timer) {
            timer = setTimeout(() => {
                timer = null;
                func.apply(this, args)
            }, delay)
        }
    }
}

38.forEach、map和for循环

forEach

  1. 没有返回值;
js var a = [1,2,3,4,5] var b = a.forEach((item) => { item = item * 2 }) console.log(b) // undefined
  1. 无法中断执行;
  2. 可以使用return跳过当前循环;
  3. 跳过数组的空位,但不会跳过nullundefined
js var a = [null, , undefined] a.forEach(item => { console.log('item', item) // null undefined })
  1. 改变数组情况;
  2. 为什么直接修改item无法修改原数组呢,因为item的值并不是相应的原数组中的值,而是重新建立的一个新变量,值和原数组相同。因此,如果item是基础数据类型,那么并不会改变数组里面的值,如果是引用类型,那么item和数组里面的值是指向同一内存地址,则都会被改变。
js var a = [1,2,3,4,5] a.forEach((item) => { item = item * 2 }) console.log(a) // [1,2,3,4,5]
  • 数组中的对象的值也没有改变,是因为新创建的变量和原数组中的对象虽然指向同一个地址,但改变的是新变量的值,也就是重新赋值,即新对象的值为2,原数组中的对象还是{num:1}
js var a = [1,'1',{num:1},true] a.forEach((item, index, arr) => { item = 2 }) console.log(a) // [1,'1',{num:1},true]
  • 由于对象是引用类型,新对象和旧对象指向的都是同一个地址,所以新对象把num变成了2,原数组中的对象也改变了。
js var a = [1,'1',{num:1},true] a.forEach((item, index, arr) => { item.num = 2 item = 2 }) console.log(a) // [1,'1',{num:2},true]
  1. 手写forEach方法;
js Array.prototype.new_forEach = function(callback) { for (let i = 0; i < this.length; i++) { callback(this[i], i, this) } }

map

  1. 有返回值;
js var a = [1,2,3,4,5] var b = a.map((item) => { return item = item * 2 }) console.log(a) // [1,2,3,4,5] console.log(b) // [2,4,6,8,10]
  1. 无法中断执行,同forEach;
  2. 可以使用return跳过当前循环,同forEach;
  3. 跳过数组的空位,但不会跳过nullundefined,同forEach;
  4. 改变数组情况,同forEach;
  5. 手写map方法;
js Array.prototype.new_map = function(callback) { const res = [] for (let i = 0; i < this.length; i++) { res.push(callback(this[i], i, this)) } return res }

for循环

  1. for循环是个语句,forEach和map则是表达式;
  2. for循环可以使用break结束循环;
  3. for循环可以使用continue语句跳过当前循环;
  4. for循环不会跳过数组的空位,会默认空位为undefined;

性能对比

  1. for 循环当然是最简单的,因为它没有任何额外的函数调用栈和上下文;
  2. forEach 其次,因为它其实比我们想象得要复杂一些,它的函数签名实际上是array.forEach(function(currentValue, index, arr), thisValue)它不是普通的 for 循环的语法糖,还有诸多参数和上下文需要在执行的时候考虑进来,这里可能拖慢性能;
  3. map 最慢,因为它的返回值是一个等长的全新的数组,数组创建和赋值产生的性能开销很大。

原型和原型链

1. 原型可以解决什么问题
    对象共享属性和共享方法
2. 谁有原型
函数拥有:prototype
对象拥有:__proto__
3. 对象查找属性或者方法的顺序
    先在对象本身查找 --> 构造函数中查找 --> 对象的原型 --> 构造函数的原型中 --> 当前原型的原型中查找
4. 原型链
    4.1 是什么?:就是把原型串联起来
    4.2 原型链的最顶端是null 

5.原型链的原理和作用:类的公有方法不会开辟新内存,以此达到节约内存的目的,可以优化项目性能;对象里面放自己独有的数据和方法代码 原型上面放对象共有的数据和代码;js用原型链来实现继承。

JS判断变量是不是数组,你能写出哪些方法?

方式一:isArray

var arr = [1,2,3];
console.log( Array.isArray( arr ) );

方式二:instanceof 【可写,可不写】

var arr = [1,2,3];
console.log( arr instanceof Array );

方式三:原型prototype

var arr = [1,2,3];
console.log( Object.prototype.toString.call(arr).indexOf('Array') > -1 );

方式四:isPrototypeOf()

var arr = [1,2,3];
console.log(  Array.prototype.isPrototypeOf(arr) )

方式五:constructor

var arr = [1,2,3];
console.log(  arr.constructor.toString().indexOf('Array') > -1 )

面试题:slice是干嘛的、splice是否会改变原数组

1. slice是来截取的
    参数可以写slice(3)、slice(1,3)、slice(-3)
    返回的是一个新的数组
2. splice 功能有:插入、删除、替换
    返回:删除的元素
    该方法会改变原数组

面试题:JS数组去重

方式一:new set

var arr1 = [1,2,3,2,4,1];
function unique(arr){
    return [...new Set(arr)]
}
console.log(  unique(arr1) );

方式二:indexOf

var arr2 = [1,2,3,2,4,1];
function unique( arr ){
    var brr = [];
    for( var i=0;i<arr.length;i++){
        if(  brr.indexOf(arr[i]) == -1 ){
            brr.push( arr[i] );
        }
    }
    return brr;
}
console.log( unique(arr2) );

方式三:sort

var arr3 = [1,2,3,2,4,1];
function unique( arr ){
    arr = arr.sort();
    var brr = [];
    for(var i=0;i<arr.length;i++){
        if( arr[i] !== arr[i-1]){
            brr.push( arr[i] );
        }
    }
    return brr;
}
console.log( unique(arr3) );

new操作符具体做了什么

1. 创建了一个空的对象
2. 将空对象的原型,指向于构造函数的原型
3. 将空对象作为构造函数的上下文(改变this指向)
4. 对构造函数有返回值的处理判断

js继承有哪些方式?

方式一:ES6

class Parent{
    constructor(){
        this.age = 18;
    }
}
​
class Child extends Parent{
    constructor(){
        super();
        this.name = '张三';
    }
}
let o1 = new Child();
console.log( o1,o1.name,o1.age );

方式二:原型链继承

function Parent(){
    this.age = 20;
}
function Child(){
    this.name = '张三'
}
Child.prototype = new Parent();
let o2 = new Child();
console.log( o2,o2.name,o2.age );

方式三:借用构造函数继承

function Parent(){
    this.age = 22;
}
function Child(){
    this.name = '张三'
    Parent.call(this);
}
let o3 = new Child();
console.log( o3,o3.name,o3.age );

方式四:组合式继承

function Parent(){
    this.age = 100;
}
function Child(){
    Parent.call(this);
    this.name = '张三'
}
Child.prototype = new Parent();
let o4 = new Child();
console.log( o4,o4.name,o4.age );

面试题:说一下call、apply、bind区别

共同点:功能一致

可以改变this指向
​
语法: 函数.call()、函数.apply()、函数.bind()

区别:

1. call、apply可以立即执行。bind不会立即执行,因为bind返回的是一个函数需要加入()执行。
2. 参数不同:apply第二个参数是数组。call和bind有多个参数需要挨个写。

场景:

1. 用apply的情况
var arr1 = [1,2,4,5,7,3,321];
console.log( Math.max.apply(null,arr1) )
​
2. 用bind的情况
var btn = document.getElementById('btn');
var h1s = document.getElementById('h1s');
btn.onclick = function(){
    console.log( this.id );
}.bind(h1s)

面试题:computed、methods、watch有什么区别?

1. computed vs methods区别
    computed是有缓存的
    methods没有缓存
    watch也没有缓存
2. computed vs watch区别
    watch是监听,数据或者路由发生了改变才可以响应(执行)
    computed计算某一个属性的改变,如果某一个值改变了,计算属性会监听到进行返回
    watch是当前监听到数据改变了,才会执行内部代码

计算属性缓存结果时每次都会重新创建变量,而侦听器是直接计算,不会创建变量保存结果。也就意味着,数据如果会反复的发生变化,计算很多次的情况下,计算属性的开销将会更大,也就意味着这种情况不适合使用计算属性,适合使用侦听器。

那么,如果一个数据反复会被使用,但是它计算依赖的内容很少发生变化的情况下,计算属性会缓存结果,就更加适合这种情况。

计算属性:

  1. 支持缓存,只有当数据放生改变时,才会重新进行计算
  2. 不支持异步,当computed内有异步操作时无效,无法将听数据的变化。
  3. computed 属性值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data中声明过或者父组件传递的props中的数据通过计算得到的值
  4. 如果一个属性是由其他属性计算而来的,这个属性依赖其他属性,是一个多对一或者一对一,一般用computed
  5. 如果computed属性属性值是函数,那么默认会走get方法;函数的返回值就是属性的属性值;在computed中的,属性都有一个get和一个set方法,当数据变化时,调用set方法。

监视属性watch:

  1. 不支持缓存,数据变,直接会触发相应的操作;
  2. watch支持异步;
  3. 监听的函数接收两个参数,第一个参数是最新的值;第二个参数是输入之前的值;
  4. 当一个属性发生变化时,需要执行对应的操作;一对多;
  5. 监听数据必须是data中声明过或者父组件传递过来的props中的数据,当数据变化时,触发其他操作,函数有两个参数,

两个重要的小原则:

  1. 所被Vue管理的函数,最好写成普通函数,这样this的指向才是vm或组件实例对象。
  2. 所有不必Vue所管理的函数(定时器的回调函数,ajax的回调函数等),最好写成箭头函数,这样this的指向才是vm或者组件实例对象。
     

面 试题:props和data优先级谁高?

四、Vue2

1.Vue的基本原理

当一个Vue实例创建时,Vue会遍历data中的属性,用Object.defineProperty将它们转为getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。每个组件实例都有相应的watcher程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而使它关联的组件得以更新

Snipaste_2022-10-07_10-07-50.png

2.响应式原理

数据劫持

利用Object.defineProperty劫持对象的访问器,在属性发生变化时我们可以获取变化,从而进行下一步操作

发布者模式和订阅者模式

在软件架构中,发布订阅是一种消息范式,发布者不会将消息直接发送给订阅者,而是将发布的消息分为不同的类别,无需了解哪些订阅者是否存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者是否存在
发布者和订阅者都不知道对方的存在,发布者只需发送消息到订阅器里,订阅者只管接收自己订阅的内容

响应式原理

vue的响应式原理就是 采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的getter和setter,在数据变动时发布消息给订阅者,触发相应的监听回调。只要分为以下几个步骤:
  1. 需要给Observe被劫持的数据对象)的数据对象进行递归遍历,包括子属性对象的属性,都加上getter、setter这样的属性。修改这个对象的某个属性,就会触发setter,那么就能监听到数据的变化
  2. CompileVue的编译器)解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加侦听数据的订阅者,当数据发生变化时会收到通知,更新视图
  3. Watcher订阅者)是ObserveCompile之间的桥梁,主要做的事情有:
  4. 在自身实例化时Dep(用于收集订阅者的订阅器)里添加自己
  5. 自身必须拥有一个update()方法
  6. 待属性变动dep.notice() 通知时,能调用自身的update() 方法,并触发Compile中绑定的回调,则功成身退
  7. MVVM作为数据绑定的入口,整合ObserverCompileWatcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起ObserverCompile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果

Snipaste_2022-10-07_10-09-27.png

Object.defineProperty(obj, prop, descriptor)

  • obj - 要定义的对象
  • prop - 要定义或修改的属性名称或Symbol
  • descriptor - 要定义或修改的属性的描述符(配置对象)
  • get 属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值
  • 默认为 [undefined]set 属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为 [undefined]

缺点:在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty() 不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题

  • 返回值:被传递给函数的对象obj

3.MVVM

MVVM(Model-View-ViewModel)模式是一种基于前端开发的架构模式,其核心是提供对 View 和 Model 的双向数据绑定,即当 View 或 Model 其中任意一个发生变化时,都会通过 ViewModel 引起另外一个的改变。

Snipaste_2022-10-07_10-12-52.png

  • MVVM的优点
  1. 双向绑定。数据层和视图层中任何一层发生变化,都会通过ViewModel使另一层变化,时刻保持数据和视图的一致性;
  2. 通过数据驱动视图更新,不再手动操作DOM元素,提高性能;

  • MVVM的缺点
  1. 由于双向绑定的技术,产生bug后无法确定问题出现在数据层还是视图层;
  2. 数据层过大会占用更多的内存资源,影响性能;

4.常用指令

指令作用
v-on缩写为@,绑定事件
v-bind简写为:,动态绑定
v-slot简写为#,组件插槽
v-for循环对象或数组元素,同时生成DOM
v-show显示内容与否
v-if显示与隐藏,决定当前DOM元素是否渲染
v-else必须和v-if连用 不能单独使用 否则报错
v-text解析文本
v-html解析html标签

5.动态绑定class与style

classstyle
绑定对象:class="{ className: isActive }" 或 :class="classNameObject":style="{color: '#ffffff'}" 或 :style="styleObject"
绑定数组:class="['active', 'is-success', { 'is-disabled': isDisabled }]":style="[styleObject1, styleObject2, styleObject3, ...]"

6.常见修饰符

v-on

修饰符作用
.stopevent.stopPropagation(),阻止单击事件继续传播(阻止默认事件)
.preventevent.preventDefault(),提交事件不再重载页面(阻止默认行为)
.native监听组件根元素的原生事件
.once点击事件将只会触发一次

v-model

修饰符作用
.lazy取代 input 监听 change 事件
.number输入值转为数值类型
.trim输入首尾空格过滤

7.v-if与v-show的区别

原理

  • v-if会调用addIfCondition方法根据条件渲染,为false时在生成vnode的时候会忽略对应节点,render的时候就不会渲染
  • 添加v-show指令的元素一定会渲染,只是通过修改display属性的值来决定是否显示

切换

  • v-if切换时,DOM元素会重复生成和销毁,会执行生命周期钩子
  • v-show切换时不会执行生命周期钩子

应用场景

需要频繁切换DOM时,使用v-show;反之则使用v-if

区别:

       v-show和v-if都是用来控制元素的呈现与隐藏。v-show只会编译一次通过控制css中的display实现频繁切换状态用v-show)。v-if编译多次,通过动态控制DOM元素实现不频繁切换)。

8.为什么避免v-for和v-if在一起使用?

Vue 处理指令时,v-for 比 v-if 具有更高的 优先级,存在性能问题。 如果你有5个元素被v-for循环,,v-if也会分别执行5次

9.v-for 循环为什么一定要绑定key

提升vue渲染性能
  • key 的作用主要是为了更高效的更新虚拟 DOM,因为它可以非常精确的找到相同节点,因此 patch 过程会非常高效
  • Vue 在 patch 过程中会判断两个节点是不是相同节点时,key 是一个必要条件。比如渲染列表时,如果不写 key,Vue 在比较的时候,就可能会导致频繁更新元素,使整个 patch 过程比较低效,影响性能
  • 应该避免使用数组下标作为 key,因为 key 值不是唯一的话可能会导致上面图中表示的 bug,使 Vue 无法区分它他,还有比如在使用相同标签元素过渡切换的时候,就会导致只替换其内部属性而不会触发过渡效果
  • Vue 判断两个节点是否相同时主要判断两者的元素类型和 key 等,如果不设置 key,就可能永远认为这两个是相同节点,只能去做更新操作,就造成大量不必要的 DOM 更新操作,明显是不可取的

10.为什么不建议用index索引作为key?

使用index作为key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多无意义的额外工作

11.v-model

v-model用于实现视图层与数据层的双向绑定,数据层变化时可以驱动视图层更新,当视图层变化时会改变数据。v-model本质上是一个语法糖,默认情况下相当于:value@input的结合。

v-model通常使用在表单项上,但也能使用在自定义组件上,表示对某个值的输入和输出控制。使用v-model可以减少大量繁琐的事件处理代码,提高开发效率。

12.v-model与.sync的对比

共同点:都是语法糖,都可以实现父子组件中数据的双向通信

不同点:

v-model.sync
父组件中使用v-model传递数据时,子组件通过@input触发事件父组件中传递数据时,子组件通过@update:xxx触发事件
一个组件只能绑定一个v-model vue2一个组件可以多个属性用.sync修饰符,可以同时"双向绑定多个“prop”
v-model针对更多的是最终操作结果,是双向绑定的结果,是value,是一种change操作.sync针对更多的是各种各样的状态,是状态的互相传递,是status,是一种update操作

13.computed与watch的区别

computed
  • 支持缓存,只有依赖的数据发生了变化,才会重新计算
  • 不支持异步,当computed中有异步操作时,无法监听数据的变化
  • computed的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,即data中声明的数据或props传递的数据
  • 如果一个属性是由其他属性计算而来,这个属性依赖其它属性,一般会使用computed
  • 如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值;在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法
watch
  • 不支持缓存,数据变化会执行相应操作
  • 支持异步监听
  • 监听的函数接收两个参数,第一个参数为更新后的值,第二个参数为更新前的值‘
  • 当一个属性发生变化时,就需要执行相应的操作
  • 监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
  • immediate:默认为false,为true时组件加载会立即触发
  • deep:默认为false,为true时开启深度监听。需要注意的是,deep无法监听到数组和对象内部的变化

当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch

总结
computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
watch 侦听器 : 更多的是 观察的作用, 无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。

14.组件通信

组件通信指的是组件通过某一种方式来传递信息以达到某个目的的过程。每个组件都是独立的,在开发中我们就是通过组件通信使各个组件的功能联动起来

props/$emit

父组件通过props向子组件传递数据,子组件通过$emit事件和父组件通信
  • props只能是父组件向子组件传递数据,使得父子组件直接形成一个向下的单向数据流。子组件不能直接修改props数据,只能通知父组件来修改
  • $emit绑定一个自定义事件,可以将参数传递给父组件;父组件中则通过v-on注册监听事件同时接收参数

provide/inject

这种方式是通过依赖注入的方式实现组件的(可跨级)通信。 依赖注入所提供的属性是非响应式的。
  • provide:用来发送数据或方法
  • inject:用来接收数据或方法

ref/$refs

在父组件中通过ref可以获取子组件实例,通过实例来访问子组件的属性和方法

$parent/$children

  • $parent可以获取上一级父组件实例,$root来访问根组件的实例
  • $children可以让组件访问所有子组件的实例,但是不能保证顺序,访问的数据也不是响应式的
  • 在根组件#app上拿$parent得到的是new Vue()的实例,在这实例上再拿$parent得到的是undefined,而在最底层的子组件拿$children是个空数组
  • $children 的值是数组,而$parent是个对象

$attrs/$listeners

inheritAttrs:默认值为true,继承所有的父组件属性(除props之外的所有属性),为false表示只继承class属性
  • $attrs:继承所有的父组件属性(除了prop传递的属性、class 和 style)
  • $listeners:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)

eventBus事件总线

eventBus事件总线适用于父子组件、非父子组件等之间的通信。这种组件通信方式会造成后期维护困难。vue3中移除了事件总线,取而代之的是插件,使用方式并无变化。

总结

子组件不能直接修改父组件传递的数据,这样做是维护父子组件之间形成的单向数据流。如果子组件随意更改父组件传递的数据,会导致数据流混乱,提高开发和维护成本

15.生命周期

生命周期指的是Vue组件从创建到销毁经历的一系列的过程
  • 创建前后
  • beforeCreate - 组件创建之前,无法获取data中的数据
  • created - 组件创建完成后,可以获取数据
  • 渲染前后
  • beforeMount - 组件挂载到DOM之前
  • mounted - 组件挂载完毕,可以获取DOM节点
  • 更新前后
  • beforeUpdate - 响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染
  • updated - 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用
  • 销毁前后
  • beforeDestroy - 实例销毁之前调用。这一步,实例仍然完全可用,this 仍能获取到实例
  • destroyed - 实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁
父子组件的生命周期执行顺序
  • 加载渲染过程
  • 父beforeCreate - 父created - 父beforeMount - 子beforeCreate - 子created - 子beforeMount - 子mounted - 父mounted
  • 更新过程
  • 父beforeUpdate - 子beforeUpdate - 子updated - 父updated
  • 销毁过程
  • 父beforeDestroy - 子beforeDestroy - 子destroyed - 父destroyed
created和mounted的区别
  • created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图
  • mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作
一般在哪个生命周期请求异步数据

我们可以在钩子函数createdbeforeMountmounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值

推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面加载时间,用户体验更好
  • SSR不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性

16.keep-alive

keep-alive用于缓存组件。在进行动态组件切换的时候对组件内部数据进行缓存,而不是走销毁流程。keep-alive是一个抽象组件,它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。 当组件在 <keep-alive>内被切换,它的 activated和  deactivated这两个生命周期钩子函数将会被对应执行

包含的参数:

  • include - 名称匹配的组件会被缓存 --> include的值为组件的name
  • exclude - 任何名称匹配的组件都不会被缓存
  • max - 决定最多可以缓存多少组件
activateddeactivated
在 keep-alive 组件激活时调用在keep-alive 组件停用时调用
该钩子函数在服务器端渲染期间不被调用该钩子在服务器端渲染期间不被调用

设置缓存后的钩子调用情况:

  • 第一次进入:beforeRouterEnter ->created->…->activated->…->deactivated> beforeRouteLeave
  • 后续进入时:beforeRouterEnter ->activated->deactivated> beforeRouteLeave

17.slot插槽

slot是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的
  • 默认插槽
子组件用 <slot>标签来确定渲染的位置,标签里面可以放 DOM结构,当父组件使用的时候没有往插槽传入内容,标签内 DOM结构就会显示在页面。父组件在使用的时候,直接在子组件的标签内写入内容即可
  • 具名插槽
子组件用 name属性来表示插槽的名字,不传为默认插槽。父组件中在使用时在默认插槽的基础上加上 slot属性,值为子组件插槽 name属性值
  • 作用域插槽
子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件 v-slot接受的对象上父组件中在使用时通过 v-slot:(简写:#)获取子组件的信息,在内容中使用

小结:

  1. v-slot属性只能在<template>上使用,但在只有默认插槽时可以在组件标签上使用
  2. 默认插槽名为default,可以省略default直接写v-slot。缩写为#时不能不写参数,写成#default
  3. 可以通过解构获取v-slot={user},还可以重命名v-slot="{user: newName}"和定义默认值v-slot="{user = '默认值'}"

18.为什么data是一个函数而不是一个对象

保证每个组件内数据的独立性,防止出现变量污染。对象为引用类型,当复用组件时,由于数据对象都指向同一个data对象,当在一个组件中修改data时,其他重用的组件中的data会同时被修改;而使用返回对象的函数,由于每次返回的都是一个新对象(Object的实例),引用地址不同,则不会出现这个问题。

19.Vue-Router

对前端路由的理解

前端路由的核心,就在于改变视图的同时不会向后端发出请求;而是加载路由对应的组件。Vue-Router就是将组件映射到路由, 然后渲染出来的
  • 拦截用户的刷新操作,避免服务端盲目响应、返回不符合预期的资源内容。把刷新这个动作完全放到前端逻辑里消化调
  • 感知URL的变化,根据这些变化使用js生成不同的内容

什么是Vue-Router,有哪些组件

Vue-Router是Vue官方的路由管理器。它和Vue.js的核心深度集成,路径和组件的映射关系使得构建SPA(Single Page Application,单页面应用)变得易如反掌
  • router-link - 实质上最终会渲染成a链接
  • router-view - 子级路由显示
  • keep-alive - 包裹组件缓存

$route和$router

  • $route 是路由信息对象,包含了path、params、hash、query、fullPath、matched、name等路由信息参数
  • $router是路由的实例对象,包含了路由的跳转方法、钩子函数等内容

路由开发的优缺点

优点:

  • 整体不刷新页面,用户体验更好
  • 数据传递简单,开发效率高

缺点:

  • 学习成本高
  • 首次加载缓慢,不利于seo

使用方式

  1. 新建index.js路由入口文件
  2. 创建路由规则
  3. 创建路由对象
  4. 将路由对象挂载到Vue.use()中
  5. 将路由对象挂载到 Vue 实例上

Hash模式

基于浏览器的hashchange事件,当url发生变化时,通过  window.location.hash 获取地址上的hash值,并通过Router类,配置routes对象设置与hash值对应的组件内容

优点:

  • hash值会出现在url中,但是不会被包含在http请求中,因此hash值改变不会重新加载页面
  • hash改变会触发hashchange事件,能控制浏览器的前进后退
  • 兼容性好

缺点:

  • 地址中携带#,不美观
  • 只可修改#后面的部分,因此只能设置与当前URL同文档的URL
  • hash有体积限制,故只可以添加短字符串
  • 设置的新值必须与原来不同才会触发hashchange事件,并将记录添加到栈中
  • 每次URL的改变不属于一次http请求,所以不利于seo优化

History模式

基于H5新增的pushState()和replaceState()两个api,以及浏览器的popstate事件,地址变化时,通过  window.location.pathname 找到对应的组件,并通过构造Router类,配置routes对象设置pathname值与对应的组件内容

优点:

  • 没有#,相对美观
  • pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL
  • pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中
  • pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中
  • pushState() 可额外设置 title 属性供后续使用
  • 浏览器的进后退能触发浏览器的popstate事件,获取window.location.pathname来控制页面的变化

缺点:

  • URL的改变属于http请求,借助history.pushState实现页面的无刷新跳转,因此会重新请求服务器。所以前端的 URL 必须和实际向后端发起请求的 URL 一致。如果用户输入的URL回车或者浏览器刷新或者分享出去某个页面路径,用户点击后,URL与后端配置的页面请求URL不一致,则匹配不到任何静态资源,就会返回404页面。所以需要后台配置支持,覆盖所有情况的候选资源,如果 URL 匹配不到任何静态资源,则应该返回app 依赖的页面或者应用首页
  • 兼容性差,特定浏览器支持

路由hash模式和history模式的区别

hash 模式是一种把前端路由的路径用  # 拼接在真实  url 后面的模式。当  # 后面的路径发生变化时,浏览器并不会重新发起请求,而是会触发  onhashchange 事件。
hash模式的特点:
  • hash变化会触发网页跳转,即浏览器的前进和后退
  • hash 可以改变 url ,但是不会触发页面重新加载(hash的改变是记录在 window.history 中),即不会刷新页面。也就是说,所有页面的跳转都是在客户端进行操作。因此,这并不算是一次 http 请求,所以这种模式不利于 SEO 优化。hash 只能修改 # 后面的部分,所以只能跳转到与当前 url 同文档的 url
  • hash 通过触发 hashchange 事件,来监听 hash 的改变,借此实现无刷新跳转的功能
  • hash 永远不会提交到 server 端(可以理解为只在前端自生自灭)

history API 是 H5 提供的新特性,允许开发者直接更改前端路由,即更新浏览器 URL 地址而不重新发起请求

  • 新的 url 可以是与当前 url 同源的任意 url ,也可以是与当前 url 一样的地址,但是这样会导致的一个问题是,会把重复的这一次操作记录到栈当中
  • 通过 history.state ,添加任意类型的数据到记录中
  • 可以额外设置 title 属性,以便后续使用
  • 通过 pushState 、 replaceState 来实现无刷新跳转的功能,需要后端配合

history模式下的404问题

  1. URL的改变属于http请求,借助history.pushState实现页面的无刷新跳转,因此会重新请求服务器
  2. 所以前端的 URL 必须和实际向后端发起请求的 URL 一致

编程式导航

方式作用
$router.push()跳转到指定的url,并在history中添加记录,点击回退返回到上一个页面
$router.replace()跳转到指定的url,但是history中不会添加记录,点击回退到上上个页面
$router.go(n)向前或者后跳转n个页面,n可以是正数也可以是负数
$router.back()后退、回到上一页
$router.forward()前进、回到下一页

路由传参的方式

  • query传参
query传参需要使用path来引入,页面跳转后参数将会出现在url中;也可以直降将参数以?xxx=xx&xxx=xx的形式拼接在路由地址中
this.$router.push({ path: '/example', query: { id: '1'}})   // 传递query参数
this.$route.query.id    // 获取query参数

以query的方式传参时,刷新页面不会导致参数丢失

  • params传参
params传参需要使用name来引入,页面跳转后参数不会出现在url中;在4.1.4版本开始,在未定义动态路由的情况下,将不能直接使用编程式导航传递params参数,目的是解决刷新页面参数丢失的问题

传参的方式总结

  • this.$router.push(path)
  • this.$router.push({path, query})
  • this.$router.push({name, params}) // 注意版本更新
  • this.$router.push({name, query})

原文链接:2023前端面试必备个人总结(持续更新中) - 知乎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值