2022面试题整理大篇

204 阅读1小时+

一、HTML相关

二、CSS相关

1、什么是BFC(块级格式化上下文)、IFC(内联格式化上下文 )、FFC(弹性盒模型)

BFC(Block formatting context),即块级格式化上下文,它作为HTML页面上的一个独立渲染区域,只有区域内元素参与渲染,且不会影响其外部元素。 简单来说,可以将 BFC 看做是一个“围城”,外面的元素进不来,里面的元素出不去(互不干扰)。

一个决定如何渲染元素的容器 ,渲染规则 :

  • 1、内部的块级元素会在垂直方向,一个接一个地放置。
  • 2、块级元素垂直方向的距离由margin决定。属于同一个BFC的两个相邻块级元素的margin会发生重叠。
  • 3、对于从左往右的格式化,每个元素(块级元素与行内元素)的左边缘,与包含块的左边缘相接触,(对于从右往左的格式化则相反)。即使包含块中的元素存在浮动也是如此,除非其中元素再生成一个BFC。
  • 4、BFC的区域不会与浮动元素重叠。
  • 5、BFC是一个隔离的独立容器,容器里面的子元素和外面的元素互不影响。
  • 6、计算BFC容器的高度时,浮动元素也参与计算。

形成BFC的条件:

1、浮动元素,float 除 none 以外的值;

2、定位元素,position(absolute,fixed);

3、display 为以下其中之一的值 inline-block,table-cell,table-caption;

4、overflow 除了 visible 以外的值(hidden,auto,scroll);

BFC 的特性:

  • 内部的 Box 会在垂直方向上一个接一个的放置。
  • 垂直方向上的距离由 margin 决定
  • bfc 的区域不会与 float 的元素区域重叠。
  • 计算 bfc 的高度时,浮动元素也参与计算
  • bfc 就是页面上的一个独立容器,容器里面的子元素不会影响外面元素。

BFC 一般用来解决以下几个问题

  • 边距重叠问题
  • 消除浮动问题
  • 自适应布局问题

2、flex: 0 1 auto; 是什么意思?

元素会根据自身宽高设置尺寸。它会缩短自身以适应 flex 容器,但不会伸长并吸收 flex 容器中的额外自由空间来适应 flex 容器 。 水平的主轴(main axis)和垂直的交叉轴(cross axis)几个属性决定按哪个轴的排列方向

  • flex-grow: 0 一个无单位数() : 它会被当作<flex-grow>的值。
  • flex-shrink: 1 一个有效的**宽度(width)**值: 它会被当作 <flex-basis>的值。
  • flex-basis: auto 关键字noneautoinitial.

放大比例、缩小比例、分配多余空间之前占据的主轴空间。

3、避免CSS全局污染

  1. scoped 属性
  2. css in js
const styles = {
  bar: {
    backgroundColor: '#000'
  }
}
const example = (props)=>{
  <div style={styles.bar} />
}
  1. CSS Modules
  2. 使用less,尽量少使用全局对选择器
// 选择器上>要记得写,免得污染所有ul下面的li
ul{
  >li{
    color:red;
  }
}

4、CSS Modules

阮一峰 CSS Modules

CSS Modules是一种构建步骤中的一个进程。通过构建工具来使指定class达到scope的过程。

CSS Modules 允许使用: :global(.className)的语法,声明一个全局规则。凡是这样声明的class,都不会被编译成哈希字符串:local(className): 做 localIdentName 规则处理,编译唯一哈希类名。

CSS Modules使用特点:

  • 不使用选择器,只使用 class 名来定义样式
  • 不层叠多个 class,只使用一个 class 把所有样式定义好
  • 不嵌套class

5、盒子模型和 box-sizing 属性

width: 160px; padding: 20px; border: 8px solid orange; 标准 box-sizing: content-box; 元素的总宽度 = 160 + 202 + 82; IE的 border-box: 总宽度160

margin/padding百分比的值时 ,基于父元素的宽度和高度的。

6、css绘制三角形

  1. 通过border 处理
// border 处理
.class {
    width: 0;
    height: 0;
    border-left: 50px solid transparent;
    border-right: 50px solid transparent;
    border-bottom: 100px solid red;
}
// 宽高+border
div {
    width: 50px;
    height: 50px;
    border: 2px solid orange;
}
  1. clip-path裁剪获得
div{
 clip-path: polygon(0 100%, 50% 0, 100% 100%);
}
  1. 渐变linear-gradient 实现
div {
  width: 200px;
  height: 200px;
  background:linear-gradient(to bottom right, #fff 0%, #fff 49.9%, rgba(148,88,255,1) 50%,rgba(185,88,255,1) 100%);
}

7、CSS两列布局的N种实现

两列布局分为两种,一种是左侧定宽、右侧自适应,另一种是两列都自适应(即左侧宽度由子元素决定,右侧补齐剩余空间)。

  1. 左侧定宽、右侧自适应如何实现
// 两个元素都设置dislpay:inline-block
.left {
    display: inline-block;
    width: 100px;
    height: 200px;
    background-color: red;
    vertical-align: top;
}
.right {
    display: inline-block;
    width: calc(100% - 100px);
    height: 400px;
    background-color: blue;
    vertical-align: top;
}
// 两个元素设置浮动,右侧自适应元素宽度使用calc函数计算
.left{
  float: left;
  width: 100px;
  height: 200px;
  background-color: red;
}
.right{
  float: left;
  width: calc(100% - 100px);
  height: 400px;
  background-color: blue;
}
// 父元素设置display:flex,自适应元素设置flex:1
.box{
    height: 600px;
    width: 100%;
    display: flex;
}
.left{
    width: 100px;
    height: 200px;
    background-color: red;
}
.right{
    flex: 1;
    height: 400px;
    background-color: blue;
}
// 父元素相对定位,左侧元素绝对定位,右侧自适应元素设置margin-left的值大于定宽元素的宽度
.left{
    position: absolute;
    width: 100px;
    height: 200px;
    background-color: red;
}
.right{
    margin-left: 100px;
    height: 400px;
    background-color: blue;
}
  1. 左右两侧元素都自适应
// flex布局 同上
// 父元素设置display:grid; grid-template-columns:auto 1fr;(这个属性定义列宽,auto关键字表示由浏览器自己决定长度。fr是一个相对尺寸单位,表示剩余空间做等分)grid-gap:20px(行间距)
.parent{
    display:grid;
    grid-template-columns:auto 1fr;
    grid-gap:20px
} 
.left{
    background-color: red;
    height: 200px;
}
.right{
    height:300px;
    background-color: blue;
}
//浮动+BFC   父元素设置overflow:hidden,左侧定宽元素浮动,右侧自适应元素设置overflow:auto创建BFC
.box{
    height: 600px;
    width: 100%;
    overflow: hidden;
}
.left{
    float: left;
    width: 100px;
    height: 200px;
    background-color: red;
}
.right{
    overflow: auto;
    height: 400px;
    background-color: blue;
}

8、CSS三列布局

  • float布局: 左边左浮动,右边右浮动,中间margin:0 100px;

  • Position布局: 左边left:0; 右边right:0; 中间left: 100px; right: 100px;

  • table布局: 父元素 display: table; 左右 width: 100px; 三个元素display: table-cell;

  • 弹性(flex)布局:父元素 display: flex; 左右 width: 100px;

  • 网格(gird)布局:

// gird提供了 gird-template-columns、grid-template-rows属性让我们设置行和列的高、宽
.div{
    width: 100%;
    display: grid;
    grid-template-rows: 100px;
    grid-template-columns: 300px auto 300px;
}

9、app与H5 如何通讯交互的?

// 兼容IOS和安卓
callMobile(parameters,messageHandlerName) {
  //handlerInterface由iOS addScriptMessageHandler与andorid addJavascriptInterface 代码注入而来。
  if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
    // alert('ios')
    window.webkit.messageHandlers[messageHandlerName].postMessage(JSON.stringify(parameters))
  } else {
    // alert('安卓')
    //安卓传输不了js json对象,只能传输string
    window.webkit[messageHandlerName](JSON.stringify(parameters))
  }
}

由app将原生方法注入到window上供js调用

messageHandlerName 约定的通信方法 parameters 需要传入的参数

10、移动端适配方案

rem是相对于HTML的根元素 em相对于父级元素的字体大小。 VW,VH 屏幕宽度高度的高分比

//按照宽度375图算, 1rem = 100px;
(function (win, doc) {
function changeSize() {
 doc.documentElement.style.fontSize = doc.documentElement.clientWidth / 3.75 + 'px';
console.log(100 * doc.documentElement.clientWidht / 3.75)
}
changeSize();
win.addEventListener('resize', changeSize, false);

})(window, document);

11、请你讲一讲 CSS 的权重和优先级

权重

  • 从0开始,一个行内样式+1000,一个id选择器+100,一个属性选择器、class或者伪类+10,一个元素选择器,或者伪元素+1,通配符+0

优先级

  • 权重相同,写在后面的覆盖前面的
  • 使用 !important 达到最大优先级,都使用 !important 时,权重大的优先级高

你必须懂的css样式权重和优先级

12、介绍 Flex 布局,flex 是什么属性的缩写

  • 弹性盒布局,CSS3 的新属性,用于方便布局,比如垂直居中
  • flex属性是 flex-growflex-shrink 和 flex-basis 的简写

阮一峰Flex

13、CSS实现自适应正方形、等宽高比矩形

  • 双重嵌套,外层 relative,内层 absolute
  • padding 撑高
  • 如果只是要相对于 body 而言的话,还可以使用 vw 和 vh
  • 伪元素设置 margin-top: 100%撑高

双重嵌套,外层 relative,内层 absolute

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .outer {
        padding-top: 50%;
        height: 0;
        background: #ccc;
        width: 50%;
        position: relative;
      }

      .inner {
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        background: blue;
      }
    </style>
  </head>
  <body>
    <div class="outer">
      <div class="inner">hello</div>
    </div>
  </body>
</html>

padding 撑高画正方形

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .outer {
        width: 400px;
        height: 600px;
        background: blue;
      }

      .inner {
        width: 100%;
        height: 0;
        padding-bottom: 100%;
        background: red;
      }
    </style>
  </head>
  <body>
    <div class="outer">
      <div class="inner"></div>
    </div>
  </body>
</html>

相对于视口 VW VH

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .inner {
        width: 1vw;
        height: 1vw;
        background: blue;
      }
    </style>
  </head>
  <body>
    <div class="outer">
      <div class="inner"></div>
    </div>
  </body>
</html>

伪元素设置 margin-top

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .inner {
        width: 100px;
        overflow: hidden;
        background: blue;
      }

      .inner::after {
        content: "";
        margin-top: 100%;
        display: block;
      }
    </style>
  </head>
  <body>
    <div class="outer">
      <div class="inner"></div>
    </div>
  </body>
</html>

14、问:visibility 和 display 的差别(还有opacity)

  • visibility 设置 hidden 会隐藏元素,但是其位置还存在与页面文档流中,不会被删除,所以会触发浏览器渲染引擎的重绘

  • display 设置了 none 属性会隐藏元素,且其位置也不会被保留下来,所以会触发浏览器渲染引擎的回流和重绘。

  • opacity 会将元素设置为透明,但是其位置也在页面文档流中,不会被删除,所以会触发浏览器渲染引擎的重绘

15、了解box-sizing吗?

box-sizing 属性可以被用来调整这些表现:

  • content-box  是默认值。如果你设置一个元素的宽为100px,那么这个元素的内容区会有100px 宽,并且任何边框和内边距的宽度都会被增加到最后绘制出来的元素宽度中。
  • border-box 告诉浏览器:你想要设置的边框和内边距的值是包含在width内的。也就是说,如果你将一个元素的width设为100px,那么这100px会包含它的border和padding,内容区的实际宽度是width减去(border + padding)的值。大多数情况下,这使得我们更容易地设定一个元素的宽高。

16、清除浮动有哪些方法?

不清楚浮动会发生高度塌陷:浮动元素父元素高度自适应(父元素不写高度时,子元素写了浮动后,父元素会发生高度塌陷)

  • clear清除浮动(添加空div法)在浮动元素下方添加空div,并给该元素写css样式:{clear:both;height:0;overflow:hidden;}
  • 给浮动元素父级设置高度
  • 父级同时浮动(需要给父级同级元素添加浮动)
  • 父级设置成inline-block,其margin: 0 auto居中方式失效
  • 给父级添加overflow:hidden 清除浮动方法
  • 万能清除法 after伪类 清浮动(现在主流方法,推荐使用)
.float_div:after{
  content:".";
  clear:both;
  display:block;
  height:0;
  overflow:hidden;
  visibility:hidden;
}
.float_div{
  zoom:1
}

17、常见的兼容性问题?

  • 不同浏览器的标签默认的margin和padding不一样。解决办法是加一个全局的

    *{margin:0;padding:0;} 来统一;

  • IE6双边距bug:块属性标签float后,又有横行的margin情况下,在IE6显示margin比设置的大。hack:display:inline; 将其转化为行内属性。渐进识别的方式,从总体中逐渐排除局部。首先,巧妙的使用“9”这一标记,将IE浏览器从所有情况中分离出来。接着,再次使用“+”将IE8和IE7、IE6分离开来,这样IE8已经独立识别。 渐进识别的方式,从总体中逐渐排除局部。首先,巧妙的使用“9”这一标记,将IE浏览器从所有情况中分离出来。接着,再次使用“+”将IE8和IE7、IE6分离开来,这样IE8已经独立识别。

{
background-color:#f1ee18;/*所有识别*/
.background-color:#00deff\9; /*IE6、7、8识别*/
+background-color:#a200ff;/*IE6、7识别*/
_background-color:#1e0bd1;/*IE6识别*/
}
  • 设置较小高度标签(一般小于10px),在IE6,IE7中高度超出自己设置高度。hack:给超出高度的标签设置overflow:hidden;或者设置行高line-height 小于你设置的高度。

  • IE下,可以使用获取常规属性的方法来获取自定义属性,也可以使用getAttribute()获取自定义属性;Firefox下,只能使用getAttribute()获取自定义属性。解决方法:统一通过getAttribute()获取自定义属性。

  • Chrome 中文界面下默认会将小于 12px 的文本强制按照 12px 显示,可通过加入 CSS 属性 -webkit-text-size-adjust: none; 解决。

  • 超链接访问过后hover样式就不出现了,因为被点击访问过的超链接样式不再具有hover和active了解决方法是改变CSS属性的排列顺序:L-V-H-A :

a:link {} 
a:visited {} 
a:hover {} 
a:active {}
  • IE下,even对象有x,y属性,但是没有pageX,pageY属性;

    Firefox下,event对象有pageX,pageY属性,但是没有x,y属性。

    解决方法:(条件注释)缺点是在IE浏览器下可能会增加额外的HTTP请求数。

  • png24位的图片在iE6浏览器上出现背景,解决方案是做成PNG8.

18、写出几种IE6 BUG的解决方法

  1. 双边距BUG float引起的 使用display

  2. 3像素问题 使用float引起的 使用dislpay:inline -3px

  3. 超链接hover 点击后失效 使用正确的书写顺序 link visited hover active

  4. Ie z-index问题 给父级添加position:relative

  5. Png 透明 使用js代码 改

  6. Min-height 最小高度 !Important 解决’

  7. select 在ie6下遮盖 使用iframe嵌套

  8. 为什么没有办法定义1px左右的宽度容器(IE6默认的行高造成的,使用over:hidden,zoom:0.08 line-height:1px)

  9. ie 6 不支持!important

参考地址:

CSS经典面试题

精心整理HTML/CSS面试题(2022求职必看)

三、JS相关

1、JS原型及原型链

1)、什么是原型?

Father.prototype就是原型,它是一个对象,我们也称它为原型对象。

2)、原型的作用是什么?

原型的作用,就是共享方法。
我们通过 Father.prototype.method 可以共享方法,不会反应开辟空间存储方法。

3)、什么是原型链?

实例对象在查找属性时,如果查找不到,就会沿着__proto__去与对象关联的原型上查找,如果还查找不到,就去找原型的原型,直至查到最顶层,这也就是原型链的概念

经典原型链图:

image.png

image.png

2、JS继承的几种方式

WX20221121-200127.png

1)原型链继承

原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。

原型链继承的主要思想是:重写子类的prototype属性,将其指向父类的实例。 下面我们结合代码来了解一下。

function Animal (name) {
  // 属性
  this.name = name
  this.type = 'Animal'
  // 实例函数
  this.sleep = function () {
    console.log(this.name + '正在睡觉');
  }
}
// 原型函数
Animal.prototype.eat = function (food) {
  console.log(`${this.name}正在吃${food}`);
}

// 子类
function Cat (name) {
  this.name = name
}
// 原型继承
Cat.prototype = new Animal()
// 将Cat的构造函数指向自身
Cat.prototype.constructor = Cat

let cat = new Cat('Tom')
console.log(cat.name) // Tom
console.log(cat.type) // Animal
cat.sleep() // Tom正在睡觉
cat.eat('猫罐头') // Tom正在吃猫罐头

在子类Cat中,我们没有增加type属性,因此会直接继承父类Animaltype属性。

在子类Cat中,我们增加了name属性,在生成子类实例时,name属性会覆盖父类Animal属性值。

同样因为Catprototype属性指向了Animal类型的实例,因此在生成实例Cat时,会继承实例函数和原型函数。

需要注意: Cat.prototype.constructor = Cat 如果不将Cat原型对象的constructor属性指向自身的构造函数,那将指向父类Animal的构造函数。

原型链继承的优点

  • 简单,易于实现,只需要设置子类的prototype属性指向父类的实例即可。
  • 可通过子类直接访问父类原型链属性和函数

原型链继承的缺点

1、子类的所有实例将共享父类的属性,子类的所有实例将共享父类的属性会带来一个很严重的问题,父类包含引用值时,子类的实例改变该引用值会在所有实例中共享

function Animal () {
  this.skill = ['eat', 'jump', 'sleep']
}
function Cat () {}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat

let cat1 = new Cat()
let cat2 = new Cat()
cat1.skill.push('walk')
console.log(cat1.skill) // ["eat", "jump", "sleep", "walk"]
console.log(cat2.skill) // ["eat", "jump", "sleep", "walk"]

2、在子类实例化时,无法向父类的构造函数传参 在通过new操作符创建子类的实例时,会调用子类的构造函数,而在子类的构造函数中并没有设置与父类关联,从而导致无法向父类的构造函数传递参数。

3、无法实现多继承 子类的prototype只能设置一个值,设置多个值时,后面的值会覆盖前面的值。

2)构造函数继承

构造函数继承的主要思想:在子类的构造函数中通过call()函数改变thi的指向,调用父类的构造函数,从而将父类的实例的属性和函数绑定到子类的this上。

// 父类
function Animal (age) {
  // 属性
  this.name = 'Animal'
  this.age = age
  // 实例函数
  this.sleep = function () {
    console.log(this.name + '正在睡觉');
  }
}
// 原型函数
Animal.prototype.eat = function (food) {
  console.log(`${this.name}正在吃${food}`);
}
function Cat (name) {
  // 核心,通过call()函数实现Animal的实例的属性和函数的继承
  Animal.call(this)
  this.name = name
}

let cat = new Cat('Tom')
cat.sleep() // Tom正在睡觉
cat.eat() // Uncaught TypeError: cat.eat is not a function

通过代码可以发现,子类可以正常调用父类的实例函数,而无法调用父类原型上的函数,这是因为子类并没有通过某种方式来调用父类原型对象上的函数

构造继承的优点

1、解决了子类实例共享父类属性的问题

call()函数实际时改变父类Animal构造函数中this的指向,然后调用this指向了子类Cat,相当于将父类的属性和函数直接绑定到了子类的this中,成了子类实例的熟属性和函数,因此生成的子类实例中是各自拥有自己的属性和函数,不会相互影响。

2、创建子类的实例时,可以向父类传参

// 父类
function Animal (age) {
  this.name = 'Animal'
  this.age = age
}
function Cat (name, parentAge) {
  // 在子类生成实例时,传递参数给call()函数,间接地传递给父类,然后被子类继承
  Animal.call(this, parentAge)
  this.name = name
}

let cat = new Cat('Tom', 10)
console.log(cat.age)

3、可以实现多继承

在子类的构造函数中,可以多次调用call()函数来继承多个父对象。

构造函数的缺点

1、实例只是子类的实例,并不是父类的实例

因为我们并未通过原型对象将子类与父类进行串联,所以生成的实例与父类并没有关系。

2、只能继承父类实例的属性和函数,并不能继承原型对象上的属性和函数 与上面原因相同。

3、无法复用父类的构造函数

因为父类的实例函数将通过call()函数绑定到子类的this中,因此子类生成的每个实例都会拥有父类实例的引用,这会造成不必要的内存消耗,影响性能。

3)组合继承

组合继承的主要思想:结合构造继承和原型继承的两种方式,一方面在子类的构造函数中通过call()函数调用父类的构造函数,将父类的实例的属性和函数绑定到子类的this中;另一方面,通过改变子类的prototype属性,继承父类的原型对象上的属性和函数。

// 父类
function Animal (age) {
  // 实例属性
  this.name = 'Animal'
  this.age = age
  this.skill = ['eat', 'jump', 'sleep']
  // 实例函数
  this.sleep = function () {
    console.log(this.name + '正在睡觉')
  }
}
// 原型函数
Animal.prototype.eat = function (food) {
  console.log(`${this.name}正在吃${food}`)
}

// 子类
function Cat (name) {
  // 通过构造函数继承实例的属性和函数
  Animal.call(this)
  this.name = name
}
// 通过原型继承原型对象上的属性和函数
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat

let cat = new Cat('Tom')
console.log(cat.name) // Tom
cat.sleep() // Tom正在睡觉
cat.eat('猫罐头') // Tom正在吃猫罐头

组合继承的优点

1、既能继承父类实例的属性和函数,又能继承原型对象上的属性和函数

2、既是子类的实例,又是父类的实例

3、不存在引用属性共享的问题

构造函数作用域优先级比原型链优先级高,所以不会出现引用属性共享的问题。 可以向父类的构造函数中传参

组合继承的缺点

父类的实例属性会被绑定两次

在子类的构造函数中,通过call()函数调用了一次父类的构造函数;在改写子类的prototype属性,生成的实例时又调用了一次父类的构造函数。

4)寄生组合继承

组合继承方案已经足够好,但是针对其存在的缺点,我们仍然可以进行优化。 在进行子类的prototype属性的设置时,可以去掉父类实例的属性的函数

//父类
function Animal (age) {
  // 实例属性
  this.name = 'Animal'
  this.age = age
  this.skill = ['eat', 'jump', 'sleep']
  // 实例函数
  this.sleep = function () {
    console.log(this.name + '正在睡觉')
  }
}
// 原型函数
Animal.prototype.eat = function (food) {
  console.log(`${this.name}正在吃${food}`)
}
// 子类
function Cat (name) {
  // 继承父类的实例和属性
  Animal.call(this)
  this.name = name
}
// 继承父类原型上的实例和属性
Cat.prototype = Object.create(Animal.prototype)
Cat.prototype.constructor = Cat
let cat = new Cat('Tom')

console.log(cat.name) // Tom
cat.sleep() // Tom正在睡觉
cat.eat('猫罐头') // Tom正在吃猫罐头

其中最关键的语句: Cat.prototype = Object.create(Animal.prototype) 只取父类Animal的prototype属性,过滤掉Animal的实例属性,从而避免了父类的实例属性绑定两次。

这种寄生组合式继承方式,基本可以解决前几种继承方式的缺点,较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能的开销。

整体看下来,这六种继承方式中,寄生组合式继承是这里面最优的继承方式。

5)class继承

class Parent {
  constructor (name) {
    this.name = name
  }
  getName () {
    console.log(this.name)
  }
}
class Child extends Parent {
  constructor (name) {
    super(name)
    this.sex = 'boy'
  }
}

参考网址:

juejin.cn/post/713582… juejin.cn/post/699517…

3、Event Loop 事件循环

Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理

1)JavaScript为什么是单线程(设计成单线程的目的是什么)?

其实这和JavaScript语言最初的设计用途有关,我们都知道JavaScript是浏览器脚本语言,主要用途是与用户互动、操作DOM。这决定了它是单线程,否则会带来很复杂的同步问题。比如,假如JavaScript同时有两个线程,一个线程在DOM上添加内容,另一个在DOM上删除内容,那这时浏览器应该以哪个线程为准呢?

首先我们来了解一下三个重要的概念

主线程

所有的同步任务都是在主线程里执行的,异步任务可能会在macrotask或者microtask里面
同步任务:  指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
异步任务:  指的是不进入主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

微任务(micro task)

promise
async
await
process.nextTick(node)
mutationObserver(html5新特性)

宏任务(macro task)

script(整体代码)
setTimeout
setInterval
setImmediate
I/O
UI render

堆,栈、队列

堆(Heap)

是一种数据结构,是利用完全二叉树维护的一组数据,分为两种,一种为最大,一种为最小堆,将根节点最大叫做最大堆大根堆,根节点最小叫做最小堆小根堆

线性数据结构,相当于一维数组,有唯一后继。

栈(Stack)

在计算机科学中是限定仅在表尾进行插入删除操作的线性表。是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底最后的数据栈顶,需要读数据的时候从栈顶开始弹出数据

是只能在某一端插入删除特殊线性表

队列(Queue)

特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和一样,队列是一种操作受限制的线性表。

进行插入操作的端称为队尾,进行删除操作的端称为队头。 队列中没有元素时,称为空队列

队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出FIFO—first in first out

浏览器中的Event Loop

Javascript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。

JS调用栈

JS调用栈采用的是后进先出的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。

同步任务和异步任务

Javascript单线程任务被分为同步任务异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。

image.png 任务队列Task Queue,即队列,是一种先进先出的一种数据结构。

image.png

事件循环的进程模型

  • 选择当前要执行的任务队列,选择任务队列中最先进入的任务,如果任务队列为空即null,则执行跳转到微任务(MicroTask)的执行步骤。

  • 将事件循环中的任务设置为已选择任务。

  • 执行任务。

  • 将事件循环中当前运行任务设置为null。

  • 将已经运行完成的任务从任务队列中删除。

  • microtasks步骤:进入microtask检查点。

  • 更新界面渲染。

  • 返回第一步。

执行进入microtask检查点时,用户代理会执行以下步骤:

  • 设置microtask检查点标志为true。

  • 当事件循环microtask执行不为空时:选择一个最先进入的microtask队列的microtask,将事件循环的microtask设置为已选择的microtask,运行microtask,将已经执行完成的microtasknull,移出microtask中的microtask

  • 清理IndexDB事务

  • 设置进入microtask检查点的标志为false。

执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去执行Task(宏任务),每次宏任务执行完毕后,检查微任务(microTask)队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务(microTask)后,设置微任务(microTask)队列为null,然后再执行宏任务,如此循环。

举个例子

console.log('script start');

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

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});
console.log('script end');

首先我们划分几个分类:

1)第一次执行

Tasks:run script、 setTimeout callback

MicrotasksPromise then	

JS stack: script	
Log: script start、script end。

执行同步代码,将宏任务(Tasks)和微任务(Microtasks)划分到各自队列中

2)第二次执行

Tasks:run script、 setTimeout callback

MicrotasksPromise2 then	

JS stack: Promise2 callback	
Log: script start、script end、promise1、promise2

执行宏任务后,检测到微任务(Microtasks)队列中不为空,执行Promise1,执行完成Promise1后,调用Promise2.then,放入微任务(Microtasks)队列中,再执行Promise2.then

3)第三次执行

TaskssetTimeout callback

MicrotasksJS stack: setTimeout callback
Log: script start、script end、promise1、promise2、setTimeout

当微任务(Microtasks)队列中为空时,执行宏任务(Tasks),执行setTimeout callback,打印日志。

4)第四次执行

TaskssetTimeout callback

MicrotasksJS stack: 
Log: script start、script end、promise1、promise2、setTimeout

清空Tasks队列和JS stack

例子2

console.log('script start')

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

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

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

console.log('script end')

这里需要先理解async/await
async/await 在底层转换成了 promise 和 then 回调函数。
也就是说,这是 promise 的语法糖。
每次我们使用 await, 解释器都创建一个 promise 对象,然后把剩下的 async 函数中的操作放到 then 回调函数中。
async/await 的实现,离不开 Promise。从字面意思来理解,async 是“异步”的简写,而 await 是 async wait 的简写可以认为是等待异步方法执行完成。

总结

  • 微任务队列优先于宏任务队列执行;

  • 微任务队列上创建的宏任务会被后添加到当前宏任务队列的尾端;

  • 微任务队列中创建的微任务会被添加到微任务队列的尾端;

  • 只要微任务队列中还有任务,宏任务队列就只会等待微任务队列执行完毕后再执行;

  • 只有运行完await语句,才把await语句后面的全部代码加入到微任务行列;

  • 在遇到await promise时,必须等await promise 函数执行完毕才能对await语句后面的全部代码加入到微任务中;

    在等待 await Promise.then微任务时:

    • 运行其他同步代码;
    • 等到同步代码运行完,开始运行 await promise.then 微任务;
    • await promise.then微任务完成后,把await语句后面的全部代码加入到微任务行列;

同步(Promise)>异步(微任务(process.nextTick ,Promises.then, Promise.catch ,resove,reject,MutationObserver)>宏任务(setTimeout,setInterval,setImmediate))

相关参考:

juejin.cn/post/714500… juejin.cn/post/705638… juejin.cn/post/687483…

4、Promise 与 Async/Await 区别

async/await是基于Promise实现的,看起来更像同步代码,

  • 不需要写匿名函数处理Promise的resolve值
  • 错误处理:Async/Await 让 try/catch 可以同时处理同步和异步错误。
  • 条件语句也跟错误处理一样简洁一点
  • 中间值处理(第一个方法返回值,用作第二个方法参数) 解决嵌套问题
  • 调试方便
   const makeRequest = () => {
        try {
            getJSON().then(result => {
                // JSON.parse可能会出错
                const data = JSON.parse(result)
                console.log(data)
            })
            // 取消注释,处理异步代码的错误
            // .catch((err) => {
            //   console.log(err)
            // })
        } catch (err) {
            console.log(err)
        }
    }

使用aync/await的话,catch能处理JSON.parse错误:

    const makeRequest = async () => {
        try {
            // this parse may fail
            const data = JSON.parse(await getJSON())
            console.log(data)
        } catch (err) {
            console.log(err)
        }
    }

promise怎么实现链式调用跟返回不同的状态

实现链式调用:使用.then()或者.catch()方法之后会返回一个promise对象,可以继续用.then()方法调用,再次调用所获取的参数是上个then方法return的内容

  1. promise的三种状态是 fulfilled(已成功)/pengding(进行中)/rejected(已拒绝)
  2. 状态只能由 Pending --> Fulfilled 或者 Pending --> Rejected,且一但发生改变便不可二次修改;
  3. Promise 中使用 resolvereject 两个函数来更改状态;
  4. then 方法内部做的事情就是状态判断:
  • 如果状态是成功,调用成功回调函数
  • 如果状态是失败,调用失败回调函数

5、函数柯里化

柯里化(Currying) 是把接收多个参数的原函数变换成接受一个单一参数(原来函数的第一个参数的函数)并返回一个新的函数,新的函数能够接受余下的参数,并返回和原函数相同的结果。

  1. 参数对复用
  2. 提高实用性
  3. 延迟执行 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。 柯里化的函数可以延迟接收参数,就是比如一个函数需要接收的参数是两个,执行的时候必须接收两个参数,否则没法执行。但是柯里化后的函数,可以先接收一个参数
// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3

6、JS对象深克隆

递归遍历对象,解决循环引用问题

解决循环引用问题,我们需要一个存储容器存放当前对象和拷贝对象的对应关系(适合用key-value的数据结构进行存储,也就是map),当进行拷贝当前对象的时候,我们先查找存储容器是否已经拷贝过当前对象,如果已经拷贝过,那么直接把返回,没有的话则是继续拷贝。

function deepClone(target) {
    const map = new Map()
    function clone (target) {
        if (isObject(target)) {
            let cloneTarget = isArray(target) ? [] : {};
            if (map.get(target)) {
                return map.get(target)
            }
            map.set(target,cloneTarget)
            for (const key in target) {
                cloneTarget[key] = clone(target[key]);
            }
            return cloneTarget;
        } else {
            return target;
        }
    }
    return clone(target)
};

7、JS模块化

nodeJS里面的模块是基于commonJS规范实现的,原理是文件的读写,导出文件要使用exportsmodule.exports,引入文件用require。 每个文件就是一个模块;每个文件里面的代码会用默认写在一个闭包函数里面 AMD规范则是非同步加载模块,允许指定回调函数,AMDRequireJS 在推广过程中对模块定义的规范化产出。

AMD推崇依赖前置, CMD推崇依赖就近。对于依赖的模块AMD是提前执行,CMD是延迟执行。

ES6中,我们可以使用 import 关键字引入模块,通过 exprot 关键字导出模块,但是由于ES6目前无法在浏览器中执行,所以,我们只能通过babel将不被支持的import编译为当前受到广泛支持的 require

CommonJs 和 ES6 模块化的区别:

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

前端模块化:juejin.cn/post/684490…

import 和 require 导入的区别

import 的ES6 标准模块; require 是 AMD规范引入方式;

import是编译时调用,所以必须放在文件开头;是解构过程 require是运行时调用,所以require理论上可以运用在代码的任何地方;是赋值过程。其实require的结果就是对象、数字、字符串、函数等,再把require的结果赋值给某个变量

8、异步加载JS方式

  1. 匿名函数自调动态创建script标签加载js
(function(){
    var scriptEle = document.createElement("script");
    scriptEle.type = "text/javasctipt";
    scriptEle.async = true;
    scriptEle.src = "http://cdn.bootcss.com/jquery/3.0.0-beta1/jquery.min.js";
    var x = document.getElementsByTagName("head")[0];
    x.insertBefore(scriptEle, x.firstChild);		
 })();
  1. async属性
// async属性规定一旦加载脚本可用,则会异步执行
<script type="text/javascript" src="xxx.js" async="async"></script>
  1. defer属性
// defer属性规定是否对脚本执行进行延迟,直到页面加载为止
<script type="text/javascript" src="xxx.js" defer="defer"></script>

9、Set、Map、WeakSet、WeakMap

Set对象可以存储任何类型的数据。 值是唯一的,没有重复的值。

Map对象保存键值对,任意值都可以成为它的键或值。

WeakSet 结构与 Set 类似,也是不重复的值的集合 . WeakMap 对象是一组键值对的集合

不同: WeakSet 的成员只能是对象,而不能是其他类型的值。 WeakSet 不可遍历

WeakMap只接受对象作为键名null除外),不接受其他类型的值作为键名。

WeakMap的键名所指向的对象,不计入垃圾回收机制。

10、call、apply

call( this,a,b,c ) 在第一个参数之后的,后续所有参数就是传入该函数的值。

apply( this,[a,b,c] ) 只有两个参数,第一个是对象,第二个是数组,这个数组就是该函数的参数。

共同之处:都可以用来代替另一个对象调用一个方法,将一个函数的对象上下文从初始的上下文改变为由thisObj指定的新对象。

所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次 所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。

addEventListener的第三个参数干嘛的,为true时捕获,false时冒泡

Object.prototype.toString.call() 判断对象类型

// new Set是实现数组去重,
// Array.from()把去重之后转换成数组
let arr2 = Array.from(new Set(arr));

11、词法作用域与作用域链

作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

ES5只有全局作用域没和函数作用域,ES6增加块级作用域

暂时性死区:在代码块内,使用 letconst 命令声明变量之前,该变量都是不可用的,语法上被称为暂时性死区。

JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。

函数的作用域在函数定义的时候就决定了。

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链

12、new关键字做

new关键字做了4件事:

function _new(constructor, ...arg) {
  // 创建一个空对象
  var obj = {};
  // 空对象的`__proto__`指向构造函数的`prototype`, 为这个新对象添加属性 
  obj.__proto__ = constructor.prototype; 
  // 构造函数的作用域赋给新对象
  var res = constructor.apply(obj, arg); 
  // 返回新对象.如果没有显式return语句,则返回this
  return Object.prototype.toString.call(res) === '[object Object]' ? res : obj; 
}

13、不应该使用箭头函数一些情况

  • 当想要函数被提升时(箭头函数是匿名的)
  • 要在函数中使用this/arguments时,由于箭头函数本身不具有this/arguments,因此它们取决于外部上下文
  • 使用命名函数(箭头函数是匿名的)
  • 使用函数作为构造函数时(箭头函数没有构造函数)
  • 当想在对象字面是以将函数作为属性添加并在其中使用对象时,因为咱们无法访问 this 即对象本身。

14、判断数组的四种方法

  • Array.isArray() 判断
  • instanceof 判断: 检验构造函数的prototype属性是否出现在对象的原型链中,返回一个布尔值。 let a = []; a instanceof Array; //true
  • constructor判断: 实例的构造函数属性constructor指向构造函数 let a = [1,3,4]; a.constructor === Array;//true
  • Object.prototype.toString.call() 判断 let a = [1,2,3]; Object.prototype.toString.call(a) === '[object Array]';//true

15、TS有什么优势

  • 静态输入:静态类型化是一种功能,可以在开发人员编写脚本时检测错误。

  • 大型的开发项目:使用TypeScript工具来进行重构更变的容易、快捷。

  • 更好的协作:类型安全是在编码期间检测错误,而不是在编译项目时检测错误。

  • 更强的生产力:干净的 ECMAScript 6 代码,自动完成和动态输入等因素有助于提高开发人员的工作效率。

16、interface 和 type的区别

  • interface 只能定义对象类型。type声明可以声明任何类型。
  • interface 能够声明合并,两个相同接口会合并。Type声明合并会报错
  • type可以类型推导
  • interface 可继承 和 type 不可继承

17、实现发布订阅

/* Pubsub */
  function Pubsub(){
    //存放事件和对应的处理方法
    this.handles = {};
  }

  Pubsub.prototype = {
    //传入事件类型type和事件处理handle
    on: function (type, handle) {
      if(!this.handles[type]){
        this.handles[type] = [];
      }
      this.handles[type].push(handle);
    },
    emit: function () {
      //通过传入参数获取事件类型
      //将arguments转为真数组
      var type = Array.prototype.shift.call(arguments);
      if(!this.handles[type]){
        return false;
      }
      for (var i = 0; i < this.handles[type].length; i++) {
        var handle = this.handles[type][i];
        //执行事件
        handle.apply(this, arguments);
      }
    },
    off: function (type, handle) {
      handles = this.handles[type];
      if(handles){
        if(!handle){
          handles.length = 0;//清空数组
        }else{
        for (var i = 0; i < handles.length; i++) {
          var _handle = handles[i];
          if(_handle === handle){
            //从数组中删除
            handles.splice(i,1);
          }
        }
      }
    }  
  }

18、promise怎么实现链式调用跟返回不同的状态

// MyPromise.js

// 先定义三个常量表示状态
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

// 新建 MyPromise 类
class MyPromise {
  constructor(executor){
    // executor 是一个执行器,进入会立即执行
    // 并传入resolve和reject方法
    executor(this.resolve, this.reject)
  }

  // 储存状态的变量,初始值是 pending
  status = PENDING;

  // resolve和reject为什么要用箭头函数?
  // 如果直接调用的话,普通函数this指向的是window或者undefined
  // 用箭头函数就可以让this指向当前实例对象
  // 成功之后的值
  value = null;
  // 失败之后的原因
  reason = null;

  // 更改成功后的状态
  resolve = (value) => {
    // 只有状态是等待,才执行状态修改
    if (this.status === PENDING) {
      // 状态修改为成功
      this.status = FULFILLED;
      // 保存成功之后的值
      this.value = value;
    }
  }

  // 更改失败后的状态
  reject = (reason) => {
    // 只有状态是等待,才执行状态修改
    if (this.status === PENDING) {
      // 状态成功为失败
      this.status = REJECTED;
      // 保存失败后的原因
      this.reason = reason;
    }
  }

    then(onFulfilled, onRejected) {
    // 判断状态
    if (this.status === FULFILLED) {
      // 调用成功回调,并且把值返回
      onFulfilled(this.value);
    } else if (this.status === REJECTED) {
      // 调用失败回调,并且把原因返回
      onRejected(this.reason);
    }
  }

}

19、实现Promise.all

// Promise.all

Promise.all = function (promises) {
  let result = [];
  let count = 0;
  return new Promise((resolve, reject) => {
    promises.forEach((p, index) => {
      // 兼容不是promise的情况 
      Promise.resolve(p).then((res) => {
        result[index] = res;
        count++;
        if(count === promises.length) {
          resolve(result)
        }
      }).catch((err) => {
        reject(err);
      })
    })
  });
}

20、对象数组转换成tree数组


> 将entries 按照 level 转换成 result 数据结构

const entries = [
    {
        "province": "浙江", "city": "杭州", "name": "西湖"
    }, {
        "province": "四川", "city": "成都", "name": "锦里"
    }, {
        "province": "四川", "city": "成都", "name": "方所"
    }, {
        "province": "四川", "city": "阿坝", "name": "九寨沟"
    }
];
	
const level = ["province", "city", "name"];

const  result = [
	{
		value:'浙江'children:[
			{
				value:'杭州',
				children:[
					{
						value:'西湖'
					}
				]
			}
		]
	},
	{
		value:'四川'children:[
			{
				value:'成都',
				children:[
					{
						value:'锦里'
					},
					{
						value:'方所'
					}
				]
			},
			{
				value:'阿坝',
				children:[
					{
						value:'九寨沟'
					}
				]
			}
		]
	},
]

思路: 涉及到树形数组,采用递归遍历的方式

function transfrom(list, level) {
  const res = [];
  list.forEach(item => {
    pushItem(res, item, 0);
  });

  function pushItem(arr, obj, i) {
    const o = {
      value: obj[level[i]],
      children: [],
    };
    // 判断传入数组里是否有value等于要传入的项
    const hasItem = arr.find(el => el.value === obj[level[i]]);
    let nowArr;
    if(hasItem) {
      // 存在,则下一次遍历传入存在项的children
      nowArr = hasItem.children;
    }else{
      // 不存在 压入arr,下一次遍历传入此项的children
      arr.push(o);
      nowArr = o.children;
    }
    if(i === level.length - 1) delete o.children;
    i++;
    if(i < level.length) {
      // 递归进行层级的遍历
      pushItem(nowArr, obj, i);
    }
  }
}

transfrom(entries, level);

21、JS instanceof方法原生实现

简单用法

function Fn () {}
const fn = new Fn()
fn instanceof Fn  // true

实现如下:

// left instanceof right
function _instanceof(left, right) {
  // 构造函数原型
  const prototype = right.prototype
  // 实列对象属性,指向其构造函数原型
  left = left.__proto__
  // 查实原型链
  while (true) {
    // 如果为null,说明原型链已经查找到最顶层了,真接返回false
    if (left === null) {
      return false
    }
    // 查找到原型
    if (prototype === left){
      return true
    }
    // 继续向上查找
    left = left.__proto__
  }
}

const str = "abc"
_instanceof(str, String) // true

22、JavaScript基础心法——this

什么是this

在传统面向对象的语言中,比如Java,this关键字用来表示当前对象本身,或当前对象的一个实例,通过this关键字可以获得当前对象的属性和调用方法。

在JavaScript中,this似乎表现地略有不同,这也是让人“讨厌”的地方~

ECMAScript规范中这样写:

this 关键字执行为当前执行环境的 ThisBinding。

MDN上这样写:

In most cases, the value of this is determined by how a function is called.
在绝大多数情况下,函数的调用方式决定了this的值。

可以这样理解,在JavaScript中,this的指向是调用时决定的,而不是创建时决定的,这就会导致this的指向会让人迷惑,简单来说,this具有运行期绑定的特性。

参考资料:this - JavaScript | MDN

调用位置

首先需要理解调用位置,调用位置就是函数在代码中被调用的位置,而不是声明的位置。

通过分析调用栈(到达当前执行位置所调用的所有函数)可以找到调用位置。

function baz(){
  console.log("baz");
  bar();
}
function bar(){
  console.log("bar");
  foo();
}
function foo(){
  console.log("foo");
}
baz();

当我们调用baz()时,它会以此调用baz()bar()foo()

对于foo():调用位置是在bar()中。
对于bar():调用位置是在baz()中。
而对于baz():调用位置是全局作用域中。

可以看出,调用位置应该是当前正在执行的函数的前一个调用中。

全局上下文

在全局执行上下文中this都指代全局对象。

  • this等价于window对象
  • var === this. === winodw.
console.log(window === this); // true
var a = 1;
this.b = 2;
window.c = 3;
console.log(a + b + c); // 6

在浏览器里面this等价于window对象,如果你声明一些全局变量,这些变量都会作为this的属性。

函数上下文

在函数内部,this的值取决于函数被调用的方式。

直接调用

this指向全局变量。

function foo(){
  return this;
}
console.log(foo() === window); // true

call()、apply()

this指向绑定的对象上。

var person = {
  name: "axuebin",
  age: 25
};
function say(job){
  console.log(this.name+":"+this.age+" "+job);
}
say.call(person,"FE"); // axuebin:25
say.apply(person,["FE"]); // axuebin:25

可以看到,定义了一个say函数是用来输出nameagejob,其中本身没有nameage属性,我们将这个函数绑定到person这个对象上,输出了本属于person的属性,说明此时this是指向对象person的。

如果传入一个原始值(字符串、布尔或数字类型)来当做this的绑定对象, 这个原始值会被转换成它的对象形式(new String()),这通常被称为“装箱”。

callapplythis的绑定角度上来说是一样的,唯一不同的是它们的第二个参数。

bind() this将永久地被绑定到了bind的第一个参数。

bindcallapply有些相似。

var person = {
  name: "axuebin",
  age: 25
};
function say(){
  console.log(this.name+":"+this.age);
}
var f = say.bind(person);
console.log(f());

箭头函数

所有的箭头函数都没有自己的this,都指向外层。

关于箭头函数的争论一直都在,可以看看下面的几个链接:

ES6 箭头函数中的 this?你可能想多了(翻译) 关于箭头函数this的理解几乎完全是错误的 #150

MDN中对于箭头函数这一部分是这样描述的:

An arrow function does not create its own this, the this value of the enclosing execution context is used.
箭头函数会捕获其所在上下文的this值,作为自己的this值。

function Person(name){
  this.name = name;
  this.say = () => {
    var name = "xb";
    return this.name;
  }
}
var person = new Person("axuebin");
console.log(person.say()); // axuebin

箭头函数常用语回调函数中,例如定时器中:

function foo() {  
  setTimeout(()=>{
    console.log(this.a);
  },100)
}
var obj = {
  a: 2
}
foo.call(obj);

附上MDN关于箭头函数this的解释:

developer.mozilla.org/zh-CN/docs/…

作为对象的一个方法 this指向调用函数的对象。

var person = {
  name: "axuebin",
  getName: function(){
    return this.name;
  }
}
console.log(person.getName()); // axuebin

这里有一个需要注意的地方。。。

var name = "xb";
var person = {
  name: "axuebin",
  getName: function(){
    return this.name;
  }
}
var getName = person.getName;
console.log(getName()); // xb

发现this又指向全局变量了,这是为什么呢?

还是那句话,this的指向得看函数调用时。 作为一个构造函数

this被绑定到正在构造的新对象。

通过构造函数创建一个对象其实执行这样几个步骤:

  1. 创建新对象
  2. 将this指向这个对象
  3. 给对象赋值(属性、方法)
  4. 返回this

所以this就是指向创建的这个对象上。

function Person(name){
  this.name = name;
  this.age = 25;
  this.say = function(){
    console.log(this.name + ":" + this.age);
  }
}
var person = new Person("axuebin");
console.log(person.name); // axuebin
person.say(); // axuebin:25

作为一个DOM事件处理函数

this指向触发事件的元素,也就是始事件处理程序所绑定到的DOM节点。

var ele = document.getElementById("id");
ele.addEventListener("click",function(e){
  console.log(this);
  console.log(this === e.target); // true
})

HTML标签内联事件处理函数

this指向所在的DOM元素

<button onclick="console.log(this);">Click Me</button>

jQuery的this

在许多情况下JQuery的this都指向DOM元素节点。

$(".btn").on("click",function(){
  console.log(this); 
});

总结 如果要判断一个函数的this绑定,就需要找到这个函数的直接调用位置。然后可以顺序按照下面四条规则来判断this的绑定对象:

  1. new调用:绑定到新创建的对象
  2. callapplybind调用:绑定到指定的对象
  3. 由上下文对象调用:绑定到上下文对象
  4. 默认:全局对象

注意:箭头函数不使用上面的绑定规则,根据外层作用域来决定this,继承外层函数调用的this绑定。

23、cookie,session,localStorage,sessionStorage的区别?

cookie 和 session 区别

  • cookie数据存放在浏览器中,session数据存放在服务器
  • cookie是不安全的,别人可以分析存放在本地的cookie并进行cookie诈骗,考虑到安全性能,应尽量使用session
  • session会在一定时间内保存在服务器上。当访问增多时,会比较占用服务器的性能。考虑到服务性能,应尽量使用cookie
  • 单个cookie保存的数据不能超过4k,很多浏览器都限制一个站点最多保存20个cookie

cookie和session都用来存储用户信息,cookie存放于客户端有可能被窃取,所以cookie一般用来存放不敏感的信息,比如用户设置的网站主题,敏感的信息用session存储,比如用户的登录信息

cookie,sessionStorage,localStorage 区别

  • HTML5中提出了webStorage的概念,webStorage包括sessionStorage和localStorage,只为了保存数据,不会与服务器进行通信

  • cookie,localStorage,sessionStorage都是在客户端(本地)保存数据,存储数据的类型:字符串

  • cookie会随着HTTP请求发送到服务器,webStorage不会随着HTTP发送到服务器端,所以安全性相对来说比cookie高,不必担心截获

  • webStorage拥有更大的存储量,cookie大小限制4kb,webStorage达到5M或更大

  • webStorage拥有setItem,getItem,removeItem,clear等api,cookie需要自己封装

  • 生命周期不同(见后文),localStorage要手动清除,sessionStorage在浏览器关闭后清除

生命周期

  • cookie :可设置失效时间,否则默认为关闭浏览器后消失
  • localStorage :除非被手动清除,否则永久保存
  • sessionStorage:仅在当前网页会话下有效,关闭页面或浏览器后就会被清除

相关参考:

juejin.cn/post/700703… juejin.cn/post/714430… juejin.cn/post/684490… juejin.cn/post/684490… juejin.cn/post/685621…

四、框架 Vue | React

1、Vue3.0 新特性

双向数据绑定 Proxy

代理,可以理解为在对象之前设置一个“拦截”,当该对象被访问的时候,都必须经过这层拦截。意味着你可以在这层拦截中进行各种操作。比如你可以在这层拦截中对原对象进行处理,返回你想返回的数据结构。

ES6 原生提供 Proxy 构造函数,MDN上的解释为:Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

const p = new Proxy(target, handler);
//target: 所要拦截的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
//handler:一个对象,定义要拦截的行为

const p = new Proxy({}, {
    get(target, propKey) {
        return '哈哈,你被我拦截了';
    }
});

console.log(p.name);

新增的属性,并不需要重新添加响应式处理,因为 Proxy 是对对象的操作,只要你访问对象,就会走到 Proxy 的逻辑中。

Vue3 Composition API

Vue3.x 推出了Composition API。 setup 是组件内使用 Composition API的入口。setup 执行时机是在 beforeCreate 之前执行.

reactive、ref 与 toRefs、isRef

Vue3.x 可以使用reactive和ref来进行数据定义。

// props 传入组件对属性
// context 一个上下文对象,包含了一些有用的属性:attrs,parent,refs
setup(props, context) {
// ref 定义数据
const year = ref(0);
// reactive 处理对象的双向绑定
const user = reactive({ nickname: "xiaofan", age: 26, gender: "女" });
setInterval(() => {
  year.value++;
  user.age++;
}, 1000);
return {
  year,
  // 使用toRefs,结构解构
  ...toRefs(user),
};
},
// 提供isRef,用于检查一个对象是否是ref对象

watchEffect 监听函数

  • watchEffect 不需要手动传入依赖
  • watchEffect 会先执行一次用来自动收集依赖
  • watchEffect 无法获取到变化前的值, 只能获取变化后的值

computed可传入get和set

用于定义可更改的计算属性

const plusOne = computed({
 get: () => count.value + 1,
 set: val => { count.value = val - 1 }
});

使用TypeScript和JSX

setup现在支持返回一个渲染函数,这个函数返回一个JSXJSX可以直接使用声明在setup作用域的响应式状态:

export default {
 setup() {
 const count = ref(0);
 return () => (<div>{count.value}</div>);
 },
};

2、Vue 跟React 对比?

相同点:

  1. 都有虚拟DOM(Virtual DOM 是一个映射真实DOM的JavaScript对象)
  2. 都提供了响应式和组件化的视图组件。

不同点:

Vue 是MVVM框架,双向数据绑定,当ViewModelModel进行更新时,通过数据绑定更新到View

React是一个单向数据流的库,状态驱动视图。State --> View --> New State --> New View ui = render (data)

模板渲染方式不同。React是通过JSX来渲染模板,而Vue是通过扩展的HTML来进行模板的渲染。

组件形式不同,Vue文件里将HTML,JS,CSS组合在一起。react提供class组件和function组

Vue封装好了一些v-if,v-for,React什么都是自己实现,自由度更高

3、Vue 初始化过程,双向数据绑定原理

vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetterdep.addSub来收集订阅的依赖,watcher监听数据的变化,在数据变动时发布消息给订阅者,触发相应的监听回调。

监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。 订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而调用对应update更新视图。

image.png

v-model 指令,它能轻松实现表单输入和应用状态之间的双向绑定。

computed: 支持缓存,只有依赖数据结果发生改变,才会重新进行计算,不支持异步操作,如果一个属性依赖其他属性,多对一,一般用computed

watch: 数据变,直接触发相应操作,支持异步,监听数据必须data中声明过或者父组件传递过来的props中的数据,当数据变化时,触发其他操作,函数有两个参数

4、vue-router实现原理

路由简介以及vue-router实现原理 原理核心就是 更新视图但不重新请求页面。路径之间的切换,也就是组件的切换。 vue-router实现单页面路由跳转模式:hash模式、history模式。根据设置mode参数

hash模式:通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置。 history模式:利用 window.history.pushState API 来完成 URL 跳转而无须重新加载页面。

5、vuex实现原理:

Vue.use(vuex)会调用vuex的install方法

beforeCreate钩子前混入vuexInit方法,vuexInit方法实现了store注入vue组件实例,并注册了 vuex store的引用属性$store

Vuexstate状态是响应式,是借助vuedata是响应式,将state存入vue实例组件的data中;

Vuexgetters则是借助vue的计算属性computed实现数据实时监听。

6、nextTick 的原理以及运行机制?

nextTick的源码分析

vue进行DOM更新内部也是调用nextTick来做异步队列控制。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。

DOM至少会在当前事件循环里面的所有数据变化完成之后,再统一更新视图。而当我们自己调用nextTick的时候,它就在更新DOM的microtask(微任务队列)后追加了我们自己的回调函数

从而确保我们的代码在DOM更新后执行,同时也避免了setTimeout可能存在的多次执行问题。 确保队列中的微任务在一次事件循环前被执行完毕。

7、Vue 实现一个高阶组件

高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。在不改变对象自身的前提下在程序运行期间动态的给对象添加一些额外的属性或行为

// 高阶组件(HOC)接收到的 props 应该透传给被包装组件即直接将原组件prop传给包装组件
// 高阶组件完全可以添加、删除、修改 props
export default function Console(BaseComponent) {
  return {
    props: BaseComponent.props,
    mounted() {
      console.log("高阶组件");
    },
    render(h) {
      console.log(this);
      // 将 this.$slots 格式化为数组,因为 h 函数第三个参数是子节点,是一个数组
      const slots = Object.keys(this.$slots)
        .reduce((arr, key) => arr.concat(this.$slots[key]), [])
        .map((vnode) => {
          vnode.context = this._self; // 绑定到高阶组件上,vm:解决具名插槽被作为默认插槽进行渲染
          return vnode;
        });
 
      // 透传props、透传事件、透传slots
      return h(
        BaseComponent,
        {
          on: this.$listeners,
          attrs: this.$attrs, // attrs 指的是那些没有被声明为 props 的属性
          props: this.$props,
        },
        slots
      );
    },
  };
}

8、Vue.component()、Vue.use()、this.$xxx()

Vue.component()方法注册全局组件。

  • 第一个参数是自定义元素名称,也就是将来在别的组件中使用这个组件的标签名称。
  • 第二个参数是将要注册的Vue组件。
import Vue from 'vue';
// 引入loading组件 
import Loading from './loading.vue';
// 将loading注册为全局组件,在别的组件中通过<loading>标签使用Loading组件
Vue.component('loading', Loading);

Vue.use注册插件,这接收一个参数。这个参数必须具有install方法。Vue.use函数内部会调用参数的install方法。

  • 如果插件没有被注册过,那么注册成功之后会给插件添加一个installed的属性值为true。Vue.use方法内部会检测插件的installed属性,从而避免重复注册插件。
  • 插件的install方法将接收两个参数,第一个是参数是Vue,第二个参数是配置项options。
  • 在install方法内部可以添加全局方法或者属性
import Vue from 'vue';

// 这个插件必须具有install方法
const plugin = {
  install (Vue, options) {
    // 添加全局方法或者属性
    Vue.myGlobMethod = function () {};
    // 添加全局指令
    Vue.directive();
    // 添加混入
    Vue.mixin();
    // 添加实例方法
    Vue.prototype.$xxx = function () {};
    // 注册全局组件
    Vue.component()
  }
}

// Vue.use内部会调用plugin的install方法
Vue.use(plugin);

将Hello方法挂载到Vue的prototype上.

import Vue from 'vue';
import Hello from './hello.js';
Vue.prototype.$hello = Hello;

vue组件中就可以this.$hello('hello world')

9、Vue父组件传递props数据,子组件修改参数

  • 父子组件传值时,父组件传递的参数,数组和对象,子组件接受之后可以直接进行修改,并且父组件相应的值也会修改。控制台中发出警告。
  • 如果传递的值是字符串,直接修改会报错。 单向数据流,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。

如果子组件想修改prop中数据:

  1. 定义一个局部变量,使用prop的值赋值
  2. 定义一个计算属性,处理prop的值并返回

10、Vue父子组件生命周期执行顺序

加载渲染过程

父beforeCreate -> 父created -> 父beforeMount-> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted

子组件更新过程

父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated

父组件更新过程

父beforeUpdate -> 父updated

销毁过程

父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed

11、Vue 自定义指令

自定义指令提供了几个钩子函数: bind:指令第一次绑定到元素时调用 inserted:被绑定元素插入父节点时调用 update:所在组件的 VNode 更新时调用

使用slot后可以在子组件内显示插入的新标签

参考网址:

Vue3.x基础使用

五、webpack 及工程化

1、webpack的生命周期,及钩子

compiler(整个生命周期 [kəmˈpaɪlər]) 钩子 webpack.docschina.org/api/compile… compilation(编译 [ˌkɑːmpɪˈleɪʃn]) 钩子

compiler 对象包含了Webpack 环境所有的的配置信息。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。

compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。当运行webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。 compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。

compiler代表了整个webpack从启动到关闭的生命周期,而compilation 只是代表了一次新的编译过程

2、webpack 编译过程

Webpack 的编译流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

3、优化项目的webpack打包编译过程

1.构建打点:构建过程中,每一个 LoaderPlugin 的执行时长,在编译 JS、CSS 的 Loader 以及对这两类代码执行压缩操作的 Plugin上消耗时长 。一款工具:speed-measure-webpack-plugin

2.缓存:大部分 Loader 都提供了 cache 配置项。cache-loader ,将 loader 的编译结果写入硬盘缓存

3.多核编译happypack项目接入多核编译,理解为happypack 将编译工作灌满所有线程

4.抽离webpack-dll-plugin 将这些静态依赖从每一次的构建逻辑中抽离出去,静态依赖单独打包,Externals将不需要打包的静态资源从构建逻辑中剔除出去,使用 CDN 引用

5.tree-shaking,虽然依赖了某个模块,但其实只使用其中的某些功能。通过 tree-shaking,将没有使用的模块剔除,来达到删除无用代码的目的。

首屏加载优化

路由懒加载:改为用import引用,以函数的形式动态引入,可以把各自的路由文件分别打包,只有在解析给定的路由时,才会下载路由组件;

element-ui按需加载:引用实际上用到的组件 ;

组件重复打包CommonsChunkPlugin配置来拆包,把使用2次及以上的包抽离出来,放进公共依赖文件,首页也有复用的组件,也会下载这个公共依赖文件;

gzip: 拆完包之后,再用gzip做一下压缩,关闭sourcemap。

UglifyJsPlugin: 生产环境,压缩混淆代码,移除console代码

CDN部署静态资源:静态请求打在nginx时,将获取静态资源的地址进行重定向CDN内容分发网络

移动端首屏加载可以使用骨架屏,自定义loading,首页单独做服务端渲染

如何进行前端性能优化(21种优化+7种定位方式)

4、webpack 热更新机制

热更新流程总结:

  • 启动本地server,让浏览器可以请求本地的静态资源
  • 页面首次打开后,服务端与客户端通过 websocket建立通信渠道,把下一次的 hash 返回前端
  • 客户端获取到hash,这个hash将作为下一次请求服务端 hot-update.js 和 hot-update.json的hash
  • 修改页面代码后,Webpack 监听到文件修改后,开始编译,编译完成后,发送 build 消息给客户端
  • 客户端获取到hash,成功后客户端构造hot-update.js script链接,然后插入主文档
  • hot-update.js 插入成功后,执行hotAPI 的 createRecord 和 reload方法,获取到 Vue 组件的 render方法,重新 render 组件, 继而实现 UI 无刷新更新。

5、webpack的 loader和plugin介绍,css-loader,style-loader的区别

loader 它就是一个转换器,将A文件进行编译形成B文件,

plugin ,它就是一个扩展器,来操作的是文件,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,会监听webpack打包过程中的某些节点(run, build-module, program)

Babel 能把ES6/ES7的代码转化成指定浏览器能支持的代码。

css-loader 的作用是把 css文件进行转码 style-loader: 使用将css-loader内部样式注入到我们的HTML页面

先使用 css-loader 转码,然后再使用 style-loader插入到文件

6、如何编写一个webpack的plugin?

webpack 插件的组成:

  • 一个 JS 命名函数或一个类(可以想下我们平时使用插件就是 new XXXPlugin()的方式)
  • 在插件类/函数的 (prototype) 上定义一个 apply 方法。
  • 通过 apply 函数中传入 compiler 并插入指定的事件钩子,在钩子回调中取到 compilation 对象
  • 通过 compilation 处理 webpack 内部特定的实例数据
  • 如果是插件是异步的,在插件的逻辑编写完后调用 webpack 提供的 callback

7、为什么 Vite 启动这么快

Webpack 会先打包,然后启动开发服务器,请求服务器时直接给予打包结果。

而 Vite 是直接启动开发服务器,请求哪个模块再对该模块进行实时编译

Vite 将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像 Webpack 那样进行打包合并

由于 Vite 在启动的时候是通过esbuild方式进行EsModel原生引入浏览器且按需更新。也就意味着不需要分析模块的依赖不需要编译。因此启动速度非常快。当浏览器请求某个模块时,再根据需要对模块内容进行编译。

8、你的脚手架是怎么做的

使用 download-git-repo 下载仓库代码demo commander:完整的 node.js 命令行解决方案。声明program,使用.option() 方法来定义选项 Inquirer.js:命令行用户界面的集合。

9、前端监控

前端监控通常包括行为监控(PV/UV,埋点接口统计)、异常监控性能监控

一个监控系统,大致可以分为四个阶段:日志采集日志存储统计与分析报告和警告

10、错误监控

Vue专门的错误警告的方法 Vue.config.errorHandler,(Vue提供只能捕获其页面生命周期内的函数,比如created,mounted)

Vue.config.errorHandler = function (err) {
console.error(‘Vue.error’,err.stack)
// 逻辑处理
};

框架:betterjsfundebug(收费) 捕获错误的脚本要放置在最前面,确保可以收集到错误信息 方法:

  1. window.onerror()当有js运行时错误触发时,onerror可以接受多个参数(message, source, lineno, colno, error)。
  2. window.addEventListener('error'), function(e) {}, true 会比window.onerror先触发,不能阻止默认事件处理函数的执行,但可以全局捕获资源加载异常的错误

前端JS错误捕获--sourceMap

11、如何监控网页崩溃?

**崩溃和卡顿有何差别?**监控错误

  1. Service Worker 有自己独立的工作线程,与网页区分开,网页崩溃了,Service Worker 一般情况下不会崩溃;

  2. Service Worker 生命周期一般要比网页还要长,可以用来监控网页的状态;

卡顿:加载中,渲染遇到阻塞

12、性能监控 && 性能优化

性能指标:

  • FP(首次绘制)
  • FCP(首次内容绘制 First contentful paint)
  • LCP(最大内容绘制时间 Largest contentful paint)
  • FPS(每秒传输帧数)
  • TTI(页面可交互时间 Time to Interactive)
  • HTTP 请求响应时间
  • DNS 解析时间
  • TCP 连接时间

性能数据采集需要使用 window.performance API , JS库 web-vitalsimport {getLCP} from 'web-vitals';

// 重定向耗时
redirect: timing.redirectEnd - timing.redirectStart,
// DOM 渲染耗时
dom: timing.domComplete - timing.domLoading,
// 页面加载耗时
load: timing.loadEventEnd - timing.navigationStart,
// 页面卸载耗时
unload: timing.unloadEventEnd - timing.unloadEventStart,
// 请求耗时
request: timing.responseEnd - timing.requestStart,
// 获取性能信息时当前时间
time: new Date().getTime(),
// DNS查询耗时
domainLookupEnd - domainLookupStart
    // TCP链接耗时
connectEnd - connectStart
    // request请求耗时
responseEnd - responseStart
    // 解析dom树耗时
domComplete - domInteractive
    // 白屏时间
domloadng - fetchStart
    // onload时间
loadEventEnd - fetchStart

性能优化常用手段:缓存技术、 预加载技术、 渲染方案。

  1. 缓存 :主要有 cdn、浏览器缓存、本地缓存以及应用离线包
  2. 预加载 :资源预拉取(prefetch)则是另一种性能优化的技术。通过预拉取可以告诉浏览器用户在未来可能用到哪些资源。
  • prefetch支持预拉取图片、脚本或者任何可以被浏览器缓存的资源。

    在head里 添加 <linkrel="prefetch"href="image.png">

  • prerender是一个重量级的选项,它可以让浏览器提前加载指定页面的所有资源。

  • subresource可以用来指定资源是最高优先级的。当前页面需要,或者马上就会用到时。

  1. 渲染方案
  • 静态渲染(SR)
  • 前端渲染(CSR)
  • 服务端渲染(SSR)
  • 客户端渲染(NSR):NSR 数据请求,首屏数据请求和数据线上与 webview 的一个初始化和框架 JS 初始化并行了起来,大大缩短了首屏时间。

image.png

13、常见的六种设计模式以及应用场景

观察者模式的概念

观察者模式模式,属于行为型模式的一种,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主体对象在状态变化时,会通知所有的观察者对象。

发布订阅者模式的概念

发布-订阅模式,消息的发送方,叫做发布者(publishers),消息不会直接发送给特定的接收者,叫做订阅者。意思就是发布者和订阅者不知道对方的存在。需要一个第三方组件,叫做信息中介,它将订阅者和发布者串联起来,它过滤和分配所有输入的消息。换句话说,发布-订阅模式用来处理不同系统组件的信息交流,即使这些组件不知道对方的存在。

需要一个第三方组件,叫做信息中介,它将订阅者和发布者串联起来

工厂模式 主要是为创建对象提供了接口。场景:在编码时不能预见需要创建哪种类的实例。

代理模式 命令模式

单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点。 (window)

1) 单例模式。

单例模式是一种常用的软件设计模式。

在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。

应用场景:如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。

2) 工厂模式。

工厂模式主要是为创建对象提供了接口。

应用场景如下:

a、 在编码时不能预见需要创建哪种类的实例。

b、 系统不应依赖于产品类实例如何被创建、组合和表达的细节。

3) 策略模式。

策略模式:定义了算法族,分别封装起来,让它们之间可以互相替换。此模式让算法的变化独立于使用算法的客户。

应用场景如下。

a、 一件事情,有很多方案可以实现。

b、我可以在任何时候,决定采用哪一种实现。

c、未来可能增加更多的方案。

d、 策略模式让方案的变化不会影响到使用方案的客户。

举例业务场景如下。

系统的操作都要有日志记录,通常会把日志记录在数据库里面,方便后续的管理,但是在记录日志到数据库的时候,可能会发生错误,比如暂时连不上数据库了,那就先记录在文件里面。日志写到数据库与文件中是两种算法,但调用方不关心,只负责写就是。

4) 观察者模式。

观察者模式又被称作发布/订阅模式,定义了对象间一对多依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。

应用场景如下:

a、对一个对象状态的更新,需要其他对象同步更新,而且其他对象的数量动态可变。

b、对象仅需要将自己的更新通知给其他对象而不需要知道其他对象的细节。

5) 迭代器模式。

迭代器模式提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。

应用场景如下:

当你需要访问一个聚集对象,而且不管这些对象是什么都需要遍 历的时候,就应该考虑用迭代器模式。其实stl容器就是很好的迭代器模式的例子。

6) 模板方法模式。

模板方法模式定义一个操作中的算法的骨架,将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些步骤。

应用场景如下:

对于一些功能,在不同的对象身上展示不同的作用,但是功能的框架是一样的。

参考相关:

代码规范(分清Eslint、Prettier、Vetur、EditorConfig)

带你快速了解webpack

六、Http 及浏览器相关

1、七层网络模型

应用层、表示层、会话层、传输层、网络层、数据链路层、物理层

TCP:面向连接、传输可靠(保证数据正确性,保证数据顺序)、用于传输大量数据(流模式)、速度慢,建立连接需要开销较多(时间,系统资源) 。(应用场景:HTP,HTTP,邮件)

UDP:面向非连接、传输不可靠、用于传输少量数据(数据包模式)、速度快 ,可能丢包(应用场景:即时通讯)

是否连接     面向连接     面向非连接
传输可靠性   可靠        不可靠
应用场合    少量数据    传输大量数据

2、https 加密解密过程

HTTPS并不是一个新协议,而是HTTP + SSL

SSL(Secure Socket Layer):是Netscape公司设计的主要用于WEB的安全传输协议。它在https协议栈中负责实现上面提到的加密层。

客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。服务器公钥放在数字证书中。

  • 服务器把自己的公开密钥登录至数字证书认证机构。

  • 数字证书认证机构用自己的私有密钥向服务器的公开密码署数字签名并颁发公钥证书。

  • 客户端拿到服务器的公钥证书后,使用数字签名认证机构的公开密钥,向数字证书认证机构验证公钥证书上的数字签名,以确认服务器的公开密钥的真实性。

  • 使用服务器的公开密钥对报文加密后发送。

  • 服务器用私有密钥对报文解密。

3、url到加载渲染全过程

  1. DNS域名解析。
  2. TCP三次握手,建立接连。
  3. 发送HTTP请求报文。
  4. 服务器处理请求返回响应报文。
  5. 浏览器解析渲染页面。
  6. 四次挥手,断开连接。

DNS 协议提供通过域名查找 IP地址,或逆向从 IP地址反查域名的服务。 DNS 是一个网络服务器,我们的域名解析简单来说就是在 DNS 上记录一条信息记录。

TCP 三次握手,四次挥手:握手挥手都是客户端发起,客户端结束。 三次握手与四次挥手详解

负载均衡:请求在进入到真正的应用服务器前,可能还会先经过负责负载均衡的机器,它的作用是将请求合理地分配到多个服务器上,转发HTTP请求;同时具备具备防攻击等功能。可分为DNS负载均衡,HTTP负载均衡,IP负载均衡,链路层负载均衡等。

Web Server: 请求经过前面的负载均衡后,将进入到对应服务器上的 Web Server,比如 ApacheTomcat

反向代理是工作在 HTTP 上的,一般都是 Nginx。全国各地访问baidu.com就肯定要通过代理访问,不可能都访问百度的那台服务器。 (VPN正向代理,代理客户端)

浏览器解析渲染过程: 返回的html传递到浏览器后,如果有gzip会先解压,找出文件编码格式,外链资源的加载 html从上往下解析,遇到js,css停止解析渲染,直到js执行完成。 解析HTML,构建DOM树 解析CSS,生成CSS规则树 合并DOM树和CSS规则,生成render树去渲染

不会引起DOM树变化,页面布局变化,改变元素样式的行为叫重绘

引起DOM树结构变化,页面布局变化的行为叫回流

GUI渲染线程负责渲染浏览器界面HTML元素,当界面需要 重绘(Repaint) 或由于某种操作引发 回流(reflow) 时,该线程就会执行。在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,也就是说被”冻结”了. 直到JS程序执行完成,才会接着执行。因此如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面,渲染前后元素数据可能不一致

GPU绘制 多进程的浏览器:主控进程,插件进程,GPU,tab页(浏览器内核) 多线程的浏览器内核:每一个tab页面可以看作是浏览器内核进程,然后这个进程是多线程的。

它有几大类子线程:

  • GUI线程
  • JS引擎线程
  • 事件触发线程
  • 定时器线程
  • HTTP请求线程

4、HTTP1跟HTTP2

HTTP2

多路复用:相同域名多个请求,共享同一个TCP连接,降低了延迟

请求优先级:给每个request设置优先级

二进制传输;之前是用纯文本传输

数据流:数据包不是按顺序发送,对数据包做标记。每个请求或回应的所有数据包成为一个数据流,

服务端推送:可以主动向客户端发送消息。

头部压缩:减少包的大小跟数量

HTTP/1.1 中的管道( pipeline)传输中如果有一个请求阻塞了,那么队列后请求也统统被阻塞住了 HTTP/2 多请求复用一个TCP连接,一旦发生丢包,就会阻塞住所有的 HTTP 请求。 HTTP/3 把 HTTP 下层的 TCP 协议改成了 UDP! http1 keep alive 串行传输

http 中的 keep-alive 有什么作用

响应头中设置 keep-alive 可以在一个 TCP 连接上发送多个 http 请求

5、浏览器缓存策略

强缓存:cache-control;no-cache max-age=<10000000>;expires;其中Cache-Conctrol的优先级比Expires高;

控制强制缓存的字段分别是Expires和Cache-Control,如果客户端的时间小于Expires的值时,直接使用缓存结果。

协商缓存:Last-Modified / If-Modified-Since和Etag / If-None-Match,其中Etag / If-None-Match的优先级比Last-Modified / 首次请求,服务器会在返回的响应头中加上Last-Modified字段,表示资源最后修改的时间。

浏览器再次请求时,请求头中会带上If-Modified-Since字段,比较两个字段,一样则证明资源未修改,返回304,否则重新返回资源,状态码为200;

垃圾回收机制:

标记清除:进入执行环境的变量都被标记,然后执行完,清除这些标记跟变量。 查看变量是否被引用。

引用计数:会记录每个值被引用的次数,当引用次数变成0后,就会被释放掉。

6、前端安全

同源策略:如果两个 URL 的协议、域名和端口都相同,我们就称这两个 URL 同源。 因为浏览器有cookies

  • XSS:跨站脚本攻击(Cross Site Scripting) input, textarea等所有可能输入文本信息的区域,输入<script src="http://恶意网站"></script>等,提交后信息会存在服务器中 。

  • CSRF:跨站请求伪造 。引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起的跨站请求。

    A站点imgsrc=B站点的请求接口,可以访问;解决:referer携带请求来源

    访问该页面后,表单自动提交, 模拟完成了一次POST操作,发送post请求

    解决:后端注入一个随机串Cookie,前端请求取出随机串添加传给后端。

  • http 劫持:电信运营商劫持

  • SQL注入

  • 点击劫持:诱使用户点击看似无害的按钮(实则点击了透明 iframe 中的按钮) ,解决后端请求头加一个字段 X-Frame-Options

  • 文件上传漏洞 :服务器未校验上传的文件

七、NODE/NPM

1、输入npm install后的之后发生了什么?

npm install 大概会经过以下图例中的几个阶段

image.png

参考网址:

juejin.cn/post/711489… juejin.cn/post/684490… juejin.cn/post/684490… juejin.cn/post/707892…

2、关于dependencies 与devDependencies的误区

1)devDependencies和dependencies是什么?

devDependencies: 开发时所依赖的工具包;

dependencies:项目正常允许时需要的依赖包;

如果只根据这两句话去思考,可能我们做不到真正的去理解devDependencies和dependencies,如果你想真正认识devDependencies和dependencies,请继续往下看。

2)什么包放在devDependencies?什么包放在dependencies?

要想搞明白这个问题,首先要清楚前端项目整个上线的一个流程:

  • 本地开发完成后将代码push到gitlab;
  • CI构建时会执行执行npm install;npm run build;从而编译出dist文件
  • 将dist编译后的文件部署到线上;

这样我们每次访问系统的时候,都会去线上服务器获取dist文件执行。

在一个项目内执行以下命令:

npm install:安装devDependencies和dependencies的依赖

npm install --production : 只安装dependencies的依赖(使用场景很少,如果在CI上配置这个命令,其实很容易导致项目构建失败,因为一旦判断错误,将应该放到dependencies的包放到devDependencies,就会导致构建失败)

在一个项目内安装A组件:

npm install A:A组件依赖的devDependencies不会被下载,只会下载A组件的dependencies

构建服务器配置的执行命令是npm install,其实我们的依赖包安装在devDependencies还是安装在dependencies,没有任何区别(前提项目不被别人依赖使用),反正都会下载,但是如果开发的项目作为一个组件库的话,还是建议严格管理好devDependencies和dependencies。

如果开发的是一个组件库,那么建议将babel-loader、style-loader等打包相关的工具包放到devDependencies,因为如果放到dependencies,别人引用你的组件时,也会把这些工具包安装上,我们引用组件,其实引用的是lib里编译后的文件,所以这些工具包我们是用不到的,所以如果开发时组件库,被业务代码使用的库安装在dependencies,其它的,例如打包相关、ESLint相关、Loader相关等等都要安装在devDependencies。

结论:

  • 如果开发的是个工程项目,依赖包安装在devDependencies还是dependencies,虽然没有实质性的区别,但是为了规范,还是建议权衡一下对依赖做个区分;
  • 如果开发的是组件库,建议将代码运行引用的库放到dependencies,其它的编译打包、eslint校验、开发相关的包放到devDependencies;

优秀网址:

2021年前端面试必读文章【超三百篇文章/赠复习导图】

面试官问:能否模拟实现JS的new操作符

前端进阶系列

「2021」高频前端面试题汇总之HTML篇

前端面试查漏补缺系列

一位前端小姐姐的五万字面试宝典

六年前端面试报告

2022年前端面试集锦