2024最新前端面试题(附答案及解析)
喝喝哈 2024-08-19 09:03:02 阅读 84
文章目录
HTML篇1、HTML5有哪些新特性?2、介绍下 BFC 及其应用3、内元素和块级元素的区别?4、Doctype作用?标准模式与混杂模式如何区分?5、引入样式时,link和@import的区别?6、介绍一下你对浏览器内核的理解?7、title与h1的区别、b与strong的区别、i与em的区别?8、从浏览器地址栏输入url到显示页面的步骤
CSS篇1. 绘制一像素的线2. css选择器3. CSS盒子模型与怪异盒模型4. CSS3动画一:过渡动画---Transitions二:Animations功能:定义多个关键帧
5. Flex布局5.1 flex-direction属性5.2 flex-wrap属性5.3 flex-flow5.4 justify-content属性5.5 align-items属性5.6 align-content属性
6. 实现一个元素水平垂直居中7. 获取dom元素的宽高
JS 基础篇1. 原始类型有哪几种?null 是对象吗?2. typeof VS instanceof3. This指向问题:4. == vs ===5. 闭包6. 原型7. 继承7.1 原型链继承7.2 构造函数继承7.3 组合式继承7.4 寄生式组合继承
8. 深浅拷贝8. new 操作符调用构造函数具体做了什么?9. 冒泡排序如何实现,时间复杂度是多少, 还可以如何改进?10. 防抖、节流10.1 防抖10.2 节流
11. TCP 三次握手和四次挥手的理解12. 介绍下重绘和回流(Repaint & Reflow),以及如何进行优化12.1 浏览器渲染机制12.2 重绘12.3 回流
13. call 和 apply 的区别是什么,哪个性能更好一些
ES6篇1. 介绍下 Set、Map、WeakSet 和 WeakMap 的区别?2. var、let 及 const 区别3. es6新增了什么?4. async/await、promise和setTimeout5. forEach Map的区别6. 箭头函数与普通函数(function)的区别是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?
VUE篇1. MVC和MVVM 、MVP2. Virtual DOM 虚拟DOM3. 路由原理4. Vue 和 React 之间的区别5. 生命周期函数6. 简要介绍Vuex原理7. 组件之间数据共享
算法篇1. 给定两个数组,写一个方法来计算它们的交集2. 随机生成一个长度为 10 的整数类型的数组,例如 [2, 10, 3, 4, 5, 11, 10, 11, 20],将其排列成一个新数组,要求新数组形式如下,例如 [[2, 3, 4, 5], [10, 11], [20]]。3. 如何把一个字符串的大小写取反(大写变小写小写变大写),例如 ’AbC' 变成 'aBc' 。4. 实现一个字符串匹配算法,从长度为 n 的字符串 S中,查找是否存在字符串 T,T 的长度是 m,若存在返回所在位置。5. 「旋转数组」6. 打印出 1 - 10000 之间的所有对称数7. 「两数之和」8. 给定两个大小为 m 和 n 的有序数组 nums1 和nums2。请找出这两个有序数组的中位数。要求算法的时间复杂度为 O(log(m+n))。
进阶篇1. 强缓存、协商缓存与cdn缓存的区别
HTML篇
1、HTML5有哪些新特性?
HTML5 现在已经不是 SGML 的子集,主要是关于图像,位置,存储,多任务等功能的增加。
(1)绘画 canvas;
(2)用于媒介回放的 video 和 audio 元素;
(3)本地离线存储 localStorage 长期存储数据,浏览器关闭后数据不丢失;
(4)sessionStorage 的数据在浏览器关闭后自动删除;
(5)语意化更好的内容元素,比如 article、footer、header、nav、section;
(6)表单控件,calendar、date、time、email、url、search;
(7)新的技术webworker, websocket, Geolocation;
(8)IE8/IE7/IE6支持通过document.createElement方法产生的标签,可以利用这一特性让这些浏览器支持HTML5新标签,浏览器支持新标签后,还需要添加标签默认的样式。当然也可以直接使用成熟的框架、比如html5shim
移除的元素:
纯表现的元素:basefont big center font s strike tt u性能较差元素:frame frameset noframes
区分:
DOCTYPE声明的方式是区分重要因素根据新增加的结构、功能来区分
2、介绍下 BFC 及其应用
BFC 就是块级格式上下文,是页面盒模型布局中的一种 CSS 渲染模式,相当于一个独立的容器,里面的元素和外部的元素相互不影响。
创建BFC的方式有:
html 根元素float 浮动绝对定位overflow 不为 visiabledisplay 为表格布局或者弹性布局
创建 BFC 的方式有:
清除浮动防止同一 BFC 容器中的相邻元素间的外边距重叠问题
3、内元素和块级元素的区别?
行内元素:不会独立出现在一行,单独使用的时候后面不会有换行符的元素。eg:span, strong, img, a
等。这些元素,默认的高宽,总是其内容的高宽。并且,margin和padding值,只有左右有效。
块级元素:独立在一行的元素,他们后面会自动带有换行符。eg:div , p ,form , ul , li , ol , dl
等。它们的出现,往往独自占领一行。在没有设置宽度的情况下,默认宽度总是其父元素的宽度。
行内元素转换成块元素,只要设置其display属性为block即可,display:block;
。块元素转换成行内元素,只要将其display属性设置为inline即可,display:inline;。
<code>(1)行内元素有:a b span img input select
(2)块级元素有:div p ul ol li dl dt dd h1-h6
(3)常见的空元素:br-换行,hr-水平分割线
4、Doctype作用?标准模式与混杂模式如何区分?
<!DOCTYPE>
告诉浏览器使用哪个版本的html规范来渲染文档。DOCTYPE不存在或形式不正确会导致html文档以混杂模式呈现。
标准模式(Standards mode)以浏览器支持的最高标准运行;混杂模式(Quirks
mode)中页面是一种比较宽松的向后兼容的方式显示。
5、引入样式时,link和@import的区别?
链接样式时,link只能在HTML页面中引入外部样式
导入样式表时,@import 既可以在HTML页面中导入外部样式,也可以在css样式文件中导入外部css样式。
6、介绍一下你对浏览器内核的理解?
主要分成两部分:渲染引擎(Layout Engine或Rendering Engine)和js引擎。
**渲染引擎:**负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入CSS等),以及计算网页的显示方式,然后会输出至显示器或打印机。浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。
**js引擎:**解析和执行JavaScript来实现网页的动态效果。
最开始渲染引擎和JS引擎并没有区分的很明确,后来JS引擎越来越独立,内核就倾向于只指渲染引擎。
7、title与h1的区别、b与strong的区别、i与em的区别?
title属性没有明确意义,只表示标题;h1表示层次明确的标题,对页面信息的抓取也有很大的影响
strong标明重点内容,语气加强含义;b是无意义的视觉表示 em表示强调文本;i是斜体,是无意义的视觉表示 视觉样式标签:b i u s
语义样式标签:strong em ins del code
8、从浏览器地址栏输入url到显示页面的步骤
浏览器根据请求的URL交给DNS域名解析,找到真实IP,向服务器发起请求;
服务器交给后台处理完成后返回数据,浏览器接收文件(HTML、JS、CSS、图象等);
浏览器对加载到的资源(HTML、JS、CSS等)进行语法解析,建立相应的内部数据结构(如HTML的DOM);
载入解析到的资源文件,渲染页面,完成。
CSS篇
1. 绘制一像素的线
canvas 、height、hr、伪元素上设置媒体查询+transfrom scaleY、边框
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(10, 100);
ctx.lineTo(300,100);
ctx.stroke();
2. css选择器
选择器的分类与优先级
标签选择器:优先级加权值为 1。伪元素或伪对象选择器:优先级加权值为 1。类选择器:优先级加权值为 10。属性选择器:优先级加权值为 10。ID选择器:优先级加权值为 100。其他选择器:优先级加权值为 0,如通配选择器等。
然后,以上面加权值数为起点来计算每个样式中选择器的总加权值数。计算的规则如下:
统计选择器中 ID 选择器的个数,然后乘以100。统计选择器中类选择器的个数,然后乘以 10。统计选择器中的标签选择器的个数,然后乘以 1。
依此方法类推,最后把所有加权值数相加,即可得到当前选择器的总加权值,最后根据加权值来决定哪个样式的优先级大。
3. CSS盒子模型与怪异盒模型
1、标准盒模型中width指的是内容区域content的宽度;height指的是内容区域content**的高度。
标准盒模型下盒子的大小 = content + border + padding + margin
2、怪异盒模型中的width指的是内容、边框、内边距总的宽度(content + border + padding);
height指的是内容、边框、内边距总的高度
怪异盒模型下盒子的大小=width(content + border + padding) + margin
3、在ie8+浏览器中使用哪个盒模型可以由box-sizing(CSS新增的属性)触发,
默认值为content-box,即标准盒模型;如果将box-sizing设为border-box则用的是IE盒模型
4、box-shadow: h-shadow v-shadow blur spread color inset;
h-shadow,v-shadow必须。水平,垂直阴影的位置。允许赋值。blur可选,模糊距离。spread可选,阴影的尺寸。color可选,阴影的颜色。inset可选,将外部阴影(outset)改为内部阴影。
4. CSS3动画
一:过渡动画—Transitions
1:过渡动画Transitions
含义:在css3中,Transitions功能通过将元素的某个属性从一个属性值在指定的时间内平滑过渡到另一个属性值来实现动画功能。
Transitions属性的使用方法如下所示:
<code>transition: property | duration | timing-function | delay
transition-property: 表示对那个属性进行平滑过渡。
transition-duration: 表示在多长时间内完成属性值的平滑过渡。
transition-timing-function 表示通过什么方法来进行平滑过渡。
linear | 规定以相同速度开始至结束的过渡效果(等于 cubic-bezier(0,0,1,1))。 |
---|---|
ease | 规定慢速开始,然后变快,然后慢速结束的过渡效果(cubicbezier(0.25,0.1,0.25,1))。 |
ease-in | 规定以慢速开始的过渡效果(等于 cubic-bezier(0.42,0,1,1))。 |
ease-out | 规定以慢速开始和结束的过渡效果(等于 cubic-bezier(0.42,0,0.58,1))。 |
cubicbezier(n,n,n,n) | 在 cubic-bezier 函数中定义自己的值。可能的值是 0 至 1 之间的数值。 |
transition-delay: 定义过渡动画延迟的时间。
默认值是 all 0 ease 0
**浏览器支持程度:**IE10,firefox4+,opera10+,safari3+及chrome8+
下面是transitions过渡功能的demo如下
HTML代码如下:
<div class="transitions">transitions过渡功能</div>code>
CSS代码如下:
.transitions { -- -->
-webkit-transition: background-color 1s ease-out;
-moz-transition: background-color 1s ease-out;
-o-transition: background-color 1s ease-out;
}.transitions:hover {
background-color: #00ffff;
}
效果如下:
transitions过渡功能
如果想要过渡多个属性,可以使用逗号分割,如下代码:
div { -webkit-transition: background-color 1s linear, color 1s linear, width 1s linear;}
2. 我们可以使用Transitions功能同时平滑过渡多个属性值。
如下HTML代码:
<h2>transitions平滑过渡多个属性值</h2><div class="transitions2">transitions平滑过渡多code>
个属性值</div>
css代码如下:
.transitions2 { -- -->
background-color:#ffff00;
color:#000000;
width:300px;
-webkit-transition: background-color 1s linear, color 1s linear, width
1s linear;
-moz-transition: background-color 1s linear, color 1s linear, width 1s
linear;
-o-transition: background-color 1s linear, color 1s linear, width 1s
linear;
}.transitions2:hover {
background-color: #003366;
color: #ffffff;
width:400px;
}
transitions平滑过渡多个属性值
transitions平滑过渡多个属性值 注意:transition-timing-function
表示通过什么方法来进行平滑过渡。它值有如下: 有ease | linear | ease-in | ease-out |
ease-in-out | cubic-bezier 至于linear
线性我们很好理解,可以理解为匀速运动,至于cubic-bezier贝塞尔曲线目前用不到,可以 忽略不计,我们现在来理解下 ease,
ease-in, easy-out 和 ease-in-out 等属性值的含义; ease: 先快后逐渐变慢; ease-in: 先慢后快
easy-out: 先快后慢
easy-in-out: 先慢后快再慢
理解上面几个属性值,如下demo:
HTML代码如下:
<div id="transBox" class="trans_box">code>
<div class="trans_list ease">ease</div>code>
<div class="trans_list ease_in">ease-in</div>code>
<div class="trans_list ease_out">ease-out</div>code>
<div class="trans_list ease_in_out">ease-in-out</div>code>
<div class="trans_list linear">linear</div></div>code>
CSS代码如下:
.trans_box { -- -->
background-color: #f0f3f9; width:100%
}.trans_list {
width: 30%;
height: 50px;
margin:10px 0;
background-color:blue;
color:#fff;
text-align:center;
}.ease {
-webkit-transition: all 4s ease;
-moz-transition: all 4s ease;
-o-transition: all 4s ease;
transition: all 4s ease;
}.ease_in {
-webkit-transition: all 4s ease-in;
-moz-transition: all 4s ease-in;
-o-transition: all 4s ease-in;
transition: all 4s ease-in;
}.ease_out {
-webkit-transition: all 4s ease-out;
-moz-transition: all 4s ease-out;
-o-transition: all 4s ease-out;
transition: all 4s ease-out;
}.ease_in_out {
-webkit-transition: all 4s ease-in-out;
-moz-transition: all 4s ease-in-out;
-o-transition: all 4s ease-in-out;
transition: all 4s ease-in-out;
}.linear {
-webkit-transition: all 4s linear;
-moz-transition: all 4s linear;
-o-transition: all 4s linear;
transition: all 4s linear;
}.trans_box:hover .trans_list{
margin-left:90%;
background-color:#beceeb;
color:#333;
-webkit-border-radius:25px;
-moz-border-radius:25px;
-o-border-radius:25px;
border-radius:25px;
-webkit-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-o-transform: rotate(360deg);
transform: rotate(360deg);
}
二:Animations功能:定义多个关键帧
Animations功能与Transitions功能相同,都是通过改变元素的属性值来实现动画效果的。
它们的区别在于:使用Transitions功能是只能通过指定属性的开始值与结束值。然后在这两个属性值之间进行平滑过渡的方式来实现动画效果,因此不能实现复杂的动画效果;而Animations则通过定义多个关键帧以及定义每个关键帧中元素的属性值来实现更为复杂的动画效果。
**语法:**animations: name duration timing-function iteration-count;
name: 关键帧集合名(通过此名创建关键帧的集合)
duration: 表示在多长时间内完成属性值的平滑过渡
timing-function: 表示通过什么方法来进行平滑过渡
iteration-count: 迭代循环次数,可设置为具体数值,或者设置为infinite进行无限循环,默认为1.
用法:@-webkit-keyframes 关键帧的集合名 {创建关键帧的代码}
如下面的代码:
@-webkit-keyframes mycolor {
0% { background-color:red;}
40% { background-color:darkblue;}
70% { background-color: yellow;}
100% { background-color:red;}}
.animate:hover {
-webkit-animation-name: mycolor;
-webkit-animation-duration: 5s;
-webkit-animation-timing-function:
5. Flex布局
设为 Flex 布局以后,子元素的 float 、 clear 和 vertical-align 属性将失效。
采用 Flex 布局的元素,称为 Flex 容器(flex container)
flex-directionflex-wrapflex-flowjustify-contentalign-itemsalign-content
5.1 flex-direction属性
flex-direction 属性决定主轴的方向(即项目的排列方向)。
.box {
flex-direction: row | row-reverse | column | column-reverse;
}
它可能有4个值。
5.2 flex-wrap属性
默认情况下,项目都排在一条线(又称"轴线")上。 flex-wrap 属性定义,如果一条轴线排不下,如何
换行。
它可能取三个值。
(1) nowrap (默认):不换行。
(2) wrap :换行,第一行在上方。
(3) wrap-reverse :换行,第一行在下方。
5.3 flex-flow
<code>flex-flow 属性是 flex-direction 属性和 flex-wrap 属性的简写形式,默认值为 row nowrap 。
.box { -- -->
flex-flow: <flex-direction> || <flex-wrap>;
}
5.4 justify-content属性
justify-content
属性定义了项目在主轴上的对齐方式
.box {
justify-content: flex-start | flex-end | center | space-between | spacearound;
}
它可能取5个值,具体对齐方式与轴的方向有关。下面假设主轴为从左到右。
flex-start (默认值):左对齐 flex-end :右对齐 center : 居中 space-between:两端对齐,
项目之间的间隔都相等。 space-around :每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大 一倍
5.5 align-items属性
align-items 属性定义项目在交叉轴上如何对齐。
.box {
align-items: flex-start | flex-end | center | baseline | stretch;
}
它可能取5个值。具体的对齐方式与交叉轴的方向有关,下面假设交叉轴从上到下。
flex-start :交叉轴的起点对齐。
flex-end:交叉轴的终点对齐。
center :交叉轴的中点对齐。
baseline : 项目的第一行文字的基线对齐。
stretch(默认值):如果项目未设置高度或设为auto,将占满整个容器的高度。
5.6 align-content属性
align-content 属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。
.box {
align-content: flex-start | flex-end | center | space-between | spacearound | stretch;
}
该属性可能取6个值。
flex-start :与交叉轴的起点对齐。
flex-end :与交叉轴的终点对齐。
center:与交叉轴的中点对齐。
space-between :与交叉轴两端对齐,轴线之间的间隔平均分布。
space-around:每根轴线两侧的间隔都相等。所以,轴线之间的间隔比轴线与边框的间隔 大一倍。
stretch (默认值):轴线占满整个交叉轴
6. 实现一个元素水平垂直居中
1)缺点:如果不知道宽高,这个效果就没法实现。如果数据除不尽,实现的效果存在误差。
width:200px;
height:200px;position:absolute或者fixed;
top:50%;
margin-top:-100px;
left:50%;
margin-left:-100px;
2)缺点:不适合未知宽高的元素水平垂直居中
width:333px;
height:333px;
position:fixed;
margin:auto;
top:0;
left:0;
right:0;
bottom:0;
3)优点:可以实现一个未知宽高的元素水平垂直居中 缺点:display:flex;css3新增加的;兼容到
IE10以上
html{ height:100%;}
body{ margin:0; display:flex; height:100%;}
div{ margin:auto;}
//其中:display:flex给的最近的父元素
4)
body{ margin:0;}
div{
position:fixed;
top:50%;
left:50%;
transform:translate(-50%,-50%);
}
/*top:50%; left:50%; 这个是浏览器视口整体宽高的一半 ;
transform:translate(-50%,-50%);是当前元素宽高的一半*/
7. 获取dom元素的宽高
1、Element.style.width/height
只能获取内联样式
var ele = document.getElementById('element');
console.log(ele.style.height); // '100px'
2、window.getComputedStyle(ele).width/height
IE9以上 可获取实时的style
var ele = document.getElementById('element');
console.log(window.getComputedStyle(ele).width); // '100px'
console.log(window.getComputedStyle(ele).height); // '100px'
3、Element.currentStyle.width/height
功能与第二点相同,只存在于旧版本IE中(IE9以下),除了做旧版IE兼容,就不要用它了。
4、Element.getBoundingClientRect().width/height
除了能够获取宽高,还能获取元素位置等信息
var ele = document.getElementById('element');
console.log(ele.getBoundingClientRect().width); // 100
console.log(ele.getBoundingClientRect().height); // 100
JS 基础篇
1. 原始类型有哪几种?null 是对象吗?
在 JS 中,存在着 6 种原始值,分别是:
booleannullundefinednumberstringsymbol
首先原始类型存储的都是值,是没有函数可以调用的,比如 undefined.toString()
另外对于 null 来说,很多人会认为他是个对象类型,其实这是错误的。虽然 typeof null 会输出
object ,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑
使用低位存储变量的类型信息, 000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断
为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
对象(Object)类型
涉及面试题:对象类型和原始类型的不同之处?函数参数是对象会发生什么问题?
在 JS 中,除了原始类型那么其他的都是对象类型了。对象类型和原始类型不同的是,原始类型存储的是值,对象类型存储的是地址(指针)。当你创建了一个对象类型的时候,计算机会在内存中帮我们开辟一个空间来存放值,但是我们需要找到这个空间,这个空间会拥有一个地址(指针)
const a = []
对于常量 a 来说,假设内存地址(指针)为 #001 ,那么在地址 #001 的位置存放了值 [] ,常量 a存放了地址(指针) #001 ,再看以下代码
const a = []
const b = a
b.push(1)
当我们将变量赋值给另外一个变量时,复制的是原本变量的地址(指针),也就是说当前变量 b 存放
的地址(指针)也是 #001 ,当我们进行数据修改的时候,就会修改存放在地址(指针) #001 上的
值,也就导致了两个变量的值都发生了改变。
接下来我们来看函数参数是对象的情况
function test(person) {
person.age = 26
person = {
name: 'yyy',
age: 30
}
return person
}
const p1 = {
name: 'yck',
age: 25
}
const p2 = test(p1)
console.log(p1) // -> { name: "yck", age: 26}
console.log(p2) // -> { name: "yyy", age: 30}
对于以上代码,你是否能正确的写出结果呢?接下来让我为你解析一番:
首先,函数传参是传递对象指针的副本到函数内部修改参数的属性这步,我相信大家都知道,当前p1的值也被修改了但是当我们重新为person分配了一个对象时就出现了分歧
所以最后person拥有了一个新的指针,也就和p1没有关系了,导致了最终两个变量的值是不同的
2. typeof VS instanceof
涉及面试题:typeof 是否能正确判断类型?instanceof 能正确判断对象的原理是什么?
typeof
对于原始类型来说,除了null
都可以显示正确的类型
typeof1//'number'
typeof'1'//'string'
typeofundefined//'undefined'typeoftrue//'boolean'
typeofSymbol()//'symbol'
typeof
对于对象来说,除了函数都会显示object,所以说typeof
并不能准确判断变量到底是什么类型
typeof[]//'object'
typeof{ }//'object'
typeofconsole.log//'function'
如果我们想判断一个对象的正确类型,这时候可以考虑使用instanceof,因为内部机制是通过原型链来判断
constPerson=function(){ }constp1=newPerson()
p1instanceofPerson//true
varstr='helloworld'code>
strinstanceofString//false
varstr1=newString('helloworld')
str1 instanceof String//true
对于原始类型来说,你想直接通过instanceof 来判断类型是不行的,当然我们还是有办法让instanceof 判断原始类型的
class PrimitiveString { -- -->
static [Symbol.hasInstance](x){
return typeof x === 'string
}
}
console.log('hello world' instanceof PrimitiveString) // true
你可能不知道 Symbol.hasInstance 是什么东西,其实就是一个能让我们自定义 instanceof 行为的
东西,以上代码等同于 typeof ‘hello world’ === ‘string’ ,所以结果自然是 true 了。这其实
也侧面反映了一个问题, instanceof 也不是百分之百可信的。
3. This指向问题:
涉及面试题:如何正确判断 this?箭头函数的 this 是什么
this 的指向,是在调用函数时根据执行上下文所动态确定的。
函数在浏览器全局环境中被简单调用(非显式/隐式绑定下)
严格模式下 this 绑定到 undefined,
否则绑定到全局对象 window/global;在执行函数时,如果函数中的this是被上一级的对象所调用,那么this指向就是上一级的对象; 否则指 向全局环境。回调函数(除事件函数):
数组的所有遍历方法forEach,map,filter,reduce,every,some,flatMap,sort;这些方法均使用了回调
函数,因此在所有使用回调函数的方法中,所有回调函数中this都被指window,
'a' + + 'b' // -> "aNaN"
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN
let a = {
valueOf() {
return 0
},
toString() {
return '1'
}
}
a > -1 // true
setInterval,setTimeOut 函数中的回调函数的因为作用域不明(不知道在哪里调用)就会指向
window:
事件函数中的this:指向侦听的对象
这里的特殊情况(事件函数)是因为:在函数执行时底层函数调用了call和apply,因此此时的回调函数中的this就会被指向绑定的侦听对象上;在定义对象属性时,obj对象还没有创建完成;this仍旧指向window一般构造函数 new 调用,绑定到新创建的对象上;一般由 call/apply/bind 方法显式调用,绑定到指定参数的对象上;
面试技巧:如果把这项放到最后说下个问题多半就是三者区别一般由上下文对象调用,绑定在该对象上;箭头函数中,根据外层上下文绑定的 this 决定 this 指向
1、全局环境下的 this
函数在浏览器全局环境中被简单调用,ES5非严格模式下指向 window,ES6严格模式下指向
undefined。
function fn1( ) {
console.log(this) }
fn1( ) // window
function fn2( ) { 'use strict'
console.log(this)}
fn2( ) // undefined
在执行函数时,如果函数中的this是被上一级的对象所调用,那么this指向就是上一级的对象; 否则指
向全局环境。
Var foo = {
bar:10,
fn:function( ) {
console.log(this)
console.log(this.bar)}
}
***\*var fn1 = foo.fn\****
fn1( ) // ***\*直接调用\****,this ***\*指向 window\****,window.bar => undefined
foo.fn( ) // 通过 foo 调用,this 指向 foo,foo.bar => 10
this.a=3;//this--->window
var b=5;
function fn(){
var b=10;
console.log(b+this.b);//this--->window
// 这种方法仅限于ES5,在ES6严格模式中this将会变成undefined
}
fn()
2、回调函数中的this
数组的所有遍历方法forEach,map,filter,reduce,every,some,flatMap,sort;这些方法均使用了回调函
数,因此在所有使用回调函数的方法中,除了特殊的情况外(事件函数),其他所有回调函数中this都被指向
window,setInterval,setTimeOut 函数中的回调函数的因为作用域不明(不知道在哪里调用)就会指向
window:
var obj = {
fn: function ( ) {
// console.log(this);
***\*return\**** function ( ) {
console.log(this);//this--->window
}
}
}
var fn=obj.fn( );
fn( );//因为是在另外的作用域调用
//return中回调函数因为相当于var fn=obj.fn( )( );是在外部执行所以会指向window
这里的特殊情况(事件函数)是因为:在函数执行时底层函数调用了call和apply,因此此时的回调函数中的this就会被指向document;
3、对象中的this
在定义属性时,obj对象还没有创建完成;this仍旧指向window
箭头函数指向当前域外的内容
var c=100;
var obj={
c:10,
b:this.c,//this--->window 定义属性时,obj对象还没有创建完成,this仍旧指向window
a:function(){
// this;//this--->obj
// console.log(obj.c);
console.log(this.c);
},
d:()=>{
//this--->window
console.log(this);
}
}
// console.log(obj);
// obj.d();
var obj1=obj;
obj=null;
obj1.a();
这里a:function( )This.c}中为什么不用obj而用this呢:因为obj的地址值可能改变;就会找不到这个引
用变量obj对象;
4、ES6class中的this
class Box{
a=3;
static abc(){
console.log(this);//Box 静态方法调用就是通过类名.方法
// Box.abc();
// 尽量不要在静态方法中使用this
}
constructor(_a){
this.a=_a }
play(){
// this就是实例化的对象
console.log(this.a);
// 这个方法是被谁执行的,this就是谁
}
let b=new Box(10);
b.play();
let c=new Box(5);
c.play();
//使用静态方法:就指向box:相当于box.abc( )调用该方法;所以指向box
class Box{
a=3;
static abc(){
console.log(this);//Box 静态方法调用就是通过类名.方法
// Box.abc();
// 尽量不要在静态方法中使用this
}
5、ES5中的this
function Box(_a){
this.a=_a;
}
Box.prototype.play=function(){
console.log(this.a);//this就是实例化的对象
}
Box.prototype.a=5;
Box.abc=function(){
//this
// 这样的方法,等同于静态方法
}
var a=new Box(10);
a.play();
Box.abc();
6、事件函数中的this:指向侦听的对象
document.addEventListener("click",clickHandler);
function clickHandler(e){
console.log(this);//this--->e.currentTarget
}
7、Call apply bind中的this:指向绑定的对象
function fn(a,b){
this.a=a;//this如果使用了call,apply,bind,this将会被指向被绑定的对象
this.b=b;
return this;
}
var obj=fn.call({ },3,5)
var obj=fn.apply({ },[3,5])
var obj=fn.bind({ })(3,5);
*8、箭头函数中的this:指向当前函数外的函数或内容与自带bind(this)的作用
var obj={
a:function(){
document.addEventListener("click",e=>{
console.log(this);//指向事件侦听外函数中的this/obj
});
var arr=[1,2,3];
arr.forEach(item=>{
console.log(this);//this-->obj
});
// 相当于自带bind(this)的作用
arr.forEach((function(item){
}).bind(this));
}
}
4. == vs ===
涉及面试题:== 和 === 有什么区别?
对于 == 来说,如果对比双方的类型不一样的话,就会进行类型转换
假如我们需要对比 x 和 y 是否相同,就会进行如下判断流程:
首先会判断两者类型是否相同。相同的话就是比大小了类型不相同的话,那么就会进行类型转换会先判断是否在对比 null 和 undefined ,是的话就会返回 true判断两者类型是否为 string 和 number ,是的话就会将字符串转换为 number
1 == '1'
↓
1 == 1
判断其中一方是否为 boolean ,是的话就会把 boolean 转为 number 再进行判断
'1' == true
↓
'1' == 1
↓
1 == 1
判断其中一方是否为 object 且另一方为 string 、 number 或者 symbol ,是的话就会把object 转为原始类型再进行判断
'1' == { name: 'yck' }
↓
'1' == '[object Object]'
5. 闭包
什么是闭包?
要理解闭包,首先必须理解Javascript特殊的变量作用域。
变量的作用域无非就是两种:全局变量和局部变量。
Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。
而闭包却是能够读取其他函数内部变量的函数。所以,在本质上,闭包就是将函数内部和函数外
部连接起来的一座桥梁。
闭包的特点
1.函数嵌套函数
2.函数内部可以引用外部的参数和变量
3.参数和变量不会被垃圾回收机制回收
因此闭包常会被用于
1可以储存一个可以长期驻扎在内存中的变量
2.避免全局变量的污染
3.保证私有成员的存在
那闭包又因为什么原因不被回收呢
简单来说,js引擎的工作分两个阶段,
一个是语法检查阶段,
一个是运行阶段。而运行阶段又分预解析和执行两个阶段。
在预解析阶段,先会创建执行上下文,执行上下文又包括变量对象、变量对象的作用域链和this指向的创建 。
创建执行上下文后,会对变量对象的属性进行填充。
进入执行代码阶段,此时执行上下文有个Scope属性该属性作为一个作用域链包含有该函数被定义时所有外层的变量对象的引用js解析器逐行读取并执行代码时当我们需要查询外部作用域的变量时,其实就是沿着作用域链,依次在这些变量对象里遍历标志符,直到最后的全局变量对象。
基于js的垃圾回收机制:在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数a被b引用,b又被a外的c引用,所以定义了闭包的函数虽然销毁了,但是其变量对象依然被绑定在函数上,只有仍被引用,变量会继续保存在内存中,这就是为什么函数a执行后不会被回收的原因。
变量对象VO:var声明的变量、function声明的函数,及当前函数的形参
作用域链:当前变量对象+所有父级作用域 [[scope]]
this值:在进入执行上下文后不再改变
PS:作用域链其实就是一个变量对象的链,函数的变量对象称之为active object,简称AO。函数创建后就静态的[[scope]]属性,直到函数销毁)
创建执行上下文后,会对变量对象的属性进行填充。所谓属性,就是var、function声明的标志符及函数形参名,至于属性对应的值:变量值为undefined,函数值为函数定义,形参值为实参,没有传入实参则为undefined。
闭包的应用场景
数据封装:闭包可以帮助我们创建具有私有变量的函数,这些变量不会被外部直接访问,从而实现封装。模块化:在JavaScript等语言中,闭包常用于模块化开发,通过闭包可以创建独立的功能模块,避免全局变量的污染。函数柯里化:闭包允许我们将一个多参数的函数转换成一系列单参数的函数,这在函数式编程中非常有用。延迟计算:闭包可以用于实现延迟执行的逻辑,例如,在用户实际需要数据时才进行计算。状态保持:在异步操作中,闭包可以保持异步操作的状态,确保回调函数能够访问到正确的上下文。事件处理:在JavaScript中,闭包常用于事件监听器,确保回调函数能够访问到绑定时的作用域。迭代器和生成器:闭包在迭代器和生成器模式中用于保存迭代状态,使得迭代可以跨多个函数调用持续进行。高阶函数:闭包使得函数可以作为参数传递给其他函数,或者作为其他函数的返回值,这是高阶函数的基础。函数工厂:闭包可以创建函数工厂,即返回一个函数的函数,这些返回的函数可以访问闭包中定义的变量。记忆化:闭包可以用于实现记忆化技术,存储函数的计算结果,避免重复计算,提高性能。定时器和动画:在JavaScript中,闭包可以用于定时器和动画的实现,确保回调函数能够访问到正确的状态。AJAX请求:在进行AJAX请求时,闭包可以确保请求完成时能够访问到请求发起时的上下文。
6. 原型
涉及面试题:如何理解原型?如何理解原型链?
1.每个对象都有 __proto__属性 ,该属性指向其构造函数的原型对象, proto 将对象和其原型对
象连接起来组成原型链
2.在调用实例的方法和属性时,如果在实例对象上找不到,就会往原型对象上找
3.构造函数的 prototype属性 也指向实例的原型对象
4.原型对象的 constructor属性 指向构造函数。
7. 继承
说到继承,最容易想到的是ES6的 extends ,当然如果只回答这个肯定不合格,我们要从函数和原型链的角度上实现继承,下面我们一步步地、递进地实现一个合格的继承实现一个方法可以从而实现对父类的属性和方法的继承,解决代码冗余重复的问题
7.1 原型链继承
原型链继承的原理很简单,
直接让子类的原型对象指向父类实例,
Child.prototype=new Parent()
当子类实例找不到对应的属性和方法时,就会往它的原型对象,也就是父类实例上找,
从而实现对父类的属性和方法的继承
原型继承的缺点:
1.由于所有Child实例原型都指向同一个Parent实例, 因此对某个Child实例的父类引用类型变量修改会影 响所有的Child实例
2.在创建子类实例时无法向父类构造传参, 即没有实现super()的功能
7.2 构造函数继承
构造函数继承,即在子类的构造函数中执行父类的构造函数,并为其绑定子类的this,
让父类的构造函数把成员属性和方法都挂到子类的this上去;
//在Child的构造函数中执行
Parent.apply(this, arguments);
这样既能避免实例之间共享一个原型实例,又能向父类构造方法传参;
js继承的方式继承不到父类原型上的属性和方法
构造函数继承的缺点
1.继承不到父类原型上的属性和方法
7.3 组合式继承
既然原型链继承和构造函数继承各有互补的优缺点, 那么我们为什么不组合起来使用呢, 所以就有了综合二者的组合式继承
Child.prototype=new Parent()
Child.prototype.constructor=Child //相当于在Child的构造函数中给Parent绑定this
组合式继承的缺点:
1.每次创建子类实例都执行了两次构造函数(Parent.call()和new Parent()),虽然这并不影响对父类的继承,但子类创建实例时,原型中会存在两份相同的属性和方法,这并不优雅
7.4 寄生式组合继承
为了解决组合式继承中构造函数被执行两次的问题,
我们将指向父类实例改为指向父类原型, 减去一次构造函数的执行
到这里我们就完成了ES5环境下的继承的实现,这种继承方式称为寄生组合式继承。
Function.prototype.extend = function (supClass) {
// 创建一个中间替代类,防止多次执行父类(超类)的构造函数
function F() { }
// 将父类的原型赋值给这个中间替代类
F.prototype = supClass.prototype;
// 将原子类的原型保存
var proto = subClass.prototype;
// 将子类的原型设置为中间替代类的实例对象
subClass.prototype = new F();
// 将原子类的原型复制到子类原型上,合并超类原型和子类原型的属性方法
// Object.assign(subClass.prototype,proto);
var names = Object.getOwnPropertyNames(proto);
for (var i = 0; i < names.length; i++) {
var desc = Object.getOwnPropertyDescriptor(proto, names[i]);
Object.defineProperty(subClass.prototype, names[i], desc);
}
// 设置子类的构造函数时自身的构造函数,以防止因为设置原型而覆盖构造函数
subClass.prototype.constructor = subClass;
// 给子类的原型中添加一个属性,可以快捷的调用到父类的原型方法
subClass.prototype.superClass = supClass.prototype;
// 如果父类的原型构造函数指向的不是父类构造函数,重新指向
if (supClass.prototype.constructor !== supClass) {
supClass.prototype.constructor = supClass;
}
}
function Ball(_a) {
this.superClass.constructor.call(this, _a);
}
Ball.prototype.play = function () {
this.superClass.play.call(this);//执行超类的play方法
console.log("end");
}
Object.defineProperty(Ball.prototype, "d", {
value: 20
})
Ball.extend(Box);
var b=new Ball(10);
console.log(b);
是目前最成熟的继承方式,babel对ES6继承的转化也是使用了寄生组合式继承
8. 深浅拷贝
涉及面试题:什么是浅拷贝?如何实现浅拷贝?什么是深拷贝?如何实现深拷贝?
浅拷贝实现
展开运算符 …Object.assign({}, a)
通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就可能需要使用到深拷贝了
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native
浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到最开始的话题了,两者享有相同的地址。要解决这个问题,我们就得使用深拷贝了。
深拷贝实现
JSON.parse(JSON.stringify(object))
但是该方法也是有局限性的:
会忽略 undefined会忽略 symbol不能序列化函数不能解决循环引用的对象在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化 原型链如何处理DOM 如何处理DateRegES6类nullboolenarraystringnumber
实现一个深拷贝是很困难的,需要我们考虑好多种边界情况,比如原型链如何处理、DOM 如何处理等等,所以这里我们实现的深拷贝只是简易版,并且我其实更推荐使用 lodash 的深拷贝函数。
lodash
function deepClone(obj) {
function isObject(o) {
return (typeof o === 'object' || typeof o === 'function') && o !== null
}
if (!isObject(obj)) {
throw new Error('非对象')
}
let isArray = Array.isArray(obj)
let newObj = isArray ? [...obj] : { ...obj }
Reflect.ownKeys(newObj).forEach(key => {
newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
})
return newObj
}
let obj = {
a: [1, 2, 3],
b: {
c: 2,
d: 3
}
}
let newObj = deepClone(obj)
newObj.b.c = 1
console.log(obj.b.c) // 2
8. new 操作符调用构造函数具体做了什么?
创建一个新的对象;将构造函数的 this 指向这个新对象;为这个对象添加属性、方法等;最终返回新对象;
9. 冒泡排序如何实现,时间复杂度是多少, 还可以如何改进?
冒泡算法的原理:
升序冒泡: 两次循环,相邻元素两两比较,如果前面的大于后面的就交换位置
降序冒泡: 两次循环,相邻元素两两比较,如果前面的小于后面的就交换位置
js 实现:
// 升序冒泡 function maopao(arr){
const array = [...arr] for(let i = 0, len = array.length; i < len -
1; i++){
for(let j = i + 1; j < len; j++) {
if (array[i] > array[j]) {
let temp = array[i]
array[i] = array[j]
array[j] = temp
}
}
}
return array
}
看起来没问题,不过一般生产环境都不用这个,原因是效率低下,冒泡排序在平均和最坏情况下的时间复杂度都是 O(n^2),最好情况下都是 O(n),空间复杂度是 O(1)。因为就算你给一个已经排好序的数组,如[1,2,3,4,5,6] 它也会走一遍流程,白白浪费资源。所以有没有什么好的解决方法呢?
答案是肯定有的:加个标识,如果已经排好序了就直接跳出循环。
优化版:
function maopao(arr){
const array = [...arr]
let isOk = true for(let i = 0, len = array.length;
i < len - 1; i++){
for(let j = i + 1; j < len; j++) {
if (array[i] > array[j]) {
let temp = array[i]
array[i] = array[j]
array[j] = temp
isOk = false
}
}
if(isOk){
Break
}
}
return array}
10. 防抖、节流
10.1 防抖
即短时间内大量触发同一事件,只会执行一次函数,防抖常用于搜索框/滚动条的监听事件处理,如果不做防抖,每输入一个字/滚动屏幕,都会触发事件处理,造成性能浪费;实现原理为设置一个定时器,约定在xx毫秒后再触发事件处理,每次触发事件都会重新设置计时器,直到xx毫秒内无第二次操作。
// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
// 缓存一个定时器id
let timer = 0
// 这里返回的函数是每次用户实际调用的防抖函数
// 如果已经设定过定时器了就清空上一次的定时器
// 开始一个新的定时器,延迟执行用户传入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
10.2 节流
防抖是 延迟执行 ,而节流是 间隔执行 ,和防抖的区别在于,防抖每次触发事件都重置定时器,而节流在定时器到时间后再清空定时器,函数节流即 每隔一段时间就执行一次 ,实现原理为 设置一个定时器,约定xx毫秒后执行事件,如果时间到了,那么执行函数并重置定时器 ,
// func是用户传入需要防抖的函数
// wait是等待时间
const throttle = (func, wait = 50) => {
// 上一次执行该函数的时间
let lastTime = 0
return function(...args) {
// 当前时间
let now = +new Date()
// 将当前时间和上一次执行函数时间对比
// 如果差值大于设置的等待时间就执行函数
if (now - lastTime > wait) {
lastTime = now
func.apply(this, args)
}
}
}
setInterval(
throttle(() => {
console.log(1)
}, 500),
1
)
11. TCP 三次握手和四次挥手的理解
TCP的三次握手和四次挥手是TCP协议中用于建立连接和断开连接的重要过程。以下是对这两个过程的理解:
1. TCP三次握手:
TCP的三次握手是用于建立可靠的连接的过程,确保双方都能确认对方的可达性和能够接收数据。
具体过程如下:
第一次握手:客户端向服务器发送一个连接请求报文段,其中包含SYN标志位设置为1,序号为A,然后进入SYN_SENT状态等待服务器的确认。客户端首先选择一个初始的序列号A,并随机生成一个初始的ISN(Initial Sequence Number)作为初始序列号。第二次握手:服务器接收到客户端的连接请求报文段后,如果同意建立连接,则向客户端发送一个确认报文段,其中包含SYN和ACK标志位都设置为1,确认号为A+1,序号为B。服务器也会为该连接选择一个自己的初始序列号B,随机生成一个ISN。第三次握手:客户端接收到服务器的确认报文段后,向服务器发送一个确认报文段,其中ACK标志位设置为1,确认号为B+1,序号为A+1。服务器接收到客户端的确认报文段后,连接建立成功,双方可以开始进行数据传输。
通过三次握手,可以确保双方都能确认对方的可达性和能够接收数据,建立了可靠的连接。若其中任何一次握手失败或超时,连接建立就会失败,双方不会建立连接。
TCP四次挥手:
TCP的四次挥手是用于断开连接的过程,由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。具体过程如下:
第一次挥手:客户端发送一个FIN报文段给服务器,关闭客户端到服务器的数据传送,客户端进入FIN_WAIT_1状态。第二次挥手:服务器收到FIN报文段后,发送一个ACK报文段给客户端,确认序号为收到序号+1,服务器进入CLOSE_WAIT状态。第三次挥手:服务器发送一个FIN报文段给客户端,关闭服务器到客户端的数据传送,服务器进入LAST_ACK状态。第四次挥手:客户端收到FIN报文段后,客户端进入TIME_WAIT状态,接着发送一个ACK报文段给服务器,确认序号为收到序号+1,服务器进入CLOSED状态。
完成四次挥手后,连接被完全释放。第二次挥手和第三次挥手都是为了确认双方都已经完成数据的发送和接收。在第二次挥手后,客户端可以继续向服务器发送数据,直到收到服务器的FIN报文段;同样地,在第三次挥手后,服务器也可以继续向客户端发送数据,直到收到客户端的ACK报文段。
12. 介绍下重绘和回流(Repaint & Reflow),以及如何进行优化
12.1 浏览器渲染机制
浏览器采用流式布局模型(Flow Based Layout) 浏览器会把 HTML 解析成 DOM,把 CSS 解析成 CSSOM,DOM
和 CSSOM 合并就产生了渲染树(Render Tree)。 有了
RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。 由于浏览器使用流式布局,对
Render Tree 的计算通常只需要遍历一次就可以完成,但 table 及其内部元素除外,他们可能需要多次计算,通常要花 3
倍于同等元素的时间,这也是为什么要避免使用 table 布局的原因之一。
12.2 重绘
由于节点的几何属性发生改变或者由于样式发生改变而不会影响布局的,称为重绘,例如 outline, visibility,
color、background-color 等,重绘的代价是高昂的,因为浏览器必须验证 DOM 树上其他节点元素的可见性。
12.3 回流
回流是布局或者几何属性需要改变就称为回流。回流是影响浏览器性能的关键 因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的回流可能会导致了其所有子元素以及 DOM 中紧随其后的节点、祖先节点元素 的随后的回流。
减少重绘与回流
CSS
1、使用 transform 替代 top
2、使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
3、避免使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局。
4、尽可能在 DOM 树的最末端改变 class,回流是不可避免的,但可以减少其影响。尽可能在 DOM 树的最末端改变 class,可以限制了回流的范围,使其影响尽可能少的节点。
5、避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。
JavaScript
1、避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class
并一次性更改 class 属性。
2、避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM
操作,最后再把它添加到文档中。
3、避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个
变量缓存起来。
4、对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素
及后续元素频繁回流。
13. call 和 apply 的区别是什么,哪个性能更好一些
Function.prototype.apply 和 Function.prototype.call 的作用是一样的,区别在于传入参数的不同;第一个参数都是,指定函数体内 this 的指向;3 第二个参数开始不同,apply 是传入带下标的集合,数组或者类数组,apply 把它传给函数作为参数,call 从第二个开始传入的参数是不固定的,都会传给函数作为参数。call 比 apply 的性能要好,平常可以多用 call, call 传入参数的格式正是内部所需要的格式
ES6篇
1. 介绍下 Set、Map、WeakSet 和 WeakMap 的区别?
Set——对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用
WeakSet——成员都是对象;成员都是弱引用,可以被垃圾回收机制回收,可以
用来保存 DOM 节点,不容易造成内存泄漏;
Map——本质上是键值对的集合,类似集合;可以遍历,方法很多,可以跟各
种数据格式转换。
WeakMap——只接受对象最为键名(null 除外),不接受其他类型的值作为键
名;键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,
此时键名是无效的;不能遍历,方法有 get、set、has、delete。
2. var、let 及 const 区别
涉及面试题:什么是提升?什么是暂时性死区?var、let 及 const 区别?
函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪
到作用域顶部
var 存在提升,我们能在声明之前使用。 let 、 const 因为暂时性死区的原因,不能在声明前使用
var 在全局作用域下声明变量会导致变量挂载在 window 上,其他两者不会
let 和 const 作用基本一致,但是后者声明的变量不能再次赋值
3. es6新增了什么?
类(Class):可以通过class关键字定义一个类,内部使用constructor定义构造方法,使用new关键字创建实例
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${ this.name}`);
}
}
const person = new Person('Alice', 25);
person.greet(); // Hello, my name is Alice
模板字符串(Template Literals):用反引号包围的字符串,可以内嵌表达式和行终止字符
const name = 'Alice';
const age = 25;
console.log(`Hello, my name is ${ name} and I am ${ age} years old.`);
箭头函数(Arrow Functions):简化了函数的定义。
const add = (a, b) => a + b;
console.log(add(5, 3)); // 8
let和const声明:提供块级作用域,let用于变量声明,const用于常量声明。
let x = 10;
x = 20; // OK
const y = 30;
// y = 40; // Error: Assignment to constant variable.
Iterator和Generator:提供了一种新的迭代方式,可以自定义迭代器。
function* fibonacci() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
// ...
Promise:用于处理异步操作,可以避免回调地狱。
const fetchData = new Promise((resolve, reject) => {
// 模拟异步操作
setTimeout(() => {
const data = 'Some data';
if (data) {
resolve(data);
} else {
reject('Error: Data not found');
}
}, 1000);
});
fetchData.then(data => console.log(data))
.catch(error => console.error(error));
4. async/await、promise和setTimeout
async/await、Promise 和 setTimeout 都是JavaScript中处理异步操作的重要概念,但它们各自有不同的用途和特性。
4.1 Promise
Promise 是代表异步操作最终完成或失败的对象。它有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
Promise 构造函数接受一个执行器函数,该函数有两个参数:resolve 和 reject,分别用于将 Promise 标记为已完成(并传递一个值)或已失败(并传递一个原因)。
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('操作成功完成');
}, 1000);
});
promise.then(result => {
console.log(result); // "操作成功完成"
}).catch(error => {
console.error(error);
});
4.2 async/await
async/await 是建立在 Promise 之上的语法糖,它使得异步代码看起来更像同步代码。
async 函数总是返回一个 Promise。如果在 async 函数中返回一个值,那么这个值会被 Promise.resolve() 包裹并返回。如果 async 函数中抛出一个错误,那么这个错误会被 Promise.reject() 包裹并返回。
await 只能在 async 函数内部使用,它会暂停 async 函数的执行并等待 Promise 完成,然后恢复 async 函数的执行并返回解析后的值(或抛出异常)。
4.3 3. setTimeout
setTimeout 是一个浏览器提供的全局函数,用于在指定的毫秒数后执行函数或指定的代码。它返回一个表示定时器的ID,可以使用 clearTimeout 函数来取消它。
setTimeout 的回调函数是在异步上下文中执行的,因此它不会阻塞其他代码的执行。
setTimeout(() => {
console.log('这是通过setTimeout在1秒后执行的');
}, 1000);
console.log('这是立即执行的');
总结:
Promise 是处理异步操作的对象,它表示一个最终可能完成(也可能失败)的异步操作及其结果值。
async/await 是建立在 Promise 之上的语法糖,它使得异步代码更易于阅读和理解。
setTimeout 是一个用于在指定时间后执行代码的浏览器API,它的回调函数是在异步上下文中执行的。
5. forEach Map的区别
在JavaScript中,forEach 和 map 都是数组(Array)对象上的方法,用于遍历数组的元素。但是,它们之间存在一些重要的差异。
forEach
forEach 方法用于遍历数组的每个元素,并对每个元素执行提供的函数。但是,forEach 不会返回一个新数组,而是直接在原始数组上进行操作(尽管它实际上并不修改数组中的元素值,除非你在回调函数中显式地这样做)。
示例:
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(function(number, index, array) {
console.log(number); // 依次输出 1, 2, 3, 4, 5
// 注意:forEach 不会返回一个新数组
});
map
map 方法也用于遍历数组的每个元素,但是它会将回调函数的结果组成一个新数组返回。原始数组不会被改变。
示例:
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(function(number) {
return number * 2;
});
console.log(doubled); // 输出 [2, 4, 6, 8, 10]
// 注意:numbers 数组没有被改变
总结:
forEach:遍历数组,对每个元素执行操作,但不返回新数组。
map:遍历数组,对每个元素执行操作,并返回一个新数组,其中包含了操作的结果。
选择使用哪个方法取决于你的具体需求:如果你只是想遍历数组而不关心返回值,那么使用 forEach;如果你需要基于原始数组创建一个新数组,那么使用 map。
6. 箭头函数与普通函数(function)的区别是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?
箭头函数是普通函数的简写,可以更优雅的定义一个函数,和普通函数相比,
有以下几点差异:
函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对 象。不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可 以用 rest 参数代替。不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。不可以使用 new 命令,因为:
1.没有自己的 this,无法调用 call,apply。
2.没有 prototype 属性 ,而 new 命令在执行时需要将构造函数的 prototype 赋值给新的对象的 proto
//new 过程大致是这样的
function newFunc(father, ...rest) {
var result = { };
result.__proto__ = father.prototype;
var result2 = father.apply(result, rest);
if (
(typeof result2 === 'object' || typeof result2 === 'function') &&
result2 !== null
) {
return result2;
}
return result;
}
VUE篇
1. MVC和MVVM 、MVP
MVC,MVP,MVVM是三种常见的前端架构模式,通过分离关注点来改进代码组织方式。MVC模式是
MVP,MVVM模式的基础,这两种模式更像是MVC模式的优化改良版,他们三个的MV即Model,view都是相同的,不同的是MV之间的桥梁连接部分。
一、MVC
视图(View):用户界面,只负责渲染 HTML
控制器(Controller):业务逻辑,负责调度 model 和 view
模型(Model):数据保存,只负责存储数据、请求数据、更新数据
MVC允许在不改变视图情况下改变视图对用户输入的响应方式,用户对View操作交给Controller处理在Controller中响应View的事件调用Model的接口对数据进行操作,一旦Model发生变化便通知相关视图View进行更新。
接受用户指令时,MVC 可以分成两种方式。一种是通过 View 接受输入,传递给 Controller。另一种是直接通过controller接受指令。此处只画了第一种情况。
但是 MVC 有一个巨大的缺陷就是控制器承担的责任太大了,随着项目愈加复杂,控制器中的代码会越来越臃肿,导致出现不利于维护的情况。
二、MVP
MVP 模式将 Controller 改名为 Presenter,同时改变了通信方向。
与MVC最大的区别就是View和Model层完全解耦,不在有依赖关系,而是通过Presenter做桥梁,用于
操作view层发出的事件传递到presenter层中,presenter层去操作model层,并且将数据返回给view
层,整个过程中view层和model层完全没有联系。
三、MVVM
MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。唯一的区别是,它采用双
向绑定(data-binding),View的变动,自动反映在 ViewModel,反之亦然。
这里我们拿典型的MVVM模式的代表,Vue,来举例:
<p>{ -- -->{ message }}</p>
<button v-on:click="reverseMessage">逆转消息</button>code>
</div>
var app5 = new Vue({ -- -->
el: '#app-5',
data: {
message: 'Hello Vue.js!'
},
methods: {
reverseMessage: function () {
this.message = this.message.split('').reverse().join('')
}
}
})
这里的html部分相当于View层,可以看到这里的View通过通过模板语法来声明式的将数据渲染进DOM
元素,当ViewModel对Model进行更新时,通过数据绑定更新到View。
2. Virtual DOM 虚拟DOM
涉及面试题:什么是 Virtual DOM?为什么 Virtual DOM 比原生 DOM 快?
想必大家都听过操作 DOM 性能很差,但是这其中的原因是什么呢?
因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作 DOM 次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM 可能还会带来重绘、回流的情况,所以也就导致了性能上的问题。那么既然 DOM 可以通过 JS 对象来模拟,反之也可以通过 JS 对象来渲染出对应的 DOM。当然了,通过 JS 来模拟 DOM 并且渲染对应的 DOM 只是第一步,难点在于如何判断新旧两个 JS 对象的最小差异并且实现局部更新 DOM。首先 DOM 是一个多叉树的结构,如果需要完整的对比两颗树的差异,那么需要的时间复杂度会是 O(n^ 3),这个复杂度肯定是不能接受的。于是 React 团队优化了算法,实现了 O(n) 的复杂度来对比差异。实现 O(n) 复杂度的关键就是只对比同层的节点,而不是跨层对比,这也是考虑到在实际业务中很少会去跨层的移动 DOM 元素。 所以判断差异的算法就分为了两步首先从上至下,从左往右遍历对象,也就是树的深度遍历,这一步中会给每个节点添加索引,便于最后渲染差异一旦节点有子元素,就去判断子元素是否有不同当我们判断出以上的差异后,就可以把这些差异记录下来。当对比完两棵树以后,就可以通过差异
去局部更新 DOM,实现性能的最优化。
当然了 Virtual DOM 提高性能是其中一个优势,其实最大的优势还是在于:
将 Virtual DOM 作为一个兼容层,让我们还能对接非 Web 端的系统,实现跨端开发。同样的,通过 Virtual DOM 我们可以渲染到其他的平台,比如实现 SSR、同构渲染等等。实现组件的高度抽象化
3. 路由原理
涉及面试题:前端路由原理?两种实现方式有什么区别?
前端路由实现起来其实很简单,本质就是监听 URL 的变化,然后匹配路由规则,显示相应的页面,并且无须刷新页面。目前前端使用的路由就只有两种实现方式
Hash 模式
点击跳转或者浏览器历史跳转当 # 后面的哈希值发生变化时,不会向服务器请求数据,可以通过
hashchange 事件来监听到 URL 的变化,从而进行跳转页面
手动刷新不会触发hashchange事件,可以采用load事件监听解析URL
匹配相应的路由规则,跳转到相应的页面,然后通过DOM替换更改页面内容History 模式
利用history API实现url地址改变, 网页内容改变
History.back()、History.forward()、History.go()移动到以前访问过的页面时,页面通常是从
浏览器缓存之中加载,而不是重新要求服务器发送新的网页
History.pushState()
用于在历史中添加一条记录。该方法接受三个参数,依次为一个与添加的记录相关联的状态对象:
state;新页面的标题title;必须与当前页面处在同一个域下的新的网址url
该方法不会触发页面刷新,只是导致 History 对象发生变化,地址栏会有反应,不会触发
hashchange事件
History.replaceState() 方法用来修改 History 对象的当前记录
路由原理:本质就是监听 URL 的变化,然后匹配路由规则,显示相应的页面,并且无须刷新
路由需要实现三个功能:
①浏览器地址变化,切换页面;
②点击浏览器【后退】、【前进】按钮,网页内容跟随变化;
③刷新浏览器,网页加载当前路由对应内容
两种模式对比
Hash 模式只可以更改 # 后面的内容,History 模式可以通过 API 设置任意的同源 URL
History 模式可以通过 API 添加任意类型的数据到历史记录中,Hash 模式只能更改哈希值,也就
是字符串
Hash 模式无需后端配置,并且兼容性好。History 模式在用户手动输入地址或者刷新页面的时候
会发起 URL 请求,后端需要配置 index.html 页面用于匹配不到静态资源的时候
4. Vue 和 React 之间的区别
Vue 的表单可以使用 v-model 支持双向绑定,相比于 React 来说开发上更加方便,当然了 v-model 其实就是个语法糖,本质上和 React 写表单的方式没什么区别。
改变数据方式不同,Vue 修改状态相比来说要简单许多,React 需要使用 setState 来改变状态,并且使用这个 API 也有一些坑点。并且 Vue 的底层使用了依赖追踪,页面更新渲染已经是最优的了,但是React 还是需要用户手动去优化这方面的问题。
React 需要使用 JSX,有一定的上手成本,并且需要一整套的工具链支持,但是完全可以通过 JS 来控制页面,更加的灵活。Vue 使用了模板语法,相比于 JSX 来说没有那么灵活,但是完全可以脱离工具链,通过直接编写 render 函数就能在浏览器中运行。
在生态上来说,两者其实没多大的差距,当然 React 的用户是远远高于 Vue 的。
在上手成本上来说,Vue 一开始的定位就是尽可能的降低前端开发的门槛,然而 React 更多的是去改变用户去接受它的概念和思想,相较于 Vue 来说上手成本略高。
5. 生命周期函数
Vue实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模板、挂载Dom、渲染→更新→渲染、销毁等一系列过程,我们称这是Vue的生命周期。通俗说就是Vue实例从创建到销毁的过程,就是生命周期。
每一个组件或者实例都会经历一个完整的生命周期,总共分为三个阶段:初始化、运行中、销毁。
实例、组件通过new Vue() 创建出来之后会初始化事件和生命周期,然后就会执行beforeCreate钩
子函数,这个时候,数据还没有挂载呢,只是一个空壳,无法访问到数据和真实的dom,一般不
做操作挂载数据,绑定事件等等,然后执行created函数,这个时候已经可以使用到数据,也可以更改数
据,在这里更改数据不会触发updated函数,在这里可以在渲染前倒数第二次更改数据的机会,不
会触发其他的钩子函数,一般可以在这里做初始数据的获取接下来开始找实例或者组件对应的模板,编译模板为虚拟dom放入到render函数中准备渲染,然
后执行beforeMount钩子函数,在这个函数中虚拟dom已经创建完成,马上就要渲染,在这里也可
以更改数据,不会触发updated,在这里可以在渲染前最后一次更改数据的机会,不会触发其他的
钩子函数,一般可以在这里做初始数据的获取接下来开始render,渲染出真实dom,然后执行mounted钩子函数,此时,组件已经出现在页面
中,数据、真实dom都已经处理好了,事件都已经挂载好了,可以在这里操作真实dom等事情…当组件或实例的数据更改之后,会立即执行beforeUpdate,然后vue的虚拟dom机制会重新构建
虚拟dom与上一次的虚拟dom树利用diff算法进行对比之后重新渲染,一般不做什么事儿当更新完成后,执行updated,数据已经更改完成,dom也重新render完成,可以操作更新后的
虚拟dom当经过某种途径调用$destroy方法后,立即执行beforeDestroy,一般在这里做一些善后工作,例
如清除计时器、清除非指令绑定的事件等等组件的数据绑定、监听…去掉后只剩下dom空壳,这个时候,执行destroyed,在这里做善后工作
也可以
beforeCreate:此时获取不到prop和data中的数据; created:可以获取到prop和data中的数据;
beforeMount:获取到了VDOM; mounted:VDOM解析成了真实DOM; beforeUpdate:在更新之前调用;
updated:在更新之后调用; keep-alive:切换组件之后,组件放进activated,之前的组件放进deactivated;
beforeDestory:在组件销毁之前调用,可以解决内存泄露的问题,如setTimeout和setInterval造成的 问题。
destory:组件销毁之后调用。
6. 简要介绍Vuex原理
Vuex实现了一个单向数据流,在全局拥有一个State存放数据,当组件要更改State中的数据时,必须通过Mutation进行,Mutation同时提供了订阅者模式供外部插件调用获取State数据的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走action,但action也是无法直接修改State的,还是需要通过Mutation来修改State的数据。最后,根据State的变化,渲染到视图上。
7. 组件之间数据共享
组件是 vue.js最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用。针对不同的使用场景,如何选择行之有效的通信方式?
1:props emit 缺点:如果组件嵌套层次多的话,数据传递比较繁琐
2:provide inject (依赖注入),缺点:不支持响应式
3:this.
r
o
o
t
t
h
i
s
.
root this.
rootthis.parent this.$refs
4: eventbus 缺点:数据不支持响应式
5: vuex 缺点:数据的读取和修改需要按照流程来操作,不适合小型项目
父子通信:
父组件向子组件传递数据可以通过 props ;
子组件向父组件是通过 $emit 、 $on 事件;
provide / inject ;
还可以通过 $root 、 $parent 、 $refs 属性相互访问组件实例;
兄弟通信: eventbus ; Vuex ;
跨级通信: eventbus ; Vuex ; provide / inject;
算法篇
1. 给定两个数组,写一个方法来计算它们的交集
例如:给定 nums1 = [1, 2, 2, 1],nums2 = [2, 2],返回 [2, 2]
var nums1 = [1, 2, 2, 1], nums2 = [2, 2, 3, 4];
// 1.
// 有个问题,
[NaN].indexOf(NaN) === -1var newArr1 = nums1.filter(function(item) {
return nums2.indexOf(item) > -1;
});
console.log(newArr1);
// 2.
var newArr2 = nums1.filter((item) => {
return nums2.includes(item);
});
console.log(newArr2);
2. 随机生成一个长度为 10 的整数类型的数组,例如 [2, 10, 3, 4, 5, 11, 10, 11, 20],将其排列成一个新数组,要求新数组形式如下,例如 [[2, 3, 4, 5], [10, 11], [20]]。
function formArray(arr: any[]) {
const sortedArr = Array.from(new Set(arr)).sort((a, b) => a - b);
const map = new Map();
sortedArr.forEach((v) => {
const key = Math.floor(v / 10);
const group = map.get(key) || [];
group.push(v);
map.set(key, group);
}); return [...map.values()];}// 求连续的版本 function
formArray1(arr: any[]) {
const sortedArr = Array.from(new Set(arr)).sort((a, b) => a - b);
return sortedArr.reduce((acc, cur) => {
const lastArr = acc.slice().pop() || [];
const lastVal = lastArr.slice().pop();
if (lastVal!=null && cur-lastVal === 1)
{
lastArr.push(cur);
} else {
acc.push([cur]);
}
return acc;
}, []);}function genNumArray(num: number, base = 100) {
return Array.from({ length: num}, () =>
Math.floor(Math.random()*base));
}const arr = genNumArray(10, 20);
//[2, 10, 3, 4, 5, 11, 10, 11, 20];
const res = formArray(arr);console.log(`res
${ JSON.stringify(res)}`);
3. 如何把一个字符串的大小写取反(大写变小写小写变大写),例如 ’AbC’ 变成 ‘aBc’ 。
function processString (s) {
var arr = s.split('');
var new_arr = arr.map((item) => {
return item === item.toUpperCase() ? item.toLowerCase() :
item.toUpperCase();
});
Return
new_arr.join('');}console.log(processString('AbC'));function
swapString(str) {
var result = ''
for (var i = 0; i < str.length; i++) {
var c = str[i]
if (c === c.toUpperCase()) {
result += c.toLowerCase()
} else {
result += c.toUpperCase()
}
}
return result}swapString('ADasfads123!@$!@#') // =>'adASFADS123!@$!@#
4. 实现一个字符串匹配算法,从长度为 n 的字符串 S中,查找是否存在字符串 T,T 的长度是 m,若存在返回所在位置。
const find = (S, T) => {
if (S.length < T.length) return -1
for (let i = 0; i < S.length; i++) {
if (S.slice(i, i + T.length) === T) return i
}
return -1
5. 「旋转数组」
给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。
示例 1:
输入: [1, 2, 3, 4, 5, 6, 7] 和 k = 3 输出: [5, 6, 7, 1, 2, 3, 4] 解释: 向右旋转 1 步:
[7, 1, 2, 3, 4, 5, 6] 向右旋转 2 步: [6, 7, 1, 2, 3, 4, 5] 向右旋转 3 步: [5, 6, 7, 1, 2, 3, 4]
示例 2:
输入: [-1, -100, 3, 99] 和 k = 2 输出: [3, 99, -1, -100] 解释: 向右旋转 1 步: [99, -1, -100, 3] 向右旋转 2 步: [3, 99, -1, -100]
function rotate(arr, k) {
const len = arr.length const step = k % len return
arr.slice(-step).concat(arr.slice(0, len - step))}// rotate([1, 2, 3, 4,
5, 6], 7) => [6, 1, 2, 3, 4, 5]
6. 打印出 1 - 10000 之间的所有对称数
[...Array(10000).keys()].filter((x) => {
return x.toString().length > 1 && x ===
Number(x.toString().split('').reverse().join(''))
})
7. 「两数之和」
给定一个整数数组和一个目标值,找出数组中和为目标值的两个数。你可以假设每个输入只对应一种答案,且同样的元素不能被重复利用。
示例:
给定 nums = [2, 7, 11, 15], target = 9 因为 nums[0] + nums[1] = 2 + 7 = 9 所以返
回 [0, 1]
function anwser (arr, target) {
let map = { } for (let i = 0; i < arr.length; i++) {
map[arr[i]] = i }
for (let i = 0; i < arr.length; i++) {
var d = target - arr[i]
if (map[d]) {
return [i, map[d]]
}
}
return new Error('404 not found')}
8. 给定两个大小为 m 和 n 的有序数组 nums1 和nums2。请找出这两个有序数组的中位数。要求算法的时间复杂度为 O(log(m+n))。
示例 1:
nums1 = [1, 3] nums2 = [2]
中位数是 2.0
示例 2:
nums1 = [1, 2] nums2 = [3, 4]
中位数是(2 + 3) / 2 = 2.5
const findMedianSortedArrays = function(
nums1: number[],
nums2: number[]
) {
const lenN1 = nums1.length;
const lenN2 = nums2.length;
const median = Math.ceil((lenN1 + lenN2 + 1) / 2);
const isOddLen = (lenN1 + lenN2) % 2 === 0;
const result = new Array<number>(median);
let i = 0; // pointer for nums1
let j = 0; // pointer for nums2
for (let k = 0; k < median; k++) {
if (i < lenN1 && j < lenN2) {
// tslint:disable-next-line:prefer-conditional-expression
if (nums1[i] < nums2[j]) {
result[i + j] = nums1[i++];
} else {
result[i + j] = nums2[j++];
}
} else if (i < lenN1) {
result[i + j] = nums1[i++];
} else if (j < lenN2) {
result[i + j] = nums2[j++];
}
}
if (isOddLen) {
return (result[median - 1] + result[median - 2]) / 2;
} else {
return result[median - 1];
}
};
进阶篇
1. 强缓存、协商缓存与cdn缓存的区别
强缓存(HTTP Cache-Control)
强缓存是浏览器缓存中的一种策略,它依赖于HTTP响应头中的Cache-Control字段,如max-age。当设置了强缓存后,浏览器在缓存有效期内不会向服务器发送请求,直接使用本地缓存的数据。强缓存适用于不经常变动的资源,比如CSS、JavaScript文件和图片等。强缓存可以减少HTTP请求的数量,从而降低服务器的负载和网络延迟。
协商缓存(HTTP ETag/Last-Modified)
协商缓存依赖于HTTP的ETag或Last-Modified响应头。当资源被缓存后,浏览器在下次请求相同资源时会发送If-None-Match或If-Modified-Since请求头,携带上次响应的ETag或Last-Modified值。服务器根据这些值判断资源是否有更新,如果没有更新,服务器返回304状态码,告诉浏览器使用本地缓存的资源。协商缓存适用于可能会更新的资源,它允许服务器控制资源的更新,确保用户总是获取到最新的内容。
CDN缓存
CDN(内容分发网络)缓存是一种分布式缓存机制,它将内容缓存在离用户地理位置更近的服务器上。CDN缓存可以减少数据传输的延迟,提高访问速度,尤其适用于静态资源。CDN通常会根据TTL(Time to Live)策略来决定资源在CDN节点上的缓存时间。当资源在CDN上缓存后,用户的请求首先到达CDN节点,如果资源在CDN上可用,就直接从CDN节点提供服务,否则CDN会从源服务器获取资源并缓存。
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。