笔记整理一

258 阅读1小时+

一、HTML+CSS

1 HTML语义化?

  • 比较利于开发人员阅读,结构清晰明了;
  • 利于SEO搜索引擎优化,搜索引擎也要分析我们的网页,可以很方便的寻找出网页的重点部分,排名靠前;
  • 有利于特殊终端的阅读(盲人阅读器)。

2 mate标签

  • <meta charset="UTF-8">:定义字符编码为UTF-8,确保页面能够正确地显示特殊字符;
  • <meta name="viewport" content="width=device-width, initial-scale=1.0">:定义视口大小为设备宽度,缩放比例为1,确保在手机等移动设备上显示正常;
  • <meta name="keywords" content="关键词1, 关键词2, ...">:定义网页的关键词,便于搜索引擎抓取和分析网页主题;
  • <meta name="description" content="网页描述">:定义网页描述信息,便于搜索引擎显示搜索结果摘要描述;
  • <meta name="author" content="作者">:定义网页作者信息;
  • <meta HTTP-EQUIV="refresh" CONTENT="3;URL=http://www.example.com/">:定义网页跳转规则,如每隔3秒自动跳转到指定网址;
  • <meta name="robots" content="index,follow">:定义搜索引擎对网页的抓取策略,"index"表示允许抓取该网页,"noindex"则表示不允许抓取;"follow"表示跟踪该网页中所有的链接,"nofollow"则表示不跟踪;
  • <meta name="format-detection" content="telephone=no">:定义是否禁止自动检测页面中的电话号码,避免误触拨打电话。

3 html5 的新特性

语义化标签: <header> <footer> <nav> <article> <aside>

新增的表单属性: placeholder 、 required 、 pattern、 min/max 、 autofocus、 mulitiple

音视频标签: <audio>、 <video>

绘图标签:<canvas>

存储: sessionStorage 、 localStorage

通信: webSocket

4 css 盒模型:

W3C盒模型box-sizing:content-box时 ,为W3C盒模型,又名标准盒模型,元素的宽高大小表现为内容的大小

box = content 

IE 盒模型 当 box-sizing:border-box时,为IE 盒模型,又名怪异盒模型,元素的宽高表现为内容 + 内边距 + 边框:

box = content + padding + border 

5 BFC?

BFC全称 Block formatting context(块级格式化上下文)。是web布局的css 渲染模式,是一个独立的渲染区域或一个隔离的独立容器

特性:

  • 块级元素,内部一个一个垂直排列
  • 垂直方向的距离由两个元素中margin 的较大值决定
  • bfc 区域不会与浮动的容器发生重叠
  • 计算元素的高度时,浮动元素也会参与计算
  • bfc 容器中,子元素不会影响外边元素
  • 属于同一个bfc的两个相邻元素的外边距发生重叠

如何触发BFC

可以通过设置css 属性来实现

  • 设置浮动float但不包括none
  • 设置定位,absoulte或者fixed
  • 行内块显示模式,设置displayinline-block
  • 设置overflowhiddenautoscroll
  • 弹性布局,flex

BFC解决了哪些问题:

  • 阻止元素被浮动元素覆盖
  • 可以利用BFC解决两个相邻元素的上下margin重叠问题;
  • 可以利用BFC解决高度塌陷问题;
  • 可以利用BFC实现多栏布局(两栏、三栏、圣杯、双飞翼等)。

6 CSS3 新特性总结

  • 选择器: E:last-child 、E:nth-child(n)、E:nth-last-child(n)

  • 边框特性:支持圆角、多层边框、彩色和图片边框

  • 背景图增加多个属性:background-image、background-repeat、background-size、background-position、background-origin和background-clip

  • 支持多种颜色模式和不透明度:加了HSL、HSLA、RGBA 和不透明度 opacity

  • 支持过渡与动画: transitionanimation

  • 引入媒体查询:mediaqueries ,支持为不同分辨率的设备设定不同的样式

  • 增加阴影:文本阴影:text-shadow,盒子阴影:box-shadow

  • 弹性盒模型布局:flex新的布局方式,用于创建具有多行和多列的灵活界面布局。

7 居中的布局

<div class='box'>
    <div class='center'></div>
</div>

第一种:(flex最方便,有兼容性问题:ie 8 以下不支持)

.box {
  width:400px;
  height: 400px;
  display: flex;
  justify-content: center;
  align-items: center;
  background: blue;
}
.center{
  width:200px;
  height: 200px;
  background-color: red;
}

第二种:(父相对定位 + 子相对定位)

.box {
  width: 400px;
  height: 400px;
  background: blue;
  position: relative;
}
.center {
  width: 200px;
  height: 200px;
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  margin: auto;
  background-color: red;
}

1.png

第三种:(使用计算属性calc)

.box {
  width:400px;
  height: 400px;
  background-color: blue;
  position: relative;
}
.center {
  width:200px;
  height: 200px;
  background-color: red;
  position: absolute;
  left: calc(50% - 100px);
  top: calc(50% - 100px);
}

第四种:(使用转换属性transform

.box {
  width:400px;
  height: 400px;
  background-color: blue;
  position: relative;
}
.center {
  width:200px;
  height: 200px;
  background-color: red;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

第五种:(定位 + margin-left:盒子的一半

.box {
  width:400px;
  height: 400px;
  background-color: blue;
  position: relative;
}
.center {
  width:200px;
  height: 200px;
  background-color: red;
  position: absolute;
  left: 50%;
  top: 50%;
  margin-left: -100px;
  margin-top: -100px;
}

8 flex布局

flex-direction:项目排列方式

justify-content:项目横轴对齐方式

flex-wrap:是否换行

align-content:在项目为多行时需要加flex-wrap:wrap; ,项目纵轴如何对齐(不能控制单行的盒子内位置变换)

align-items(控制容器):项目纵轴如何对齐(能控制单行的盒子内位置变换)

align-self(控制子项):容器子项纵轴如何对齐

9 响应式 (当数据变换的时候,页面发生变化,视图发生变化)

px是绝对长度单位

em是相对长度单位,但是相对于父元素(不常用)

rem是相对长度单位,相对于根元素,所以常用于响应式布局

window.screem.height //屏幕高度

window.innerHeight //网页视口高度

document.body.clientHeight //body高度

rem存在一定的弊端,就是存在“阶梯性” 。而vw/vh相对的不是父节点或者页面的根节点。而是由网页视口大小来决定的。

vh:网页视口高度的1/100

vw:网页视口宽度的1/100

vmax 取上面两者中最大的长度;vmin:取上面两者中最小的长度

vwvvh% 百分比的区别

  • **%**是相对于父元素的大小设定的比率,vwvh 是视窗大小决定的。
  • vwvh 优势在于能够直接获取高度,而用 % 在没有设置 body 高度的情况下,是无法正确获得可视区域的高度的,所以这是挺不错的优势。

如何实现响应式布局?

  • 先使用媒体查询(media-query),根据不同的屏幕宽度设置根元素的font-size
  • 后续页面开发中使用基于根元素的rem相对长度单位

10 回流(reflow)和重绘(repaints)?什么场景下会触发?

浏览器渲染机制

  • 浏览器采用流式布局模型(Flow Based Layout)
  • 浏览器会把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了渲染树(Render Tree)。
  • 有了RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。
  • 由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一。

回流: 当render tree中的一部分(或全部),因为元素的规模尺寸、布局、隐藏等改变 而需要重新构建,这就是回流(reflow)

  • 每个页面至少回流一次,即页面首次加载
  • 回流时,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树
  • 回流完成后,浏览器会重新绘制受影响的部分,是重绘过程 ** 重绘**:当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外 观、风格,而不影响布局(例如:background-color),则称为重绘(repaints)

特点:回流必将引起重绘,重绘不一定引起回流 回流比重绘的代价更高

下述情况会发生回流

1)添加或者删除可见的DOM元素;
(2)元素位置改变;
(3)元素尺寸改变——边距、填充、边框、宽度和高度
(4)内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变;
(5)页面渲染初始化;
(6)浏览器窗口尺寸改变——resize事件发生时;

let box = document.getElementById("box").style;
box.padding = "2px";   // 回流+重绘
box.border = "1px solid red";  // 再一次 回流+重绘
box.fontSize = "14px";    // 回流+重绘
document.getElementById("box").appendChild(document.createTextNode('abc!'));

下述情况会发生重绘

元素的属性或者样式发生变化。
let box = document.getElementById("box").style;
box.color = "red";    // 重绘
box.backgroud-color = "blue";    // 重绘
document.getElementById("box").appendChild(document.createTextNode('abc!'));

因回流的开销较大,如果每个操作都去回流重绘的话,浏览器可能就会受不了。所以很多浏览器都会优化这些操作。 多次的回流、重绘变成一次回流重绘:

浏览器会维护1个队列,把所有会引起回流、重绘的操作放入这个队列,等
队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会flush
队列,进行一个批处理。

但是有时上面的方法会失效,原因是:

有些情况,当请求向浏览器请求一些style信息的时候,就会让浏览器强制flush队列,比如:
(1)offsetTop, offsetLeft, offsetWidth, offsetHeight
(2) scrollTop/Left/Width/Height3)clientTop/Left/Width/Height4width,height5)请求了getComputedStyle(), 或者 IE的 currentStyle

当你请求上面的一些属性的时候,浏览器为了给你最精确的值,需要flush队列,因为队列中可能会有影响到这些值的操作。即使你获取元素的布局和样式信息跟最近发生或改变的布局信息无关,浏览器都会强行刷新渲染队列。

这样一来,浏览器的优化就显得力不从心,所以我们需要一些方法,尽可能的避免或减少浏览器的回流、重绘

如何避免、减少回流和重绘

  • 减少对render tree的操作【合并多次多DOM和样式的修改】
  • 减少对一些style信息的请求,尽量利用好浏览器的优化策略
(1)添加css样式,而不是利用js控制样式
(2)让要操作的元素进行“离线处理”,处理完后一起更新
    当用DocumentFragment进行缓存操作,引发一次回流和重绘
    使用display:none技术,只引发两次回流和重绘
    使用cloneNode(true or false)和replaceChild技术,引发一次回流和重绘
(3)直接改变className,如果动态改变样式,则使用cssText(考虑没有优化的浏览器)
    // bad
    elem.style.left = x + "px";
    elem.style.top = y + "px";
    // good
    elem.style.cssText += ";left: " + x + "px;top: " + y + "px;";
(4)不要经常访问会引起浏览器flush队列的属性,如果你确实要访问,利用缓存
    // bad
    for (var i = 0; i < len; i++) {
      el.style.left = el.offsetLeft + x + "px";
      el.style.top = el.offsetTop + y + "px";
    }
    // good
    var x = el.offsetLeft,
        y = el.offsetTop;
    for (var i = 0; i < len; i++) {
      x += 10;
      y += 10;
      el.style = x + "px";
      el.style = y + "px";
    }
(5)让元素脱离动画流,减少回流的Render Tree的规模
    $("#block1").animate({left:50});
    $("#block2").animate({marginLeft:50});
(6)将需要多次重排的元素,position属性设为absolute或fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位;
(7)避免使用table布局:尽量不要使用表格布局,如果没有定宽表格一列的宽度由最宽的一列决定,那么很可能在最后一行的宽度超出之前的列宽,引起整体回流造成table可能需要多次计算才能确定好其在渲染树中节点的属性,通常要花3倍于同等元素的时间。
(8)尽量将需要改变DOM的操作一次完成
    let box = document.getElementById("box").style;
    // bad
    box.color = "red";    // 重绘
    box.size = "14px";    // 回流、重绘
    // good
    box.bord = '1px solid red'
(9)尽可能在DOM树的最末端改变class,尽可能在DOM树的里面改变class(可以限制回流的范围)
(10)IE中避免使用JavaScript表达式

来源【细谈页面回流与重绘】juejin.cn/post/684490…

11 动画

transition 渐变动画(过渡)

transform转变动画(变形+移动)

animation(自定义动画)

transition

/* 应用于1个属性 */
/* 属性值 | 持续时间 */
transition: margin-right 4s;
​
/* 属性值 | 持续时间 | 延迟 */
transition: margin-right 4s 1s;
​
/* 属性值 | 持续时间 | 持续时间 | 动画效果 */
transition: margin-right 4s ease-in-out;
​
/* 属性值 | 持续时间 | 动画效果 | 延迟 */
transition: margin-right 4s ease-in-out 1s;
​
/* 同时应用2个属性*/
transition: margin-right 4s, color 1s;
​
/* 应用于所有属性 | 持续时间 | 动画效果 */
transition: all 0.5s ease-out;
​
/* 全局变量 */
transition: inherit;
transition: initial;
transition: unset;

animation

/* 持续时间 | 动画效果 | 延迟 | 重复次数 | 方向 | 过渡方式 | 是否暂停 | 动画名 */
animation: 3s ease-in 1s 2 reverse both paused xxx;
​
/* 持续时间 | 动画效果 | 延迟 | 动画名 */
animation: 3s linear 1s xxx;
​
/* 持续时间 | 动画名 */
animation: 3s xxx;

12讲一下三栏布局实现?圣杯布局、双飞翼布局和flex布局

三栏布局是指一个网页由三个栏目组成的布局,分别是左栏、右栏和中间栏。下面是三种实现三栏布局的方法:

  1. 圣杯布局

圣杯布局是一种使用浮动和负边距实现的三栏布局。中间栏先放在html结构中,使用负边距将左右栏移动到中间栏的两侧,再使用相对定位将左右栏拉回原来的位置。这种布局可以使得中间栏优先渲染,兼顾SEO和用户体验。

  1. 双飞翼布局

双飞翼布局也是一种使用浮动和负边距实现的三栏布局。与圣杯布局不同的是,左右栏使用margin负值撑开中间栏的宽度。这种布局与圣杯布局相比,代码更简单易懂。

  1. Flex布局

Flex布局是CSS3引入的一种新的布局方式,通过flex容器和flex项目的属性设置,可以轻松实现三栏布局。设置左右栏的宽度为固定值,中间栏的宽度使用flex-grow属性自动填充。这种布局适用于移动端和PC端,具有响应式的特点。

13 使用过哪些CSS预处理器?它们有什么优劣?

Less和Sass这两个常见的CSS预处理器。它们的优势是可以使用变量、嵌套规则和函数等功能,可以更简单更高效地编写CSS代码。缺点是需要进行额外的预处理工作,增加了开发成本。

14 如何解决CSS样式在不同浏览器中的兼容性问题?

解决CSS样式在不同浏览器中的兼容性问题可以使用一些通用的方法,如使用CSS Reset,避免使用CSS Hack和浏览器前缀,使用标准的组件库,尽量使用标准的CSS属性和属性值等。

15 margin塌陷和margin合并以及解决方案?

margin塌陷margin合并 都是 CSS 中描述 margin 行为的术语。它们分别指 margin 在不同场景下的特殊表现。

  1. Margin塌陷:Margin塌陷是指当一个元素的上外边距(margin-top)和相邻的另一个元素的下外边距(margin-bottom)相遇时,它们之间的距离实际上等于两个外边距中的较大值,而不是它们的总和。这种现象主要发生在具有相邻兄弟元素的块级元素之间。
  2. Margin合并:Margin合并是指在父子元素之间发生的现象。当一个元素的外边距与其父元素的外边距相遇时,它们之间的距离实际上等于两个外边距中的较大值,而不是它们的总和。Margin合并通常发生在没有边框、内边距或行内内容分隔的父元素与其第一个或最后一个子元素之间。

解决方案:

针对 margin 塌陷和合并的现象,有以下几种解决方案:

  1. 使用内边距(padding):如果适用,可以使用内边距代替外边距来调整元素之间的距离。内边距不会发生塌陷或合并。
  2. 添加边框(border)或内边距(padding):在父子元素间的 margin 合并问题上,可以通过给父元素添加一个边框或一个很小的内边距来阻止 margin 合并。
  3. 使用 BFC(块格式化上下文):创建一个新的 BFC(如通过设置 overflow 属性为 autohidden)可以防止父子元素间的 margin 合并。
  4. 使用伪元素:可以通过在两个相邻的兄弟元素之间插入一个透明的伪元素(如 ::before::after),并为其添加 display: inline-block; 属性来防止兄弟元素间的 margin 塌陷。
  5. 避免使用外边距:在某些情况下,可以使用其他布局技术(如 Flexbox 或 Grid)来调整元素之间的距离,从而避免 margin 塌陷和合并的问题。

了解 margin 塌陷和合并现象以及如何解决这些问题可以帮助你更好地控制布局和元素间距。

二、JS + ES6

1 JS 的数据类型有哪些?

JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。

其中 Symbol 和 BigInt 是ES6 中新增的数据类型:

  • Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
  • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

这些数据可以分为原始数据类型和引用数据类型:

  • 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
  • 堆:引用数据类型(对象、数组和函数)

两种类型的区别在于存储位置的不同:

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

堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:

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

在操作系统中,内存被分为栈区和堆区:

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

2 null 和 undefined 的区别?

null表示一个对象被定义了,值为“空值”。

  • 作为函数的参数,表示该函数的参数不是对象。
  • 作为对象原型链的终点。

undefined表示不存在这个值。就是此处应该有一个值,但是还没有定义,当尝试读取时就会返回 undefined。

  • 函数没有返回值时,默认返回 undefined。
  • 变量已声明,没有赋值时,为 undefined。
  • 对象中没有赋值的属性,该属性的值为 undefined。
  • 调用函数时,应该提供的参数没有提供,该参数等于 undefined。

3 如何判断 JS 的数据类型?

  1. typeof

typeof可以区分除了Null类型以外的其他基本数据类型,以及从对象类型中识别出函数(function)。

其返回值有:numberstringbooleanundefinedsymbolbigintfunctionobject

其中, typeof null返回 "object"

如果要识别null,可直接使用===全等运算符来判断。

typeof 1 // 'number'
typeof '1' // 'string'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object'
typeof console // 'object'
typeof console.log // 'function'

2. instanceof

instanceof一般是用来判断引用数据类型,但不能正确判断基本数据类型,根据在原型链中查找判断当前数据的原型对象是否存在返回布尔类型。

1 instanceof Number; // false
true instanceof Boolean; // false
'str' instanceof String; // false
[] instanceof Array; // true
function(){} instanceof Function; // true
{} instanceof Object; // true
let date = new Date();
date instance of Date; // true

3. Object.prototype.toString

Object.prototype.toString({}) // "[object Object]"
Object.prototype.toString.call({}) // 同上结果,加上call也ok
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('1') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(function () {}) // "[object Function]"
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g) //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([]) //"[object Array]"
Object.prototype.toString.call(document) //"[object HTMLDocument]"
Object.prototype.toString.call(window) //"[object Window]"

4. Array.isArray

Array.isArray(value)可以判断 value 是否为数组。

Array.isArray([]); // true
Array.isArray({}); // false
Array.isArray(1); // false
Array.isArray('string'); // false
Array.isArray(true); // false

4 为什么0.1+0.2 ! == 0.3,如何让其相等 ?

计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。0.1的二进制是0.0001100110011001100...(1100循环),0.2的二进制是:0.00110011001100...(1100循环),这两个数的二进制都是无限循环的数。那JavaScript是如何处理无限循环的二进制小数呢?

其实就是保留53位有效数字,剩余的需要舍去,遵从“0舍1入”的原则。

要想等于0.3,就要把它进行转化:

(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入
toFixed(num)方法可把 Number 四舍五入为指定小数位数的数字。

5 for...in和for...of的区别?

遍历Map/Set/generator数组/字符串:用for...of...(可迭代),得到value

遍历对象/数组/字符串:用for...in...(可枚举数据),得到key

for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链;

for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;

总结: for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。

6 如何判断两个对象是否相等?

  • Object.is(obj1, obj2),判断两个对象都引用地址是否一致,true 则一致,false 不一致。

  • 判断两个对象内容是否一致,思路是遍历对象的所有键名和键值是否都一致

    ① 判断两个对象是否指向同一内存
    ② 使用 Object.getOwnPropertyNames 获取对象所有键名数组
    ③ 判断两个对象的键名数组是否相等
    ④ 遍历键名,判断键值是否都相等

function isObjValueEqual(a, b) {
  // 判断两个对象是否指向同一内存,指向同一内存返回 true
  if (a === b) return true;
  // 获取两个对象的键名数组
  let aProps = Object.getOwnPropertyNames(a);
  let bProps = Object.getOwnPropertyNames(b);
  // 判断两键名数组长度是否一致,不一致返回 false
  if (aProps.length !== bProps.length) return false;
  // 遍历对象的键值
  for (let prop in a) {
    // 判断 a 的键名,在 b 中是否存在,不存在,直接返回 false
    if (b.hasOwnProperty(prop)) {
      // 判断 a 的键值是否为对象,是对象的话需要递归;
      // 不是对象,直接判断键值是否相等,不相等则返回 false
      if (typeof a[prop] === 'object') {
        if (!isObjValueEqual(a[prop], b[prop])) return false;
      } else if (a[prop] !== b[prop]){
        return false
      }
    } else {
      return false
    }
  }
  return true;
}

7 intanceof 操作符的实现原理及实现

instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。

function myInstanceof(left, right) {
  // 获取对象的原型
  let proto = Object.getPrototypeOf(left)
  // 获取构造函数的 prototype 对象
  let prototype = right.prototype; 
 
  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    // 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
    proto = Object.getPrototypeOf(proto);
  }
}

8 如何获取安全的 undefined 值?

因为 undefined 是一个标识符,所以可以被当作变量来使用和赋值,但是这样会影响 undefined 的正常判断。表达式 void ___ 没有返回值,因此返回结果是 undefined。void 并不改变表达式的结果,只是让表达式不返回值。因此可以用 void 0 来获得 undefined。

9 Object.is() 与比较操作符 “===”、“==” 的区别?

  • 使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
  • 使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。
  • 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。

10 什么是 JavaScript 中的包装类型?

在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象,如:

const a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"

在访问'abc'.length时,JavaScript 将'abc'在后台转换成String('abc'),然后再访问其length属性。 JavaScript也可以使用Object函数显式地将基本类型转换为包装类型:

var a = 'abc'
Object(a) // String {"abc"}

也可以使用valueOf方法将包装类型倒转成基本类型:

var a = 'abc'
var b = Object(a)
var c = b.valueOf() // 'abc'
var a = new Boolean( false );
if (!a) {
  console.log( "Oops" ); // never runs
}

答案是什么都不会打印,因为虽然包裹的基本类型是false,但是false被包裹成包装类型后就成了对象,所以其非值为false,所以循环体中的内容不会运行。

11 new操作符的实现原理

new操作符的执行过程:

(1)首先创建了一个新的空对象

(2)设置原型,将对象的原型设置为函数的 prototype 对象。

(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)

(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

具体实现:

function objectFactory() {
  let newObject = null;
  let constructor = Array.prototype.shift.call(arguments);
  let result = null;
  // 判断参数是否是一个函数
  if (typeof constructor !== "function") {
    console.error("type error");
    return;
  }
  // 新建一个空对象,对象的原型为构造函数的 prototype 对象
  newObject = Object.create(constructor.prototype);
  // 将 this 指向新建对象,并执行函数
  result = constructor.apply(newObject, arguments);
  // 判断返回对象
  let flag = result && (typeof result === "object" || typeof result === "function");
  // 判断返回结果
  return flag ? result : newObject;
}
// 使用方法
objectFactory(构造函数, 初始化参数);

12 JavaScript有哪些内置对象

全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。这里说的全局的对象是说在 全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。

标准内置对象的分类:

(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。例如 Infinity、NaN、undefined、null 字面量

(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。例如 eval()、parseFloat()、parseInt() 等

(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。例如 Object、Function、Boolean、Symbol、Error 等

(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。例如 Number、Math、Date

(5)字符串,用来表示和操作字符串的对象。例如 String、RegExp

(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array

(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。 例如 Map、Set、WeakMap、WeakSet

(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。 例如 SIMD 等

(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。例如 JSON 等

(10)控制抽象对象 例如 Promise、Generator 等

(11)反射。例如 Reflect、Proxy

(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。例如 Intl、Intl.Collator 等

(13)WebAssembly

(14)其他。例如 arguments

总结: js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。

13. ajax、axios、fetch的区别

(1)AJAX Ajax 即“AsynchronousJavascriptAndXML”(异步 JavaScript 和 XML),是指一种创建交互式网页应用的网页开发技术。它是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。传统的网页(不使用 Ajax)如果需要更新内容,必须重载整个网页页面。其缺点如下:

  • 本身是针对MVC编程,不符合前端MVVM的浪潮
  • 基于原生XHR开发,XHR本身的架构不清晰
  • 不符合关注分离(Separation of Concerns)的原则
  • 配置和调用方式非常混乱,而且基于事件的异步模型不友好。

(2)Fetch fetch号称是AJAX的替代品,是在ES6出现的,使用了ES6中的promise对象。Fetch是基于promise设计的。Fetch的代码结构比起ajax简单多。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象

fetch的优点:

  • 语法简洁,更加语义化
  • 基于标准 Promise 实现,支持 async/await
  • 更加底层,提供的API丰富(request, response)
  • 脱离了XHR,是ES规范里新的实现方式

fetch的缺点:

  • fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
  • fetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: 'include'})
  • fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
  • fetch没有办法原生监测请求的进度,而XHR可以

(3)Axios Axios 是一种基于Promise封装的HTTP客户端,其特点如下:

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

14 原型与原型链

原型、原型链相等关系理解

要理解原型,首先就要理解__proto__prototype的区别与联系,这里先抛出一个结论:

  1. 对象都有隐式原型__proto__(除了nullObject.create(null));
  2. 函数都有显式原型prototype
  3. 实例对象的__proto__指向其构造函数的prototype
  • js分为函数对象普通对象,每个对象都有__proto__属性,但是只有函数对象才有prototype属性

  • Object、Function都是js内置的函数, 类似的还有我们常用到的Array、RegExp、Date、Boolean、Number、String

  • 属性__proto__是一个对象,它有两个属性,constructor和__proto__;

  • 原型对象prototype有一个默认的constructor属性,用于记录实例是由哪个构造函数创建

 function Person(name, age){ 
    this.name = name;
    this.age = age;
 }
 
 Person.prototype.motherland = 'China'

let person01 = new Person('小明', 18);

1. Person.prototype.constructor == Person // **准则1:原型对象(即Person.prototype)的constructor指向构造函数本身**
2. person01.__proto__ == Person.prototype // **准则2:实例(即person01)的__proto__和原型对象指向同一个地方**

function Foo()
let f1 = new Foo();
let f2 = new Foo();

f1.__proto__ = Foo.prototype; // 准则2
f2.__proto__ = Foo.prototype; // 准则2
Foo.prototype.__proto__ = Object.prototype; // 准则2 (Foo.prototype本质也是普通对象,可适用准则2)
Object.prototype.__proto__ = null; // 原型链到此停止
Foo.prototype.constructor = Foo; // 准则1
Foo.__proto__ = Function.prototype; // 准则2
Function.prototype.__proto__  = Object.prototype; //  准则2 (Function.prototype本质也是普通对象,可适用准则2)
Object.prototype.__proto__ = null; // 原型链到此停止
// **此处注意Foo 和 Function的区别, Foo是 Function的实例**

// 从中间 function Object()开始分析这一张经典之图
function Object()
let o1 = new  Object();
let o2 = new  Object();

o1.__proto__ = Object.prototype; // 准则2
o2.__proto__ = Object.prototype; // 准则2
Object.prototype.__proto__ = null; // 原型链到此停止
Object.prototype.constructor = Object; // 准则1
// 所有函数的__proto__  都和 Function.prototype指向同一个地方
Object.__proto__ = Function.prototype // 准则2 (Object本质也是函数);
// 此处有点绕
Function.prototype.__proto__ =  Object.prototype; // 准则2 (Function.prototype本质也是普通对象,可适用准则2)
Object.prototype.__proto__ = null; // 原型链到此停止

function Function()
Function.__proto__ = Function.prototype // 准则2
Function.prototype.constructor = Function; // 准则1


由此可以得出结论: 除了Object的原型对象(Object.prototype)的__proto__指向null,其他内置函数对象的原型对象(例如:Array.prototype)和自定义构造函数的 __proto__都指向Object.prototype, 因为原型对象本身是普通对象。 即:

Object.prototype.__proto__ = null;
Array.prototype.__proto__ = Object.prototype;
Foo.prototype.__proto__  = Object.prototype;

原型链:每一个对象都有一个隐式原型叫__proto__,它的指向是构造函数的原型对象。当查找某个属性或方法时,先从自身上查找,没有找到会沿着__proto_找到构造函数的原型对象,仍然没有找到会继续沿着__proto__向上查找到它构造函数原型对象的原型对象,直到找到顶级对象object为null,由此形成的链条为原型链。

15 什么是闭包?

MDN中的解释为:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

使用闭包主要是为了设计私有的方法和变量。闭包的优点是可以避免全局变量的污染缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。在js中,函数即闭包,只有函数才会产生作用域的概念

闭包有三个特性:

  1. 函数嵌套函数
  2. 函数内部可以引用外部的参数和变量
  3. 参数和变量不会被F垃圾回收机制回收

闭包有两个常用的用途。

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

创建闭包

function outer(){
    var a = 1;
    function inner(){
        console.log(a);
    }
    inner(); // 1
}
outer();

使用闭包实现每隔一秒打印 1,2,3,4

// 打印出5个5
for( var i = 0; i < 5; i++ ) {
    setTimeout(() => {
        console.log( i );
    }, 1000)
}

// 使用闭包实现
for (var i = 0; i < 5; i++) {
    (function (i) {
        setTimeout(function() {
            console.log(i);
        }, i * 1000);
    })(i)
}

// 使用 let 块级作用域
for(let i = 0; i < 5; i++){
    setTimeout(function() {
        console.log(i);
    }, i * 1000);
}

在 inner 函数中,我们可以通过作用域链访问到 a 变量,因此这就可以算是构成了一个闭包,因为 a 变量是其他函数作用域中的变量。 弄懂以下代码,你就弄懂了闭包的原理

function makeAdder(x) {
    return function(y) {
        return x + y;
    };
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12
function f1(){
    var n=999;

    nAdd=function(){n+=1}

    function f2(){
      alert(n);
    }
    return f2;
}

var result=f1();
result(); // 999
nAdd();
result(); // 1000

result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是nAdd=function(){n+=1}这一行,首先在nAdd前面没有使用var关键字,因此 nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。


 var name = "The Window";

  var object = {

    name : "My Object",

    getNameFunc : function(){

      return function(){

        return this.name;

     };

    }

};

alert(object.getNameFunc()()); //"The Window"

this对象是在运行时基于函数的执行环境绑定的,在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性。每个函数在被调用时,其活动对象都会自动取得两个特殊变量:this和arguments.内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。

var name = "The Window";  
var object = { 
    name: "My Object",
    getNameFunc: function() {
        var that = this; 
        return function() {
            return that.name; 
        }; 
    }
};
alert(object.getNameFunc()()); //"My Object"

在定义匿名函数之前,我们把this对象赋值给了一个名叫that的变量,而在定义了闭包之后,闭包也可以访问这个变量,因为它是我们在包含函数中特意声明的一个变量。即使在函数返回之后,that也仍然引用着object,所以调用object.getNameFunc()()就返回了"My Object"。(this和arguments也存在同样的问题,如果想访问作用域中的arguments对象,必须将该对象的引用保存到另一个闭包能够访问的变量中。)

内容来自: 链接:juejin.cn/post/699571…

16 作用域、作用域链

在JavaScript中,作用域、作用域链和执行上下文是密切相关的概念,它们与变量和函数的查找、访问以及生命周期有关。

  1. 作用域(Scope):

    作用域是一个变量或函数的可访问范围。JavaScript中有三种作用域:全局作用域局部(函数)作用域块级作用域。全局作用域中声明的变量和函数可以在整个代码中访问,局部作用域中声明的变量和函数只能在特定的函数内部访问,块级作用域在一对花括号内定义,对letconst关键字声明的变量有效。

    变量的生命周期受其作用域的限制。全局作用域中的变量在整个程序执行过程中持续存在,局部作用域中的变量在函数执行结束时销毁,块级作用域在代码块执行结束时,块级作用域中的变量将被销毁。

  2. 作用域链(Scope Chain):

    当代码执行过程中访问一个变量或函数时,JavaScript引擎会沿着作用域链查找该标识符。作用域链是由当前执行上下文的作用域和其所有父级作用域组成的链表。

    查找过程从当前作用域开始,然后逐级向上查找,直到找到目标标识符或到达全局作用域。如果在全局作用域中仍未找到目标标识符,则返回undefined

  3. 执行上下文(Execution Context):

    执行上下文是JavaScript代码执行过程中的环境。每当进入一个新的函数执行或全局代码执行时,都会创建一个新的执行上下文。执行上下文包含了当前执行的代码所需的所有信息,如变量、函数、作用域链等。

    JavaScript引擎使用执行上下文栈(Execution Context Stack)来管理执行上下文。栈顶的执行上下文为当前执行的代码环境。当一个函数被调用时,一个新的执行上下文被压入栈顶;当函数执行结束时,执行上下文从栈顶弹出,返回到调用者的上下文环境。

总结起来,作用域是变量和函数的可访问范围;作用域链是由当前执行上下文的作用域和其父级作用域组成的链表,用于在代码执行过程中查找变量和函数;执行上下文是代码执行过程中的环境,包含了当前执行的代码所需的所有信息。这三者共同决定了代码执行过程中变量和函数的查找、访问以及生命周期。

17 如何创建一个没有原型的对象?

可以使用 Object.create(null) 方法创建一个没有原型的对象。这个方法创建一个全新的对象并将其原型设置为 null,因此它没有继承任何属性或方法。例如:

const obj = Object.create(null);
console.log(obj.toString); // undefined

18 JSON.stringify有什么缺点?

JSON.stringify()是一个将JavaScript对象转换为JSON字符串的方法。尽管它在许多情况下非常有用,但它确实存在一些限制和缺点:

  1. 循环引用:JSON.stringify()无法处理具有循环引用的对象。如果一个对象的属性直接或间接引用了自身,JSON.stringify()将抛出一个错误,表示存在循环引用。
  2. undefined、函数和Symbol忽略:JSON.stringify()不会序列化对象中的undefined、函数和Symbol类型的属性。这些属性将被忽略,不会出现在生成的JSON字符串中,单独转换则会返回undefined
  3. 丢失原型链:在对象序列化后,原型链上的属性和方法将丢失。只有对象自身的可枚举属性会被序列化。因此,在反序列化(使用JSON.parse())后,原始对象的原型链信息将不复存在。
  4. 日期对象处理:当使用JSON.stringify()序列化日期对象时,日期对象会被转换为它们的ISO字符串表示形式。在反序列化时,这些日期将被视为普通字符串,而不是日期对象。
  5. 非数组和非对象的值:对于不是数组或对象的顶层值(例如:字符串、数字、布尔值等),JSON.stringify()会直接返回其对应的JSON表示,而不会将其包装在对象或数组中。

19 谈谈你对V8垃圾回收的理解?

V8引擎主要采用了两种算法来处理垃圾回收:分代收集(Generational Collection)和增量标记(Incremental Marking)。

  1. 分代收集:V8将内存分为两个代:新生代(Young Generation)和老生代(Old Generation)。新生代中的对象存活时间较短,而老生代中的对象存活时间较长。新生代使用Scavenge算法进行垃圾回收,通常采用Cheney算法,将内存分为两个半区(From Space和To Space),每次垃圾回收时,会将存活的对象复制到To Space中,并清空From Space。当一个对象在新生代中经历了多次垃圾回收仍然存活时,会将其移动到老生代。
  2. 增量标记:老生代采用标记-清除(Mark-Sweep)算法进行垃圾回收。这种算法首先会标记所有可达的对象,然后清除所有未被标记的对象。为了避免在标记过程中产生长时间的停顿(Stop-The-World),V8采用了增量标记策略。这种策略将标记过程分为多个阶段,在每个阶段之间,JavaScript程序可以继续执行。这样可以降低垃圾回收对程序性能的影响。

V8垃圾回收是对JavaScript垃圾回收策略的具体实现。它采用分代收集和增量标记两种算法,有效降低了垃圾回收对程序性能的影响。

20 类数组和数组的区别,dom 的类数组如何转换成数组

类数组(Array-like)和数组(Array)都是用于存储多个值的数据结构,但它们之间存在一些关键区别:

  1. 类型:数组是JavaScript的内置对象类型,继承自Array.prototype,具有一系列数组方法(如push()pop()map()等)。类数组是普通的对象,其属性名为索引(如012等),具有一个length属性,但不具备数组的方法。
  2. 原型:数组的原型为Array.prototype,因此具有数组的所有方法。类数组的原型通常为Object.prototype,并不包含数组的方法。

要将DOM的类数组(例如,通过document.getElementsByClassName()document.querySelectorAll()获取的元素集合)转换为数组,可以使用以下方法之一:

  1. 使用Array.from()方法:

    let nodeList = document.querySelectorAll('div');
    let array = Array.from(nodeList);
    

    Array.from()方法会创建一个新数组,并将类数组的元素逐个复制到新数组中。

  2. 使用扩展运算符(Spread Operator):

    let nodeList = document.querySelectorAll('div');
    let array = [...nodeList];
    

    扩展运算符...可以将类数组直接转换为数组。

  3. 使用Array.prototype.slice.call()

    let nodeList = document.querySelectorAll('div');
    let array = Array.prototype.slice.call(nodeList);
    

Array.prototype.slice.call()方法会将类数组作为上下文,并创建一个新数组,将类数组的元素逐个复制到新数组中。

这些方法可以将类数组转换为数组,这样就可以在转换后的数组上使用数组的方法了。注意,这些方法不仅适用于DOM类数组,还适用于其他类数组对象。

21 offsetWidth/offsetHeight,clientWidth/clientHeight 与 scrollWidth/scrollHeight 的区别

offsetWidth/offsetHeight是元素的可见宽度/高度加上padding、border和滚动条(如果存在)的宽度/高度。

clientWidth/clientHeight是元素的可见宽度/高度,不包括padding和滚动条。

scrollWidth/scrollHeight是元素内容的完整宽度/高度,包括溢出部分。如果元素没有溢出,则scrollWidth/scrollHeight等于clientWidth/clientHeight。如果有溢出,则scrollWidth/scrollHeight大于clientWidth/clientHeight。

22 mouseover/mouseout 与 mouseenter/mouseleave 的区别与联系

mouseover和mouseout是HTML DOM事件,它们会在鼠标移入或移出元素时触发。它们也会在鼠标指针进入或离开子元素时触发。这也就是说,如果在父元素上有mouseover事件,并且鼠标指针进入子元素,则该元素上仍然会触发mouseover事件。mouseout同理。

mouseenter和mouseleave事件也是在鼠标进入或离开元素时触发。与mouseover和mouseout不同的是,mouseenter和mouseleave事件不会传播到子元素。因此,如果鼠标指针进入或离开元素的子元素,则不会触发mouseenter和mouseleave事件。

23 事件捕获、事件冒泡以及事件代理

eventflow.svg 上图是W3C标准的DOM事件流模型图,从图中可以看出,元素事件响应在DOM树中是从顶层的Window开始“流向”目标元素(),然后又从目标元素“流向”顶层的Window。

通常,我们将这种事件流向分为三个阶段:捕获阶段,目标阶段,冒泡阶段

  • 捕获阶段是指事件响应从最外层的Window开始,逐级向内层前进,直到具体事件目标元素。在捕获阶段,不会处理响应元素注册的冒泡事件。
  • 目标阶段指触发事件的最底层的元素,如上图中的。
  • 冒泡阶段与捕获阶段相反,事件的响应是从最底层开始一层一层往外传递到最外层的Window。

现在,我们就可以知道,DOM事件流的三个阶段是先捕获阶段,然后是目标阶段,最后才是冒泡阶段。我们时常面试所说的先捕获后冒泡也由此而来。事件代理就是利用事件冒泡或事件捕获的机制把一系列的内层元素事件绑定到外层元素。

事件冒泡和事件捕获

实际操作中,我们可以通过 element.addEventListener() 设置一个元素的事件模型为冒泡事件或者捕获事件。
先来看一下 addEventListener 函数的语法:

element.addEventListener(type, listener, useCapture)
  • type
    监听事件类型的字符串
  • listener
    事件监听回调函数,即事件触发后要处理的函数
  • useCapture
    默认值false,表示事件冒泡;设为true时,表示事件捕获 事件冒泡举例
<div id="a" style="width: 100%; height: 300px;background-color: antiquewhite;">
	a
	<div id="b" style="width: 100%; height: 200px;background-color: burlywood;">
		b
		<div id="c" style="width: 100%; height: 100px;background-color: cornflowerblue;">
			c
		</div>
	</div>
</div>
<script>
	var a = document.getElementById('a')
	var b = document.getElementById('b')
	var c = document.getElementById('c')
  //注册冒泡事件监听器
	a.addEventListener('click', () => {console.log("冒泡a")})
	b.addEventListener('click', () => {console.log('冒泡b')})
	c.addEventListener('click', () => {console.log("冒泡c")})
</script>

冒泡事件的执行顺序为:c -> b -> a

<div id="a" style="width: 100%; height: 300px;background-color: antiquewhite;">
	a
	<div id="b" style="width: 100%; height: 200px;background-color: burlywood;">
		b
		<div id="c" style="width: 100%; height: 100px;background-color: cornflowerblue;">
			c
		</div>
	</div>
</div>
<script>
	var a = document.getElementById('a')
	var b = document.getElementById('b')
	var c = document.getElementById('c')
  //注册捕获事件监听器
  a.addEventListener('click', () => {console.log("捕获a")}, true)
  b.addEventListener('click', () => {console.log('捕获b')}, true)
  c.addEventListener('click', () => {console.log("捕获c")}, true)
</script>

捕获事件的执行顺序为:a -> b -> c

我们将上述的代码a,b,c三个元素都注册捕获和冒泡事件,并以元素c作为触发事件的主体,即事件流中的目标阶段。

<div id="a" style="width: 100%; height: 300px;background-color: antiquewhite;">
	a
	<div id="b" style="width: 100%; height: 200px;background-color: burlywood;">
		b
		<div id="c" style="width: 100%; height: 100px;background-color: cornflowerblue;">
			c
		</div>
	</div>
</div>
<script>
	var a = document.getElementById('a')
	var b = document.getElementById('b')
	var c = document.getElementById('c')
	a.addEventListener('click', () => {console.log("冒泡a")})
	b.addEventListener('click', () => {console.log('冒泡b')})
	c.addEventListener('click', () => {console.log("冒泡c")})
	a.addEventListener('click', () => {console.log("捕获a")}, true)
	b.addEventListener('click', () => {console.log('捕获b')}, true)
	c.addEventListener('click', () => {console.log("捕获c")}, true)
</script>

打印结果:

  • 捕获a
  • 捕获b
  • 捕获c
  • 冒泡c
  • 冒泡b
  • 冒泡a

从执行结果可以看到,a,b两个元素的事件响应都是先捕获后冒泡的,但对于触发事件的目标元素c,事件的响应也是遵循先捕获后冒泡的规则

如果我们对于某个元素需要先执行冒泡事件再执行捕获事件,我们可以在注册监听器时通过定时起暂缓执行捕获事件,等冒泡事件执行完之后,在执行捕获事件, 例子如下:

<div id="a" style="width: 100%; height: 300px;background-color: antiquewhite;">
	a
	<div id="b" style="width: 100%; height: 200px;background-color: burlywood;">
		b
		<div id="c" style="width: 100%; height: 100px;background-color: cornflowerblue;">
			c
		</div>
	</div>
</div>
<script>
	var b = document.getElementById('b')
	var c = document.getElementById('c')
	b.addEventListener('click', () => {console.log('冒泡b')})
	c.addEventListener('click', () => {console.log("冒泡c")})
	b.addEventListener('click', () => {
            // 非目标元素,注册定时器,暂缓执行
          setTimeout(() => {
            console.log('捕获b')
          })
        }, true)
        c.addEventListener('click', () => {
          // 目标元素,注册定时器,暂缓执行
          setTimeout(() => {
            console.log("捕获c")
          })
        }, true)
</script>

打印结果:

  • 冒泡c
  • 冒泡b
  • 捕获b
  • 捕获c

上面这个例子,就是通过定时器来暂缓执行捕获事件,从而实现事件执行先冒泡后捕获的效果

事件代理(事件委托)

我们知道了事件冒泡和事件捕获的原理,那么对于事件委托就比较容易理解了。
重复一遍,**事件代理就是利用事件冒泡或事件捕获的机制把一系列的内层元素事件绑定到外层元素。**至于为什么通常我们说事件代理是利用事件冒泡的机制来实现的,只是大家习以为常而已。

<ul id="item-list">
	<li>item1</li>
	<li>item2</li>
	<li>item3</li>
	<li>item4</li>
</ul>

对于上述的列表元素,我们希望将用户点击了哪个item打印出来,通常我们可以给每个item注册点击事件监听器,但是需要对每个元素进行事件监听器的注册;但是通过事件代理,我们可以将多个事件监听器减少为一个,这样就减少代码的重复编写了。
利用事件冒泡或事件捕获实现事件代理:

var items = document.getElementById('item-list');
//事件捕获实现事件代理
items.addEventListener('click', (e) => {console.log('捕获:click ',e.target.innerHTML)}, true);
//事件冒泡实现事件代理
items.addEventListener('click', (e) => {console.log('冒泡:click ',e.target.innerHTML)}, false);

打印结果:

微信图片_20230624020744.png

因此,事件代理既可以通过事件冒泡来实现,也可以通过事件捕获来实现

总结

以上的东西总结起来就是有以下几点:

  • DOM事件流有3个阶段:捕获阶段,目标阶段,冒泡阶段;三个阶段的顺序为:捕获阶段——目标阶段——冒泡阶段;
  • 不管对于非目标阶段或者目标阶段的元素,事件响应执行顺序都是遵循先捕获后冒泡的原则;通过使用定时器暂缓执行捕获事件,可以达到先冒泡后捕获的效果;
  • 事件捕获是从顶层的Window逐层向内执行,事件冒泡则相反;
  • 事件委托(事件代理)是根据事件冒泡或事件捕获的机制来实现的。

来源链接:juejin.cn/post/684490…

24 说一下事件循环机制Event Loop

事件循环(Event Loop)是 JavaScript 运行时环境中的一个核心概念,它负责协调异步操作和同步代码的执行。JavaScript 是单线程的,这意味着它一次只能执行一个任务。事件循环使 JavaScript 能够在执行同步代码的同时,处理异步操作(如定时器、用户交互和网络请求)的回调。

事件循环的工作原理大致如下:

  1. 首先,JavaScript 引擎执行全局同步代码(例如来自<script>标签或 Node.js 文件的代码)。
  2. 当遇到异步操作(如 setTimeoutsetIntervalPromisefetch 等),它们的回调函数会被放入相应的任务队列中(微任务队列或宏任务队列)。
  3. 同步代码执行完成后,事件循环开始检查微任务队列。如果队列中有任务,事件循环将依次执行它们,直到队列为空。
  4. 接下来,事件循环检查宏任务队列。如果队列中有任务,事件循环将执行第一个任务,然后返回到微任务队列,检查是否有新的微任务需要执行。
  5. 事件循环在微任务队列和宏任务队列之间循环,依次执行队列中的任务。当两个队列都为空时,事件循环将等待新的任务(如用户交互或网络请求回调)。
  6. 当新任务出现时,事件循环将其添加到相应的队列中,并继续循环执行任务。

事件循环的目标是在处理同步代码和异步回调之间保持平衡,确保 JavaScript 代码的执行效率和响应能力。通过这种方式,事件循环允许 JavaScript 在单线程环境中有效地处理并发操作。

宏任务有哪些?微任务有哪些?和DOM渲染的关系

JavaScript 运行时的事件循环机制中,任务分为宏任务(macro task)和微任务(micro task)。

常见的宏任务有:

  • setTimeout 和 setInterval 的回调函数
  • DOM 事件
  • XMLHttpRequest 中的readystatechange事件
  • requestAnimationFrame 中的回调函数
  • I/O 操作和网络请求的回调函数
  • Node.js 中的文件读写操作的回调函数
  • Node.js 中的进程事件

常见的微任务有:

  • Promise 的回调函数(then、catch、finally)
  • MutationObserver 监听函数
  • process.nextTick 回调函数
  • Object.observe 的回调函数

25 ES、CommonJS、AMD:三种模块化方案的区别与实现原理

ES6 的模块化

ES6 标准在 2015 年发布,引入了一套全新的模块化系统,该系统被称为 ES6 模块化。ES6 模块化是代码静态编译时进行解析的,因此模块内部不能使用条件语句等动态语言特性。 导出

ES6 模块化使用 export 关键字将变量或函数导出:

// 导出一个变量
export const name = 'Tom';

// 导出一个函数
export function sayHello() {
  console.log('Hello!');
}

// 导出一个类
export class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayHi() {
    console.log(`Hi, I'm ${this.name}.`);
  }
}

导入

ES6 模块化使用 import 关键字将模块导入:

// 导入一个变量或函数
import { name, sayHello } from './module';

// 导入一个类
import { Person } from './module';

// 导入全部
import * as module from './module';

特点

ES6 模块化是官方标准,将成为未来 web 开发的主流方案。其特点如下:

  • 代码静态编译时进行解析,不支持动态语言特性;
  • 编写简单,易于理解和维护;
  • 支持循环依赖;
  • 可以通过 export default 导出一个默认值,而其他方案不支持此功能。
CommonJS

CommonJS 是 Node.js 的模块化方案,在 Node.js 中,每个文件都是一个模块。

导出

CommonJS 使用 module.exports 将模块导出:

// 导出一个变量
module.exports.name = 'Tom';

// 导出一个函数
module.exports.sayHello = function() {
  console.log('Hello!');
};

// 导出一个类
module.exports.Person = class {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayHi() {
    console.log(`Hi, I'm ${this.name}.`);
  }
};

导入

CommonJS 使用 require 函数将模块导入:

// 导入一个变量或函数
const { name, sayHello } = require('./module');

// 导入一个类
const { Person } = require('./module');

// 导入全部
const module = require('./module');

特点

CommonJS 是 Node.js 自带的模块化方案,也是目前大多数 Node.js 开发者使用的方案。其特点如下:

  • 支持动态语言特性;
  • 不能在浏览器中使用,因为浏览器不支持 module.exports 语法。
AMD

AMD(Asynchronous Module Definition)是由 RequireJS 提出的一种 JavaScript 模块化规范,主要用于浏览器端的模块化开发。 导出

AMD 使用 define 函数将模块导出:

define('module', [], function() {
  var name = 'Tom';

  function sayHello() {
    console.log('Hello!');
  }

  return {
    name: name,
    sayHello: sayHello
  };
});

AMD 使用 require 函数将模块导入: 导入

require(['module'], function(module) {
  console.log(module.name);
  module.sayHello();
});

特点

AMD 是一种异步的模块化方案,主要用于浏览器端的模块化开发。其特点如下:

  • 支持动态加载模块;
  • 适用于多个模块之间存在依赖关系的情况。
ES6、CommonJS 和 AMD 的比较

加载方式

ES6 模块化是通过静态分析,在代码编译时就确定了模块之间的依赖关系。在浏览器中,可以通过 <script type="module"> 标签来加载 ES6 模块。

CommonJS 是通过 require 函数动态加载模块的,而且 require 函数是同步进行的。

AMD 是通过 define 函数定义模块并异步加载模块的,或者使用 require 函数进行异步加载。

兼容性

ES6 模块化标准是现代 web 开发的趋势,但是目前仍有一些浏览器不支持 ES6 模块化,需要通过工具(如 Babel)进行转换后才能使用。

CommonJS 是 Node.js 自带的模块化方案,在浏览器中无法使用。

AMD 是由 RequireJS 提出的一种模块化规范,需要引入 RequireJS 库才能使用。

循环依赖

ES6 模块化支持循环依赖,即模块 A 可以依赖于模块 B,同时模块 B 也可以依赖于模块 A。

CommonJS 和 AMD 都支持循环依赖,但是需要小心处理,否则会导致程序陷入死循环。

动态语言特性

ES6 模块化不支持动态语言特性,因为它是在静态编译时进行解析的。

CommonJS 支持动态语言特性,因为它是在运行时进行加载和执行的。

AMD 也支持动态语言特性,因为它是异步加载模块的。

总结

三种 JavaScript 模块化方案各有优缺点,开发者可以根据自己的需求选择合适的方案。ES6 模块化是未来 web 开发的主流方案,而 CommonJS 和 AMD 则更加适用于 Node.js 和浏览器端的模块化开发。在实际开发中,开发者可以根据项目需求、技术栈等因素综合考虑,灵活应用这三种方案。

来源链接:juejin.cn/post/724374…

26 解释下JavaScript栈内存和堆内存?

在 JavaScript 中,栈内存(Stack Memory)和堆内存(Heap Memory)扮演着不同的角色,它们分别负责存储不同类型的数据。以下是它们在 JavaScript 中的简要说明:

  1. 栈内存(Stack Memory):
    • 栈内存主要用于存储基本类型(原始类型)的值,如 numberstringbooleannullundefined。这些类型的值通常较小且固定大小。
    • 栈内存还负责存储函数调用的执行上下文、局部变量和临时数据。
    • 栈内存遵循后进先出(LIFO)的原则进行分配和释放空间。当函数被调用时,函数的执行上下文、局部变量和相关信息会被压入栈中;当函数返回时,这些数据会从栈中弹出。
    • 栈内存的分配和回收速度较快,因为内存管理由 JavaScript 引擎自动完成。
    • 由于栈内存有限,如果递归调用过深或者分配大量的局部变量,可能导致栈溢出。
  2. 堆内存(Heap Memory):
    • 堆内存主要用于存储引用类型的值,如对象(object)、数组(array)和函数(function)。这些类型的值通常较大,大小不固定。
    • JavaScript 引擎使用垃圾回收机制自动管理堆内存中的对象。当对象不再被引用时,它们会被标记为垃圾,并在下一次垃圾回收时释放内存。
    • 与栈内存相比,堆内存分配和回收速度较慢,因为需要管理更复杂的数据结构和垃圾回收机制。
    • 堆内存可以动态分配,因此可以存储更多数据。
    • 如果没有正确处理引用关系,可能导致内存泄漏。

总结一下,在 JavaScript 中,栈内存用于存储基本类型的值、函数调用的执行上下文和局部变量,堆内存用于存储引用类型的值。理解栈内存和堆内存的差异有助于编写高效且内存友好的 JavaScript 程序。

27 箭头函数与普通函数区别?

箭头函数(Arrow Functions)与普通函数(常被称为函数声明或函数表达式)在 JavaScript 中有一些重要的区别。这些区别包括语法、this 关键字的绑定、arguments 对象的使用、构造函数行为以及原型链。以下是箭头函数和普通函数之间的主要区别:

  1. this 关键字绑定:
    • 箭头函数没有自己的 this,它从包围它的普通函数或全局作用域继承 this。这使得在事件处理器或回调函数中使用箭头函数非常方便,因为它们自动捕获外部的 this
    • 普通函数有自己的 this,它的值在函数调用时确定。根据函数调用的方式(如通过对象方法调用、直接调用、构造函数调用等),this 的值可能会有所不同。
  2. arguments 对象:
    • 箭头函数没有自己的 arguments 对象。它们可以访问包围它们的普通函数的 arguments 对象。
    • 普通函数有自己的 arguments 对象,这是一个类数组对象,包含了传递给函数的参数。
  3. 构造函数行为:
    • 箭头函数不能作为构造函数使用,因此不能使用 new 关键字调用。它们也没有 prototype 属性。
    • 普通函数可以作为构造函数使用,通过 new 关键字创建新的对象实例。
  4. 原型链:
    • 由于箭头函数没有 prototype 属性,它们不能作为其他对象的原型。
    • 普通函数有 prototype 属性,可以作为其他对象的原型。
  5. 生成器:
    • 箭头函数不能使用yield关键字。
    • 普通函数可以使用yield关键字变成生成器函数。

28 谈谈你对this的理解

在 JavaScript 中,this 是一个特殊的关键字,它在函数调用时动态地引用了一个对象。this 的值取决于函数的调用方式,不同的调用方式会导致 this 指向不同的对象。以下是一些关于 this 的不同用法和场景:

  1. 全局上下文:

    当在全局作用域中使用 this 时,它指向全局对象。在浏览器环境中,全局对象是 window;在 Node.js 环境中,全局对象是 global

  2. 函数调用:

    当在函数内部使用 this 且函数作为普通函数调用时(非对象方法调用),this 通常指向全局对象。但在严格模式下(使用 "use strict"),this 会被设置为 undefined

  3. 对象方法调用:

    当在对象的方法内部使用 this 时,this 指向调用该方法的对象。这也适用于原型链中的方法。

  4. 构造函数调用:

    当在构造函数内部使用 this 且使用 new 关键字调用构造函数时,this 指向新创建的对象实例。

  5. 显式绑定:

    使用 callapplybind 方法调用函数时,可以显式地将 this 绑定到一个指定的对象。

  6. 箭头函数:

    箭头函数没有自己的 this,它从包围它的普通函数或全局作用域继承 this。这使得在事件处理器或回调函数中使用箭头函数非常方便,因为它们自动捕获外部的 this

总之,this 是 JavaScript 中一个动态上下文的关键字,它的值取决于函数调用的方式。

29 谈谈你对严格模式的理解

在JavaScript中,严格模式(strict mode)和非严格模式(sloppy mode)主要有以下几个区别:

  1. 变量声明: 在严格模式下,必须明确地声明变量(使用letconstvar关键字)。否则,将会抛出一个引用错误(ReferenceError)。在非严格模式下,如果没有声明变量,JavaScript会自动将其声明为全局变量,这可能会导致意外的全局污染。
  2. this指针: 在严格模式下,全局作用域中的this值为undefined。在非严格模式下,全局作用域中的this值为全局对象(浏览器环境中为window对象,Node.js环境中为global对象)。此外,在严格模式下,不允许使用callapplybindthis值设置为nullundefined
  3. 禁止使用未来保留字: 严格模式中,不能将一些未来保留字(如implementsinterfaceletpackageprivateprotectedpublicstaticyield)用作变量名或函数名。
  4. 禁止使用八进制字面量: 在严格模式下,不允许使用八进制字面量(如0123)。非严格模式下,八进制字面量是允许的。
  5. 禁止删除变量、函数和函数参数: 严格模式中,使用delete操作符删除变量、函数和函数参数会引发语法错误(SyntaxError)。在非严格模式下,这样的操作是允许的,但实际上不会删除这些对象。
  6. 限制函数参数的重复声明: 在严格模式下,如果一个函数具有多个相同名称的参数,将会抛出一个语法错误。非严格模式下允许这种重复声明,但只有最后一个参数值会生效。
  7. 错误处理: 严格模式相较于非严格模式,更严格地处理某些类型的错误。例如,当试图修改只读属性、给不可扩展的对象添加属性或删除不可配置的属性时,严格模式会抛出类型错误(TypeError),而非严格模式下则会静默失败。

要启用严格模式,可以在脚本或函数开头添加"use strict";指令。这将对整个脚本或函数体中的代码启用严格模式。推荐使用严格模式编写代码,因为它可以帮助发现潜在的错误并避免一些不良的编程实践。

30 谈谈你对Promise的理解

Promise是一种在JavaScript中用于处理异步操作的编程模式。它表示一个尚未完成但预计在未来某个时刻完成的操作的结果。Promise允许我们以更简洁、易读的方式处理异步操作,避免了传统的回调地狱(callback hell)问题。

Promise有三种状态:

  1. pending(待定):初始状态,既不是fulfilled,也不是rejected。
  2. fulfilled(已实现):表示异步操作已成功完成。
  3. rejected(已拒绝):表示异步操作失败。

Promise具有以下特点:

  1. Promise对象是不可变的,一旦创建,其状态就不能再被改变。
  2. Promise状态只能从pending变为fulfilled或rejected,不能逆向改变,且只能改变一次。
  3. Promise允许我们将成功和失败的处理函数分开,增加代码的可读性。

缺点:

  1. 无法取消:一旦创建了 Promise,就无法取消它。这可能导致在某些情况下,不再需要结果的异步操作仍然在执行。
  2. 总是异步:Promise 的回调总是异步执行,即使操作已经完成。这可能会导致一些意外的行为,特别是在执行顺序敏感的情况下。
  3. 调试困难:由于 Promise 的链式调用和异步特性,调试 Promise 可能比调试同步代码更具挑战性。错误堆栈可能不够清晰,难以确定问题出在哪里。

Promise基本用法包括:

  1. 创建Promise对象:通过new Promise(executor)创建一个Promise对象,其中executor是一个执行器函数,接受两个参数:resolve和reject。成功时调用resolve函数并传递结果,失败时调用reject函数并传递原因。
  2. 链式调用:通过.then()方法处理fulfilled状态,接受一个回调函数作为参数,当Promise状态变为fulfilled时调用。.catch()方法处理rejected状态,接受一个回调函数作为参数,当Promise状态变为rejected时调用。
  3. Promise.all:接受一个Promise数组作为参数,当所有Promise都变为fulfilled状态时返回一个新的Promise,其值为所有Promise结果的数组。如果有任意一个Promise变为rejected状态,则返回的Promise也变为rejected,且返回原因是第一个rejected的Promise的原因。
  4. Promise.race:接受一个Promise数组作为参数,返回一个新的Promise,其状态和结果与第一个完成(无论是fulfilled还是rejected)的Promise相同。

通过使用Promise,我们可以更有效地处理异步操作,降低代码复杂性,提高可维护性。在现代JavaScript开发中,Promise已成为处理异步操作的重要基石。

31 ES5、ES6 如何实现继承?

ES5:①原型链继承 ②构造函数继承 ③组合继承 ④寄生组合继承

ES6:ES6 中引入了 class 关键字, class 可以通过 extends 关键字实现继承。ES6的继承中super是用来①调用父类函数 ②指向父类的原型

32 JS实现继承的方式

原型链继承

什么是原型链继承?让一个构造函数(ParentType)的原型是另一个构造函数(SuperType)的实例,则这个构造函数(ParentType)new出来的实例(child1_type|child2_type)就具有该实例(Type)的属性和方法。

当访问一个对象的属性和方法时,它不仅会在该对象上查找,还会查找该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到名字匹配的属性和方法或到达原型链的末尾。

// 原型链继承
SuperType.prototype.greet = function() {
    return 'hello~,' + this.name;
}
function SuperType() {
    this.name = '美羊羊',
        this.hobby = {
            boy: '喜洋洋',
            book: '白雪公主和七个小矮人'
        }
}
// 让构造函数(ParentType)的原型等于另外一个构造函数(SuperType)的实例,会继承另外一个构造函数上的属性和方法
//ParentType.prototype = new SuperType();
ParentType.prototype = Object.create(SuperType.prototype);
function ParentType() {
    this.name = '懒洋洋'
}
let child1_type = new ParentType()
let child2_type = new ParentType()
console.log(child1_type.greet()); //hello~,懒洋洋
console.log(child2_type.name); //懒洋洋
console.log(child1_type.hobby.boy = '暖洋洋'); //暖洋洋
console.log(child2_type); //SuperType { name: '懒洋洋' }
console.log(child2_type.hobby); //{ boy: '暖洋洋', book: '白雪公主和七个小矮人' }

(1)优点

易于上手和理解,写法方便简单。

父类可以复用。

(2)不足

对象实例共享所有继承的属性和方法。也就是说会在子类实例上共享父类所有的实例属性。父类的所有引用属性(子类改不动父类的原始类型)会被所有子类共享,更改一个子类的引用属性,其他子类也会受影响。 子类不能给父类构造函数传参,因为这个对象是一次性创建的(不支持定制化)。

经典继承(伪造对象)

// 经典继承(伪造对象)
SuperType.prototype.greet = function() {
    return 'hello~' + this.person.name;
}
function SuperType(name) {
    this.person = {
        name: name || '大耳朵图图',
        gender: 'boy',
        age: 19
    }
}
function ParentType(name) {
    SuperType.call(this, name)
}
let child1_type = new ParentType('张小丽');
let child2_type = new ParentType();
child1_type.person.gender = 'female';
console.log(child1_type.person); //{ name: '张小丽', gender: 'female', age: 19 }
console.log(child2_type.person); //{ name: '大耳朵图图', gender: 'boy', age: 19 }
console.log(child2_type.greet()); //TypeError: child2_type.greet is not a function 继承不到父类原型上的属性和方法

(1)优点

解决了原型链实现继承的不能传参的问题和父类的原型共享的问题。

(2)不足

函数不能复用 ​ 子类只能继承到父类实例上的属性,继承不到父类原型上的属性

组合继承(伪经典继承)

原型链继承经典继承 组合。使用原型链继承方式实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又保证每个实例都有各自的属性。

// 组合继承(伪经典继承)
SuperType.prototype.greet = function() {
    return 'hello~,' + this.card.name;
}
​
function SuperType(gender) {
    this.card = {
        name: '佩奇',
        age: 20,
        gender: gender
    }
}
​
// 利用原型链继承
//ParentType.prototype = new SuperType();
ParentType.prototype = Object.create(SuperType.prototype);
// 因为ParentType.prototype被重写成为一个实例对象,没有constructor,所以得弥补一个constructor
ParentType.prototype.constructor = ParentType;
ParentType.prototype.sayHobby = function() {
    return 'I like ' + this.hobby;
}
​
function ParentType(gender, hobby) {
    this.hobby = 'singing';
​
    // 利用经典继承,借用构造函数
    SuperType.call(this, gender)
}
​
let child1_type = new ParentType('female')
let child2_type = new ParentType('male')
​
console.log(child1_type.greet()); //hello~,佩奇
console.log(child1_type.sayHobby()); //I like singing
console.log(child1_type.card); //{ name: '佩奇', age: 20, gender: 'female' }
​
child2_type.card.name = '黑猫警长';
console.log(child2_type.card); //{ name: '黑猫警长', age: 20, gender: 'male' }

(1)优点

解决了原型链继承和借用构造函数继承造成的影响

(2)不足

无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部

以上三种继承方式,是当构造函数return存在引用类型的时候。接下来看看对象上的继承。

原型式继承

1 方法一:借用构造函数

借用构造函数,在一个函数内部创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。

本质上,函数对传入的对象执行了一次浅复制。引用类型的属性始终会被继承和共享。

// 借用构造函数
function helperFun(targetObj) {
    function newFun() {}
    newFun.prototype = targetObj;
    return new newFun();
}
​
let myObj = {
    username: 'dog',
    age: 14,
    other: {
        hobby: 'reading',
        taste: 'sore'
    }
}
​
console.log(myObj);
let newObj = helperFun(myObj)
newObj.username = 'cat';
newObj.other.hobby = 'sleeping';
console.log(newObj);
​
​

2、方法二、Object.create()

---把现有对象的属性,挂到新建对象的原型上,新建对象为空对象。

ES5通过增加Object.create()方法,将原型式继承的概念规范化了。该方法接收两个参数:作为新对象原型的对象、以及给新对象定义额外属性的对象(第二个可选)。在只有一个参数时,Object.create()与方法一效果相同。引用类型的属性始终会被继承和共享。

// Object.create()
let presentIbj = {
    username: '熊大',
    sex: 'male',
    age: 19,
    greet() {
        console.log('name is  ' + this.username);
    }
}
​
let newObj1 = Object.create(presentIbj);
let newObj2 = Object.create(presentIbj);
newObj1.username = '熊二';
console.log(newObj1);
console.log(newObj2);
​

** 寄生式继承**

寄生式继承就是创建一个仅用于封装继承过程的函数,该函数在内部以某种方式(使用原型式继承对一个目标对象进行浅复制,增强这个浅复制的能力)来增强对象,最后就好像像真的是它做了所有工作一样返回对象。引用类型的属性始终会被继承和共享。

// 寄生式继承
function helperFun(targetObj) {
    function newFun() {};
    newFun.prototype = targetObj;
    return new newFun();
}
​
function packInherit(originObj) {
    let evalObj = helperFun(originObj);
    evalObj.greet = function() {
        console.log('hello~,' + this.name);
    };
    return evalObj;
}
​
let initObj = {
    name: '虹猫',
    like: ['reading', 'running'],
    age: 20
}
​
let newObj1 = packInherit(initObj);
console.log(newObj1); //[ 'reading', 'running', '蓝兔' ]
newObj1.like.push("蓝兔");
console.log(newObj1.like); //[ 'reading', 'running', '蓝兔' ]
newObj1.greet(); //hello~,虹猫let newObj2 = packInherit(initObj);
newObj2.name = '蓝兔';
console.log(newObj2); //{ greet: [Function (anonymous)], name: '蓝兔' }
newObj2.greet(); //hello~,蓝兔
console.log(newObj2.like); //[ 'reading', 'running', '蓝兔' ]

(1)优点

上手简单,无需单独创建构造函数。

(2)不足

寄生式继承给对象添加函数会导致函数难以重用,因此不能做到函数复用而效率降低

寄生组合式继承

寄生组合继承是为降低父类构造函数的开销。通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

// 寄生组合式
Super.prototype.say = function() {
    console.log(this.name);
}
function Super(name) {
    this.name = name,
    this.colors = ['red', 'blue', 'green']
}
function Son(name, age) {
    this.age = age;
    Super.call(this, name)
}
​
var another = Object.create(Super.prototype);
another.constructor = Son;
var another = Object.assign(Son.prototype(), Super.prototype())
// Son.prototype = another;
var instance1 = new Son('duck', 19)
instance1.
instance1.colors.push('pink')
var instance2 = new Son('cat', 18)

1)优点

高效率,只调用一次父构造函数,并且避免了在子原型上添加不必要,多余的属性。与此同时,原型链还能保持不变

(2)不足

代码复杂 对比类的继承 ES6 的继承机制的实质是先将父类实例对象的属性和方法,加到this上面(所以先调用super方法),然后再用子类的构造函数修改this。

子类继承父类:class 子类 extends 父类;在子类的构造方法中调用父类的构造方法:super()。

// class继承
class Parent {
    constructor(name, gender) {
        this.name = name;
        this.gender = gender;
        this.greet = function() {
            console.log('greet');
        };
    }
    speak() {
        console.log("parent speak")
    }
​
    static speak() {
        console.log("static speak")
    }
}
​
//class 子类 extends 父类
class Son extends Parent {
    //在子类的构造方法中调用父类的构造方法
    constructor(name, gender, hobby) {
        super(name, gender);
        this.hobby = hobby;
    }
    //子类中声明的方法名和父类中的方法名相同时,子类中的方法将覆盖继承于父类的方法
    speak() {
        console.log("Son speak");
    }
}
const grandson = new Son('lucky', 'male', 'reading');
console.log(grandson.name, grandson.gender, grandson.hobby); //lucky male reading
grandson.greet(); //greet
grandson.speak(); //Son speak
Son.speak(); //static speak

内容来源: 链接:juejin.cn/post/715655…

33 JS内存管理以及垃圾回收机制(引用计数、标记清除)

JS 这类高级语言,隐藏了内存管理功能。但无论开发人员是否注意,内存管理都在那,所有编程语言最终要与操作系统打交道,在内存大小固定的硬件上工作。不幸的是,即使不考虑垃圾回收对性能的影响,2017 年最新的垃圾回收算法,也无法智能回收所有极端的情况。

唯有程序员自己才知道何时进行垃圾回收,而 JS 由于没有暴露显示内存管理接口,导致触发垃圾回收的代码看起来像“垃圾”,或者优化垃圾回收的代码段看起来不优雅、甚至不可读。

所以在 JS 这类高级语言中,有必要掌握基础内存分配原理,在对内存敏感的场景,比如 nodejs 代码做严格检查与优化。谨慎使用 dom 操作、主动删除没有业务意义的变量、避免提前优化、过度优化,在保证代码可读性的前提下,利用性能监控工具,通过调用栈定位问题代码。

不管什么程序语言,内存生命周期基本是一致的:

  1. 分配你所需要的内存
  2. 使用分配到的内存(读、写)
  3. 不需要时将其释放\归还

理论上这3点我们在编写 JavaScript 代码都不太需要去关注,毕竟 JavaScript 有一套自动的内存回收机制去做些事情。

我们之所以还是要去关注这些问题的原因是,这套自动回收机制在有些方面做的不够好,因此需要我们去手动销毁一些不再使用的内存。

** JavaScript 垃圾回收机制**

垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。

在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)。

引用计数

这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

var o = { 
  a: {
    b:2
  }
}; 
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集


var o2 = o; // o2变量是第二个对“这个对象”的引用

o = 1;      // 现在,“这个对象”只有一个o2变量的引用了,“这个对象”的原始引用o已经没有

var oa = o2.a; // 引用“这个对象”的a属性
               // 现在,“这个对象”有两个引用了,一个是o2,一个是oa

o2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
           // 但是它的属性a的对象还在被oa引用,所以还不能回收

oa = null; // a属性的那个对象现在也是零引用了
           // 它可以被垃圾回收了

缺点是:循环引用的问题

该算法有个限制:无法处理循环引用的事例。在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

** 标记清除**

标记清除算法将“不再使用的对象”定义为“无法达到的对象”。 简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使用的。 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。 从这个概念可以看出,无法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是无法触及的对象)。 但反之未必成立。

工作流程:

  1. 垃圾收集器会在运行的时候会给存储在内存中的所有变量都加上标记。
  2. 从根部出发将能触及到的对象的标记清除。
  3. 那些还存在标记的变量被视为准备删除的变量。
  4. 最后垃圾收集器会执行最后一步内存清除的工作,销毁那些带标记的值并回收它们所占用的内存空间。

循环引用不再是问题了,两个循环引用的对象在垃圾收集时从全局对象出发无法再获取他们的引用。 因此,他们将会被垃圾回收器回收。

从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。

什么是内存泄露

本质上讲, 内存泄露就是不再被需要的内存, 由于某种原因, 无法被释放.

常见的内存泄露案例有哪些呢?

全局变量

function foo() {
    bar1 = 'some text'; // 没有声明变量 实际上是全局变量 => window.bar1
    this.bar2 = 'some text' // 全局变量 => window.bar2
}
foo();

意外的创建了两个全局变量 bar1 和 bar2

未销毁的定时器和回调函数

在很多库中, 如果使用了观察者模式, 都会提供回调方法, 来调用一些回调函数。 要记得回收这些回调函数。举一个 setInterval 的例子:

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); // 每 5 秒调用一次

如果后续 renderer 元素被移除,整个定时器实际上没有任何作用。 但如果你没有回收定时器,整个定时器依然有效, 不但定时器无法被内存回收, 定时器函数中的依赖也无法回收。在这个案例中的 serverData 也无法被回收。

闭包

在 JS 开发中,我们会经常用到闭包,一个内部函数,有权访问包含其的外部函数中的变量。 下面这种情况下,闭包也会造成内存泄露:

var closure = function(){
    var count = 0;
    return function(){
       return count ++;
    }
}
const fn = closure(); 
console.log(fn()); // 0
console.log(fn()); // 1
console.log(fn()); // 2

每次调用fn时,count值都基于上一次的值增加1,说明count的引用一直保存在内存中得不到销毁。只能手动进行销毁fn=null;

DOM 引用

很多时候, 我们对 Dom 的操作, 会把 Dom 的引用保存在一个数组或者 Map 中

var elements = {
    image: document.getElementById('image')
};
function doStuff() {
    elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
    document.body.removeChild(document.getElementById('image'));
    // 这个时候我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收.
}

上述案例中, 即使我们对于 image 元素进行了移除, 但是仍然有对 image 元素的引用, 依然无法对其进行内存回收

如何理解 ES6 中 WeakSet 与 WeakMap WeakMap的键名所指向的对象,不计入垃圾回收机制。(标记清除算法查找时不会去查找WeakMap的键的引用,当WeakMap的键没有其它引用时,内存便会自动被回收)

[注意] WeakMap的key必须是一个对象且不能是null

var wm = new WeakMap();

var element = document.getElementById('example');

wm.set(element, 'some information');
wm.get(element) // "some information"

它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。 因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

当该 DOM 元素被清除,其所对应的WeakMap记录就会自动被移除。

document.body.removeChild(document.getElementById('example'));

内容来源: 链接:juejin.cn/post/684490…

34 跨域常用的实现方法?

  • JSONP:通过动态创建 script 标签,向其他域名发起 GET 请求,从而获得数据。

  • CORS:在服务器端设置响应头,允许其他域名的请求访问数据。

  • 代理:在自己的服务器端设置代理,将跨域请求转发到其他域名的服务器上,从而获得数据。

  • PostMessage:使用 HTML5 新增的 API,在不同的窗口之间进行安全的跨域通信。

  • WebSocket:使用 HTML5 新增的 API,实现全双工通信,从而避免跨域问题。

35 从输入 url 到渲染出页面的整个过程

①下载资源:各个资源类型,下载过程。

  1. 浏览器判断内容
  2. 查找缓存
  3. DNS 解析:首先需要将 URL 解析成 IP 地址。浏览器会先查找本地 DNS 缓存,如果没有找到则向本地 DNS 服务器发送请求,获取目标服务器的 IP 地址。
  4. TCP 连接:通过 IP 地址和端口号建立 TCP 连接,发起三次握手。
  5. 发送 HTTP 请求:使用 HTTP 协议向目标服务器发送请求,请求中包含请求方法、请求头、请求体等信息。
  6. 服务器处理请求并返回响应:服务器接收到请求后,根据请求内容进行处理,并返回响应。响应中包含状态码、响应头、响应体等信息。
  7. 浏览器解析响应内容:浏览器接收到响应后,根据响应头中的内容判断响应类型,如果是 HTML 页面,则开始解析 HTML 文档。

②渲染页面:

  1. 构建 DOM 树:浏览器解析 HTML 文档,根据 HTML 标记构建出 DOM 树。
  2. 构建 CSSOM 树:浏览器解析 CSS 样式表,构建出 CSSOM 树。
  3. 合并 DOM 树和 CSSOM 树:将 DOM 树和 CSSOM 树合并,生成渲染树。
  4. 布局和绘制:根据渲染树中的信息进行布局和绘制,生成位图并显示在屏幕上。遇到
  5. 网络资源加载:在上述过程中,如果页面中包含了其他资源(如图片、脚本、样式表等),则需要再次向服务器发起请求,获取这些资源并进行渲染。

36 ES6

ES6新特性

1、箭头函数的使用

2、class关键字与继承的使用

3、Promise的使用

4、使用关键字 import 导入模块(ES5用require)

5、数据类型添加Symblo,Set,Map

6、let const

let、const和var的区别?

var存在变量提升;

let const不存在变量提升,只在自己的块级作用域中起作用,存在暂时性死区。

const声明之后要马上赋值,数值变量赋值后补课更改,引用数据类型赋值后可更改。

ES6中对象新增的方法有哪些?

1、Object.assign()方法用于对象的合并,将源对象的所有可枚举属性,复制到目标对象(target)。

2、Object.is它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。值和对象类型的值都可以,NAN这种特殊值也可以处理。

3、Object.values方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值

4、Object.keys方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名

class和function的区别

相同点:1. 函数作为构造函数

不同点:

  1. class构造函数必须使用new操作符
  2. class声明不可以提升
  3. class不可以用call、apply、bind改变this指向。 ** Symbol 含义及使用方法** symbol 英文意思为 符号、象征、标记、记号,在 js 中更确切的翻译应该为 独一无二的值。

使用: 作为对象属性 、模拟类的私有方法

如何理解和使用Promise.all和Promise.race

Pomise.all的使用

Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。

Promise.race的使用

顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。

set和map数据结构

set 实例属性和方法:

  • add(value):添加某个值,返回Set结构本身。
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员,没有返回值。
s.add(1).add(3).add(3);
// 注意3被加入了两次

s.size // 2

s.has(1) // true
s.has(2) // false

s.delete(3);
s.has(3) // false

遍历操作:

  • keys():返回键名的遍历器
  • values():返回键值的遍历器
  • entries():返回键值对的遍历器
  • forEach():使用回调函数遍历每个成员

Set的遍历顺序就是插入顺序,这个特性有时非常有用,比如使用Set保存一个回调函数列表,调用时就能保证按照添加顺序调用。

// 数组去重
let arr = [1223];
let unique = [...new Set(arr)];
// or
function dedupe(array) {
  return Array.from(new Set(array));
}

let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}

// 差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}

map 实例属性和方法:

  • size属性: 返回Map结构的成员总数
  • set(key, value): set方法设置key所对应的键值,然后返回整个Map结构。如果key已经有值,则键值会被更新,否则就新生成该键,set方法返回的是Map本身,因此可以采用链式写法
  • get(key) : get方法读取key对应的键值,如果找不到key,返回undefined
  • has(key) : has方法返回一个布尔值,表示某个键是否在Map数据结构中
  • delete(key) : delete方法删除某个键,返回true。如果删除失败,返回false
  • clear() : clear方法清除所有成员,没有返回值

遍历方法和set类似,Map结构转为数组结构,比较快速的方法是结合使用扩展运算符(...):

let map = new Map([  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

[...map.keys()]
// [1, 2, 3]

[...map.values()]
// ['one', 'two', 'three']

[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]

[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]

数组转map:

new Map([[true, 7], [{foo: 3}, ['abc']]])
// Map {true => 7, Object {foo: 3} => ['abc']}

Map转为对象:

function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}

let myMap = new Map().set('yes', true).set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }

对象转为Map

function objToStrMap(obj) {
  let strMap = new Map();
  for (let k of Object.keys(obj)) {
    strMap.set(k, obj[k]);
  }
  return strMap;
}

objToStrMap({yes: true, no: false})
// [ [ 'yes', true ], [ 'no', false ] ]

Proxy

在ES6中全新设计了一个叫Proxy的类型,Proxy 这个词的原意是代理,用在这里表示由它来"代理"某些操作,可以译为"代理器",Proxy就是专门为对象设置访问代理器的,无论是读还是写都要经过代理,通过Proxy就能轻松监视对象的读写过程

如何使用Proxy监视对象的读写过程呢?定义一个person对象,对象当中有一个name属性和height属性,然后通过new Proxy的方式为person创建一个代理对象

Proxy的构造函数需要2个参数,一个是需要代理的目标对象,另一个是代理的处理对象,在这个处理对象中可以通过get()方法监视对象属性的访问,通过set()方法监视对象设置属性的过程

const person={
    name:'zzz',
    height:185
}
const proxy=new Proxy(person,{
    get(){//监视对象属性的访问

    },
    set(){//监视对象设置属性的过程

    }
})

get()方法可以接收两个参数,第一个是代理的目标对象,第二个是访问的属性名

set()方法正常的逻辑应该是为代理目标设置指定属性,在设置之前先做一些数据校验,例如属性名为height,那么那么就要判断它的是否是一个数字,不是就抛出错误

相比于Object.defineProperty,Proxy更为强大

defineProperty只能监视属性的读写,而Proxy可以监视到更多的对象操作,例如delete操作,对对象当中方法的调用......

在Proxy对象的处理对象当中添加一个deleteProperty()代理方法,这个方法会在外部在对代理对象进行操作的时候自动执行

这个方法接收两个参数,代理目标对象和所要删除的属性名称,在这方法内部打印一下要删除的属性名称,然后正常执行delete操作,之后在外部去进行delete操作

这是defineProperty做不到的,除了delete以外还有很多其他的对象操作能被监视到:

  • apply(tagert,obj,args):拦截函数的调用、callapply操作
  • has(tagert,key):判断对象是否具有某个属性
  • construct(tagert,args):拦截new命令
  • deleteProperty(tagert,key):拦截delete操作
  • defineProperty(tagert,key,desc):拦截Object.defineProperty()操作
  • getOwnPropertyDescriptor(tagert,key):拦截Object.getOwnPropertyDescriptor()
  • getPrototypeOf(tagert):拦截获取对象原型
  • isExtensible(tagert):拦截Object.isExtensible()操作
  • ownKeys(tagert):拦截对象自身属性的读取操作
  • preventExtensions(tagert):拦截Object.preventExtensions()
  • setPrototypeOf(tagert,proto):拦截Object.setPrototypeOf()方法

Proxy能更好的支持数组对象的监视

以往通过Object.defineProperty监视数组的操作最常见的就是通过重写数组的操作方法了,在vue.js里也是使用这种方式通过自定义方法覆盖掉数组原型对象上的push(),shift()等方法,以此来劫持对应方法调用的过程

那我们如何使用Proxy对象来监视数组?先定义一个数组,为这个数组定义一个Proxy对象,在这个Proxy对象的处理对象上添加set()方法监视数据的写入,将写入的值赋值给目标对象对应的属性名,然后返回一个true代表写入成功

三、vue

1 为什么 data 在组件内必须是函数,而 vue 的根实例则没有此限制?

在 Vue 组件中,data 必须是一个函数,因为组件可能会被多次实例化。如果 data 是一个对象,那么所有组件实例将共享相同的数据对象。这样一来,当一个组件实例修改了数据时,所有其他实例的数据也会被更改,这是我们不希望看到的。使用函数可以确保每次实例化组件时,都会返回一个全新的数据对象副本,从而保证组件实例之间的数据隔离。

在 Vue 的根实例中,这个限制并不适用。根实例只会被创建一次,因此不需要担心多个实例共享相同的数据对象。然而,将 data 作为函数返回对象的方式也是可以在根实例中使用的,这样可以保持一致性。但在实际开发中,为了简便,我们通常在根实例中直接使用对象作为 data

2 vue2中选项props、data、method、computed、watch的优先级?

在 Vue2 中,props、data、methods、computed 和 watch 这些选项都扮演着不同的角色,因此它们之间没有严格的优先级顺序。然而,我们可以从两个方面来理解这些选项的关系和执行顺序:数据初始化和数据更新。

  1. 数据初始化:

在 Vue 组件实例创建和挂载的过程中,各选项的执行顺序如下:

a) props:父组件向子组件传递数据。当子组件实例创建时,首先从父组件接收 props 数据。

b) data:组件的本地状态数据。在接收 props 数据后,组件会初始化 data。

c) methods:用于在组件中定义各种方法。methods 选项在 data 初始化之后定义,这样可以在其他地方(如计算属性或侦听器)调用这些方法。

d) computed:计算属性是基于其他数据(如 props、data、methods 等)计算而来的。因为计算属性依赖于其他数据,所以它们在 data 和 methods 初始化之后计算。

e) watch:侦听器用于观察和响应 Vue 实例上的数据变化。watch 选项在数据初始化完成之后设置,以便在数据发生变化时触发回调函数。

  1. 数据更新:

当组件的数据发生变化时,Vue 会根据依赖关系来更新 computed 和 watch。在这种情况下,它们的执行顺序如下:

a) 数据变化:当 props 或 data 中的数据发生变化时,会触发更新。

b) computed:当依赖的数据发生变化时,计算属性会重新计算。由于计算属性具有缓存机制,只有当依赖数据发生变化时,它们才会重新计算。

c) watch:当被观察的数据发生变化时,侦听器会触发相应的回调函数。与计算属性不同,侦听器没有缓存机制,每次数据变化都会触发回调函数。

总结:在 Vue2 中,props、data、methods、computed 和 watch 这些选项都扮演着不同的角色。在组件实例创建和挂载的过程中,各选项按照特定顺序执行。在数据更新时,computed 和 watch 根据依赖关系来触发更新。

3 谈谈你对vue2以及vue3双向绑定原理的理解

Vue.js 是一个用于构建用户界面的渐进式 JavaScript 框架。Vue 具有响应式数据绑定功能,使得数据和 DOM 之间能够双向绑定。Vue2 和 Vue3 的双向绑定原理有所不同,接下来分别介绍它们的实现原理:

  1. Vue2 双向绑定原理:

Vue2 使用的双向绑定核心原理是基于数据劫持和发布-订阅模式。Vue2 的双向绑定分为两部分:数据劫持(通过 Object.defineProperty()) 和 Watcher 类。

  • 数据劫持:Vue2 使用 Object.defineProperty() 方法劫持数据对象的属性,对属性的 getter 和 setter 进行拦截。当属性值被访问或修改时,会触发 getter 和 setter,实现数据的响应式。
  • Watcher 类:Watcher 用于订阅数据变化和更新视图。每个数据属性都有一个 Watcher 实例,当数据发生变化时,触发 setter,并通知 Watcher,然后 Watcher 会调用其更新函数,将新值应用到 DOM。
  1. Vue3 双向绑定原理:

Vue3 的双向绑定原理基于 Proxy 和 Reflect。Vue3 使用 Proxy 对象对数据进行代理,而不是像 Vue2 那样使用 Object.defineProperty() 进行数据劫持。

  • Proxy:Vue3 使用 Proxy 对象创建一个数据代理,当代理对象的属性被访问或修改时,会触发 Proxy 的拦截器函数(如 get 或 set),实现数据的响应式。
  • Reflect:Vue3 使用 Reflect API 进行对象操作,如获取属性值、设置属性值等。Reflect API 提供了一种更简洁、安全的方法来操作对象,同时具有更好的性能。

Vue3 相较于 Vue2 的优势:

  • Vue3 使用 Proxy 代替 Object.defineProperty(),可以直接监听对象的变化,而不仅仅是属性。这解决了 Vue2 中无法监听数组变化和对象属性添加的问题。
  • Vue3 使用 Proxy 可以提高性能,因为 Proxy 是原生支持的,而 Object.defineProperty() 是基于 JavaScript 层面的劫持。
  • Vue3 代码结构更简洁,易于维护。

总结:Vue2 和 Vue3 的双向绑定原理都是基于数据劫持,但它们使用的技术实现方式不同。Vue2 使用 Object.defineProperty() 和发布-订阅模式,而 Vue3 使用 Proxy 和 Reflect。Vue3 相对于 Vue2 在性能和功能上有所改进。

4 Vue中响应式属性、dep以及watcher之间的关系是什么?

Vue.js 中的响应式系统是其核心特性之一,该系统使得 Vue.js 可以在数据改变时更新视图。这个响应式系统主要包含以下三个核心概念:响应式属性、Dep(依赖)以及 Watcher(观察者)。

  1. 响应式属性:在 Vue.js 中,当我们在 data 对象中定义属性时,Vue.js 会将这些属性转化为 getter/setter 形式,这样就实现了响应式。当我们访问或修改一个属性时,getter/setter 就会被调用。

  2. Dep(依赖):Dep 可以看作是一个订阅器,它维护着一个 Watcher 列表,当响应式属性被修改时,Dep 会通知它的所有 Watcher,告诉它们数据已经被更新。Dep 在 getter 中收集 Watcher,在 setter 中触发 Watcher 更新。

  3. Watcher(观察者):Watcher 是一个观察者对象,它观察某个响应式属性的变化。当响应式属性的 getter 被访问时,Dep 会将当前的 Watcher 添加到自己的订阅者列表中。当响应式属性被修改时,Dep 会通知 Watcher,然后 Watcher 会执行相应的操作(比如更新视图)。

所以,响应式属性、Dep 和 Watcher 之间的关系可以这样理解:响应式属性是被观察的目标,Dep 是观察者(Watcher)和目标(响应式属性)之间的桥梁,它负责添加观察者,也负责在目标发生改变时通知观察者。Watcher 则是观察者,它观察响应式属性的变化,当变化发生时,执行相应的操作。

总的来说,这种关系构成了 Vue.js 的响应式系统,使得 Vue.js 可以在数据改变时自动更新视图。

5 Vue中,Watcher有哪些类型?

在 Vue.js 中,Watcher(观察者)是响应式系统的重要组成部分,它用于在某个数据发生变化时执行特定的回调函数。在 Vue.js 中,主要有以下几种类型的 Watcher:

  1. 渲染 Watcher:每一个组件实例都有对应的一个渲染 Watcher。当组件的数据变化时,渲染 Watcher 会被触发,进而重新渲染组件。这种类型的 Watcher 是 Vue 内部自动创建的,用于保证组件视图的更新。

  2. 用户 Watcher:这是用户通过 vm.$watch() API 或组件的 watch 选项创建的 Watcher。用户可以通过这种方式来监听某个数据的变化,并在变化时执行特定的回调函数。

  3. 计算属性 Watcher:这种类型的 Watcher 用于计算属性(computed property)。当计算属性所依赖的数据发生变化时,计算属性 Watcher 会被触发,从而重新计算属性的值。

以上就是 Vue.js 中主要的几种 Watcher。这些 Watcher 都在 Vue 的响应式系统中起到了关键的作用,使得 Vue 可以在数据改变时自动更新视图。

6 vue2和vue3分别的父组件和子组件的渲染时机?

vue2:

  • 初始化渲染时机

    父beforeCreate => 父created => 父beforeMount => 子beforeCreate => 子created => 子beforeMount => 子mounted => 父mounted

  • 更新过程

    父beforeUpdate => 子beforeUpdate => 子updated => 父updated

  • 销毁过程

    父beforeDestory => 子beforeDestory => 子destoryed => 父destoryed

vue3:

  • 初始化渲染时机

    父setup => 父beforeCreate => 父created => 父beforeMount => 子setup => 子beforeCreate => 子created => 子beforeMount => 子mounted => 父mounted

  • 更新过程

    父beforeUpdate => 子beforeUpdate => 子updated => 父updated

  • 销毁过程

    父beforeUnmount => 子beforeUnmount => 子unmounted => 父unmounted

7 谈谈你对vue2以及vue3整个渲染过程的理解

Vue.js 是一个用于构建用户界面的渐进式 JavaScript 框架。Vue2 和 Vue3 的渲染过程有所不同,下面分别介绍它们的渲染过程:

  1. Vue2 渲染过程:

Vue2 的渲染过程主要包括以下步骤:

  1. 解析模板:Vue2 使用基于 HTML 的模板语法。Vue 会将模板解析成抽象语法树(AST)。

  2. 生成渲染函数:Vue2 会将 AST 转换为渲染函数(render function)。渲染函数是一个纯 JavaScript 函数,用于创建和更新虚拟 DOM 树。

  3. 响应式数据:Vue2 使用 Object.defineProperty() 为数据对象创建 getter 和 setter。当数据发生变化时,会触发 setter,并通知对应的 Watcher 实例。

  4. 创建 Watcher:对于每个数据属性,Vue2 会创建一个 Watcher 实例。Watcher 负责订阅数据变化,并在数据更新时调用渲染函数。

  5. 首次渲染:在实例创建时,Vue2 会调用渲染函数生成虚拟 DOM 树,并将其映射到实际的 DOM 节点上。

  6. 更新:当数据发生变化时,Vue2 会重新调用渲染函数生成新的虚拟 DOM 树。然后,使用虚拟 DOM 的 diff 算法(称为 patching)找出变化的部分,并更新实际的 DOM。

Vue3 渲染过程:

Vue3 的渲染过程与 Vue2 类似,但有一些关键的改进和优化。以下是 Vue3 渲染过程的主要步骤:

  1. 解析模板:Vue3 同样使用基于 HTML 的模板语法,并将模板解析成抽象语法树(AST)。
  2. 生成渲染函数:Vue3 会将 AST 转换为渲染函数。Vue3 的渲染函数使用了一种新的编译策略,称为 "优化模式",可以在编译阶段静态地分析模板中的动态绑定,并生成更高效的代码。
  3. 响应式数据:Vue3 使用 Proxy 对象代替 Object.defineProperty(),创建数据的响应式代理。这提供了更好的性能和更广泛的数据监听能力。
  4. 创建 Watcher:Vue3 中仍然使用 Watcher 实例订阅数据变化,并在数据更新时调用渲染函数。但 Vue3 的 Watcher 实现有所优化,减少了不必要的计算和渲染。
  5. 首次渲染:与 Vue2 类似,Vue3 会在实例创建时调用渲染函数生成虚拟 DOM 树,并将其映射到实际的 DOM节点上。
  6. 更新:当数据发生变化时,Vue3 会重新调用渲染函数生成新的虚拟 DOM 树。然后,使用优化后的虚拟 DOM diff 算法找出变化的部分,并更新实际的 DOM。Vue3 的 diff 算法经过优化,可以更快地找到差异并更新 DOM。
  7. 组合式 API:Vue3 引入了组合式 API,它是一种更灵活的组织和复用组件逻辑的方式。使用组合式 API,可以更容易地将代码分解为独立的、可重用的函数。这对于更大型的项目和更复杂的组件逻辑非常有用。

总结:Vue2 和 Vue3 的渲染过程有很多相似之处,如解析模板、生成渲染函数、响应式数据、创建 Watcher、首次渲染和更新。然而,Vue3 在性能、响应式系统和组件逻辑复用方面进行了优化和改进,提供了更高效和灵活的渲染过程。

8 说下nextTick的原理

Vue 的 nextTick 函数是一个非常实用的工具方法,它允许我们在 DOM 更新完成后延迟执行一个回调函数。这在某些情况下非常有用,例如当你需要在数据变化后操作 DOM 元}{素时。接下来我们来详细了解 nextTick 的原理。

Vue 中的数据变化是异步的。当数据发生变化时,Vue 不会立即更新 DOM,而是将更新任务推入一个队列。在同一事件循环中发生的所有数据变化都会被加入到这个队列中。在下一个事件循环(也就是下一个 "tick")开始时,Vue 会清空队列,并批量执行 DOM 更新。这种机制可以避免不必要的 DOM 更新,从而提高性能。

nextTick 的作用就是在这个队列清空并且 DOM 更新完成后,执行我们传给它的回调函数。这样我们可以确保回调函数在 DOM 更新后执行,让我们可以安全地操作已经更新过的 DOM 元素。

为了实现 nextTick,Vue 使用了一个任务队列和一种任务调度策略。具体实现取决于浏览器支持的 API。Vue 首选使用 Promise.then()MutationObserversetImmediate 进行异步调度。如果浏览器不支持这些 API,Vue 会退回到使用 setTimeout(fn, 0)

总结,nextTick 的原理是基于 Vue 的异步更新队列和任务调度策略。通过使用 nextTick,我们可以在 DOM 更新完成后执行回调函数,确保在操作 DOM 时,数据已经被更新。

9 谈谈你对keep-alive的理解

keep-alive 是 Vue 中的一个内置组件,它用于缓存组件的状态以提高性能。当我们在不同组件之间切换时,通常组件会被销毁并重新创建。然而,在某些情况下,我们可能希望保留组件的状态,以避免不必要的重新渲染。这时,我们可以使用 keep-alive 组件来实现这个目的。

以下是关于 keep-alive 的一些关键点:

  1. 缓存组件:将组件包裹在 keep-alive 标签内,可以使其状态得到缓存。当组件被切换时,它不会被销毁,而是被缓存起来。当组件重新被激活时,它的状态会被恢复,而不是重新创建。
  2. 生命周期钩子:当组件被 keep-alive 包裹时,组件的生命周期钩子会发生变化。组件在被激活和停用时,分别触发 activateddeactivated 生命周期钩子。这使得我们可以在这两个钩子函数中执行一些特定的逻辑,如获取数据或重置状态。
  3. 包含和排除组件:keep-alive 组件提供了 includeexclude 属性,允许我们有选择地缓存特定的组件。我们可以通过组件名称或正则表达式来指定要缓存的组件。
  4. 缓存策略:keep-alive 还提供了一个 max 属性,允许我们设置缓存组件的最大数量。当缓存组件的数量超过这个限制时,最早的组件会被销毁。

总结:keep-alive 是 Vue 的内置组件,用于缓存组件状态以提高性能。通过将组件包裹在 keep-alive 标签内,我们可以在不同组件之间切换时保留它们的状态。keep-alive 还提供了一些属性来控制缓存行为,如包含和排除组件、设置缓存最大数量等。同时,keep-alive 影响了组件的生命周期钩子,引入了 activateddeactivated 钩子。

10 讲讲vue组件之间的通信

组件通信有如下分类:

  • 父子组件之间的通信
    • props/$emit
    • $parent/$children
    • ref
    • provide/inject
    • $attrs/$listeners => vue3已移除
    • $on/$emit => vue3已移除
  • 兄弟组件之间的通信
    • eventBus
    • vuex
  • 跨级通信
    • eventBus
    • vuex
    • provide/inject
    • $attrs/$listeners
    • $on/$emit

这里讲下eventBus,eventBus又称为事件总线,在vue中可以用来作为组件间的沟通桥梁,所有组件公用相同的事件中心,可以向该中心发送事件和监听事件。eventBus的缺点是就是当项目较大时,容易造成难以维护的灾难。

// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()

// Children1.vue
this.$bus.$emit('foo') 

// Children2.vue
this.$bus.$on('foo', this.handle) 

11 谈谈你对vue2以及vue3生命周期的理解

Vue 生命周期指的是 Vue 组件从创建到销毁经历的不同阶段。在组件的生命周期中,Vue 提供了一系列生命周期钩子函数,允许我们在特定时刻执行一些自定义逻辑。Vue2 和 Vue3 的生命周期钩子有些许不同,下面分别介绍它们。

  1. Vue2 生命周期钩子:

a) beforeCreate:在实例创建之后,数据观测、属性计算等初始化之前触发。

b) created:在实例创建完成后,数据观测、属性计算等已经初始化完毕,但尚未开始 DOM 编译和挂载。

c) beforeMount:在模板编译完成、挂载 DOM 之前触发。此时,虚拟 DOM 已创建,真实 DOM 尚未更新。

d) mounted:在模板编译完成、挂载 DOM 之后触发。此时,真实 DOM 已经更新。

e) beforeUpdate:在数据发生变化,组件重新渲染之前触发。此时,可以获取到旧的 DOM 结构。

f) updated:在数据发生变化,组件重新渲染并更新 DOM 之后触发。此时,可以获取到新的 DOM 结构。

g) beforeDestroy:在实例销毁之前触发。此时,实例仍然完全可用。

h) destroyed:在实例销毁之后触发。此时,实例的所有指令绑定、事件监听器等都已经解除。

  1. Vue3 生命周期钩子:

Vue3 的生命周期钩子基本与 Vue2 类似,但有一些命名上的变化。这些变化主要是为了与 Vue3 的组合式 API 保持一致:

a) beforeCreate -> setup:在 Vue3 中,setup 函数取代了 beforeCreate 和 created 生命周期钩子。组件的数据和方法在 setup 函数中定义。

b) created:由于有了 setup 函数,created 生命周期钩子在 Vue3 中不再使用。

c) beforeMount:与 Vue2 中相同。

d) mounted:与 Vue2 中相同。

e) beforeUpdate:与 Vue2 中相同。

f) updated:与 Vue2 中相同。

g) beforeUnmount:Vue3 中将 beforeDestroy 重命名为 beforeUnmount。

h) unmounted:Vue3 中将 destroyed 重命名为 unmounted。

总结:Vue2 和 Vue3 的生命周期钩子基本相似,允许我们在组件的不同阶段执行自定义逻辑。主要区别在于 Vue3 引入了 setup 函数取代了 beforeCreate 和 created 生命周期钩子,并将 beforeDestroy 和 destroyed 重命名为 beforeUnmount 和 unmounted。这些变化使得 Vue3 生命周期钩子与组合式 API 保持一致。

12 什么情况下会创建Watcher(观察者)?什么情况下会创建Dep(依赖容器)对象?

在 Vue.js 中,Watcher 对象和 Dep 对象的创建时机如下:

  1. 创建 Watcher 对象的情况:
  • 编译模板:在编译模板时,Vue.js 解析模板中的指令(如 v-model、v-bind 等)和插值表达式(如 {{message}}),为每个指令或表达式创建一个 Watcher 对象。这些 Watcher 对象负责监听数据变化并在数据发生变化时更新视图。
  • 手动实例化:当需要手动监控某个表达式或计算属性时,可以创建一个 Watcher 对象。例如,在 Vue 组件中,可以使用 vm.$watch() 方法创建一个 Watcher 对象以监听某个数据属性或计算属性的变化。
  1. 创建 Dep 对象的情况:
  • 响应式数据:当 Vue 组件实例化时,Vue.js 会遍历组件的 data 对象。对于 data 对象中的每个属性,Vue.js 使用 Object.defineProperty() 方法进行劫持。在这个过程中,会为每个属性创建一个 Dep 对象。Dep 对象(依赖容器)负责收集所有与该属性相关的 Watcher 对象(观察者)。当属性被访问时,Dep 会将当前的 Watcher 对象添加到其依赖列表中,实现依赖收集。

总结一下,Watcher 对象主要在编译模板和手动监控表达式或计算属性时创建。Dep 对象主要在 Vue 组件实例化时为 data 对象中的每个属性创建。这两种对象共同构成了 Vue.js 的响应式系统,实现数据与视图之间的双向绑定。

13 vue3相比vue2新增了什么?

Vue3 相对于 Vue2 引入了许多新特性和优化,这些变化使得 Vue3 在性能、可扩展性和易用性方面有了很大提升。以下是 Vue3 相比于 Vue2 的主要新增内容:

  1. Composition API:Vue3 引入了组合式 API,这是一种新的组件逻辑组织方式,允许更灵活地复用和组合组件逻辑。相比于 Vue2 的选项式 API,组合式 API 更容易让我们在大型项目中管理和维护代码。
  2. 更好的性能:Vue3 在性能方面进行了很多优化,包括更小的打包体积、更快的渲染速度以及更高效的组件更新。这些优化使得 Vue3 的性能比 Vue2 更强大。
  3. 更小的体积:Vue3 的编译器和运行时都经过了优化,使得打包后的体积更小。此外,Vue3 支持 tree-shaking,可以进一步减小最终构建文件的大小。
  4. 更好的 TypeScript 支持:Vue3 的源代码完全使用 TypeScript 重写,因此 Vue3 提供了更好的 TypeScript 支持和类型推导。
  5. 新的生命周期钩子和更改:Vue3 为了与组合式 API 保持一致,对生命周期钩子进行了一些重命名,例如 beforeDestroy 变为 beforeUnmount,destroyed 变为 unmounted。同时,Vue3 引入了 setup 函数来代替 beforeCreate 和 created 生命周期钩子。
  6. 更强大的响应式系统:Vue3 使用 Proxy 对象重写了响应式系统,解决了 Vue2 中的一些限制(例如,对象属性的动态添加和删除)。新的响应式系统还提供了更好的性能和内存管理。
  7. Fragment 和 Teleport:Vue3 支持 Fragment(片段),允许一个组件具有多个根元素。此外,Vue3 引入了 Teleport 组件,可以将子组件渲染到 DOM 中的任意位置,解决了一些特殊场景下的渲染问题。
  8. Suspense:Vue3 引入了 Suspense 组件,允许我们在异步组件加载时展示一个 fallback 内容。这使得异步组件的加载和错误处理变得更加简单和优雅。

总结:Vue3 相比于 Vue2 引入了许多新特性和优化,包括组合式 API、更好的性能、更小的体积、更好的 TypeScript 支持、新的生命周期钩子和更改、更强大的响应式系统、Fragment 和 Teleport 组件以及 Suspense 组件。这些变化使得 Vue3 在性能、可扩展性和易用性方面有了很大提升。

14 谈谈你对Vuex以及Pinia的理解,以及它们之间的区别

Vuex 和 Pinia 都是 Vue.js 的状态管理库,它们帮助我们在 Vue 应用中管理和维护共享状态。这两者有一定的相似性,但也存在一些关键的区别。

  1. Vuex:

Vuex 是 Vue 官方推荐的状态管理库,适用于 Vue2 和 Vue3。它提供了一种集中式存储来管理应用程序中所有组件的状态。Vuex 的核心概念包括:

  • State:存储应用程序的状态数据。
  • Getters:从 state 中派生出新的状态,类似于计算属性。
  • Mutations:用于更改 state 的同步方法。
  • Actions:用于执行异步操作(例如 API 调用)并触发 mutations。

Vuex 遵循严格的单向数据流,确保状态更改的可预测性。同时,Vuex 还提供了一些开发者工具,帮助我们在开发过程中跟踪和调试状态更改。

  1. Pinia:

Pinia 是一个轻量级的状态管理库,专为 Vue3 设计。它充分利用了 Vue3 的组合式 API 和响应式系统,使得状态管理更加简洁和灵活。Pinia 的核心概念包括:

  • Store:存储应用程序的状态数据和相关方法。
  • State:用于存储状态的响应式对象。
  • Actions:用于执行异步操作和更改 state。

Pinia 的使用方法与 Vuex 类似,但其 API 更简洁,易于学习和使用。此外,Pinia 同样支持开发者工具,方便我们跟踪和调试状态更改。

  1. 区别:
  • 适用范围:Vuex 适用于 Vue2 和 Vue3,而 Pinia 专为 Vue3 设计。
  • API 设计:Pinia 的 API 更简洁,易于学习和使用。它充分利用了 Vue3 的组合式 API 和响应式系统。
  • 状态更新:Vuex 通过 mutations 和 actions 分别处理同步和异步状态更新,而 Pinia 将这两者合并为 actions。
  • 体积:Pinia 是一个轻量级库,相比于 Vuex 有更小的体积。
  • 生命周期:Pinia store 支持更好的生命周期管理,如 onBeforeMount、onMounted 等。

总结:Vuex 和 Pinia 都是 Vue 的状态管理库,用于管理和维护共享状态。它们之间的主要区别在于适用范围、API 设计、状态更新方式、体积和生命周期管理。对于 Vue3 项目,Pinia 可能是一个更轻量、更简洁的选择,但 Vuex 作为官方推荐的库,在稳定性和生态方面仍具有优势。

15 谈谈你对vue2以及vue3中diff算法的理解

Vue 的 diff 算法是用于在虚拟 DOM(Virtual DOM)更新过程中比较新旧两个虚拟节点树的差异,从而仅对有差异的部分进行真实 DOM 的更新,以提高性能。Vue2 和 Vue3 中的 diff 算法都基于 Snabbdom 库,但在 Vue3 中,diff 算法进行了一些优化,使得性能更高。

以下是对 Vue2 和 Vue3 中 diff 算法的理解:

  1. Vue2 diff 算法:

Vue2 的 diff 算法主要通过同级节点之间的比较来进行。在对比新旧虚拟节点时,它采用双端比较的策略。首先分别比较新旧虚拟节点树的头部和尾部节点,通过四种可能的情况进行节点的移动、删除和创建。具体步骤如下:

  • 如果新旧头部节点相同,将两个头部节点向后移动。
  • 如果新旧尾部节点相同,将两个尾部节点向前移动。
  • 如果旧头部节点和新尾部节点相同,将旧头部节点移动到尾部。
  • 如果旧尾部节点和新头部节点相同,将旧尾部节点移动到头部。

如果以上四种情况都不满足,Vue2 会创建一个新的 key 到 index 的映射表,然后遍历新的子节点,查找旧节点中是否存在相同的 key。如果找到相同的 key,将旧节点移动到正确的位置。否则,创建一个新节点并插入到正确的位置。最后,删除旧节点中未匹配的节点。

  1. Vue3 diff 算法:

Vue3 的 diff 算法在 Vue2 的基础上进行了优化。Vue3 利用了静态节点和动态节点的概念,通过对静态节点进行跳过,减少了不必要的比较。此外,Vue3 对于静态节点和动态节点的处理也进行了优化。在处理动态节点时,Vue3 使用了一个名为 lis(Longest Increasing Subsequence,最长递增子序列)的算法,通过查找最长递增子序列,找到需要移动的最少节点数量,从而减少节点移动操作,提高性能。

总结:Vue 的 diff 算法用于比较新旧虚拟节点树的差异,从而实现高效的 DOM 更新。Vue2 和 Vue3 的 diff 算法都基于 Snabbdom 库,采用双端比较策略。Vue3 在 Vue2 的基础上进行了优化,引入了静态节点和动态节点的概念,通过跳过静态节点的比较和使用 lis 算法减少节点移动操作,提高了性能。

尽管 Vue3 的 diff 算法相较于 Vue2 进行了优化,但在实际应用中,性能提升的程度还取决于组件的结构和数据变化。以下是一些建议,可以帮助我们在使用 Vue 时充分利用 diff 算法的优势:

  1. 使用 key:为列表中的每个节点分配唯一的 key,可以帮助 diff 算法更快地找到相应的节点,从而提高性能。尽量避免使用不稳定的值(例如随机数或索引)作为 key。
  2. 避免不必要的节点更新:尽量避免在没有实际更改的情况下触发组件的重新渲染。可以使用计算属性、watchers 和 Vue 的性能优化功能(如 shouldComponentUpdatekeep-alive)来减少不必要的渲染。
  3. 合理划分组件:将大型组件拆分为更小的子组件,以便更好地控制组件的更新。当某个子组件的状态发生变化时,只需更新该子组件,而不会影响其他子组件。
  4. 优化动态节点:在 Vue3 中,利用静态节点和动态节点的概念,确保动态节点的数量和位置合理。这可以帮助减少 diff 算法的计算量,提高性能。

通过了解 Vue2 和 Vue3 中的 diff 算法原理,并结合实际项目中的组件结构和数据变化情况,我们可以更好地利用 Vue 的性能优势,构建高效的前端应用。

16 为什么虚拟DOM会提高性能?

虚拟 DOM(Virtual DOM)是一种在内存中表示真实 DOM 的数据结构。它允许我们在内存中对 DOM 进行操作,而不是直接操作真实的 DOM。虚拟 DOM 的主要优势是性能提升,原因如下:

  1. 减少 DOM 操作次数:真实 DOM 的操作(如创建、更新、删除元素)通常比内存操作更耗时。虚拟 DOM 允许我们在内存中进行大量操作,然后一次性将这些操作应用到真实 DOM 上,减少了对真实 DOM 的操作次数。
  2. 最小化更新范围:虚拟 DOM 结合 diff 算法,可以找出新旧虚拟 DOM 之间的差异,从而仅对有差异的部分进行真实 DOM 的更新。这可以减少不必要的 DOM 操作,提高性能。
  3. 批量更新:当有多个更改需要应用到真实 DOM 时,虚拟 DOM 可以将这些更改合并为一次更新。这有助于避免因多次操作导致的布局抖动(Layout Thrashing)和重绘,从而提高性能。
  4. 更好的跨平台兼容性:虚拟 DOM 不仅可以表示 Web 页面中的 DOM,还可以表示其他平台的 UI(例如移动应用或桌面应用)。这意味着使用虚拟 DOM 的框架(如 Vue 或 React)可以更容易地实现跨平台应用程序,而不必为每个平台编写特定的代码。

虚拟 DOM 的性能提升并非绝对,它主要适用于大型应用和频繁更新的场景。对于简单的应用或更新较少的情况,虚拟 DOM 可能带来一定的开销。然而,在许多情况下,虚拟 DOM 提供了一种有效的方法来减少真实 DOM 操作,从而提高应用程序的性能。

17 谈谈你对纯函数的理解

在React中,纯函数指的是给定相同的输入,始终返回相同的输出,而且没有副作用的函数。它们不会改变其输入,也不会影响到系统的任何其他部分,例如修改全局变量、修改传入的对象等。

在React中,纯函数特别重要,因为当父组件的状态或属性改变时,React会重新渲染整个组件树。如果组件内部存在副作用,那么每次渲染时都会重新触发这些副作用,导致性能下降。而纯函数则不会有这个问题,它只会在必要的情况下被调用,从而优化了应用程序的性能。

18 为什么useState不推荐放在if判断里面

useState 是 React Hooks 的一部分,用于在函数式组件中引入状态。将 useState 放在 if 语句中是不被推荐的,因为这违反了 Hooks 的使用规则。具体来说,以下是几个关键原因:

  1. Hooks规则:React 需要保证在每次组件渲染时,Hooks 的调用顺序和次数保持一致。将 useState 放在条件语句(如 if)中可能导致不一致的调用顺序,从而使 React 无法正确追踪状态变化。
  2. 组件重新渲染:当组件重新渲染时,React 会根据先前的 Hooks 调用顺序来更新状态和执行副作用。如果将 useState 放在条件语句中,可能会导致组件在不同渲染阶段调用不同数量的 Hooks,进而引发错误。

为了遵循 Hooks 的使用规则,确保在函数式组件的顶层调用 useState。如果需要根据条件判断来决定是否使用状态,可以考虑将组件拆分成多个子组件,并在相应的子组件中使用 useState。这样可以保持 Hooks 的调用顺序一致,同时满足组件的逻辑需求。

19 谈谈你对函数式组件和类组件的理解

在React中,有两种主要的组件类型:函数式组件(Functional Component)和类组件(Class Component)。下面分别介绍它们的特点和区别。

函数式组件:

  1. 通过定义一个纯JavaScript函数来创建的,接收props作为参数并返回React元素。
  2. 在React 16.8之前,函数式组件仅支持接收props,不支持state和生命周期方法。
  3. 自React 16.8引入Hooks后,函数式组件可以使用useStateuseEffect等Hooks来实现状态管理和生命周期方法的功能。
  4. 函数式组件通常更简洁,易于阅读和测试。
  5. 在性能方面,由于没有生命周期方法和实例化过程,函数式组件在某些情况下可能比类组件更快。

类组件:

  1. 是通过定义一个继承自React.Component的JavaScript类来创建的,该类包含一个render方法,接收props和state作为输入,并返回React元素。
  2. 支持state和生命周期方法,如componentDidMountcomponentDidUpdatecomponentWillUnmount等。
  3. 需要通过this关键字来访问props和state。
  4. 类组件通常相对复杂,可能难以理解和测试。
  5. 在性能方面,由于有生命周期方法和实例化过程,类组件在某些情况下可能比函数式组件稍慢。

两者之间的主要区别:

  1. 定义方式:函数式组件是纯函数,而类组件是继承自React.Component的类。
  2. 状态管理:在React 16.8之前,只有类组件可以使用state。自React 16.8引入Hooks后,函数式组件也可以使用useState来管理状态。
  3. 生命周期方法:在React 16.8之前,只有类组件支持生命周期方法。自React 16.8引入Hooks后,函数式组件可以使用useEffect等Hooks来实现生命周期方法的功能。
  4. 语法和结构:函数式组件通常更简洁,而类组件可能相对复杂。
  5. 性能:由于没有生命周期方法和实例化过程,函数式组件在某些情况下可能比类组件更快。

总的来说,随着React Hooks的引入,函数式组件的功能已经基本与类组件相当。在实际开发中,建议优先使用函数式组件,以保持代码简洁和易于维护。当然,在一些特定场景下,例如需要使用到生命周期方法或者需要访问组件实例(如使用refs)时,类组件仍然具有一定的优势。

20 react和vue在列表渲染时都需要提供key,请问key有什么作用?

在 React 和 Vue 中,当你使用列表渲染时,需要为每个列表项提供一个唯一的 key。key 的作用主要有两点:

  1. 提高性能:

在列表渲染时,当列表中的元素发生变化(例如添加、删除或重新排序)时,React 和 Vue 都使用 diff 算法来比较新旧虚拟 DOM 树。通过为列表项分配唯一的 key,框架可以更快地识别和追踪每个元素的变化。这样,在列表更新时,只需重新渲染有所变化的元素,而不是整个列表,从而提高渲染性能。

  1. 保持组件状态:

在 React 和 Vue 中,组件实例的状态(state)和列表项是相关联的。如果列表项没有分配唯一的 key,框架将很难正确追踪组件实例与列表项之间的关系。这可能导致组件状态在更新时出现错误或丢失。

例如,如果你有一个包含输入框的列表,用户在输入框中输入了一些内容。当列表发生变化时,没有分配 key 的情况下,输入框的值可能会显示在错误的列表项中,或者完全丢失。

因此,为列表项分配唯一的 key 可以确保列表更新时,框架能够正确地追踪和保持组件实例的状态。

总之,在 React 和 Vue 中使用列表渲染时,为每个列表项提供一个唯一的 key 可以提高性能,并确保组件状态在更新过程中保持正确。通常,我们使用从后端获取的数据中的唯一标识(如 ID)作为 key,如果没有唯一标识,可以使用其他可靠且唯一的值。避免使用数组的索引作为 key,因为它可能会导致性能问题和状态错误。

21 vue和react框架之间有什么不同?

Vue 和 React 都是现代前端框架,分别由 Evan You 和 Facebook 团队开发。它们旨在帮助开发者构建高效、可维护的用户界面。尽管它们有许多相似之处,但在一些关键方面存在一些不同。以下是 Vue 和 React 之间的一些主要差异:

  1. 模板语法和 JSX:

Vue 使用模板语法,将 HTML、CSS 和 JavaScript 集成在一起。Vue 的模板是基于 HTML 的,这使得它们对于前端开发者来说非常熟悉。Vue 提供了一些特殊的属性和指令(如 v-for、v-if 等),以便于操作 DOM 和组件。

React 使用 JSX(JavaScript XML),它是一种 JavaScript 语法扩展,允许在 JavaScript 代码中编写类似 HTML 的结构。与 Vue 的模板语法不同,JSX 更接近于 JavaScript,需要熟悉 JavaScript 语法的开发者。

  1. 数据绑定:

Vue 提供了双向数据绑定,通过 v-model 指令可以轻松实现。这使得在表单元素和数据之间建立双向绑定变得非常简单。

React 默认使用单向数据流,父组件通过属性(props)将数据传递给子组件。实现双向数据绑定需要编写更多的代码,通常需要使用回调函数或状态管理库(如 Redux)。

  1. 组件通信:

Vue 为组件通信提供了内置的事件系统(通过 emitemit 和 on),以及 props。这使得在 Vue 应用中实现组件间通信相对简单。

React 使用 props 和回调函数进行组件间通信。虽然它没有内置的事件系统,但可以使用第三方库(如 Redux 或 MobX)来实现更复杂的通信。

  1. 生态系统:

Vue 拥有一个相对更小但紧密的生态系统。Vue 的官方库(如 Vuex、Vue Router 等)为开发者提供了许多功能。Vue 社区也积极维护了许多插件和库。

React 拥有一个庞大的生态系统,可以为开发者提供各种各样的解决方案。React 社区很大,拥有大量的库和插件,可以满足不同的需求。但是,这也意味着在选择最佳实践和工具时可能需要进行更多的研究。

  1. 学习曲线:

Vue 通常被认为具有较低的学习曲线,尤其是对于那些熟悉 HTML、CSS 和 JavaScript 的前端开发者。Vue 的文档易于理解,模板语法直观,使得初学者更容易上手。

React 的学习曲线可能会略高一些,因为 JSX 和函数式编程概念需要一些时间适应。然而,React 的文档也相当详细,并有大量的社区资源可供参考。

  1. 可扩展性:

Vue 为开发者提供了灵活的选项,可以根据项目的需求进行配置。Vue 提供了许多内置功能和官方库,有助于保持一致性和实现快速开发。

React 本身非常灵活,可以很好地与各种库和工具集成。这使得 React 更容易适应不同类型的项目。然而,这种灵活性也意味着开发者需要在选择最佳实践和工具时进行更多的研究。

  1. 性能:

Vue 和 React 都具有出色的性能。它们都使用虚拟 DOM 技术,通过高效地比较新旧虚拟 DOM 来实现最小化的真实 DOM 更新。尽管在大多数情况下性能差异不大,但根据应用程序的具体需求和实现方式,两者之间可能存在一些差异。

  1. 企业和社区支持:

React 由 Facebook 开发和维护,拥有大量的企业和社区支持。这使得 React 成为一个非常稳定和可靠的选择,特别是对于大型企业级应用程序。

Vue 是一个独立的开源项目,由 Evan You 和一个活跃的社区维护。Vue 在亚洲市场尤其受欢迎,但在全球范围内也越来越受到认可。虽然它可能没有 React 那样庞大的支持,但 Vue 仍然是一个非常可靠和稳定的框架。

总结:

Vue 和 React 分别有各自的优势和特点。Vue 的模板语法和双向数据绑定使其易于上手和快速开发,而 React 提供了高度灵活的架构和庞大的生态系统。在选择框架时,需要根据项目需求、团队经验和个人偏好来决定使用哪一个。

22 MVC和MVVM框架的区别?

MVC(Model-View-Controller)和 MVVM(Model-View-ViewModel)都是软件架构设计模式,用于分离应用程序的关注点,以提高可维护性和可扩展性。尽管它们有相似之处,但它们的实现方式和组件之间的交互有所不同。

  1. MVC(Model-View-Controller):
  • Model:代表应用程序的数据模型和业务逻辑。它负责处理数据存储和检索。
  • View:代表用户界面,展示数据给用户,并接收用户输入。
  • Controller:处理用户输入,将用户操作转换为 Model 更新,并通知 View 更新。

在 MVC 架构中,View 和 Controller 之间存在较强的依赖关系。用户输入由 Controller 处理,Controller 更新 Model,然后 Model 通知 View 更新。这样的双向通信使得 View 和 Controller 的耦合度较高。

  1. MVVM(Model-View-ViewModel):
  • Model:与 MVC 中的 Model 相同,代表应用程序的数据模型和业务逻辑。
  • View:代表用户界面,负责展示数据和接收用户输入。但在 MVVM 架构中,View 不直接与 Model 交互。
  • ViewModel:扮演 View 和 Model 之间的中介,负责将 Model 中的数据转换为 View 可以显示的数据,同时将 View 的用户输入转换为 Model 可以理解的操作。

MVVM 架构的关键特点是数据绑定(Data Binding),它允许 View 和 ViewModel 之间自动同步数据。这样,当 Model 数据发生变化时,View 会自动更新;当用户在 View 中进行操作时,ViewModel 会自动更新 Model。这种自动同步减少了 View 和 ViewModel 之间的直接交互,降低了它们之间的耦合度。

总结:MVC 和 MVVM 都是用于分离关注点的架构设计模式。MVC 通过 Controller 来处理用户输入并更新 Model 和 View,而 MVVM 利用 ViewModel 作为 Model 和 View 之间的中介,实现数据绑定。MVVM 架构相较于 MVC,降低了组件之间的耦合度,使得代码更易于维护和扩展。许多现代前端框架(如 Vue 和 React)采用了类似 MVVM 的设计模式。

四、前端工程化

1 对Webpack做过哪些优化?

Webpack 是一个非常强大的模块打包器,它可以帮助开发者处理代码依赖、打包、压缩等任务。在使用 Webpack 的过程中,我们可以通过一些方法优化项目,提高性能和用户体验。以下是一些常见的 Webpack 优化方法:

  1. Tree shaking:通过此技术,Webpack 可以消除未使用的代码,从而减少最终包的大小。为了实现这一点,确保在 package.json 文件中设置 "sideEffects" 选项。
  2. 代码分割(Code Splitting):将代码分割成不同的 chunks,从而实现按需加载和并行加载。这可以减少首次加载时间和浏览器解析时间。可以使用 SplitChunksPlugin 和动态 import() 实现代码分割。
  3. 懒加载(Lazy Loading):懒加载是一种按需加载策略,只有在实际需要时才加载某些代码。这可以显著减少首屏加载时间。
  4. 使用缓存:通过设置 cache-loader、HardSourceWebpackPlugin 或其他缓存插件,可以利用缓存加快构建速度。
  5. 压缩代码:使用插件如 TerserPlugin(用于 JavaScript)和 MiniCssExtractPlugin(用于 CSS)对代码进行压缩,减少代码体积,提高加载速度。
  6. 使用 DLL:通过使用 DllPlugin 和 DllReferencePlugin,可以将第三方库与应用程序代码分离,从而减少构建时间。
  7. 配置 resolve.alias:通过配置 resolve.alias,可以缩短查找模块的路径,从而提高构建速度。
  8. 使用 Web Workers 或 Service Workers:通过将一些任务放在后台线程中处理,可以提高应用程序的性能。
  9. 提取 CSS:通过使用 MiniCssExtractPlugin,可以将 CSS 从 JS 中分离出来,提高加载性能。
  10. 使用 Loaders 和 Babel:通过配置不同的 loaders 和 Babel 插件,可以在构建过程中优化代码,例如转换 ES6 语法、移除 console.log 等。
  11. 配置性能提示:通过配置 performance.hints 和 performance.assetFilter,可以监控和优化构建产物的大小。
  12. 使用 Webpack Bundle Analyzer:通过这个插件,可以分析和可视化 Webpack 输出的文件,从而帮助发现潜在的优化点。

这些是在使用 Webpack 时可以采取的一些优化方法。针对具体项目,可能还需要结合实际情况进行更多优化。

2 Wepback的生命周期有哪些?

Webpack 的生命周期主要由以下几个阶段组成:

  1. 初始化(Initialization):在这个阶段,Webpack 会读取配置文件(如:webpack.config.js)和命令行参数,然后创建一个新的 Compiler 实例。这个实例包含了整个构建过程中的配置、插件、Loaders 等相关信息。
  2. 解析(Resolution):Webpack 根据入口文件(entry)开始逐层解析依赖关系。对于每个解析到的模块文件,Webpack 都会检查是否需要使用对应的 Loaders 进行转换和处理。此阶段的主要目的是创建一个依赖图(Dependency Graph),其中包含了项目中所有模块及其相互依赖关系。
  3. 编译(Compilation):在这个阶段,Webpack 开始根据依赖图逐个编译模块。对于每个模块,Webpack 会首先执行预编译任务(如使用 Babel 转换 ES6 语法),然后调用相应的 Loaders 处理模块内容。编译完成后,Webpack 会生成一个中间表示(Intermediate Representation,简称 IR),这是一个包含所有模块及其处理结果的对象。
  4. 输出(Emit):Webpack 将 IR 转换为最终的输出文件(如:bundle.js)。在这个阶段,Webpack 会执行优化任务(如代码压缩、文件名哈希化),并将处理后的文件写入磁盘。输出完成后,Webpack 会触发相应的钩子(如:onEmit、afterEmit),以便插件可以执行自定义操作。
  5. 完成(Done):构建流程完成后,Webpack 会触发一系列完成钩子(如:onDone、afterDone),以便插件可以执行清理和统计任务。此时,Webpack 会输出构建结果,包括处理后的文件、错误、警告等信息。

这些阶段概述了 Webpack 的生命周期。在这个过程中,Webpack 会调用许多内置的插件和钩子函数来处理文件和资源。开发者还可以通过自定义插件和钩子来扩展和控制 Webpack 的行为。

3 Webpack有哪些常见的Loader?

  1. babel-loader:用于将 ES6/ES7 语法转换为浏览器兼容的 ES5 语法。
  2. css-loader:解析 CSS 文件中的 @importurl(),将 CSS 转换为 JavaScript 模块。
  3. style-loader:将 CSS 作为样式标签插入到 HTML 文档中。
  4. less-loader:将 Less 代码转换为 CSS 代码。
  5. sass-loader:将 Sass/SCSS 代码转换为 CSS 代码。
  6. postcss-loader:使用 PostCSS 对 CSS 进行处理,如自动添加浏览器前缀、压缩 CSS 等。
  7. file-loader:处理文件引用,将文件复制到输出目录,并返回文件的 URL。
  8. url-loader:将文件以 base64 编码的形式内联到代码中,可以减少 HTTP 请求次数。
  9. image-webpack-loader:压缩和优化图像文件。
  10. ts-loader:将 TypeScript 转换为 JavaScript。

4 Webpack有哪些常见的Plugin?

  1. HtmlWebpackPlugin:生成一个 HTML 文件,并自动引入所有生成的脚本和样式。
  2. MiniCssExtractPlugin:将 CSS 提取为单独的文件,而不是将其内联到 JavaScript 中。
  3. CleanWebpackPlugin:在每次构建前清理输出目录。
  4. DefinePlugin:允许在编译时创建全局常量,用于在开发和生产环境中区分不同的行为。
  5. TerserPlugin:压缩和优化 JavaScript 代码。
  6. OptimizeCSSAssetsPlugin:压缩和优化 CSS 文件。
  7. HotModuleReplacementPlugin:实现模块热替换,用于开发环境。
  8. CopyWebpackPlugin:将静态资源复制到输出目录。
  9. SplitChunksPlugin:实现代码分割,提高加载性能。
  10. CompressionWebpackPlugin:使用 Gzip 或 Brotli 压缩生成的资源文件。

5 Webpack中Loader和Plugin的区别?

  1. Loader 用于转换和处理模块。它是一个函数,接收源文件作为输入,并输出处理后的结果。Loader 的作用是对源代码进行处理,例如编译、压缩、转换等。常见的 Loader 任务包括:将 Sass/SCSS 转换为 CSS、将 ES6 代码转换为浏览器兼容的代码等。
  2. Plugin 是用于扩展 Webpack 功能的插件。它可以在构建流程中的不同阶段执行不同的任务,如清理输出目录、生成 HTML 文件等。Plugin 的作用范围更广泛,它可以访问 Webpack 的整个编译过程,从而实现各种复杂的功能。

Loader 主要负责对模块的转换和处理,而 Plugin 负责完成更广泛的任务,包括编译过程中的各种钩子和自定义功能。

6 Wepback5有哪些新特性?

  1. 持久缓存:Webpack 5 引入了文件系统缓存,可以在多次构建之间持久存储中间结果。这可以显著提高重复构建的速度。
  2. 模块联邦(Module Federation):这是一个允许多个独立构建共享代码的新特性。模块联邦可以在不牺牲性能的情况下实现微前端架构。
  3. 更好的 Tree Shaking:Webpack 5 改进了 Tree Shaking,可以更有效地移除无用代码。
  4. 默认支持 Content Hash:现在默认为输出文件名添加内容哈希,以实现长期缓存优化。
  5. 更小的运行时代码:Webpack 5 的运行时代码更小,有助于减小最终生成的包的大小。
  6. 改进的代码分割策略:Webpack 5 对 SplitChunksPlugin 的默认配置进行了优化,以更好地支持公共代码的提取。
  7. 移除了一些过时的特性:Webpack 5 移除了一些不推荐使用的特性,如 Node.js 的 polyfill。

7 谈谈你对Source map的理解,生产环境如何使用?

Source Map 是一种映射文件,用于将压缩、转换后的代码映射回原始源代码。这可以帮助开发者在浏览器中调试压缩、转换后的代码。Source Map 文件通常具有 .map 扩展名,与生成的代码文件一起分发。浏览器可以解析这些 Source Map 文件,从而在调试时显示原始代码。

在生产环境中,你可能希望隐藏源代码或者避免额外的 HTTP 请求。一种方法是将 Source Map 上传到错误跟踪服务(如 Sentry),这样只有当出现错误时,错误跟踪服务才会下载并使用 Source Map。另一种方法是将 Source Map 内联到生成的代码文件中,这样可以避免额外的 HTTP 请求。然而,这会增加生成的文件大小,可能会影响性能。

8 谈谈你对热更新的理解

热更新(Hot Module Replacement,简称 HMR)是一种用于提高开发效率的技术。在应用程序运行过程中,HMR 可以实时替换、添加或删除模块,而无需刷新整个页面。这样,开发者可以立即看到更改的效果,而不会丢失应用程序的状态。Webpack 的 HotModuleReplacementPlugin 插件可以实现 HMR 功能。

9 谈谈你对Babel的理解,并说下它的原理

Babel 是一个 JavaScript 编译器,用于将最新的 JavaScript 语法(如 ES6、ES7)转换为浏览器兼容的 ES5 语法。Babel 的主要功能是确保你编写的代码在所有浏览器中都能正常运行,不受浏览器对新特性支持程度的影响。

Babel 的原理:

  1. 解析(Parsing):Babel 首先将源代码解析为一个抽象语法树(Abstract Syntax Tree,简称 AST)。解析过程中,Babel 将源代码分解成词素(Tokens),然后根据语法规则将词素组合成 AST。AST 是一种树状结构,用于表示源代码的语法结构。
  2. 转换(Transforming):在 AST 的基础上,Babel 使用插件(plugins)和预设(presets)进行转换。插件是用于实现具体语法转换或优化的功能模块,例如将箭头函数转换为普通函数。预设是一组插件的集合,用于处理一组相关的语法特性,如 ES2015+ 的新特性。
  3. 生成(Generating):在转换阶段完成后,Babel 将修改后的 AST 转换回 JavaScript 代码。生成阶段可以保留原始代码中的格式和注释,或者使用压缩插件对代码进行优化。

Babel 允许开发者使用最新的 JavaScript 语法和特性,同时确保代码在各种浏览器中兼容。通过对代码进行转换和优化,Babel 可以提高代码的可维护性和性能。

10 谈谈你对Vite的理解

Vite(法语单词,意为“快速”的意思)是一种现代化的前端构建工具,由 Vue.js 作者尤雨溪(Evan You)创建。Vite 的目标是为现代浏览器提供一个更轻量级、快速的开发和构建工具。它主要解决了一些传统构建工具(如 Webpack)在开发过程中的瓶颈问题,从而提高了开发者的效率。

Vite 的主要特点和优势包括:

  1. 基于浏览器原生 ES modules:Vite 利用浏览器原生支持的 ES modules 功能,实现了按需加载和快速的开发服务器。这消除了开发环境中构建和热更新的瓶颈,从而大幅提高了开发速度。
  2. 快速冷启动:与 Webpack 等传统构建工具相比,Vite 具有更快的冷启动速度。这是因为 Vite 在开发环境下无需进行整体构建,而是直接为请求的模块提供服务。
  3. 轻量级:Vite 的核心功能非常轻量,使得它在下载、安装和运行时更加高效。
  4. 热模块替换(HMR):Vite 支持 HMR,这使得开发者在开发过程中可以在不刷新页面的情况下看到更改的结果,从而提高开发效率。
  5. 构建性能优化:虽然 Vite 旨在提供快速的开发环境,但它也支持高效的生产环境构建。Vite 使用 Rollup 进行生产环境构建,可以实现 Tree Shaking、代码分割等优化功能。
  6. 插件系统:Vite 提供了一个简单易用的插件系统,可以方便地扩展和定制 Vite 的功能。许多社区插件可以满足不同需求,如 CSS 处理、图片优化等。
  7. 框架支持:Vite 不仅支持 Vue.js,还支持 React、Preact、Svelte 等其他流行的前端框架。
  8. 开箱即用的特性:Vite 集成了一些常用的开发工具和特性,如 CSS 预处理器支持(Sass、Less等)、TypeScript 支持等,无需额外配置即可使用。

总之,Vite 是一种新型的前端构建工具,它提供了快速的开发环境、高效的构建性能和易用的插件系统。尤其对于开发现代前端应用程序,Vite 可以大大提高开发效率。

11 谈谈你对Gulp的理解

Gulp 是一个流行的前端自动化构建工具,通过它可以自动执行各种重复性、繁琐的任务,从而提高开发效率。Gulp使用流(stream)来处理文件,这样可以减少磁盘 I/O,提高任务执行速度。

以下是关于 Gulp 的一些主要理解:

  1. 简单易用:Gulp 的 API 非常简单直观,只需要几个主要的方法(如 src、dest、watch、task 和 series/parallel)就可以编写自动化任务。Gulpfile.js(Gulp 配置文件)通常是易于阅读和维护的。
  2. 插件生态:Gulp 拥有丰富的插件生态系统,可以通过安装和配置插件来实现各种任务,如编译 SASS、压缩 JavaScript、优化图片等。
  3. 基于流(Stream):Gulp 的核心特点是使用 Node.js 流来处理文件,这使得 Gulp 可以在内存中处理文件,避免了不必要的磁盘读写。这种处理方式使得 Gulp 任务执行速度更快。
  4. 代码即配置:与基于配置文件的构建工具(如 Webpack、Grunt)不同,Gulp 使用代码来定义任务。这使得 Gulpfile.js 更加灵活和可定制,可以根据项目需求编写特定的任务。
  5. 自动化任务管理:Gulp 可以监视文件变化,当检测到变化时自动执行相关任务。这可以确保开发过程中,项目始终处于最新状态,提高开发效率。
  6. 并行和串行任务执行:Gulp 提供了 series() 和 parallel() 方法,可以方便地组合任务,实现串行或并行执行。这可以最大程度地利用多核 CPU 的性能,提高任务执行速度。

尽管现在前端构建工具有很多选择,如 Webpack、Parcel 和 Vite 等,但 Gulp 仍然在一些特定场景下具有优势。例如,对于一些简单的前端项目,或者需要灵活、定制化的构建流程,Gulp 是一个很好的选择。

12 谈谈Webpack、Vite和Gulp三者之间的区别

Webpack、Vite 和 Gulp 是三种流行的前端构建工具,它们之间有一些显著的区别:

  1. Webpack:
    • Webpack 是一个模块打包器,主要用于 JavaScript 应用程序的打包和优化。
    • 它支持各种资源(如 JS、CSS、图片、字体等)的加载和处理。
    • Webpack 支持代码分割、懒加载、Tree Shaking 等优化策略,有助于提高应用程序的性能。
    • 通过插件系统,Webpack 可以进行高度定制,满足各种项目需求。
    • 缺点是配置相对复杂,构建速度在某些情况下较慢。
  2. Vite:
    • Vite 是一个基于 ES modules 的开发服务器和构建工具,由 Vue.js 作者尤雨溪创建。
    • Vite 利用原生 ES 模块(ESM)特性,实现快速开发服务器和按需编译。
    • Vite 支持 HMR(热模块替换),提高开发效率。
    • Vite 使用 Rollup 进行生产环境构建,具有出色的 Tree Shaking 能力。
    • Vite 配置相对简单,易于上手,但某些场景下可能没有 Webpack 那么灵活。
  3. Gulp:
    • Gulp 是一个基于流(stream)的任务运行器,主要用于自动化处理前端资源。
    • Gulp 通过编写任务,可以实现各种复杂的构建流程,如编译 SASS、压缩 JS 等。
    • Gulp 的核心优势在于其流式处理,减少磁盘 I/O,提高任务执行速度。
    • Gulp 使用代码定义任务,具有很好的灵活性,适用于简单项目或高度定制化的构建需求。
    • 缺点是 Gulp 不支持模块打包,需要与其他工具(如 Webpack、Rollup)结合使用以实现完整的构建流程。

总结:

Webpack、Vite 和 Gulp 之间的主要区别在于它们的使用场景、核心功能和处理方式。Webpack 是一个功能丰富的模块打包器,适用于各种类型的项目。Vite 是一个轻量级、高性能的开发服务器和构建工具,尤其适用于现代框架项目。Gulp 是一个灵活的任务运行器,用于处理前端资源和自动化工作流。在实际项目中,开发者可以根据需求选择合适的工具,甚至将它们组合使用以实现最佳的构建流程。

结尾 文章参考

github.com/qaz62482455…