面试

52 阅读42分钟

# HTML

常见概念

常见问题

问:移动端适配

《前端面试之移动端适配篇》

问:a标签中href和onclick哪个先执行?

onclick 事件执行, href 属性下的动作执行(页面跳转或 javascript 伪链接)

《a标签的 onclick 和 href 哪个先执行?》

CSS

常见概念

CSS盒模型和怪异盒模型

1、标准盒模型

width指的是内容区域content的宽度,

height指的是内容区域content的高度。

标准盒模型下盒子的大小 = content + border + padding + margin

image.png

2、怪异盒模型

width指的是内容、边框、内边距总的宽度(content + border + padding);

height指的是内容、边框、内边距总的高度

怪异盒模型下盒子的大小=width(content+border+padding)+ margin

3、box-sizing: 在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)改为内部阴影

HTML5在标签、属性、存储、API上的新特性

•标签:

新增语义化标签( aside / figure / section / header / footer / nav 等),

增加多媒体标签 video 和 audio ,使得样式和结构更加分离

•属性:

增强表单,主要是增强了 input 的type属性;

meta 增加charset以设置字符集;

script 增加async以异步加载脚本

•存储:增加 localStorage 、 sessionStorage 和 indexedDB ,引入了 application cache 对web和应用进行缓存

•API:增加 拖放API 、 地理定位 、 SVG绘图 、 canvas绘图 、 Web Worker 、 WebSocket

CSS3动画

Transitions --- 过渡动画

1:过渡动画Transitions

含义:在css3中,Transitions功能通过将元素的某个属性从一个属性值在指定的时间内平滑过渡到另一个属性值来实现动画功能。

Transitions属性的使用方法如下所示:

transition: property | duration | timing-function | delay

transition-property: 表示对那个属性进行平滑过渡。

transition-duration: 表示在多长时间内完成属性值的平滑过渡。

transition-timing-function 表示通过什么方法来进行平滑过渡。

image.png

transition-delay: 定义过渡动画延迟的时间。

默认值是 all 0 ease 0

浏览器支持程度:IE10,firefox4+,opera10+,safari3+及chrome8+

<div class="transitions">transitions过渡功能</div>
.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;

}

效果如图所示:

20250626115631_rec_.gif

2、过渡多个属性,可以使用逗号分割,如下代码:

div { 
    -webkit-transition: background-color 1s linear, color 1s linear, width 1s linear;
    }

我们可以使用Transitions功能同时平滑过渡多个属性值。

如下HTML代码:

<h2>transitions平滑过渡多个属性值</h2>
<div class="transitions2">transitions平滑过渡多个属性值</div>
.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;

}

效果如图所示:

20250626135940_rec_.gif

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">

<div class="trans_list ease">ease</div>

<div class="trans_list ease_in">ease-in</div>

<div class="trans_list ease_out">ease-out</div>

<div class="trans_list ease_in_out">ease-in-out</div>

<div class="trans_list linear">linear</div></div>

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);
}

效果图如下:

20250626140759_rec_.gif

Animations --- 定义多个关键帧

Animations功能与Transitions功能相同,都是通过改变元素的属性值来实现动画效果的。

它们的区别在于:使用Transitions功能是只能通过指定属性的开始值与结束值。然后在这两个属性值之间进行平滑过渡的方式来实现动画效果,因此不能实现复杂的动画效果;而Animations则通过定义多个关键帧以及定义每个关键帧中元素的属性值来实现更为复杂的动画效果。

语法:animations: name duration timing-function iteration-count;

name: 关键帧集合名(通过此名创建关键帧的集合)

duration: 表示在多长时间内完成属性值的平滑过渡

timing-function: 表示通过什么方法来进行平滑过渡

iteration-count: 迭代循环次数,可设置为具体数值,或者设置为infinite进行无限循环,默认为1.

用法:@-webkit-keyframes 关键帧的集合名 {创建关键帧的代码}

html代码

<div class="box"></div>

css代码如下

@keyframes slide {
  0% {
    left: 0;
    top: 100px;
  }
  25% {
    left: 200px;
    top: 200px;
  }
  50% {
    left: 400px;
    top: 100px;
  }
  75% {
    left: 300px;
    top: 0px;
  }
  100% {
    left: 0;
    top: 100px;
  }
}
/* 应用动画到元素 */
.box {
  width: 100px;
  height: 100px;
  background-color: royalblue;
  position: relative;
  animation: slide 2s ease-in-out infinite; /* 启动动画 */
}

Flex布局

设为 Flex 布局以后,子元素的 float 、 clearvertical-align 属性将失效。

采用 Flex 布局的元素,称为 Flex 容器(flex container)

1、容器的属性
  • flex-direction

  • flex-wrap

  • flex-flow

  • justify-content

  • align-items

  • align-content

1、flex-direction属性

flex-direction 属性决定主轴的方向(即项目的排列方向)。

.box {
   flex-direction: row | row-reverse | column | column-reverse;
}

image.png 它可能有4个值。

  • row (默认值):主轴为水平方向,起点在左端。

  • row-reverse :主轴为水平方向,起点在右端。

  • column :主轴为垂直方向,起点在上沿。

  • column-reverse :主轴为垂直方向,起点在下沿。

2、flex-wrap属性

默认情况下,项目都排在一条线(又称"轴线")上。 flex-wrap 属性定义,如果一条轴线排不下,如何换行。

image.png

.box{
    flex-wrap: nowrap | wrap | wrap-reverse;
}

它可能取三个值。

(1) nowrap (默认):不换行 image.png (2) wrap :换行,第一行在上方。 image.png (3) wrap-reverse :换行,第一行在下方 image.png

3、flex-flow属性

flex-flow 属性是 flex-direction 属性和 flex-wrap 属性的简写形式,默认值为 row nowrap 。

.box {
    flex-flow: <flex-direction> || <flex-wrap>;
}
4、justify-content属性

justify-content 属性定义了项目在主轴上的对齐方式。

box {
    justify-content: flex-start | flex-end | center | space-between | space-around;
}

image.png

它可能取5个值,具体对齐方式与轴的方向有关。下面假设主轴为从左到右。

  • flex-start (默认值):左对齐

  • flex-end :右对齐

  • center : 居中

  • space-between :两端对齐,项目之间的间隔都相等。

  • space-around :每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍。

5、align-items属性

align-items 属性定义项目在交叉轴上如何对齐。

box {
    align-items: flex-start | flex-end | center | baseline | stretch;
}

image.png

它可能取5个值。具体的对齐方式与交叉轴的方向有关,下面假设交叉轴从上到下。

  • stretch (默认值):如果项目未设置高度或设为auto,将占满整个容器的高度。

  • flex-start :交叉轴的起点对齐。

  • flex-end :交叉轴的终点对齐。

  • center :交叉轴的中点对齐。

  • baseline : 项目的第一行文字的基线对齐。

6、align-content属性

align-content 属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。

.box {
    align-content: flex-start | flex-end | center | space-between | space-around | stretch;
}

image.png

该属性可能取6个值。

  • stretch (默认值):轴线占满整个交叉轴。

  • flex-start :与交叉轴的起点对齐。

  • flex-end :与交叉轴的终点对齐。

  • center :与交叉轴的中点对齐。

  • space-between :与交叉轴两端对齐,轴线之间的间隔平均分布。

  • space-around :每根轴线两侧的间隔都相等。所以,轴线之间的间隔比轴线与边框的间隔大一倍。

2、项目的属性

以下6个属性设置在项目上。

  • order

  • flex-grow

  • flex-shrink

  • flex-basis

  • flex

  • align-self

order属性

定义项目的排列顺序

order 属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。

.item{
    order: 0
}

image.png

flex-grow属性

flex-grow 属性定义项目的放大比例,默认为 0 ,即如果存在剩余空间,也不放大。

.item {
    flex-grow: <number>; /* default 0 */
}

image.png

如果所有项目的 flex-grow 属性都为1,则它们将等分剩余空间(如果有的话)。

如果一个项目的flex-grow 属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。

flex-shrink属性

flex-shrink 属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小

item {
    flex-shrink: <number>; /* default 1 */
}

image.png

如果所有项目的 flex-shrink 属性都为1,当空间不足时,都将等比例缩小。

如果一个项目的 flex-shrink 属性为0,其他项目都为1,则空间不足时,前者不缩小。

负值对该属性无效。

flex-basis属性

flex-basis 属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为 auto ,即项目的本来大小。

.item {
    flex-basis: <length> | auto; /* default auto */
}

它可以设为跟 width 或 height 属性一样的值(比如350px),则项目将占据固定空间。

flex属性

flex 属性是 flex-grow , flex-shrink 和 flex-basis 的简写,默认值为 0 1 auto 。后两个属性可选。

.item {
    flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
}

该属性有两个快捷值: auto ( 1 1 auto ) 和 none ( 0 0 auto )。

建议优先使用这个属性,而不是单独写三个分离的属性,因为浏览器会推算相关值。

align-self属性

align-self 属性允许单个项目有与其他项目不一样的对齐方式,可覆盖 align-items 属性。

默认值为auto ,表示继承父元素的 align-items 属性,如果没有父元素,则等同于 stretch 。

.item {
    align-self: auto | flex-start | flex-end | center | baseline | stretch;
}

image.png

常见问题

问:flex的几种缩写0,1,auto,none

《一文搞懂flex:0,1,auto,none》

问:两个元素z-index分别为1和2,相互重叠,怎么点击到z-index为1的元素

设置z-idnex为2的元素的css属性:pointer-events: none;(点击穿透)

问:用div画css三角形

div{
	width:0px;
	height:0px;
	border-top:50px solid transparent;
	border-right:50px solid transparent;
	border-bottom:50px solid transparent;
	border-left:50px solid blue;
}

问:position属性中relative是相对于什么定位?absolute是相对于那个来定位?

absolute定位的基准是相对于最近一级的不是默认值static的父元素,可以是(absolute/relative/fixed等)来进行定位的,而不仅仅是相对于为position为relative的父级元素。父级元素还可以是absolute、fixed定位

问:css中哪些属性可以继承,哪些不能被继承?

《css中哪些可以被继承,哪些不能》 1、字体系列属性

  font-family:字体系列
  font-weight:字体的粗细
  font-size:字体的大小
  font-style:字体的风格

  2、文本系列属性

  text-indent:文本缩进
  text-align:文本水平对齐
  line-height:行高
  word-spacing:单词之间的间距
  letter-spacing:中文或者字母之间的间距
  text-transform:控制文本大小写(就是uppercase、lowercase、capitalize这三个)
  color:文本颜色

 3、元素可见性:

visibility:控制元素显示隐藏

4、列表布局属性:

list-style:列表风格,包括list-style-type、list-style-image等

5、光标属性:

 cursor:光标显示为何种形态

三、JavaScript

JS基础面试题

问:为什么JS中基本数据类型,如字符串'1'可以toString方法

toString() 按官方的说法,它是一个对象的方法,那为什么用字符串调用这个方法也可以呢?

因为原始数据类型(boolean,Number、String)在调用方法时,JS 将会创建对象,以便调用方法属性,而在使用完毕后将会销毁该对象。

其实在这个语句运行的过程中做了这样几件事情:

var s = new Object('1'); 

s.toString(); 

s = null;

第一步:创建Object类实例。注意为什么不是String ? 由于Symbol和BigInt的出现,对它们调用new都会报错,目前ES6规范也不建议用new来创建基本类型的包装类;

第二步:调用实例方法

第三步:执行完方法立即销毁这个实例

整个过程体现了 基本包装类型 的性质,而基本包装类型恰恰属于基本数据类型,包括Boolean, Number和String

问:Number()、parseInt() 和 parseFloat() 的区别

《Number()、parseInt() 和 parseFloat() 的区别》

问:JS中0.1+0.2 !== 0.3原因和解决办法

《深度剖析0.1+0.2 !== 0.3》

解决方法: 1、ES6新增的Number.EPSILON方法 2、0.1和0.2先乘10,结果再除以10 3、parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true

问:JS中 [ ]== ![ ] 为 true,{ }==!{ }为false原因

《简单分析JS基础原理》

问:js中对象里this指向问题

《js中对象里this的指向问题》

问:js中call、apply和bind的区别

《call、apply和bind的区别》

问:for in和for of都可以用于对象或数组么

《for,for...in,for...of,forEach对于数组、对象、Set和Map》

问:Object.prototype.toString.call()原理

《Object.prototype.toString.call()原理》

问:JS中什么是伪数组,和普通数组有什么区别?

伪数组 (ArrayLike) ,又称类数组。是一个类似数组的对象,是一种按照索引存储数据且具有 length 属性的对象。

但是有如下几个特征:

1、按索引方式储存数据

0: xxx, 1: xxx, 2: xxx...

2、具有length属性。但是length属性不是动态的,不会随着成员的变化而改变

3、不具有数组的push()、forEach()等方法

《js伪数组如何转成常规数组》

JS进阶面试题

1、闭包

2、原型和原型链

__proto__:任何对象( JS 中万物皆对象)都有__proto__属性(隐式原型)。每个对象都有 __proto__ 属性 ,该属性指向其构造函数原型对象 prototype:所有函数(仅限函数)都拥有 prototype 属性(显式原型) constructor:所有的 prototype实例化对象 都有一个constructor属性,都指向关联的构造函数本身

  • 原型:每一个 JavaScript 对象(null 除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性,其实就是 prototype对象。
  • 原型链:由相互关联的原型组成的链状结构就是原型链, 即由__proto__将对象和其原型对象(prototype)串联起来的链状结构。
  • 在调用实例的方法和属性时,如果在实例对象上找不到,就会往原型对象上找。
  • 构造函数的 prototype属性指向 实例的原型对象
  • 原型对象的 constructor属性 指向 构造函数

必看文章:

《深入理解javascript原型和原型链》——猫老板的豆

问:js如何判断属性在实例中还是原型中?

答: obj.hasOwnProperty可以用来判断该属性是否在实例中;

in操作符可以判断该属性是否存在于实例和其原型

《JS中的原型---什么是原型?如何判断属性在实例中还是原型中?》

3、继承

《JavaScript中的继承》 《对于原型和继承的理解》

4、JS中ES6知识点

问:var、let和const的区别?

《js中var、let和const的区别》

问:ES6 module和Commonjs的区别?

《JS高级笔记:CommonJs与ESModule的区别》

问:ES6中set和map的区别?

答:《es6中set和map的区别》

注意:向 Set 加入值的时候,不会发生类型转换。精确运算符下,NaN是不等于自身的,但是Set中认为NaN和NaN是相等的。而{ }在Set中被认为是不相等的。 在这里插入图片描述 在这里插入图片描述

四、框架(Vue/React)

1、React生命周期

《React生命周期详细描述》 《React生命周期执行顺序问题》

2、Virtual DOM / Diff算法

《前端学习,五分钟带你看懂Virtual DOM及diff算法在vue中的应用》 《React diff的原理》 react和vuediff算法的区别

  • 相同点
Vue和react的diff算法,都是不进行跨层级比较,只做同级比较。
  • 不同点
1Vue进行diff时,调用patch打补丁函数,一边比较一边给真实的DOM打补丁;
2Vue对比节点,当节点元素类型相同,但是className不同时,认为是不同类型的元素,删除重新创建;而react则认为是同类型节点,进行修改操作;
3、(1)Vue的列表比对,采用从两端到中间的方式,旧集合和新集合两端各存在两个指针,两两进行比较,如果匹配上了就按照新集合去调整旧集合,每次对比结束后,指针向队列中间移动;
   (2)而react则是从左往右依次对比,利用元素的index和标识lastIndex进行比较,如果满足index < lastIndex就移动元素,删除和添加则各自按照规则调整;
   (3)当一个集合把最后一个节点移动到最前面,react会把前面的节点依次向后移动,而Vue只会把最后一个节点放在最前面,这样的操作来看,Vue的diff性能是高于react;

3、Fiber算法

《浅谈React Fiber》

4、React Hooks原理

《react-hooks原理- - - 详细版》 《react-hooks原理- - - useState和useEffect为例,简易版》 《【react 源码系列】useMemo 与 useCallback 解析》

5、setState原理

《setState是同步还是异步?原理是什么?》 在这里插入图片描述

6、setState打印问题

这种题:

  • 控制台打印的console.log的值,一般都是初始值,外面的setCount是异步的原因,及时值更新了,也无法拿到最新的count,而在循环或者setTimeout里面的会因为闭包的原因,获取到的是外面声明的时候被赋予的初始值,所以console.log打印的一般是初始值。
  • setCount((count) => count + 1)这种写法,后面页面上展示的count会依赖前面的值。
  • setCount(count + 1)这种写法,后面页面上展示的count不会依赖前面的值。

案例一: 纯 setCount((count) => count + 1),答案:打印值全部为0,页面count从0变为3再变为4

function App() {
  const [count, setCount] = useState(0);
  const onOk = () => {
    setCount((count) => count + 1);
    console.log("1", count);
    setCount((count) => count + 1);
    console.log("2", count);
    setCount((count) => count + 1);
    console.log("3", count);
    for(let i = 0; i < 5; i++){
        setTimeout(() => {
            //每次打印会依赖上一次的count
            setCount((count) => count + 1);
            //由于console.log(count);是在setTimeout的回调中调用的,
            //并且它打印的是setTimeout被定义时的count值(由于闭包),
            //所以它会打印五次相同的值,即0。
            console.log(count);
        }, 1000)
    }
  };
  return (
    <>
      <div onClick={onOk}>按钮</div>
      <div>当前数量:{count}</div>
    </>
  );
}

案例二:纯 setCount( count + 1),答案:打印值全部为0,页面count从0变为1并保持不变

function App() {
  const [count, setCount] = useState(0);
  const onOk = () => {
    setCount(count + 1);
    console.log("1", count);
    setCount(count + 1);
    console.log("2", count);
    setCount(count + 1);
    console.log("3", count);
    for(let i = 0; i < 5; i++){
        setTimeout(() => {
            setCount(count + 1);
            //由于console.log(count);是在setTimeout的回调中调用的,
            //并且它打印的是setTimeout被定义时的count值(由于闭包),
            //所以它会打印五次相同的值,即0。
            console.log(count);
        }, 1000)
    }
  };
  return (
    <>
      <div onClick={onOk}>按钮</div>
      <div>当前数量:{count}</div>
    </>
  );
}

案例三:先 setCount( count + 1)setCount((count) => count + 1)。答案:打印值全部为0,页面count从0变为1,再变为6

function App() {
  const [count, setCount] = useState(0);
  const onOk = () => {
    setCount(count + 1);
    console.log("1", count);
    setCount(count + 1);
    console.log("2", count);
    setCount(count + 1);
    console.log("3", count);
    for(let i = 0; i < 5; i++){
        setTimeout(() => {
            setCount((count)=>count + 1);
            //由于console.log(count);是在setTimeout的回调中调用的,
            //并且它打印的是setTimeout被定义时的count值(由于闭包),
            //所以它会打印五次相同的值,即0。
            console.log(count);
        }, 1000)
    }
  };
  return (
    <>
      <div onClick={onOk}>按钮</div>
      <div>当前数量:{count}</div>
    </>
  );
}

案例四:先setCount((count) => count + 1),再 setCount( count + 1)。答案:打印值全部为0,页面count从0变为3,再变为1

function App() {
  const [count, setCount] = useState(0);
  const onOk = () => {
    setCount((count) => count + 1);
    console.log("1", count);
     setCount((count) => count + 1);
    console.log("2", count);
    setCount((count) => count + 1);
    console.log("3", count);
    for(let i = 0; i < 5; i++){
        setTimeout(() => {
            setCount(count + 1);
            //由于console.log(count);是在setTimeout的回调中调用的,
            //并且它打印的是setTimeout被定义时的count值(由于闭包),
            //所以它会打印五次相同的值,即0。
            console.log(count);
        }, 1000)
    }
  };
  return (
    <>
      <div onClick={onOk}>按钮</div>
      <div>当前数量:{count}</div>
    </>
  );
}

五、Webpack5

问:Webpack5性能优化?

一般从打包体积打包时间方面考虑。

  • 参考:

1、《webpack学习之路》

一、减少webpack打包体积

1、压缩代码

使用代码压缩工具,将代码压缩成更小的体积。Webpack在生产模式下默认启用压缩。

a、JS压缩:使用TerserWebpackPlugin 来压缩。

(webpack5 自带最新的 terser-webpack-plugin,无需手动安装。并且TerserWebpackPlugin 默认就开启了多进程和缓存,无需再引入 ParallelUglifyPlugin

b、CSS压缩:使用 CssMinimizerWebpackPlugin 压缩 CSS 文件。

const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
  optimization: {
    minimizer: [
    	new TerserPlugin({
          parallel: 4,
          terserOptions: {
             parse: {
               ecma: 8,
             },
             compress: {
               ecma: 5,
               warnings: false,
               comparisons: false,
               inline: 2,
             },
             mangle: {
               safari10: true,
             },
             output: {
               ecma: 5,
               comments: false,
               ascii_only: true,
             },
           },
        }),
      	new CssMinimizerPlugin({
          parallel: 4,
        }),
    ],
  }
}
2、代码分割(code splitting)

代码分离能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,可以缩短页面加载时间。

a、抽离公共模块

使用SplitChunksPlugin(Webpack内置)将第三方库分割到单独的bundle中。可以通过调整配置来满足要求。

比如: 1、会被共享的代码块或者 node_mudules 文件夹中的代码块; 2、体积大于30KB的代码块(在gz压缩前); 3、按需加载代码块时的并行请求数量不超过5个; 4、加载初始页面时的并行请求数量不超过3个; 5、模块最少引用几次才会被拆分; 在这里插入图片描述 b、CSS 文件分离 MiniCssExtractPlugin 插件将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。

3、按需加载(Lazy Loading):

对于部分组件、路由等,采用懒加载的方式,只在需要时才加载它们,从而减少初始加载时的代码体积。比如我们使用lodash这种三方库的时候,也可以使用按需加载。

a、使用动态import语法。代码中使用动态import语法可以实现按需加载;

import('module-name').then(module => {
	// 使用模 
});

b、使用webpackChunkName注释:在动态import语句中添加webpackChunkName注释可以指定生成的chunk名称,例如

import(/* webpackChunkName: "my-chunk" */ 'module-name').then(module => {
  // 使用模块
});
4、Tree Shaking:

通过静态分析,删除项目中未被使用的代码(死代码)。这要求项目中的模块使用ES6模块语法(import/export),并且Webpack配置中开启了mode: 'production'

5、使用CDN:

将静态资源(如第三方库)通过CDN加载,可以减少服务器的压力,并可能提高加载速度,因为这些资源可能会从用户更近的服务器上加载。

二、缩短Webpack打包时间

1、减少重复打包

通过配置noParse选项来避免对已经编译过的文件进行重复打包。

(noParse配置是作为 module 的一个属性值,即不解析某些模块,所谓不解析,就是不去分析某个模块中的依赖关系,即不去管某个文件是否 import(依赖)了某个文件,比如让webpack不去解析jquery的依赖关系,提高打包速度)

module.exports = {
	module: {
		noParse:/jquery/,//不去解析jquery中的依赖库
	}
}
2、优化 loader 配置

a、将非必须的 loader、plugins 删除,并且使用includeexclude选项来精确指定loader需要处理的文件范围,避免不必要的处理。

每个的 loader、plugin 都有其启动时间。尽量少地使用工具。 在这里插入图片描述 b、利用Webpack 5自带的持久化缓存功能,通过配置cache: { type: 'filesystem' }来缓存构建结果,从而在重新构建时能够复用缓存数据,减少构建时间。

module.exports = {
    cache: {
      type: 'filesystem', // 使用文件缓存
    },
}

(官方推荐上面cache写法,说 webpack5 开箱即用的持久缓存cache是比 dll 更优的解决方案) DllPlugin是将不常改变的第三方库单独打包成动态链接库(DLL),然后在主项目中引用这些DLL,以减少构建时间。 在这里插入图片描述 c、对于大型项目,可以考虑使用thread-loader()来将loader的工作分配到多个工作线程中,以实现并行处理。

(在 webpack5 就不要再使用 happypack 了,官方也已经不再维护了)

d、使用 webpack5 资源模块 asset module (如asset/resource,asset/inline,asset/source)代替旧的 assets loader(如 file-loader/url-loader/raw-loader 等),减少 loader 配置数量。

3、优化 resolve 配置

resolve 用来配置 webpack 如何解析模块,可通过优化 resolve 配置来覆盖默认配置项,减少解析范围。

a、配置 alias。alias 可以创建 import 或 require 的别名,用来简化模块引入。

b、配置extensions。定义 extensions,以覆盖 webpack 默认的 extensions,加快解析速度。

// webpack.config.js
module.exports = {
  resolve: {
    // 配置解析模块时附加到请求的扩展名数组
    extensions: ['.js', '.json', '.jsx', '.ts'],
    // 配置解析异步模块时要使用的扩展名
    mainFields: ['main'],
    // 配置解析时想要使用的特定目录
    directories: ['node_modules'],
    // 配置模块的别名
    alias: {
      '@components': path.resolve(__dirname, 'src/components/'),
      '@utils': path.resolve(__dirname, 'src/utils/'),
    },
    // 是否应将依赖项的`.js`扩展名解析为`.jsx`
    enforceExtension: false,
  },
};

六、浏览器相关

1、TCP和UDP协议

《深入理解TCP、UDP协议及两者的区别》 《如果你正在准备面试TCP,看这一篇就够了,前端开发工作》

2、GET和POST请求

《深入理解get和post请求的区别》

3、HTTP1、HTTP2、HTTP3

《http1 http2 http 3 区别》

4、从输入URL到页面渲染发生了什么

《从输入一个URL到页面渲染的流程简介》

5、HTTPS协议

《通俗易懂的HTTPS协议》

6、DNS到底是什么

《字节面试被虐后,是时候搞懂 DNS 了》

7、Cookie的Same-site属性解析

《Cookie的Same-site属性》

8、V8引擎是怎么执行JavaScript代码的?(简述)

在这里插入图片描述 在这里插入图片描述

9、hash和history路由的区别

《浏览器中hash和history的区别》 《History对象中pushstate和replacestate对popstate事件影响》

七、手写题

《最常见前端手写代码及方法实现》

1、将扁平数据结构改为树状结构

 let arr = [ {
      id: 1,
         parentId: 0,
         name: "1"
     },{
          id: 2,
          parentId: 0,
          name: "2"
      },{
          id: 3,
          parentId: 1,
          name: "3"
      },{
          id: 4,
          parentId: 2,
          name: "4"
      }, {
          id: 5,
          parentId: 3,
          name: "5"
      }, {
          id: 6,
          parentId: 4,
          name: "6"
      }, {
          id: 7,
          parentId: 6,
          name: "7"
      }]
  function flatTree(sourceArray , parentId = 0){
       let tree = []
        sourceArray.map((item)=>{
            if(item.parentId === parentId){
                item.children = flatTree(sourceArray,item.id)
                tree.push(item)
            }
        })
        return tree
}

2、将树形数据结构扁平化

// 树形数据
const arr = [
 { id: "1", rank: 1 },
 { id: "2", rank: 1,
  children:[ 
   { id: "2.1", rank: 2 },
   { id: "2.2", rank: 2 } 
  ] 
 },
 { id: "3", rank:1,
  children:[ 
   { id: "3.1", rank:2, 
    children: [ 
     { id:'3.1.1', rank:3,
      children:[ 
       { id: "3.1.1.1", rank: 4, 
        children:[
         { id: "3.1.1.1.1", rank: 5 }
        ] 
       } 
      ] 
     } 
    ] 
   } 
  ] 
 }
]
  
function toLine(data){
    return data.reduce((prev, {id, rank,children = []}) =>
       prev.concat([{id,rank}], toLine(children))
    ,[])
}

3、排序

《前端10大排序》

3.1、快速排序

原理:首先从序列中选取一个数作为基准数,将比这个数大的数全部放到它的右边,把小于或者等于它的数全部放到它的左边 (一次快排 partition),然后分别对基准的左右两边重复以上的操作,直到数组完全排序

function quickSort(array) {
  if (array.length < 2) {
    return array;
  }
  const target = array[0];
  const left = [];
  const right = [];
  for (let i = 1; i < array.length; i++) {
    if (array[i] < target) {
      left.push(array[i]);
    } else {
      right.push(array[i]);
    }
  }
  return quickSort(left).concat([target], quickSort(right));
}

3.2、冒泡排序

思想: 两两依次比较,如果前一个比后一个大,就交换,直到最后,那么,最后一个数就是最大的,交换到没有可以交换的时候,排序结束。 具体步骤: 1、比较相邻的元素,如果第一个比第二个大,交换两个元素的位置; 2、对每一对相邻的元素做同样的工作,直到最后一对,最后的元素会是最大的数; 3、针对除已经排序好的元素重复同样的步骤,直到没有可以交换的元素,排序结束。

let arr = [5, 3, 1, 4, 2];
function bubbleSort (arr) {
  let len = arr.length;
  for (let i = 0; i < len; i++) {
    for (let j = 0; j < len - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        // 交换 arr[j] 和 arr[j+1] 的值
        // 交换后,arr[j] 就排在了后面,所以后面的数字要向前移动一位
        // 使用交换的方法,可以让每次循环都把最大的数字放在最后
        let temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
      }
    }
  }
  return arr;
}
console.log(bubbleSort(arr));//[ 1, 2, 3, 4, 5 ]

3.3、选择排序

原理:从未排序的序列中找到最大(或最小的)放在已排序序列的末尾(为空则放在起始位置),重复该操作,知道所有数据都已放入已排序序列中。

function selectionSort(arr) {
  let l = arr.length
  for(let i = 0; i < l; i++) {
    for(let j = i + 1; j < l; j++) {
      if (arr[i] > arr[j]) {
        [arr[i], arr[j]] = [arr[j], arr[i]]
      }
    }
  }
}
let arr = [1, 3, 2, 9 , 5, 4]
selectionSort(arr)
console.log(arr) // [1, 2, 3, 4, 5, 9]

4、数组去重

《前端常见去重方法及效果》

4.1、Set实现

var arr = [1,1,8,8,12,12,15,15,16,16];
function unique (arr) {
  return Array.from(new Set(arr))
}

console.log(unique(arr))
 //[1,8,12,15,16]

4.2、filter实现

var arr = [1, 1, 8, 8, 12, 12, 15, 15, 16, 16];
function unlink(arr) {
   return arr.filter(function (item, index, arr) {
       //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
       return arr.indexOf(item, 0) === index;
   });
}
console.log(unlink(arr));

4.3、includes实现

注意includes能够检测到数组中包含NaN

这涉及到includes底层的实现。在进行判断是否包含某元素时会调用sameValueZero方法进行比较,如果为NaN,则会使用isNaN()进行转化。

var arr = [1, 1, 8, 8, 12, 12, 15, 15, 16, 16];
function unique(arr) {
    var array =[];
    for(var i = 0; i < arr.length; i++) {
     //includes 检测数组是否有某个值
        if( !array.includes( arr[i]) ) {
            array.push(arr[i]);
        }
     }
   return array
}
console.log(unique(arr))

4.4、indexOf实现

注意:indexOf没有办法对NaN去重

const arr = [1, 2, 2, 'abc', 'abc', true, true, false, false, undefined, undefined, NaN, NaN]

function unique(arr) {
  	const newArr = []
  	arr.forEach(item => {
        if (newArr.indexOf(item) === -1) {
        	newArr.push(item)
   	    }
   	 })
    return newArr // 返回一个新数组
}

const result = unique(arr)
console.log(result) // [ 1, 2, 'abc', true, false, undefined, NaN, NaN ]

5、Promise系列

5.1、Promise

class Prom {
 static resolve(value) {
   if (value && value.then) {
     return value;
   }
   return new Prom((resolve) => resolve(value));
 }

 constructor(fn) {
   this.value = undefined;
   this.reason = undefined;
   this.status = "PENDING";

   // 维护一个 resolve/pending 的函数队列
   this.resolveFns = [];
   this.rejectFns = [];

   const resolve = (value) => {
     // 注意此处的 setTimeout
     setTimeout(() => {
       this.status = "RESOLVED";
       this.value = value;
       this.resolveFns.forEach(({ fn, resolve: res, reject: rej }) =>
         res(fn(value))
       );
     });
   };

   const reject = (e) => {
     setTimeout(() => {
       this.status = "REJECTED";
       this.reason = e;
       this.rejectFns.forEach(({ fn, resolve: res, reject: rej }) =>
         rej(fn(e))
       );
     });
   };

   fn(resolve, reject);
 }

 then(fn) {
   if (this.status === "RESOLVED") {
     const result = fn(this.value);
     // 需要返回一个 Promise
     // 如果状态为 resolved,直接执行
     return Prom.resolve(result);
   }
   if (this.status === "PENDING") {
     // 也是返回一个 Promise
     return new Prom((resolve, reject) => {
       // 推进队列中,resolved 后统一执行
       this.resolveFns.push({ fn, resolve, reject });
     });
   }
 }

 catch(fn) {
   if (this.status === "REJECTED") {
     const result = fn(this.value);
     return Prom.resolve(result);
   }
   if (this.status === "PENDING") {
     return new Prom((resolve, reject) => {
       this.rejectFns.push({ fn, resolve, reject });
     });
   }
 }
}

Prom.resolve(10)
 .then((o) => o * 10)
 .then((o) => o + 10)
 .then((o) => {
   console.log(o);
 });

5.2、Promise.all

function all(arr){
   //函数返回一个 promise 对象
   return new Promise((resolve,reject) => {
       let count = 0;
       let result = [];
       //并发执行每一个 promise
       for(let i = 0;i < arr.length;i++){
           //arr里有可能是promsie 或者 常量,用promise包装
           Promise.resolve(arr[i]).then(res => {
               //用下标存储结果,保证输出顺序和arr一致
               //因为promise 对象执行时间可能不同,用push会破坏顺序
               result[i] = res 
               count++
               if(count === arr.length){
                   resolve(result)
               }
           }).catch(err => reject(err))
       }
   })
}
const p0 = 'p0'
const p1 = new Promise((res, rej) => {
   setTimeout(() => {
       res('p1')
   }, 1000)
})

const p2 = new Promise((res, rej) => {
   setTimeout(() => {
       res('p2')
   }, 2000)
})

const p3 = new Promise((res, rej) => {
   setTimeout(() => {
       res('p3')
   }, 3000)
})

const test = all([p0, p2, p3])
   .then(res => console.log(res))
   .catch(e => console.log(e))

console.log(test); //[ 'p0', 'p2', 'p3' ]

5.3、Promise.race

function race(arr){
   return new Promise((resolve,reject) => {
       for(let i = 0;i < arr.length;i++){
           Promise.resolve(arr[i]).then(res => {
               //某一个 promise 完成后直接返回其值
               resolve(res)
           }).catch(err => {
               //如果有错误,直接结束循环,并返回错误
               reject(err)
           })
       }
   })
}

const p1 = new Promise((res, rej) => {
   setTimeout(() => {
       res('p1')
   }, 1000)
})

const p2 = new Promise((res, rej) => {
   setTimeout(() => {
       res('p2')
   }, 2000)
})

const p3 = new Promise((res, rej) => {
   setTimeout(() => {
       res('p3')
   }, 3000)
})

const test = race([p1, p2, p3])
   .then(res => console.log(res))
   .catch(e => console.log(e))

console.log(test); //p1 速度快的先返回

6、Call()

Function.prototype.myCall = function(context) {
 if (typeof this !== 'function') {
   throw Error("not a function")
 }
 context = context || window
 context.fn = this
 const args = [...arguments].slice(1)
 const result = context.fn(...args)
 delete context.fn
 return result
}
​```
- 首先 `context` 为可选参数,如果不传的话默认上下文为 `window`
- 接下来给 `context` 创建一个 `fn` 属性,并将值设置为需要调用的函数
- 因为 `call` 可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
- 然后调用函数并将对象上的函数删除

7、Apply()

Function.prototype.myApply = function(context) {
 if (typeof this !== 'function') {
   throw Error("not a function")
 }
 context = context || window
 context.fn = this
 let result
 // 处理参数和 call 有区别
 if (arguments[1]) {
   result = context.fn(...arguments[1])
 } else {
   result = context.fn()
 }
 delete context.fn
 return result
}

8、Bind()

《bind的原理和实现》 《bind 函数的实现原理》 好理解版本:

 Function.prototype.bind = function (context) { 
    // 调用 bind 的不是函数,需要抛出异常 
      if (typeof this !== "function") { 
          throw new Error('Error')
      } 
      // this 指向调用者 
      var _this = this; 
      //  bind后面紧跟的括号,里面的参数
      var args = Array.prototype.slice.call(arguments, 1); 
      // 创建一个空对象 
      var F = function () {}; 
      // 空对象的原型指向绑定函数的原型 
      F.prototype = this.prototype;
      // 返回一个函数 
      var fBound = function () { 
          // bind 返回函数的参数,具体调用时传的参数
          var bindArgs = Array.prototype.slice.call(arguments); 
          // 然后同传入参数合并成一个参数数组,并作为 _this.apply() 的第二个参数 
          return _this.apply(this instanceof F ? this : context, args.concat(bindArgs)); 

      } 
     // 空对象的实例赋值给 fBound.prototype 
      fBound.prototype = new F(); 
      return fBound; 
 } 

学长版本

Function.prototype.myBind = function (context) {
     if (typeof this !== 'function') {
       throw new TypeError('Error')
     }
     const _this = this
     const args = [...arguments].slice(1)
     // 返回一个函数
     return function F() {
       // 因为返回了一个函数,我们可以 new F(),所以需要判断
       if (this instanceof F) {
         return new _this(...args, ...arguments)
       }
       return _this.apply(context, args.concat(...arguments))
     }
}

9、防抖(延迟执行)

// 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、节流(间隔执行)

// 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、深拷贝

1、常用版本

function deepCopy(object) {
  if (!object || typeof object !== "object") return;
  let newObject = Array.isArray(object) ? [] : {};
  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] =typeof object[key] === "object" ? deepCopy(object[key]) : object[key];
    }
  }
  return newObject;
}

2、 高分版(使用Map数据结构,解决循环嵌套问题)

function deepCopy(obj, m = new Map()) {
 if (obj === null || typeof obj !== 'object') return obj //原始值
 if (obj instanceof Date) return new Date(obj) //日期值
 if (obj instanceof RegExp) return new RegExp(obj) //正则

 if (m.has(obj)) return m.get(obj) //防止循环引用情况
 //如果当前属性是数组,它的constructor就是Array就相当于new Array
 //如果当前属性是对象的话,同理new Object
 //相当于let copyObj  = obj.push?[]:{}
 let copyObj = new obj.constructor() //创建一个和obj类型一样的对象
 m.set(obj, copyObj) //放入缓存中
 for (let key in obj) {
   if (obj.hasOwnProperty(key)) {
     copyObj[key] = deepCopy(obj[key], m)
   }
 }
 return copyObj
}
//例子
const obj = {
 name: 'Jack',
 address: {
   x: 100,
   y: 200
 }
} 
obj.a = obj
let copyObj = deepCopy(obj)
console.log(copyObj)
console.log(copyObj.address === obj.address)  //false

详细版

  function cloneObject(target, source) {
    //获取该对象的所有属性的集合
   var propNames = Object.getOwnPropertyNames(source) 
   for (let i = 0 i < names.length i++) {
      //获取属性的描述对象
      var desc = Object.getOwnPropertyDescriptor(source, propNames[i])  
      //解决多层对象与循环引用的问题,同时处理嵌套的DOM、正则、实例、Date等特殊对象
      if (typeof desc.value === "object" && desc.value !== null) {
           var obj
           //单独处理DOM;用原型链判断DOM类型
           if (desc.value instanceof HTMLElement) {
               //使用document.createElement(标签名)创建DOM
               obj = document.createElement(desc.value.nodeName) 
           } else {
               //其他对象使用constructor判断类型---大写;使用 new constructor(arguments)构建
               switch (desc.value.constructor) {
                 case Box:
                 obj = new desc.value.constructor(desc.value[Box.ARG[0]],                                       desc.value[Box.ARG[1]]) 
                 break
                 case RegExp://(正则,修饰符)
                 obj = new desc.value.constructor(desc.value.source,desc.value.flags) 
                  break
                 default :
                 obj = new desc.value.constructor() 
                } 
              }
          //递归多层对象
          cloneObject(obj, desc.value) 
          //然后在设置属性的描述对象
          Object.defineProperty(target, propNames[i], {
                    value:obj,
                    enumerable:desc.enumerable,
                    writable:desc.writable,
                    configurable:desc.configurable
                }) 
      } 
       else {
        //第一层不是对象或者为null直接使用Object.defineProperty自定义属性
        Object.defineProperty(target, propNames[i], desc) 
      }
   }
   return target
 } 

12、实现一个New

function myNew(constrc, ...args) {
   const obj = {}; // 1. 创建一个空对象
   obj.__proto__ = constrc.prototype; // 2. 将obj的[[prototype]]属性指向构造函数的原型对象
   // 或者使用自带方法:Object.setPrototypeOf(obj, constrc.prototype)
   const result = constrc.apply(obj, args); // 3.将constrc执行的上下文this绑定到obj上,并执行
   return result instanceof Object ? result : obj;  //4. 如果构造函数返回的是对象,则使用构造函数执行的结果。否则,返回新创建的对象
}

// 使用的例子:
function Person(name, age){
   this.name = name;
   this.age = age;
}
const person1 = myNew(Person, 'Tom', 20)
console.log(person1)  // Person {name: "Tom", age: 20}

13、实现一个instanceOf


function myInstanceof(a,b){
    let left=a.__proto__
    let right=b.prototype
    while(true){
     if(left==right){
          return true
      }
      if(left==null || left == undefined){
          return false
      }
      left=left.__proto__
    }
}

14、Promise任务队列事件并发控制器

第一种: 想要实现的效果

class Scheduler {
 add(promiseMaker) {}
}

const timeout = (time) =>
 new Promise((resolve) => {
   setTimeout(resolve, time);
 });

const scheduler = new Scheduler();
const addTask = (time, order) => {
 scheduler.add(() => timeout(time).then(() => console.log(order)));
};

addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
// output:2 3 1 4
// 一开始,1,2两个任务进入队列。
// 500ms 时,2完成,输出2,任务3入队。
// 800ms 时,3完成,输出3,任务4入队。
// 1000ms 时,1完成,输出1。

实现方法

class Scheduler {
  constructor() {
    this.waitTasks = []; // 待执行的任务队列
    this.excutingTasks = []; // 正在执行的任务队列
    this.maxExcutingNum = 2; // 允许同时运行的任务数量
  }

  add(promiseMaker) {
    if (this.excutingTasks.length < this.maxExcutingNum) {
      this.run(promiseMaker);
    } else {
      this.waitTasks.push(promiseMaker);
    }
  }

  run(promiseMaker) {
    const len = this.excutingTasks.push(promiseMaker);
    const index = len - 1;
    promiseMaker().then(() => {
      this.excutingTasks.splice(index, 1);
      if (this.waitTasks.length > 0) {
        this.run(this.waitTasks.shift());
      }
    });
  }
}

第二种:(面试经常考,渡一袁老师版本)

给定任务队列和最大并发数,依次执行,但是没有要求收集返回值

function(tasks ,maxLen = 4){
      return new Promise((resolve) => {
        if (tasks.length === 0) {
          resolve();
          return
        }
        let nextIndex   = 0; //当前任务下标
        let finishCount = 0; //记录任务完成的数量
        function _run(){
          //运行下一个任务
          const task = tasks[nextIndex];
          nextIndex++
          task().then(() => {
            finishCount++
            if(nextIndex < tasks.length) {
              _run()
            }
            else if( finishCount === tasks.length){
              resolve()
            }

          })
        }
        for(let i = 0; i < maxLen && i < tasks.length; i++){
          _run()
        }
      })
    }

第三种:

给定任务队列和最大并发数,依次执行,无论成功还是失败,要求收集其返回值

 function runLimitedConcurrentPromises(promises, maxConcurrent = 5) {  
    // 初始化一个数组来存储所有Promise的结果  
    const results = [];  
    // 当前正在运行的Promise数量  
    let running = 0;  
    // 已经完成的Promise个数
    let finishCount = 0
  
    // 递归函数,用于控制并发数  
    function runNext() {  
        // 当没有更多Promise要运行,或者已经达到最大并发数时,直接返回  
        if (promises.length === 0 || running > maxConcurrent) {  
            return;  
        }  
        // 从promises数组中取出一个Promise  
        const nextPromise = promises.shift();  
  
        // 增加正在运行的Promise数量  
        running++;  

		 // 执行Promise
		nextPromise.then(result => {
			results.push({status: 'fulfilled', value: result})
		}, err => {
			results.push({status: 'rejected', reason: err})
		}).finally(() => {
			// 减少正在运行的Promise数量  
            running--;  
            finishCount++;
            // 如果当前任务数量还大于0,并且当前执行的数量不能大于阿最大
			if (promises.length > 0 && running < maxConcurrent) {  
                runNext();  
            }  
            if(finishCount === promises.length){
            	resolve(results)
            }
		})
  
      
    }  
  
    // 开始执行  
    for (let i = 0; i < Math.min(promises.length, maxConcurrent); i++) {
        runNext();  
    }
    
  	/* 看情况,上面写了最后resolve()后,下面这个就不需要再写了 */
    // 返回一个Promise,该Promise在所有Promise都解决后解决,并包含所有结果  
    return new Promise(resolve => {  
    // 当所有原始Promise都被处理(即results数组长度等于原始promises数组长度)时,解决这个Promise  
        const interval = setInterval(() => {  
            if (results.length === promises.length) {  
                clearInterval(interval);  
                resolve(results);  
            }  
        }, 100); // 每100ms检查一次,可以根据需要调整  
    });  
}  

15、柯里化

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function (...nextArgs) {
        return curried.apply(this, args.concat(nextArgs));
      };
    }
  };
}
 
// 原函数
function add(x, y, z) {
  return x + y + z;
}
 
// 柯里化后的函数
const curriedAdd = curry(add);
 
// 柯里化函数的调用
curriedAdd(1)(2)(3); // 返回 6
 
// 也可以一次传入多个参数
curriedAdd(1, 2)(3); // 返回 6
function curry(fn, args) {
  var length = fn.length;
  var args = args || [];
  return function () {
    var newArgs = args.concat(Array.prototype.slice.call(arguments));
    if (newArgs.length < length) {
      return curry.call(this, fn, newArgs);
    } else {
      return fn.apply(this, newArgs);
    }
  }
}
function multiFn(a, b, c) {
  return a * b * c;
}
var multi = curry(multiFn);
multi(2)(3)(4);
multi(2, 3, 4);
multi(2)(3, 4);
multi(2, 3)(4);
function fn() {  
    let args = [];  
  
    // 返回一个函数,该函数可以接收任意数量的参数  
    function accumulator(...newArgs) {  
        // 将新参数添加到args数组中  
        args.push(...newArgs);  
  
        // 如果没有更多的参数(即newArgs为空),则返回累加结果  
        if (newArgs.length === 0) {  
            return args.reduce((sum, value) => sum + value, 0);  
        }  
  
        // 否则,返回一个新的accumulator函数,它将继续接收参数  
        return function(...moreArgs) {  
            // 递归地调用accumulator来处理更多的参数  
            return accumulator(...moreArgs);  
        };  
    }  
  
    // 初始调用时,根据是否传递了参数来调用accumulator  
    return arguments.length === 0 ? accumulator : accumulator.apply(null, arguments);  
}  
  
// 测试用例  
console.log(fn(1, 2, 3)()); // 输出: 6
console.log(fn(1)(2)(3)()); // 输出: 6 
console.log(fn(1, 2)(3, 4)());

16、重构

16.1、forEach

Array.prototype._forEach = function (fn, thisArg) {
   if (typeof fn !== 'function') throw '参数必须是函数'
   // if (!Array.isArray(thisArg)) throw '只能数组使用'

   let arr = this
   for (let i = 0; i < arr.length; i++) {
       fn.call(thisArg, arr[i], i, arr)
   }
}

16.2、map

Array.prototype._map = function (fn, thisArg) {
    if (typeof fn !== 'function') throw '参数必须是函数'
    if (!Array.isArray(thisArg)) throw '只能数组使用'
    const arr = this
    //返回的新数字
    const newArr = []
    for (let i = 0, len = arr.length; i < len; i++) {
        //将每次执行回调函数的结果push到新数组中
        newArr.push(fn.call(thisArg, arr[i], i, arr))
    }
    return newArr
}

16.3、reduce

第一种

function array_reduce(arr,fn,initValue){
    if(arr.length=0) return
    if(arr.length===1) return arr[0]
    //假设不存在初始值
    var index=1
    var value=arr[0]
    if(initValue!==undefined){
        index=0;value=initValue
    }
    for(;index<arr.length;index++){
        value=fn(valeu,arr[index],index,arr)
    }
    return value
}

第二种

Array.prototype.myReduce = function (cb, initialValue) {
  const arr = this; //this就是调用reduce方法的数组
  let total = initialValue ? initialValue : arr[0]; //不传默认取数组第一项
  let startIndex = initialValue ? 0 : 1; // 有初始值的话从0遍历,否则从1遍历
  for (let i = startIndex; i < arr.length; i++) {
    total = cb(total, arr[i], i, arr); //参数为初始值、当前值、索引、当前数组
  }
  return total;
};
 
//测试
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let res = arr.myReduce((total, cur) => {
  return total + cur;
}, 0);
console.log(res);//55

17、JS判断字符串是否是回文

(忽略标点符号和大小写)


function palindrome(str) {
 
  var  str1 = str.toLowerCase();  
  var reg = /[\W\_]/g;                     
  var str2 = str1.replace(reg, ""); //删除非字母、数字、下划线
  var str3 = str2.split("");  
  var str4 = str3.reverse(); 
  var str5 = str4.join("");  
 
  return str2 === str5;
}
palindrome("eye") //true
palindrome("eye 12 afd") //false

18、实现类似es6模板字符串的功能

function render(template, data) {
  const reg = /\{\{(\w+)\}\}/
  if(reg.test(template)) {
    const name = reg.exec(template)[1]
    template = template.replace(reg, data[name])
    return render(template, data)
  }
  // 若所有变量替换完毕,返回
  return template
}

let template = '我叫{{name}},来自{{address}},今年{{age}}岁'
let person = {
  name: '小明',
  address: '北京',
  age: 20
}
console.log(render(template, person))
// 我叫小明,来自北京,今年20岁

19、找出字符串(数组)中出现次数最多的元素和次数

注意:网上好多写法都没有考虑到存在多个数量相同的元素的情况,导致最后结果只有一个,下面这种写法是最保险的,没有bug

let arr = 'afdafwerrsdfsadaf'
// arr = arr.toLowerCase() //不区分大小写
// 先把它切了转换成数组在转换成对象
arr = arr.split('') 
let newArr = arr.reduce((pre, cur) => {
	(cur in pre) ? pre[cur]++: pre[cur] = 1
	return pre
}, {})
console.log(newArr)
// 获取对象中最大的值(就是出现最多的次数)
let max = 0
for(let key in newArr){
	if(max < newArr[key]) {
		max = newArr[key]
	}
}
// 根据最大的值 把那个字符用数组存储起来
// 注意了,这里很容易就出现bug了,我们分不清最大值是不是唯一的,比如出现次数 a==b==c==max
// 所以就要用数组存储出现次数最多的字符 
let brr = []
for(let key in newArr) {
	if(max == newArr[key]) {
		brr.push(key)
	}
}
console.log("出现最多的字符串:" + brr.join())
console.log("次数:" + max)

20、判断输出值

for(var i = 0 ; i < 3 ; i++ ){ 
   setTimeout(() => { console.log(i)},0) 
}
打印33
for(let i = 0 ; i < 3 ; i++ ){ 
   setTimeout(() => { console.log(i)},0) 
}
打印012
for(var i = 0 ; i < 3 ; i++ ){ 
    (function(){
        setTimeout(() => { console.log(i)},0) 
    })(i)
}
打印33

注意:上面这个立即执行函数没有传i,下面这个传入了i值

for(var i = 0 ; i < 3 ; i++ ){ 
    (function(i){
        setTimeout(() => { console.log(i)},0) 
    })(i)
}
打印012

21、继承

ES6extends继承ES5寄生组合式继承

21.1、寄生组合式继承

寄生组合式继承是对组合式继承(调用了2次父构造方法)的改进,使用父类的原型的副本来作为子类的原型,这样就只调用一次父构造函数,避免了创建不必要的属性。

function Parent (name) {
   this.name = name;
   this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
   console.log(this.name)
}
function Child (name, age) {
   Parent.call(this, name);//借用构造函数的方式来实现属性的继承和传参
   this.age = age;
}
 
//这里不用Child.prototype = new Parent()原型链方式的原因是会调用2次父类的构造方法,导致子类的原型上多了不需要的父类属性
Child.prototype = Object.create(Parent.prototype);//这里就是对组合继承的改进,创建了父类原型的副本
Child.prototype.constructor = Child;//把子类的构造指向子类本身
 
var child1 = new Child('AK、dadada', '18');
console.log(child1.colors);//[ 'red', 'blue', 'green' ]
child1.getName();//AK、dadada

21.2、ES6继承

在ES6中,可以使用class类去实现继承。使用extends表明继承自哪个父类,并且在子类构造函数中必须调用super。

class Parent {
  constructor(name) {
    this.name = name;
  }
  getName() {
    console.log(this.name);
  }
}

class Child extends Parent {
  constructor(name, age) {
    //使用this之前必须先调用super(),它调用父类的构造函数并绑定父类的属性和方法
    super(name);
    //之后子类的构造函数再进一步访问和修改 this
    this.age = age;
  }
}

// 测试
let child = new Child("AK、dadada", 18);
console.log(child.name); // AK、dadada
console.log(child.age); // 18
child.getName(); // AK、dadada

ES5继承和ES6继承的区别:

  • ES5继承是先创建子类的实例对象,然后再将父类方法添加到this(Parent.call(this))上。
  • ES6的继承不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

22、观察者模式

观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

// 被观察者 学生
class Subject {
  constructor() {
    this.state = "happy";
    this.observers = []; // 存储所有的观察者
  }
  //新增观察者
  add(o) {
    this.observers.push(o);
  }
  //获取状态
  getState() {
    return this.state;
  }
  // 更新状态并通知
  setState(newState) {
    this.state = newState;
    this.notify();
  }
  //通知所有的观察者
  notify() {
    this.observers.forEach((o) => o.update(this));
  }
}
 
// 观察者 父母和老师
class Observer {
  constructor(name) {
    this.name = name;
  }
  //更新
  update(student) {
    console.log(`亲爱的${this.name} 通知您当前学生的状态是${student.getState()}`);
  }
}
 
let student = new Subject();
let parent = new Observer("父母");
let teacher = new Observer("老师");
//添加观察者
student.add(parent);
student.add(teacher);
//设置被观察者的状态
student.setState("刚刚好");

23、发布订阅模式

发布订阅模式跟观察者模式很像,但它发布和订阅是不互相依赖的,因为有一个统一调度中心

class EventEmitter {  
    constructor() {  
        this.events = {};  
    }  
    // 订阅事件  
    on(eventName, callback) {  
        if (!this.events[eventName]) {  
            this.events[eventName] = [];  
        }  
        this.events[eventName].push(callback);  
    }  
    // 取消订阅  
    off(eventName, callback) {  
        if (this.events[eventName]) {  
            this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);  
        }  
    }  
    // 触发事件  
    emit(eventName, ...args) {  
        if (this.events[eventName]) {  
            this.events[eventName].forEach(callback => {  
                callback(...args);  
            });  
        }  
    }  
    // 一次性订阅  
    once(eventName, callback) {  
        const wrapper = (...args) => {  
            callback(...args);  
            this.off(eventName, wrapper);  
        };  
        this.on(eventName, wrapper);  
    }  
}  
// 使用示例  
const eventEmitter = new EventEmitter();  
// 订阅事件  
eventEmitter.on('message', (msg) => {  
    console.log('Received message:', msg);  
});  
// 触发事件  
eventEmitter.emit('message', 'Hello, World!');  
// 一次性订阅  
eventEmitter.once('onceMessage', (msg) => {  
    console.log('Received once message:', msg);  
});  
// 触发一次性订阅的事件  
eventEmitter.emit('onceMessage', 'This will only be shown once.');  
// 再次触发,但不会有输出  
eventEmitter.emit('onceMessage', 'This will not be shown.');

24、实现日期格式化

const dateFormat = (dateInput, format) => {
  var day = dateInput.getDate();
  var month = dateInput.getMonth() + 1;
  var year = dateInput.getFullYear();
  format = format.replace(/yyyy/, year);
  format = format.replace(/MM/, month);
  format = format.replace(/dd/, day);
  console.log(format);
  return format;
};

dateFormat(new Date("2024-02-01"), "yyyy/MM/dd"); // 2024/02/01
dateFormat(new Date("2024-02-20"), "yyyy/MM/dd"); // 2024/02/20
dateFormat(new Date("2024-02-20"), "yyyy年MM月dd日"); // 2024年02月20日

25、用setTimeout实现setInterval

function mySetInterval(fn, timeout) {
  // 控制器,控制定时器是否继续执行
  var timer = {
    flag: true,
  };
  // 设置递归函数,模拟定时器执行
  function interval() {
    if (timer.flag) {
      fn();
      setTimeout(interval, timeout);//递归
    }
  }
  // 启动定时器
  setTimeout(interval, timeout);
  // 返回控制器
  return timer;
}
 
let timer = mySetInterval(() => {
  console.log("1");
}, 1000);
//3秒后停止定时器
setTimeout(() => (timer.flag = false), 3000);

26、实现每隔一秒打印1,2,3,4

// 1.使用 let 块级作用域
for (let i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i);
  }, i * 1000);
}
 
// 2.使用闭包实现
for (var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(() => {
      console.log(j);
    }, j * 1000);
  })(i);
}

27、 循环打印红黄绿

场景:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯? 红绿灯函数

function red() {
  console.log("red");
}
function green() {
  console.log("green");
}
function yellow() {
  console.log("yellow");
}

Promise方法

const task = (timer, light) => 
    new Promise((resolve, reject) => {
        setTimeout(() => {
            if (light === 'red') {
                red()
            }
            else if (light === 'green') {
                green()
            }
            else if (light === 'yellow') {
                yellow()
            }
            resolve()
        }, timer)
    })
const step = () => {
    task(3000, 'red')
        .then(() => task(2000, 'green'))
        .then(() => task(1000, 'yellow'))
        .then(step)
}
step()

async/await实现

const task = (timer, light) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (light === "red") {
        red();
      } else if (light === "green") {
        green();
      } else if (light === "yellow") {
        yellow();
      }
      resolve(); //注意,要resolve让Promise状态变成fulfilled,不然会一直是pending,无法往下执行
    }, timer);
  });
};
const taskRunner = async () => {
  await task(3000, "red");
  await task(2000, "green");
  await task(1000, "yellow");
  taskRunner(); //递归
};
taskRunner();

八、TypeSctipt

问:type和interface的区别

共同点:

1、interfacetype都可以用来描述对象和函数类型;

interface User {
	name: string
	age: number
}

interface SetUser {
    (name: string, age: number): void;
}

type User = {
    name: string
    age: number
};

type SetUser = (name: string, age: number): void;

2、扩展性:都允许扩展(extends)

interfacetype 都可以拓展,并且两者并不是相互独立的,也就是说 interface 可以 extends type, type 也可以 extends interface 。 虽然效果差不多,但是两者语法不同。

//interface extends interface
interface Name { 
	name: string; 
}
interface User extends Name { 
	age: number; 
}

//type extends type
type Name = { 
	name: string; 
}
type User = Name & { age: number }

//interface extends type
type Name = { 
	name: string; 
}
interface User extends Name { 
	age: number; 
}

//type extends interface
interface Name { 
	name: string; 
}
type User = Name & { 
	age: number; 
}

不同点: 1、合并声明

interface可以重复对某个接口定义属性和方法,即定义两个相同名称的接口interface会合并声明; type定义是别名,别名是不能重复的,即定义两个同名的 type 会出现异常

interface name{
	name:string
}
interface name{
	age:number
}
其实合并等价于
name{
	name:string,
	age:number
}

type name{
	name:string
}
type name{
	age:number
}
//直接报错

2、type可以通过typeof获取实例的类型进行赋值

let div = document.createElement('div')
type B = typeof div

3、定义类型的范围

一般interface只能定义对象类型, 而 type 声明可以声明任何类型,包括基础类型联合类型交叉类型

基本类型

type Person = string;

联合类型

interface Dog {
  name: string;
}
interface Cat {
  age: number;
}
type animal = Dog | Cat;

元组

interface Dog {
  name: string;
}
interface Cat {
  age: number;
}
type animal = [Dog, Cat];

4、映射类型只能在类型别名(type)中使用,不能在接口(interface)中使用 在这里插入图片描述

( key in sskeys表示key可以是sskeys中的任意一个,类似于for in )

在这里插入图片描述

问:ts中Omit,Pick,Partial的意思

《ts(TypeScript)常用语法(Omit、Pick、Partial、Required)》

问:TypeScript中any和unknown的区别

《ts中any和unknown的区别》


《TypeScript常见的面试题》

九、项目中难点

1、1px边框问题

《1px边框问题》

2、安卓和ios开发中遇到的问题

《安卓和ios相关问题》

3、移动端H5适配布局等问题

h5布局等问题——github

4、内容不固定,底部Footer固定或展开/收起标签等问题

通过JS方式:《JS学习笔记——数据未加载时,内容高度不确定的展开收起按钮显隐问题》 通过CSS方式:《完美解决HTML中footer保持在页面底部问题》

5、前端与客户端通信Hybrid(JSBridge)

《JS如何调用客户端方法 JS Bridge Hybrid》 《JSBridge 实现原理及开发实践》