• 史上最全前端八股文来了


    引言

    由于最近比较忙活没时间学习新东西,现在得空想着能不能好好整理出一些有用的东西,让记忆深刻一点,免得到时候实习找工作面试的时候一问三不知,也希望大家能指正出错误和对大家有点帮助,一起进步,加油奥里给!!!

    那么废话不多说直接进入正题,如果觉得可以家人们给个三连😀!!!

    正文

    HTML+CSS

    HTML5的新特性、语义化

    语义化指对文本内容的结构化(内容语义化),选择合乎语义的标签(代码语义化),便于开发者阅读,维护和写出更优雅的代码的同时,让浏览器的爬虫和辅助技术更好的解析。通过使用恰当语义的HTML标签,可以有效提高可访问性、可检索性、国际化和互用性。

    优点:

    1. 在没有 CSS 样式的情况下页面的排版结构也很清晰,便于阅读。
    2. 可以让页面代码结构更清晰,提高互用性,减少网页间的差异性,帮助其他开发者了解网页的结构,方便后期开发和维护。
    3. 还可以提高可访问性,帮助辅助技术更好地阅读和转译网页,利于无障碍阅读。
    4. 它们还可以提高国际化,让各国开发者更容易弄懂网页的结构。

    新特性:

    • 语义化标签:
    • 本地存储:localStorage和sessionStorage
    • 兼容特性:让网页在不同的浏览器中都能正常显示。
    • 2D/3D:让网页具有更丰富的视觉效果。
    • 动画/过渡:让网页元素之间的变化更加平滑自然。
    • 性能与集成:提高网页的运行速度和稳定性。
    • 多媒体标签:
    • CSS3 特性:可以让网页样式更加丰富多彩。
    • 新的表单控件:required、placeholder、autofocus、autocomplete、multiple
    • input类型:color、date、email、month、number、search、tel、time、url、week

    CSS3

    层叠样式表(Cascading Style Sheets,缩写为 CSS)是一种样式表语言,用来描述 HTML 或 XML(包括如 SVG、MathML 或 XHTML 之类的 XML 分支语言)文档的呈现。CSS 描述了在屏幕、纸质、音频等其他媒体上的元素应该如何被渲染的问题。CSS3 是 CSS(层叠样式表)技术的升级版本,CSS演进的一个主要变化就是W3C决定将CSS3分成一系列模块。

    新特性:

    • 选择器:新的属性选择器、伪类选择器和伪元素选择器,增强了页面元素的选择范围和对特定元素样式的控制。
    • 盒模型:引入了新的盒模型,使得我们可以通过box-sizing属性来定义元素的盒模型是"content-box"(默认值)还是"border-box",深度影响了元素的计算宽度和高度值。
    • 版式:新的版式属性包括:多列版式、Flexbox、Grid等,增加了网页版式的灵活性和复杂性。
    • 背景图像:使用 background-size 属性,CSS3 允许我们调整背景图片的大小并让其适应我们的容器。
    • 过渡和动画:CSS3支持 创建平滑过渡 和 流畅动画 完成CSS的转换效果。
    • 渐变:我们可以使用CSS3 gradient属性为元素添加渐变背景。渐变背景可以是线性的或径向(环形)的。
    • 字体:新增加字体嵌入@font-face、属性font-stretch、text-shadow等。
    • 阴影:强大的 CSS3 模糊阴影效果,我们可以使用box-shadow属性模拟出漂亮的阴影效果,而不需要深入解决重影粒度问题。
    • 圆角:CSS3 border-radius 属性是圆角属性,可以使我们轻松地实现不同圆角半径的定制化效果。
    • 弹性盒子模型:弹性盒子模型又叫Flexible Box布局模型,可用于彻底改变 Web 页面的布局方式。

    常用选择器:

    1. ID选择器: #id
    2. 类别选择器: .class
    3. 元素选择器: element
    4. 伪类选择器: :link,:visited,:hover,:active,:focus
    5. 属性选择器: [attribute]
    6. 后代选择器: A B
    7. 子元素选择器:A > B
    8. 兄弟选择器:A + B, A ~ B
    9. 通用选择器: *

    优先级顺序:

    • !important >
    • 内联样式 >
    • ID选择器 >
    • 类选择器/属性选择器/伪类选择器 >
    • 元素选择器/伪元素选择器 >
    • 关系选择器/通用选择器
      !important > 内联样式 > ID选择器 > 类别选择器、属性选择器、伪类选择器 > 元素选择器 > 通配符 > 继承

    示例:

    // 1. 使用ID选择器:
    #main-nav {
      background-color: blue;
    }
    
    // 2. 元素选择器:
    div{
      width:100%;
      height:100%;
    }
    
    // 3. 使用类别选择器:
    .button {
      background-color: red;
    }
    
    // 4. 使用属性选择器:
    a[href^="https://"] {
      color: green;
    }
    
    // 5. 使用伪类选择器:
    a:hover {
      color: yellow;
    }
    
    // 6. 使用后代选择器:
    .main-nav ul {
      padding: 0;
    }
    
    // 7. 使用子元素选择器:
    .main-nav > ul {
      list-style-type: none;
    }
    
    // 8. 使用兄弟选择器:
    h1 + p {
      font-size: 16px;
    }
    
    // 9. 使用通用选择器:
    * {
      box-sizing: border-box;
    }
    

    CSS盒子模型和box-sizing属性

    CSS盒子模型(Box Model)是网页布局的基础,可以将页面上所有元素看作一个个矩形的盒子。这些盒子由四个部分组成:内容区(content)、内边距(padding)、边框(border)、外边距(margin)。CSS盒子模型分为标准盒子模型怪异盒子模型,这两个概念与盒子模型的计算方式有关。



    在CSS3中,通过box-sizing属性可以控制盒子模型的计算方式。CSS3中的 box-sizing 属性有三个值:content-box,border-box和inherit。

    1. content-box:默认值,模型的宽度和高度只包括内容,不包括边框和内边距。(标准盒子模型)
    2. border-box:模型的宽度和高度包括内容、内边距和边框,但不包括外边距。(怪异盒子模型)
    3. inherit:继承父元素的 box-sizing 值。

    区别:box-sizing属性用于控制盒子模型的计算方式,更改CSS盒子模型的大小计算方式,使得需要计算的尺寸更加精确和方便。

    标准盒子模型:

    标准盒子模型是CSS2.1规范定义的,也被称为W3C盒子模型。在标准盒子模型下,一个元素的尺寸由其content(内容)的宽度、内边距padding、边框border和外边距margin四个部分组成。其中content的大小可以通过width和height属性进行设置,padding、border和margin的大小可以通过相应的属性进行设置。

    标准盒子模型的计算公式如下:

    总宽度 = width + padding-left + padding-right + border-left-width + border-right-width + margin-left + margin-right
    总高度 = height + padding-top + padding-bottom + border-top-width + border-bottom-width + margin-top + margin-bottom

    示例:

    // HTML 代码为:
    
    "box">盒子模型
    // CSS 代码为: .box { width: 200px; height: 100px; padding: 10px; border: 5px solid #000; margin: 0 auto; }

    怪异盒子模型

    怪异盒子模型也被称为IE盒子模型,是IE5~IE6浏览器采用的盒子模型,由于该模型与标准盒子模型不同,因此被称为怪异盒子模型。

    标准盒子模型的计算公式如下:
    总宽度 = width + margin-left + margin-right
    总高度 = height + margin-top + margin-bottom

    也就是说,在怪异盒子模型中,内边距和边框的大小并没有算入元素的总尺寸。(既 width 已经包含了 padding 和 border 值)

    示例:

    // HTML 代码为:
    
    "box">怪异盒子模型
    // CSS 代码为: .box { width: 200px; height: 100px; padding: 10px; border: 5px solid #000; margin: 0 auto; box-sizing: border-box; /* 显示使用IE怪异盒子模型 */ }

    解释:当设置一个元素的box-sizing属性为 border-box 时,即可使用怪异盒子模型进行盒子尺寸的计算,而采用其他值(如 content-box)则会使用标准盒子模型进行盒子尺寸的计算。

    其实在默认的 content-box 模式下,盒子模型就是标准的盒子模型,元素的宽度和高度仅包含内容,不包括内边距(padding)、边框(border)和外边距(margin)。而使用设置为 border-box 的 box-sizing 属性时,元素的宽度和高度包括了内边距、边框和内容,但不包括外边距。

    具体而言,在 content-box 模式下,当我们设置宽度为200px时,它并不包括 padding、border 和 margin 的尺寸,因此该元素的实际宽度可能会比我们期望的要大一些。而在 border-box 模式下,设置的宽度200px已经包含了 padding 和 border 的尺寸,因此该元素的实际宽度也就比较准确了。

    BFC和IFC

    BFC和IFC是CSS布局中的概念,他们分别代表“块级格式化上下文”和“内联格式化上下文”。

    区别:

    • BFC是块级格式化上下文,它是一个独立的布局环境,其中块级盒子垂直排列。在BFC中,盒子的垂直边距会发生折叠,浮动元素也会参与高度计算。
    • IFC是行内格式化上下文,它是一种水平的格式化上下文,其中行内级盒子从左到右水平排列,直到一行被填满,然后换行。在IFC中,盒子的垂直对齐方式由vertical-align属性决定。行高由包含该行内级盒子中最高的盒子决定。

    BFC(块级格式化上下文)

    BFC(Block Formatting Context),即块级格式化上下文。指的是一个独立的块级渲染区域(布局环境),它具有一定的隔离特性,内部元素的定位、清除浮动、高度塌陷等计算方式与外部元素保持独立。

    BFC的原理布局规则:

    • 内部的Box会在垂直方向一个接着一个地放置。
    • Box垂直方向上的距离由margin决定。属于同一个BFC的两个相邻的Box的margin会发生重叠。
    • 每个盒子的左外边框紧挨着包含块的左边框,即使浮动元素也是如此。
    • BFC的区域不会与float box重叠。
    • 元素的类型和display属性,决定了这个Box的类型。不同类型的Box会参与不同的Formatting Context(一个决定如何渲染文档的容器),因此Box内的元素会以不同的方式渲染。
    • 计算BFC的高度时,浮动子元素也参与计算。

    扩展:Box是CSS布局的对象和基本单位,一个页面是由很多个Box组成的。元素的类型和display属性决定了这个Box的类型。

    BFC的生成规则有如下几条:

    • 根元素即为一个BFC。
    • 浮动元素(float不为none)。
    • 绝对定位元素(position为absolute或fixed)。
    • display值为inline-block、table-caption、flex、inline-flex、grid、inline-grid的元素。
    • overflow值不为visible的块元素。

    示例:

    // HTML 代码为:
    
    "container">
    "box">
    "box">
    // CSS 代码为: .container { border: 1px solid black; overflow: hidden; } .box { width: 100px; height: 100px; margin: 10px; float: left; background-color: lightblue; }

    解释:在这个示例中,我们创建了一个BFC来包裹两个浮动元素,通过设置 overflow:hidden,让它会触发BFC,在BFC中,BFC自适应高度,浮动元素也会参与高度计算,因此解决了浮动元素引发的高度塌陷问题。

    应用场景:

    • 解决浮动元素引发的高度塌陷问题。
    • 防止垂直外边距重叠。
    • 创建自适应两栏布局。
    • 实现多列文本布局。

    扩展:如果页面布局造成了浮动塌陷,除了使用清除浮动(Clearfix)技术强制容器在浮动元素之后换行,还可以为容器设置一个触发BFC的样式,就是上面那个例子中为 container 设置了 overflow: hidde 的样式。

    IFC(内联格式化上下文)

    IFC指的是一个内联元素渲染区域,它是一种水平的格式化上下文,具有一定的隔离特性,同一个IFC内部的元素在渲染时互相影响,但与外部元素不产生任何影响。在IFC中,盒子从左到右水平排列,直到一行被填满,然后换行。行内级盒子的垂直对齐方式由 vertical-align 属性决定。行高由包含该行内级盒子中最高的盒子决定。

    IFC中的布局规则包括:

    • 行内级盒子从左到右水平排列。
    • 盒子的垂直对齐方式由vertical-align属性决定。
    • 行高由包含该行内级盒子中最高的盒子决定。
    • 当一行被填满时,盒子会换行。

    IFC的生成规则有如下几条:

    • 根元素即为一个IFC。
    • inline-block元素。
    • 表格单元格(table-cell)。
    • display值为inline-flex、inline-grid的元素。
    • img元素、input元素、textarea元素。

    示例:

    // HTML 代码为:
    
    "container">
    "box">
    "box">
    "box">
    // CSS 代码为: .container { border: 1px solid black; width: 300px; } .box { display: inline-block; width: 100px; height: 100px; margin: 10px; background-color: lightblue; }

    解释:这里没有用flex布局,有兴趣的可以自己试一试噢!在这个例子中,我们有一个包含三个盒子的容器。盒子被设置为display: inline-block,这使它们成为行内块级元素。由于容器的宽度只有300像素,所以第三个盒子会换行。

    应用场景:

    • 内联元素的居中对齐。
    • 解决内联元素导致的空隙问题。
    • 禁止文本被浮动元素覆盖。
    • 实现多行文本的两端对齐布局。

    总而言之,BFC和IFC在CSS布局中扮演了至关重要的角色,可以解决很多常见的布局问题,对于理解CSS的渲染流程、排版规则有很大帮助。

    页面布局

    在我的第一篇博客文章中有介绍了前端常见的十种布局方式,所以这里就不再详细介绍了,大家可以去看看,我就简单提一下就好了:

    • 静态布局:常见于pc端,是给页面设定固定的宽高且居中布局,web网站开发的单位一般用px。
    • 浮动布局:浮动布局是调用浮动属性来使得元素向左或者向右移动从而共享一行,直到碰到包含框或者另一个浮动框。浮动元素是脱离文档流的,不占据页面空间,但不脱离文本流,且浮动会将行内元素和行内块元素转化为块元素。
    • 定位布局:定位布局是给元素设置 position 属性从而控制元素显示在不规则的位置,偏向于单个元素定位操作。
    • 栅格布局:栅格布局也被称为网格布局,它是一种新兴的布局方式,常用的有瀑布流等。它的布局很简单,就是把一个区域划分为一个个的格子排列好,再把需要的元素填充进去。
    • table布局:table 布局是在父元素使用 display:table; 子元素使用 display:table-row或 display:table-cell; 子元素会默认自动平均划分父元素的空间。
    • 弹性(flex)布局:flexible 模型又被称为 flexbox,它不像栅格布局可以同时处理行跟列,只能处理单行或者当列,是一维的布局模型。
    • 圣杯布局:圣杯布局跟双飞翼的布局区别在于中间是否有包括两边的区域,圣杯布局是没有的,两边或者一边非主要部分填充父元素的 padding;而双飞翼布局是有的,但多了一层 dom 节点,非主要部分用的是 center 部分的 margin 空间。
    • 自适应布局:总结的来说就是创建多个静态布局,每个布局对应一个屏幕的分辨率范围,每个静态布局页面的元素大小不会因为窗口的改变而变化,除非从一个静态布局变到另外一个布局,不然在同一设备下还是固定的布局。常用的方式有使用 CSS 的 @media 媒体查询,也有高成本的 JS 进行设计开发,或者使用第三方开源框架 bootstrap,这个能够很好的支持多个浏览器。
    • 流式布局:流式布局也叫百分比布局(也有叫非固定像素布局),是页面中的元素根据屏幕分辨率自动进行适配调整,页面元素大小会发生变化,但是整体布局不会发生变化,始终都是满屏显示。它使用的是百分比定义宽,但高一般会被固定住,这种布局在早期是为了适应不同尺寸的PC屏幕,但现在在移动端比较常见。
    • 响应式布局:响应式通过检测视口分辨率判断是pc端、平板还是手机,针对不同的客户端在客户端做处理,来展示不同的布局和内容从而达到令人满意的效果,屏幕大小的变化会导致元素的位置和大小都改变,可以说是流式布局和自适应布局的结合体,一套界面布局即可适应所有不同的尺寸和终端,可想而知设计考虑的比自适应复杂的多。

    其他

    伪类和伪元素

    伪类和伪元素都是CSS选择器,它们用来选择文档树以外的元素,或者选择文档树中无法用简单选择器表示的状态。但它们之间有一些重要的区别。

    伪类用来选择元素的特殊状态。例如,:hover伪类用来选择鼠标悬停在其上的元素,:focus伪类用来选择获得焦点的元素。伪类通常用于添加一些特殊的样式,以反映元素的状态。

    伪元素用来创建一些不在文档树中的元素,并为其添加样式。例如,::before伪元素用来在一个元素之前插入内容,::after伪元素用来在一个元素之后插入内容。伪元素通常用于添加装饰性内容。

    伪类和伪元素的主要区别在于它们的作用对象不同。伪类作用于已经存在的元素,而伪元素创建新的元素。

    长度单位px、em和rem

    px、em和rem都是CSS中的长度单位,但它们之间有一些重要的区别。

    px(像素)是一个绝对长度单位,它表示屏幕上的一个物理像素。由于不同设备的屏幕分辨率不同,所以1px在不同设备上可能表示不同的物理尺寸。

    em是一个相对长度单位,它相对于当前元素的字体大小。例如,如果一个元素的字体大小为16px,那么1em就等于16px。em单位常用于设置元素的字体大小、边距和填充等属性。

    rem(root em)也是一个相对长度单位,它相对于根元素(元素)的字体大小。例如,如果根元素的字体大小为16px,那么1rem就等于16px。rem单位常用于实现响应式布局。

    position属性

    • static:默认值。元素按照正常文档流进行定位。
    • relative:元素按照正常文档流进行定位,但可以通过top、right、bottom和left属性相对于其正常位置进行偏移。
    • absolute:元素脱离正常文档流,相对于最近的非static定位祖先元素进行定位。如果没有非static定位的祖先元素,则相对于初始包含块进行定位。
    • fixed:元素脱离正常文档流,相对于浏览器窗口进行定位。
    • sticky:元素在正常文档流中,但可以根据用户的滚动固定在指定位置。

    想要了解更多或者详细一点可以看我第一篇文章前端常见的十种布局方式中的定位布局。

    让一个元素水平垂直居中

    常见方法:

    1. 使用flex布局:父元素上设置 display: flex;,并且使用 align-items: center; 和 justify-content: center; 来实现水平垂直居中。
    2. 使用绝对定位和负边距:父元素上设置 position: relative;,然后在子元素上设置 position: absolute;,并且使用 top: 50%;、left: 50%; 和负边距(例如 margin-top: -10px; margin-left: -10px;)来实现水平垂直居中。
    3. 使用绝对定位和transform:父元素上设置 position: relative;,然后在子元素上设置 position: absolute;,并且使用 top: 50%;、left: 50%; 和 transform: translate(-50%, -50%); 来实现水平垂直居中。
    4. 使用表格布局:父元素上设置 display: table-cell; vertical-align: middle; text-align:center; 来实现水平垂直居中。
    5. 使用网格布局:父元素上设置 display: grid;,并且使用 place-items: center; 来实现水平垂直居中。
    6. 使用行内块元素和文本对齐:父元素上设置 text-align: center; 和 line-height: 200px;(其中200px是父元素的高度),然后在子元素上设置 display: inline-block; vertical-align: middle; 来实现水平垂直居中。

    隐藏页面中某个元素

    常见方法:

    1. 使用 display: none;:这会将元素从页面布局中完全移除,就像它从未存在过一样。
    2. 使用 visibility: hidden;:这会将元素隐藏,但它仍然占据页面布局中的空间。
    3. 使用 opacity: 0;:这会将元素的透明度设置为0,使其完全透明,但它仍然占据页面布局中的空间,并且仍然可以与用户交互(例如,可以点击)。
    4. 使用 position: absolute;left: -9999px;:这会将元素移出屏幕外,使其不可见。

    JS、ES6

    ES6,全称 ECMAScript 6.0,是 JavaScript 语言的下一代标准,于 2015 年 6 月正式发布。它为 JavaScript 带来了许多新的语法特性和功能,使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

    ES6 的一些主要新语法特性包括:

    • 新的原始类型和变量声明:let 和 const 关键字用于声明块级作用域的变量和常量。
    • 箭头函数:使用 => 符号定义函数,可以更简洁地编写函数。
    • 模板字符串:使用反引号(`)定义字符串,可以在字符串中嵌入表达式。
    • 解构赋值:允许从数组或对象中提取值并赋值给变量。
    • 类:使用 class 关键字定义类,支持继承、构造函数、静态方法等面向对象编程特性。
    • 模块化:使用 import 和 export 关键字导入和导出模块。
    • Promise:用于处理异步操作的结果。
    • 迭代器生成器:支持迭代器和生成器,可以更方便地遍历数据结构。
    • Set 和 Map 数据结构:新增了 Set 和 Map 数据结构,用于存储唯一值和键值对。

    迭代器和生成器的简单示例:

    // 简单的迭代器示例,它实现了一个next()方法,用于遍历数组中的元素:
    function makeIterator(array) {
        let nextIndex = 0;
        return {
            next: function() {
                return nextIndex < array.length ?
                    {value: array[nextIndex++], done: false} :
                    {done: true};
            }
        };
    }
    
    let it = makeIterator(['a', 'b', 'c']);
    console.log(it.next().value); // 'a'
    console.log(it.next().value); // 'b'
    console.log(it.next().value); // 'c'
    console.log(it.next().done);  // true
    
    // 简单的生成器示例,它使用yield表达式来暂停函数执行并返回一个值:
    function* idMaker() {
        let index = 0;
        while (true)
            yield index++;
    }
    
    let gen = idMaker();
    console.log(gen.next().value); // 0
    console.log(gen.next().value); // 1
    console.log(gen.next().value); // 2
    

    数据类型

    分为两大类:包括值类型(基本对象类型)和引用类型(复杂对象类型)
    值类型:字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、Symbol和BigInt。其中,Symbol是ES6引入的一种新的原始数据类型,表示独一无二的值。
    引用数据类型:对象(Object)、数组(Array)和函数(Function),还有两个特殊的对象:正则(RegExp)和日期(Date)。

    示例:

    // 值类型
    let myString = 'Hello, World!'; // 字符串
    let myNumber = 3.14; // 数字
    let myBoolean = true; // 布尔
    let myNull = null; // 空
    let myUndefined = undefined; // 未定义
    let mySymbol = Symbol(); // Symbol
    let myBigInt = 123n; // BigInt
    
    // 引用数据类型
    let myObject = {name: '幼儿园技术家', age: 25}; // 对象
    let myArray = [1, 2, 3]; // 数组
    let myFunction = function() {console.log('Hello, World!')}; // 函数
    let myRegExp = /hello/i; // 正则表达式
    let myDate = new Date(); // 日期
    

    我相信大家很少见过 symbol 和 Bigint 吧,如果面试问到估计只有少部分大佬能聊出来(反正我不行)。

    先详细解释一下吧:

    1. Symbol是ES6中新增的一种基本数据类型,它表示独一无二的值。每个通过Symbol()生成的值都是唯一的。Symbol可以用作对象的唯一属性名,这样其他人就不会改写或覆盖你设置的属性值。
    2. BigInt是ES10中新增的一种基本数据类型,它提供了一种方法来表示大于2^53-1的整数。BigInt可以表示任意大的整数。

    示例:

    let mySymbol = Symbol('mySymbol');
    let obj = {};
    obj[mySymbol] = 'Hello, World!';
    console.log(obj[mySymbol]); // 输出'Hello, World!'
    
    let myBigInt = 1234567890123456789012345678901234567890n;
    console.log(myBigInt * 2n); // 输出2469135780246913578024691357802469135780n
    

    好处:

    Symbol的好处在于它能够创建独一无二的值,这样就可以避免属性名冲突的问题。例如,当你想要给一个对象添加一个新属性时,你可以使用Symbol来创建一个唯一的属性名,这样就不用担心这个属性名会与对象中已有的属性名冲突。

    BigInt的好处在于它能够表示任意大的整数,这样就可以避免整数溢出的问题。例如,在对大整数进行数学运算时,以任意精度表示整数的能力尤为重要。有了BigInt,整数溢出将不再是一个问题。此外,你可以安全地使用高精度时间戳、大整数ID等,而不必使用任何变通方法。

    数据类型常用检测方法

    1. typeof:typeof操作符可以返回一个字符串,表示未经计算的操作数的类型。优点在于它简单易用,可以快速检测基本数据类型。但它也有一些缺点,例如它无法区分Object、Array和Null,因为都会返回"object"。
    示例:

    console.log(typeof 'Hello, World!'); // 输出'string'
    console.log(typeof 3.14); // 输出'number'
    console.log(typeof true); // 输出'boolean'
    console.log(typeof undefined); // 输出'undefined'
    console.log(typeof null); // 输出'object'
    console.log(typeof Symbol()); // 输出'symbol'
    console.log(typeof 123n); // 输出'bigint'
    console.log(typeof {}); // 输出'object'
    console.log(typeof []); // 输出'object'
    console.log(typeof function() {}); // 输出'function'
    

    2. instanceof:instanceof操作符主要用于检测引用数据类型,它用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。因此,它并不适用于检测所有数据类型。优点在于它可以检测引用数据类型,判断一个实例是否属于某个类。但它也有一些缺点,例如它无法检测基本数据类型。
    示例:

    console.log([] instanceof Array); // 输出true
    console.log({} instanceof Object); // 输出true
    console.log(function() {} instanceof Function); // 输出true
    

    3. Object.prototype.toString.call():这种方法可以用来检测对象的类型。优点在于它可以准确地检测所有数据类型,包括基本数据类型和引用数据类型。但它也有一些缺点,例如使用起来比较麻烦,需要调用Object.prototype.toString.call()方法,并传入要检测的值作为参数。
    示例:

    console.log(Object.prototype.toString.call('Hello, World!')); // 输出'[object String]'
    console.log(Object.prototype.toString.call(3.14)); // 输出'[object Number]'
    console.log(Object.prototype.toString.call(true)); // 输出'[object Boolean]'
    console.log(Object.prototype.toString.call(undefined)); // 输出'[object Undefined]'
    console.log(Object.prototype.toString.call(null)); // 输出'[object Null]'
    console.log(Object.prototype.toString.call(Symbol())); // 输出'[object Symbol]'
    console.log(Object.prototype.toString.call(123n)); // 输出'[object BigInt]'
    console.log(Object.prototype.toString.call({})); // 输出'[object Object]'
    console.log(Object.prototype.toString.call([])); // 输出'[object Array]'
    console.log(Object.prototype.toString.call(function() {})); // 输出'[object Function]'
    

    数据类型转换方法

    在JavaScript中,数据类型转换分为两种:隐式类型转换和显式类型转换。
    隐式类型转换:指在运算过程中,JavaScript会自动将一种数据类型转换为另一种数据类型,以便进行运算。例如,在字符串和数字相加时,数字会被自动转换为字符串,然后进行字符串拼接。
    示例:

    let x = '3' + 4; // x的值为'34'
    let y = '3' - 4; // y的值为-1
    

    显式类型转换:指通过调用特定的函数或方法来手动进行数据类型转换。例如,可以使用Number()函数将字符串转换为数字,或使用String()函数将数字转换为字符串。
    示例:

    // 使用Number()函数将字符串转换为整数
    let a = Number('3') + 4; // a的值为7
    
    // 使用String()函数将整数转换为字符串
    let b = String(3) + 4; // b的值为'34'
    
    // 使用一元加号运算符将字符串转换为数字
    let x = +'3'; // x的值为3
    
    // 使用一元减号运算符将字符串转换为数字
    let y = -'3'; // y的值为-3
    
    // 使用parseInt()函数将字符串转换为整数
    let a = parseInt('3.14'); // a的值为3
    
    // 使用parseFloat()函数将字符串转换为浮点数
    let b = parseFloat('3.14'); // b的值为3.14
    
    // 使用toString()方法将数字转换为字符串
    let c = (3).toString(); // c的值为'3'
    

    深拷贝和浅拷贝

    深拷贝和浅拷贝是针对引用数据类型(如Object和Array)的概念。浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

    当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

    实现方法
    1. 浅拷贝可以通过多种方法实现。例如,可以使用Object.assign()方法进行浅拷贝,也可以使用扩展运算符...进行浅拷贝。此外,还可以使用Array.prototype.concat()和Array.prototype.slice()方法对数组进行浅拷贝。
      示例:
    // 使用Object.assign()进行浅拷贝:
    let obj1 = { a: 1, b: { c: 2 } };
    let obj2 = Object.assign({}, obj1);
    obj1.b.c = 3;
    console.log(obj2.b.c); // 输出3,因为obj2.b和obj1.b指向同一个对象
    
    // 使用扩展运算符...进行浅拷贝:
    let obj1 = { a: 1, b: { c: 2 } };
    let obj2 = {...obj1};
    obj1.b.c = 3;
    console.log(obj2.b.c); // 输出3,因为obj2.b和obj1.b指向同一个对象
    
    // 使用Array.prototype.concat()对数组进行浅拷贝:
    let arr1 = [1, 2, { a: 3 }];
    let arr2 = arr1.concat();
    arr1[2].a = 4;
    console.log(arr2[2].a); // 输出4,因为arr2[2]和arr1[2]指向同一个对象
    
    // 使用Array.prototype.slice()对数组进行浅拷贝:
    let arr1 = [1, 2, { a: 3 }];
    let arr2 = arr1.slice();
    arr1[2].a = 4;
    console.log(arr2[2].a); // 输出4,因为arr2[2]和arr1[2]指向同一个对象
    
    1. 深拷贝可以通过多种方法实现。例如,可以使用递归的方式实现深拷贝,也可以通过JSON对象实现深拷贝,即先使用JSON.stringify()将对象转换为JSON字符串,再使用JSON.parse()将字符串解析成新的对象。
      示例:
    // 使用递归实现深拷贝:
    function deepClone(obj) {
        if (typeof obj !== 'object' || obj === null) {
            return obj;
        }
        let result = Array.isArray(obj) ? [] : {};
        for (let key in obj) {
            if (obj.hasOwnProperty(key)) {
                result[key] = deepClone(obj[key]);
            }
        }
        return result;
    }
    let obj1 = { a: 1, b: { c: 2 } };
    let obj2 = deepClone(obj1);
    obj1.b.c = 3;
    console.log(obj2.b.c); // 输出2,因为obj2是obj1的深拷贝,它们之间没有引用关系
    
    // 使用JSON.stringify()和JSON.parse()实现深拷贝:
    let obj1 = { a: 1, b: { c: 2 } };
    let obj2 = JSON.parse(JSON.stringify(obj1));
    obj1.b.c = 3;
    console.log(obj2.b.c); // 输出2,因为obj2是obj1的深拷贝,它们之间没有引用关系
    

    此外,还可以通过jQuery的extend方法实现深浅拷贝: extend()方法的第一个参数是一个布尔值,用来指定是否进行深拷贝。如果该参数为true,则进行深拷贝;否则进行浅拷贝。
    示例:

    let obj1 = { a: 1, b: { c: 2 } };
    let obj2 = jQuery.extend(true, {}, obj1);
    obj1.b.c = 3;
    console.log(obj2.b.c); // 输出2,因为obj2是obj1的深拷贝,它们之间没有引用关系
    

    作用域链和闭包

    作用域链

    作用域链是指在JavaScript中,变量的查找机制。当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。这个作用域链保证了对执行环境有权访问的所有变量和函数的有序访问。

    作用域链的前端是当前执行环境的变量对象,如果这个执行环境是函数,则将其活动对象作为变量对象。活动对象在最开始时只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,再下一个变量对象则来自下一个包含环境。这样一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

    其实作用域链的理解比较简单,就是当查找变量时,会从作用域链的前端开始,逐级向后查找,直到找到为止。如果在整个作用域链中都没有找到该变量,则该变量未定义。
    示例1(查找成功):

    let x = 1;
    
    function outer() {
      let y = 2;
      console.log(x + y);
    }
    
    outer(); // 输出 3
    

    示例2(查找失败):

    function outer() {
      let y = 2;
      console.log(x + y);
    }
    
    outer(); // 报错:ReferenceError: x is not defined
    
    

    闭包

    闭包是指一个函数能够访问其定义时的词法作用域,即使这个函数在其定义时的作用域之外执行。闭包可以让你从内部函数访问外部函数作用域。

    在JavaScript中,函数在创建时会保存一个指向其定义时的词法作用域的引用。当这个函数被调用时,它会使用这个引用来确定其外部变量的值。这就是闭包。

    优点:

    1. 封装:闭包可以用来封装私有变量,防止外部访问。
    2. 记忆:闭包可以用来记忆函数的状态,例如计数器。
    3. 柯里化:闭包可以用来实现柯里化,即将一个多参数函数转换为一系列单参数函数。

    缺点:

    1. 内存占用:由于闭包会引用外部函数的变量,因此它会占用更多的内存。如果不需要使用闭包,应该及时释放内存。
    2. 性能问题:由于闭包需要在作用域链中查找变量,因此它的性能可能不如直接访问全局变量。

    避免闭包导致的内存泄漏:

    • 及时释放不再使用的闭包,以便垃圾回收器可以回收它们占用的内存。
    • 避免在闭包中捕获不必要的变量,尽量只捕获必要的变量。
    • 注意循环引用,避免在闭包中捕获会导致循环引用的变量。

    我们常常使用的定时器、事件处理、Ajax请求等常用于异步操作用了回调函数,但是回调函数其实是可以使用闭包也可以不使用闭包的,并不是说回调一定是在使用闭包。

    回调示例1(使用闭包):

    let x = 1;
    
    function doSomething(callback) {
      // 执行一些操作
      let result = x + 1;
      // 调用回调函数
      callback(result);
    }
    
    doSomething(function(result) {
      console.log(result); // 输出 2
    });
    
    

    回调示例2(不使用闭包):

    function doSomething(callback) {
      // 执行一些操作
      let result = 1 + 1;
      // 调用回调函数
      callback(result);
    }
    
    doSomething(function(result) {
      console.log(result); // 输出 2
    });
    
    
    那么闭包中定义的变量怎么回收呢?

    在JavaScript中,内存管理是自动进行的。当一个变量不再被引用时,它所占用的内存就会被垃圾回收器回收。
    在闭包中定义的变量也是如此。当闭包不再被引用时,它所引用的外部变量也就不再被引用,因此它们所占用的内存就会被垃圾回收器回收。
    所以有两种情况:

    • 第一是当全局变量作为闭包变量的时候,那么闭包变量就会因为上下文的存在(一直被引用)而保存到页面关闭。
    • 第二是当局部变量作为闭包变量的时候,其一是引用完毕立即回收(可以赋予null),其二是可以一直引用依然保存在内存中直到不再被引用则会回收。

    第二种情况示例1(立即回收):

    function fn() {
      let x = 1;
      return function() {
        console.log(x);
      }
    }
    
    for (let i = 0; i < 10; i++) {
      fn()(); // 输出10次1
    }
    
    

    第二种情况示例2(等到不再引用则回收):

    function fn() {
      let x = 1;
      return function() {
        console.log(x++);
      }
    }
    
    let closure = fn();
    for (let i = 0; i < 10; i++) {
      closure(); // 输出 1,2,3,...,10
    }
    
    closure = null; // 释放对闭包的引用
    
    
    
    经典面试题

    涉及for循环和闭包:

    var data = [];
    for (var i = 0; i < 3; i++) {
      data[i] = function () {
        console.log(i);
      };
    }
    data[0](); // 输出什么?
    data[1](); // 输出什么?
    data[2](); // 输出什么?
    
    // 连续输出33
    
    -- 原因:在这段代码中,i 是全局变量,共用一个作用域。当函数被执行的时候,此时的 i 已经变成了3,导致输出的结果都是3。 -->
    

    如果预期输出1、2、3,使用闭包改善:

    var data = [];
    for (var i = 0; i < 3; i++) {
      (function (j) {
        data[j] = function () {
          console.log(j);
        };
      })(i);
    }
    data[0](); // 输出1
    data[1](); // 输出2
    data[2](); // 输出3
    
    -- 原因:在这个例子中,我们使用了一个自执行函数和闭包来创建3个互不干扰的私有作用域。
    这样,每次循环时都会创建一个新的闭包,并将当前的 i 值传递给闭包,使得每个闭包都有自己独立的 j 值。
    因此,当我们调用 data[0]()、data[1]() 和 data[2]() 时,它们分别输出1、2和3。 -->
    

    原型和原型链

    原型(prototype)是一个对象,它是用来创建其他对象的模板。每个函数都有一个 prototype 属性,它指向该函数的原型对象。

    原型链是由一系列原型对象组成的链条。每个对象都有一个原型对象与之关联,这个原型对象也是一个普通对象,它也有自己的原型对象,这样层层递进,就形成了一个链条,这个链条就是原型链。

    原型链的作用是实现继承。当访问一个对象的属性时,如果该属性不存在于该对象中,则会沿着原型链向上查找,直到找到该属性或者到达原型链的顶端。

    img

    原型关系: 指的是对象与其原型对象之间的关系。每个对象都有一个内部属性 [[Prototype]],它指向该对象的原型对象。在 JavaScript 中,可以通过 __proto__ 属性来访问这个内部属性。
    示例:

    // 假设我们有一个构造函数 Person 和一个实例对象 p:
    
    function Person(name) {
      this.name = name;
    }
    
    Person.prototype.sayName = function() {
      console.log(this.name);
    }
    
    var p = new Person('Tom');
    
    // 在这个例子中,p 的原型对象就是 Person.prototype。我们可以通过 p.__proto__ 来访问它:
    
    console.log(p.__proto__ === Person.prototype); // true
    

    ES6新语法特性:let && const

    ES6之前创建变量用的是var,之后创建变量用的是let/const,当然也会用var,那么区别在哪呢?

    var,let和const都是用来声明变量的,但它们之间有一些区别。var声明的变量属于函数作用域,而let和const声明的变量属于块级作用域。此外,var声明的变量存在变量提升现象,而let和const没有。在同一块级作用域中,let变量不能重新声明,而const常量不能修改。简单的来说就是,var定义全局变量且可以覆盖,let定义块级作用域变量且不能再一次进行声明({}),const定义不允许修改的块级作用域常量。

    示例:

    function exampleVar() {
      var x = 1;
      if (true) {
        var x = 2;
        console.log(x); // 输出2
      }
      console.log(x); // 输出2
    }
    
    function exampleLet() {
      let x = 1;
      if (true) {
        let x = 2;
        console.log(x); // 输出2
      }
      console.log(x); // 输出1
    }
    
    function exampleConst() {
      const x = 1;
      if (true) {
        const x = 2;
        console.log(x); // 输出2
      }
      console.log(x); // 输出1
    }
    

    解释:
    在exampleVar函数中,由于var声明的变量属于函数作用域,所以在if语句块中重新声明的变量x会覆盖函数作用域中的变量x。
    而在exampleLet和exampleConst函数中,由于let和const声明的变量属于块级作用域,所以在if语句块中声明的变量x不会影响到外部作用域中的变量x。

    this指向问题

    在JavaScript中,this关键字指向函数执行时的当前对象。this的指向取决于函数调用的方式,而不是函数定义的位置。

    • 在全局作用域中,this指向全局对象(在浏览器中是window对象,在Node.js中是global对象)。
    • 在函数调用中,如果函数不是作为对象的方法被调用,那么this指向全局对象。
    • 在作为对象方法调用时,this指向调用该方法的对象。
    • 在构造函数中,this指向新创建的对象。
    • 在事件处理程序中,this指向触发事件的元素。

    此外,可以使用call()、apply()和bind()方法显式地设置函数调用时的this值。

    示例:

    // 1.在全局作用域中,this指向全局对象:
    console.log(this === window); // 输出true(在浏览器中)
    
    // 2.在函数调用中,如果函数不是作为对象的方法被调用,那么this指向全局对象:
    function foo() {
        console.log(this === window); // 输出true(在浏览器中)
    }
    foo();
    
    // 3.在作为对象方法调用时,this指向调用该方法的对象:
    let obj = {
        myMethod: function() {
            console.log(this === obj); // 输出true
        }
    };
    obj.myMethod();
    
    // 4.在构造函数中,this指向新创建的对象:
    function MyConstructor() {
        this.myProperty = 'Hello World!';
        console.log(this instanceof MyConstructor); // 输出true
    }
    let myInstance = new MyConstructor();
    
    // 5.在事件处理程序中,this指向触发事件的元素:
    <button id="myButton">点击!button>
    <script>
        let button = document.getElementById('myButton');
        button.onclick = function() {
            console.log(this === button); // 输出true
        };
    script>
    
    // 6.使用call()、apply()和bind()方法显式地设置函数调用时的this值:
    function foo() {
        console.log(this);
    }
    let obj = { a: 1 };
    foo.call(obj); // 输出{ a: 1 }
    foo.apply(obj); // 输出{ a: 1 }
    let bar = foo.bind(obj);
    bar(); // 输出{ a: 1 }
    

    此外还有一些特殊情况会影响this的指向问题:

    1. 在严格模式下,如果函数不是作为对象的方法被调用,那么this的值为undefined。
    2. 在DOM事件处理程序中,如果使用addEventListener()方法添加事件处理程序,那么事件处理程序中的this指向触发事件的元素。但是,如果使用attachEvent()方法(仅在旧版本的IE中可用),那么事件处理程序中的this指向全局对象。
    3. 在回调函数中,this的指向取决于回调函数被调用的方式。例如,在setTimeout()和setInterval()中,回调函数中的this指向全局对象。在数组方法(如forEach()、map()、filter()等)中,回调函数中的this指向全局对象,除非显式地设置了thisArg参数。
    4. 在箭头函数中,this的值取决于箭头函数定义时所在的上下文。箭头函数不会创建自己的this值,而是从外层作用域继承this值。
    5. 如果使用了ES6的类语法,那么类中的方法默认是在严格模式下执行的,因此类方法中的this指向取决于方法调用的方式。

    示例:

    // 1.在严格模式下,函数调用中的this指向undefined:
    'use strict';
    function foo() {
        console.log(this);
    }
    foo(); // 输出undefined
    
    // 2.在DOM事件处理程序中,使用addEventListener()方法添加事件处理程序,事件处理程序中的this指向触发事件的元素:
    <button id="myButton">Click me!button>
    <script>
        let button = document.getElementById('myButton');
        button.addEventListener('click', function() {
            console.log(this); // 输出
        });
    script>
    
    // 3.在回调函数中,this的指向取决于回调函数被调用的方式:
    // 在setTimeout()中,回调函数中的this指向全局对象
    setTimeout(function() {
        console.log(this === window); // 输出true(在浏览器中)
    }, 1000);
    
    // 在数组方法中,回调函数中的this指向全局对象,除非显式地设置了thisArg参数
    let arr = [1, 2, 3];
    arr.forEach(function() {
        console.log(this === window); // 输出true(在浏览器中)
    });
    arr.forEach(function() {
        console.log(this === obj);
    }, obj); // 输出true
    
    // 4.在箭头函数中,this的值取决于箭头函数定义时所在的上下文:
    let obj = {
        myMethod: function() {
            let arrowFunction = () => {
                console.log(this === obj); // 输出true
            };
            arrowFunction();
        }
    };
    obj.myMethod();
    
    // 5.在类方法中,this指向取决于方法调用的方式:
    class MyClass {
        myMethod() {
            console.log(this);
        }
    }
    let myInstance = new MyClass();
    myInstance.myMethod(); // 输出MyClass实例
    let myMethod = myInstance.myMethod;
    myMethod(); // 输出undefined(在严格模式下)或全局对象(在非严格模式下)
    

    EventLoop 事件循环

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

    JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。在事件循环期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数。

    这个模型与其他语言中的模型截然不同,比如 C 和 Java。它永不阻塞,处理 I/O 通常通过事件和回调来执行,所以当一个应用正等待一个 IndexedDB 查询返回或者一个 XHR 请求返回时,它仍然可以处理其他事情,比如用户输入。

    宏任务和微任务

    在 JavaScript 引擎中,任务分为两种类型:微任务(microtask)和宏任务(macrotask)。微任务是指在当前任务执行结束后立即执行的任务,它可以看作是在当前任务的“尾巴”添加的任务。常见的微任务包括 Promise 回调和 process.nextTick。宏任务是指在下一轮事件循环中执行的任务。常见的宏任务包括 setTimeout、setInterval、setImmediate、requestAnimationFrame 等。

    微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。比如一个宏任务在执行过程中,产生了 100 个微任务,执行每个微任务的时间是 10 毫秒,那么执行这 100 个微任务的时间就是 1000 毫秒,也可以说这 100 个微任务让宏任务的执行时间延长了 1000 毫秒。

    宏任务和微任务与事件循环有着密切的关系。在事件循环中,每个宏任务执行完后,都会检查微任务队列并执行队列中的所有微任务,然后再执行下一个宏任务。这个过程会一直重复,直到队列中没有消息为止。
    img
    示例:

    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');
    
    // 输出结果:
    script start
    script end
    promise1
    promise2
    setTimeout
    

    解释:首先,同步代码 console.log('script start') 和 console.log('script end') 被执行。然后,setTimeout 被添加到宏任务队列中。接着,Promise.resolve().then 中的回调被添加到微任务队列中。当同步代码执行完后,事件循环检查微任务队列并执行队列中的所有微任务,即 console.log('promise1') 和 console.log('promise2')。最后,事件循环执行下一个宏任务,即 setTimeout 中的回调。

    setTimeout Promise Async/Await 的区别

    1. setTimeout 是 JavaScript 中的一个异步函数,用于在指定的时间间隔后执行一段代码。它属于延迟方法,会被放到最后,也就是主线程空闲的时候才会触发。
    2. Promise 是 JavaScript 中的一种对象,用于处理异步操作的结果。它本身是同步的立即执行函数,当在执行体中执行 resolve() 或者 reject() 的时候,此时是异步操作,会先执行 then/catch 等,等主栈完成后,才会去执行 resolve()/reject() 中的方法。
    3. Async/Await 是 JavaScript 中的一种语法,用于处理异步操作,使代码看起来像同步代码一样。async 用于定义一个异步函数,await 用于等待异步操作的结果。当遇到 await 的时候,会让出主线程,阻塞后面的代码的执行。async 函数需要等待 await 后的函数执行完成并且有了返回结果(Promise 对象)之后,才能继续执行下面的代码。

    优先级:
    Promise 的回调属于微任务,所以它会在当前宏任务执行完后立即执行。
    setTimeout 属于宏任务,所以它会在下一轮事件循环中执行。
    Async/Await 是基于 Promise 的语法糖,它能实现的效果都能用 then 链来实现。当遇到 await 的时候,会让出主线程,阻塞后面的代码的执行。所以 await 后面的代码相当于 promise.then() 里面的代码。

    示例:

    console.log('script start');
    
    setTimeout(function() {
      console.log('setTimeout');
    }, 0);
    
    Promise.resolve().then(function() {
      console.log('promise1');
    }).then(function() {
      console.log('promise2');
    });
    
    async function async1() {
      console.log('async1 start');
      await async2();
      console.log('async1 end');
    }
    
    async function async2() {
      console.log('async2');
    }
    
    async1();
    
    console.log('script end');
    
    // 输出结果:
    script start
    async1 start
    async2
    script end
    promise1
    promise2
    async1 end
    setTimeout
    

    解释: 首先,同步代码 console.log('script start')、console.log('async1 start')、console.log('async2') 和 console.log('script end') 被执行。然后,setTimeout 被添加到宏任务队列中。接着,Promise.resolve().then 中的回调被添加到微任务队列中。当同步代码执行完后,事件循环检查微任务队列并执行队列中的所有微任务,即 console.log('promise1') 和 console.log('promise2')。最后,事件循环执行下一个宏任务,即 setTimeout 中的回调。

    节流&&触底加载 防抖&&实时搜索

    节流

    节流(Throttle)是一种控制函数执行频率的技术。当事件被频繁触发时,节流函数会按照一定的频率来执行函数。它可以保证在一段时间内,不管事件触发了多少次,函数都只会执行一次,且是最先被触发调用的那次。

    举个例子,假设你正在滚动一个页面,每滚动一段距离就会触发一个事件。如果这个事件被频繁触发,可能会导致页面卡顿。这时候,你可以使用节流来控制事件的执行频率,让它每隔一段时间才执行一次。

    节流通常用于优化性能,避免因为事件触发过于频繁而导致的页面卡顿或浏览器崩溃。

    场景:

    • 滚动事件:当用户滚动页面时,可以使用节流来控制滚动事件的执行频率,让它每隔一段时间才执行一次。
    • 窗口大小调整:当用户调整浏览器窗口大小时,可以使用节流来控制调整事件的执行频率,让它每隔一段时间才执行一次。
    • 鼠标移动:当用户移动鼠标时,可以使用节流来控制鼠标移动事件的执行频率,让它每隔一段时间才执行一次。

    滚动事件当然是 触底加载 比较多了。现在用这个作为示例:

    // 节流函数
    function throttle(fn, delay) {
      let timer = null;
      return function() {
        if (!timer) {
          timer = setTimeout(() => {
            fn.apply(this, arguments);
            timer = null;
          }, delay);
        }
      }
    }
    
    // 加载函数
    function loadMore() {
      // 加载更多内容
      console.log('Loading more content...');
    }
    
    // 监听滚动事件
    window.addEventListener('scroll', throttle(function() {
      // 滚动到页面底部时触发加载函数
      if (document.documentElement.scrollTop + window.innerHeight === document.documentElement.scrollHeight) {
        loadMore();
      }
    }, 500));
    

    解释: 在这个例子中,我们定义了一个节流函数 throttle,它接受两个参数:一个是要执行的函数 fn,另一个是延迟时间 delay。当事件被触发时,节流函数会按照指定的频率来执行函数。然后,我们定义了一个加载函数 loadMore,用来加载更多内容。接着,我们监听了滚动事件,并使用节流函数来控制加载函数的执行频率。当滚动到页面底部时,会触发加载函数。

    防抖

    防抖(Debounce)是一种控制函数执行频率的技术。当事件被频繁触发时,防抖函数会推迟执行函数。只有当等待一段时间后也没有再次触发该事件,那么才会真正执行函数。

    举个例子,假设你正在输入一个搜索关键词,每输入一个字符就会触发一个搜索事件。如果这个事件被频繁触发,可能会导致页面卡顿或浏览器崩溃。这时候,你可以使用防抖来控制搜索事件的执行频率,让它在用户停止输入一段时间后才执行。

    防抖通常用于优化性能,避免因为事件触发过于频繁而导致的页面卡顿或浏览器崩溃。

    场景:

    • 输入框实时搜索:当用户在输入框中输入内容时,可以使用防抖来控制搜索事件的执行频率,让它在用户停止输入一段时间后才执行。
    • 窗口大小调整:当用户调整浏览器窗口大小时,可以使用防抖来控制调整事件的执行频率,让它在用户停止调整一段时间后才执行。
    • 按钮点击:当用户点击一个按钮时,可以使用防抖来防止用户连续点击,避免重复提交表单。

    那么就用 实时搜索 作为示例:

    // 防抖函数
    function debounce(fn, delay) {
      let timer = null;
      return function() {
        clearTimeout(timer);
        timer = setTimeout(() => {
          fn.apply(this, arguments);
        }, delay);
      }
    }
    
    // 搜索函数
    function search(keyword) {
      // 执行搜索操作
      console.log(`Searching for ${keyword}...`);
    }
    
    // 获取输入框元素
    const input = document.querySelector('input');
    
    // 监听输入事件
    input.addEventListener('input', debounce(function(event) {
      // 获取输入框的值
      const keyword = event.target.value;
      // 执行搜索操作
      search(keyword);
    }, 500));
    

    解释: 在这个例子中,我们定义了一个防抖函数 debounce,它接受两个参数:一个是要执行的函数 fn,另一个是延迟时间 delay。当事件被触发时,防抖函数会推迟执行函数。如果在等待时间内再次触发该事件,那么会重新计算等待时间。然后,我们定义了一个搜索函数 search,用来执行搜索操作。接着,我们获取了输入框元素,并监听了输入事件。当用户在输入框中输入内容时,会触发输入事件。我们使用防抖函数来控制搜索函数的执行频率,让它在用户停止输入一段时间后才执行。

    垃圾回收机制

    JavaScript 的垃圾回收机制是用来防止内存泄漏的。内存泄漏指的是当已经不需要某块内存时,这块内存还存在着。在项目中,如果存在大量不被释放的内存(堆/栈/上下文),页面性能会变得很慢。当某些代码操作不能被合理释放,就会造成内存泄漏。垃圾回收机制就是间歇性地、不定期地寻找到不再使用的变量,并释放掉它们所指向的内存。

    JavaScript 的垃圾回收算法主要有两种:引用计数(reference counting)和标记清除(mark-and-sweep)。

    引用计数算法通过跟踪每个值被引用的次数来工作。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因此就可以将其占用的内存空间回收回来。

    标记清除算法将“不再使用的变量”定义为“无法访问到这个变量”。垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量即为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

    其他

    new

    过程:

    1. 首先,创建一个全新的对象。然后,将这个对象的原型链(proto)指向函数的 .prototype。
    2. 接着,将这个对象绑定到函数中的 this,然后执行函数,函数内部可以借助 this 给这个对象添加属性。
    3. 最后,如果这个函数没有返回其他对象的话,new 操作符就会将上面步骤创建的对象返回出去。但如果该函数最后返回了一个其他对象的话,new 操作符就会把这个函数返回的对象返回出去。也就是判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

    示例:

    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    
    var person1 = new Person('幼儿园技术家', 25);
    console.log(person1.name); // 输出: 幼儿园技术家
    console.log(person1.age); // 输出: 25
    

    三种常用方法实现继承

    1. 使用原型链。
      示例:
    function Animal(name) {
      this.name = name;
    }
    
    Animal.prototype.sayName = function() {
      console.log(this.name);
    }
    
    function Dog(name, breed) {
      Animal.call(this, name);
      this.breed = breed;
    }
    
    Dog.prototype = Object.create(Animal.prototype);
    Dog.prototype.constructor = Dog;
    
    Dog.prototype.bark = function() {
      console.log('Woof!');
    }
    
    let dog = new Dog('Max', 'German Shepherd');
    dog.sayName(); // Max
    dog.bark(); // Woof!
    
    1. 使用 class 关键字来定义类,并使用 extends 关键字来实现继承。
      示例:
    class Animal {
      constructor(name) {
        this.name = name;
      }
    
      sayName() {
        console.log(this.name);
      }
    }
    
    class Dog extends Animal {
      constructor(name, breed) {
        super(name);
        this.breed = breed;
      }
    
      bark() {
        console.log('Woof!');
      }
    }
    
    let dog = new Dog('Max', 'German Shepherd');
    dog.sayName(); // Max
    dog.bark(); // Woof!
    
    1. 使用混入(Mixin)。
      示例:
    let Animal = {
      sayName: function() {
        console.log(this.name);
      }
    }
    
    function Dog(name, breed) {
      this.name = name;
      this.breed = breed;
    }
    
    Object.assign(Dog.prototype, Animal);
    
    Dog.prototype.bark = function() {
      console.log('Woof!');
    }
    
    let dog = new Dog('Max', 'German Shepherd');
    dog.sayName(); // Max
    dog.bark(); // Woof!
    

    手写bind方法

    // 可以通过在 Function.prototype 上添加一个新方法来手写实现 bind 方法
    Function.prototype.myBind = function(context) {
      var self = this;
      var args = Array.prototype.slice.call(arguments, 1);
      return function() {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(context, args.concat(bindArgs));
      }
    }
    
    var obj = {
      name: '幼儿园技术家'
    }
    
    function sayName(age) {
      console.log(this.name);
      console.log(age);
    }
    
    var boundSayName = sayName.myBind(obj, 25);
    boundSayName(); // 输出: 幼儿园技术家 \n 25
    

    解释:在上面的示例中,我们定义了一个 myBind 方法,它接受一个参数 context,表示绑定的上下文对象。然后我们使用 apply 方法将函数的执行上下文绑定到指定的对象上,并传入相应的参数。最后我们可以调用绑定后的函数。

    CmmonJS和ESM

    CommonJS和ESM是两种不同的JavaScript模块化规范。CommonJS主要用于服务器端,比如Node.js,而ESM是ECMAScript 6中引入的模块化标准,它既可以用于前端,也可以用于后端。

    CommonJS和ESM之间有一些主要区别:
    首先,它们的语法不同。CommonJS使用 require 和 module.exports 来导入和导出模块,而ESM使用 import 和 export 关键字。
    其次,CommonJS模块是运行时加载的,而ESM模块是编译时输出接口的。此外,CommonJS是同步加载模块的,而ESM支持异步加载。

    示例:

    // CommonJS
    var foo = require('foo');
    module.exports = foo;
    
    // ESM
    import foo from 'foo';
    export default foo;
    

    柯里化

    在上面的闭包中我们有提到柯里化,那么这里简单介绍一下。要思考柯里化是什么?有什么用?怎么实现?

    柯里化(Currying)是一种处理多元函数的方法,它是指将一个多参数的函数转化为单参数函数的方法。它是数学家柯里(Haskell Curry)提出的。

    柯里化的主要作用是将一个复杂的函数拆分成多个简单的函数,使得每个函数只接受一个参数。这样做可以让我们更灵活地使用这些函数,比如可以将它们组合起来,或者将它们作为参数传递给其他函数。

    示例:

    function add(x, y) {
      return x + y;
    }
    
    function curriedAdd(x) {
      return function(y) {
        return add(x, y);
      }
    }
    
    var add5 = curriedAdd(5);
    console.log(add5(3)); // 输出: 8
    

    call bind apply

    在解决this指向问题中提到了call、apply 和 bind,那么现在来介绍一下。

    call、apply 和 bind 都是JavaScript中的函数方法,它们都可以用来改变函数的执行上下文(即函数内部的 this 指向)。

    call 和 apply 的作用相似,它们都可以用来立即调用一个函数,并指定函数内部的 this 指向。它们的区别在于传递参数的方式不同:call 方法接受若干个参数,第一个参数是 this 指向的对象,后面的参数依次传递给函数;而 apply 方法接受两个参数,第一个参数是 this 指向的对象,第二个参数是一个数组,数组中的元素依次传递给函数。

    bind 方法与 call 和 apply 不同,它不会立即调用函数,而是返回一个新的函数。这个新函数与原函数具有相同的行为,但是它内部的 this 指向被绑定到了 bind 方法的第一个参数上。除了第一个参数外,bind 方法还可以接受若干个参数,这些参数会被预先传递给新函数。

    示例:

    function sayName(greeting) {
      console.log(`${greeting}, my name is ${this.name}`);
    }
    
    var obj = {
      name: '幼儿园技术家'
    }
    
    sayName.call(obj, 'Hello'); // 输出: Hello, my name is 幼儿园技术家
    sayName.apply(obj, ['Hello']); // 输出: Hello, my name is 幼儿园技术家
    
    var boundSayName = sayName.bind(obj);
    boundSayName('Hello'); // 输出: Hello, my name is 幼儿园技术家
    

    Vue

    Vue (发音为 /vjuː/,类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。

    Vue 是一个典型的 MVVM 模型的框架。MVVM 是 Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对 View 和 ViewModel 的双向数据绑定。这使得 ViewModel 的状态改变可以自动传递给 View,即所谓的数据双向绑定。

    优点:

    • 易于学习和使用:Vue提供了一个平滑的学习曲线,使其适用于初学者和专业开发人员。它有着丰富的文档和教程,并且有着庞大的社区支持。
    • 高性能:Vue使用虚拟DOM来提高应用的性能和渲染效率。虚拟DOM是一个轻量级的JavaScript对象,它是真实DOM的抽象表示。当组件的状态发生变化时,Vue会根据新的状态创建一个新的虚拟DOM树。然后,Vue会使用一个高效的算法来比较新旧虚拟DOM树,计算出最小的更新操作来更新真实DOM。
    • 灵活性:Vue非常灵活,可以与现有项目无缝集成。它提供了许多高级功能,如计算属性、侦听器和过渡效果等,可以帮助开发人员更快地构建复杂的应用程序。

    缺点:

    • 生态系统不够成熟:相比其他前端框架,Vue的生态系统不够成熟。它缺少一些高质量的插件和工具,这可能会影响开发人员的工作效率。
    • 文档不足:尽管Vue有着丰富的文档和教程,但由于其快速发展,有时文档可能不够完整或过时。
    • 学习曲线陡峭:尽管Vue相对容易学习,但要真正掌握它并开发复杂的应用程序仍然需要一定的时间和精力。

    既然提到了 mvvm,那么就简单说一下 MVC 以及 MVVM 和 MVC 之间的区别:

    MVC 和 MVVM 都是一种设计模式,它们都旨在将应用程序分成不同的部分,以便更好地管理和维护。

    MVC

    MVC 是 Model-View-Controller 的缩写,它将应用程序分成三个部分:Model 负责存储数据和业务逻辑,View 负责展示数据,Controller 负责接收用户输入并更新 Model 和 View。在 MVC 模式中,View 和 Model 是相互独立的,它们之间通过 Controller 来进行通信。

    优点:

    1. 耦合度低:MVC 的三个部件(Model、View 和 Controller)是相互独立的,改变其中一个不会影响其他两个。
    2. 重用性高:多个视图可以使用同一个模型。
    3. 可维护性高:由于各个部件之间的分离,MVC 模式下的应用程序更容易维护。

    缺点:

    1. 不适合小型项目开发。
    2. 视图与控制器联系过于紧密,妨碍了它们的独立重用。
    MVVM

    MVVM 是 Model-View-ViewModel 的缩写,它也将应用程序分成三个部分:Model 负责存储数据和业务逻辑,View 负责展示数据,ViewModel 则负责连接 View 和 Model。与 MVC 不同的是,在 MVVM 模式中,View 和 ViewModel 之间有着双向数据绑定的联系。这意味着当 ViewModel 中的数据发生变化时,View 会自动更新;而当 View 中的数据发生变化时,ViewModel 也会自动更新。

    优点:

    1. 低耦合:视图(View)可以独立于 Model 变化和修改,一个 Model 可以绑定到不同的 View 上。当 View 变化时,Model 可以不变化;当 Model 变化时,View 也可以不变。
    2. 可重用性:你可以把一些视图逻辑放在一个 Model 里面,让很多 View 重用这段视图逻辑。
    3. 独立开发:双向数据绑定的模式实现了 View 和 Model 的自动同步,因此开发者只需要专注对数据的维护操作即可,而不需要一直操作 DOM。

    缺点:

    1. 增加了代码复杂度,并且对于简单的应用来说可能会显得过于繁琐。
    2. 由于 MVVM 模式依赖于双向数据绑定,因此它也可能会带来一些性能问题。

    MVC 和 MVVM 的主要区别在于它们对 View 和 Model 之间通信方式的不同处理。MVC 通过 Controller 来进行通信,而 MVVM 则通过双向数据绑定来实现通信。这两种模式各有优缺点,具体使用哪种模式取决于具体的应用场景。

    底层实现原理

    Vue 的底层实现原理主要包括数据双向绑定虚拟 DOM两部分。

    数据双向绑定是指当数据发生变化时,视图会自动更新;而当视图发生变化时,数据也会自动更新。Vue 实现数据双向绑定的方式是通过数据劫持发布订阅模式相结合。

    • 数据劫持:Vue 会拦截 data 对象中所有属性的读取和写入操作。在 Vue 2.x 版本中,数据劫持是通过 Object.defineProperty() 方法实现的;而在 Vue 3.x 版本中,数据劫持则是通过 Proxy 对象实现的。
    • 发布订阅模式:当我们修改 data 中的某个属性时,Vue 会通知所有订阅了该属性变化的观察者(Watcher),并执行相应的回调函数。这些回调函数通常会更新视图,以保证视图与数据保持同步。

    虚拟 DOM 是一种用 JavaScript 对象表示 DOM 的技术。它可以让我们在不直接操作 DOM 的情况下更新视图。Vue 在更新视图时会先生成一个新的虚拟 DOM 树,然后将新旧虚拟 DOM 树进行对比,找出它们之间的差异。最后,Vue 会根据这些差异来更新真实的 DOM 树。这个过程被称为“patching”。

    使用虚拟DOM有以下几个好处:

    1. 提高渲染性能:直接操作真实DOM通常是非常慢的,因为浏览器需要执行很多额外的工作,如样式计算、布局和重绘。使用虚拟DOM可以减少对真实DOM的操作次数,从而提高渲染性能。
    2. 跨平台:虚拟DOM是一个抽象层,它可以运行在任何支持JavaScript的平台上。这意味着你可以使用Vue来构建跨平台应用,如桌面应用、移动应用和Web应用。
    3. 更容易测试:由于虚拟DOM是一个纯粹的数据结构,它更容易进行测试和调试。

    相对于手动操作真实DOM,使用虚拟DOM通常可以获得更好的性能。但这并不是绝对的,因为虚拟DOM也有一些开销,如创建虚拟DOM树和计算差异。在某些情况下,手动操作真实DOM可能会更快。但总体来说,使用虚拟DOM可以让我们更容易地构建高性能和跨平台的应用。

    生命周期

    Vue 的生命周期指的是 Vue 实例从创建到销毁的整个过程。在这个过程中,Vue 实例会经历一系列的生命周期钩子函数,这些钩子函数可以让我们在特定的时刻执行特定的操作。

    • beforeCreate:在实例初始化之后,数据观测和事件配置之前被调用。
      created:在实例创建完成后被立即调用。此时,实例已完成以下配置:数据观测、属性和方法的运算、watch/event 事件回调。但是,挂载阶段还没开始,$el 属性目前不可见。
    • beforeMount:在挂载开始之前被调用。相关的 render 函数首次被调用。
      mounted:在 el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。如果根实例挂载了一个文档内元素,当 mounted 被调用时,vm.$el 也在文档内。
    • beforeUpdate:在数据更新之前调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM。
      updated:在由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。
    • beforeDestroy:在实例销毁之前调用。此时实例仍然完全可用。
      destroyed:在实例销毁之后调用。此时,所有的指令绑定都被解除,所有的事件监听器都被移除,所有的子实例也都被销毁。

    Vuex

    Vuex是一个专为Vue.js应用程序开发的状态管理模式+库。使用Vuex时,每一个Vuex应用的核心就是store(仓库)。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。它可以帮助我们管理共享状态,解决多组件数据通信问题。

    img

    简单来说,Vuex就像一个容器,它包含了你的应用中大部分的状态。当Vue组件从store中读取状态时,若store中的状态发生变化,那么相应的组件也会相应地得到高效更新。

    你可以通过store.state来获取状态对象,并通过store.commit方法触发状态变更。在Vue组件中,可以通过this.$store访问store实例,但不能直接改变store中的状态。改变store中的状态的唯一途径就是显式地提交mutation,而非直接改变store.state.count。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

    Vuex主要包括以下几个核心模块:

    • State:Vuex使用单一状态树,用一个对象就包含了全部的应用层级状态。每个应用将仅仅包含一个store实例。单一状态树让我们能够直接定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。
    • Getter:有时候我们需要从store中的state中派生出一些状态,例如对列表进行过滤并计数。Vuex允许我们在store中定义getter(可以认为是store的计算属性)。就像计算属性一样,getter的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
    • Mutation:更改Vuex的store中的状态的唯一方法是提交mutation。Vuex中的mutation非常类似于事件:每个mutation都有一个字符串的事件类型(type)和一个回调函数(handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受state作为第一个参数。
    • Action:Action类似于mutation,不同在于:Action提交的是mutation,而不是直接变更状态;Action可以包含任意异步操作。Action函数接受一个与store实例具有相同方法和属性的context对象,因此你可以调用context.commit提交一个mutation,或者通过context.state和context.getters来获取state和getters。
    • Module:由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store对象就有可能变得相当臃肿。为了解决这个问题,Vuex允许我们将store分割成模块(module)。每个模块拥有自己的state、mutation、action、getter、甚至是嵌套子模块。

    一些常见的Vuex使用场景包括:用户的个人信息管理模块、电商项目的购物车模块、我的订单模块(订单列表中点击取消订单,然后更新对应的订单列表)、在订单结算页获取需要的优惠券并更新订单优惠信息等。

    示例:

    // store.js
    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      state: {
        count: 0
      },
      getters: {
        doubleCount: state => state.count * 2
      },
      mutations: {
        increment(state) {
          state.count++
        }
      },
      actions: {
        increment(context) {
          context.commit('increment')
        }
      }
    })
    
    // main.js
    import Vue from 'vue'
    import App from './App.vue'
    import store from './store'
    
    new Vue({
      el: '#app',
      store,
      render: h => h(App)
    })
    
    // App.vue
    
    
    <script>
    export default {
      computed: {
        count() {
          return this.$store.state.count
        }
      },
      methods: {
        increment() {
          this.$store.dispatch('increment')
        }
      }
    }
    script>
    

    组件之间的通信方式

    除了以上说的 Vuex 进行组件之间的通讯外,常见的组件通讯还有以下几种方式:

    1. props / $emit:父组件通过props向子组件传递数据,子组件通过$emit向父组件传递数据。
      示例:
    // 父组件
    
    
    <script>
    import Child from './Child.vue'
    export default {
      components: { Child },
      data() {
        return {
          msg: 'Hello'
        }
      },
      methods: {
        changeMsg(newMsg) {
          this.msg = newMsg
        }
      }
    }
    script>
    
    // 子组件
    <template>
      <div>
        <p>{{ msg }}p>
        <button @click="changeMsg">Change Msgbutton>
      div>
    template>
    
    <script>
    export default {
      props: ['msg'],
      methods: {
        changeMsg() {
          this.$emit('changeMsg', 'Hi')
        }
      }
    }
    script>
    
    1. ref / $refs:父组件可以通过$refs获取子组件的实例,从而调用子组件的方法或访问子组件的数据。
      示例:
    // 父组件
    
    
    <script>
    import Child from './Child.vue'
    export default {
      components: { Child },
      methods: {
        getChildMsg() {
          console.log(this.$refs.child.msg)
        }
      }
    }
    script>
    
    // 子组件
    <template>
      <div>{{ msg }}div>
    template>
    
    <script>
    export default {
      data() {
        return {
          msg: 'Hello'
        }
      }
    }
    script>
    
    1. eventBus事件总线($emit / $on):可以创建一个空的Vue实例作为事件总线,在组件中通过$emit触发事件,在另一个组件中通过$on监听事件,从而实现组件间通信。
      示例:
    // eventBus.js
    import Vue from 'vue'
    export const eventBus = new Vue()
    
    // 组件A
    
    
    <script>
    import { eventBus } from './eventBus.js'
    export default {
      methods: {
        emitEvent() {
          eventBus.$emit('myEvent', 'Hello')
        }
      }
    }
    script>
    
    // 组件B
    <template>
      <div>{{ msg }}div>
    template>
    
    <script>
    import { eventBus } from './eventBus.js'
    export default {
      data() {
        return {
          msg: ''
        }
      },
      mounted() {
        eventBus.$on('myEvent', (data) => {
          this.msg = data
        })
      }
    }
    script>
    
    1. $parent / $children:子组件可以通过$parent访问父组件实例,父组件可以通过$children访问子组件实例。
      示例:
    // 父组件
    
    
    <script>
    import Child from './Child.vue'
    export default {
      components: { Child },
      methods: {
        getChildMsg() {
          console.log(this.$children[0].msg)
        }
      }
    }
    script>
    
    // 子组件
    <template>
      <div>{{ msg }}div>
    template>
    
    <script>
    export default {
      data() {
        return {
          msg: 'Hello'
        }
      }
    }
    script> 
    
    1. $attrs/ $listeners:$attrs包含了父组件中不作为prop被识别且获取的特性绑定,$listeners包含了父组件中的v-on事件监听器。
      示例:
    // 父组件
     
    
    <script> 
    import Middle from './Middle.vue' 
    export default { 
       components: { Middle }, 
       data() { 
          return { 
             msg: 'Hello' 
          } 
       }, 
       methods: { 
          changeMsg(newMsg) { 
             this.msg = newMsg 
          } 
       } 
    } 
    script> 
    
    // 中间组件
    <template> 
    <div> 
       <child v-bind="$attrs" v-on="$listeners">child> 
    div> 
    template> 
    
    <script> 
    import Child from './Child.vue' 
    export default { 
       components: { Child }, 
       inheritAttrs: false // 不继承父组件的属性,避免将属性绑定到根元素上。 
    } 
    script> 
    
    // 子组件
    <template> 
    <div> 
       <p>{{ msg }}p> 
       <button @click="changeMsg">Change Msgbutton> 
    div> 
    template> 
    
    <script> 
    export default { 
       props: ['msg'], 
       methods: { 
          changeMsg() { 
             this.$emit('changeMsg', 'Hi') 
          } 
       } 
    } 
    script>
    
    1. provide/inject:祖先组件通过provide提供变量,然后在子孙组件中通过inject来注入变量。
      示例:
    // 祖先组件
    
    
    <script>
    import Child from './Child.vue'
    export default {
      components: { Child },
      provide() {
        return {
          msg: 'Hello'
        }
      }
    }
    script>
    
    // 子孙组件
    <template>
      <div>{{ msg }}div>
    template>
    
    <script>
    export default {
      inject: ['msg']
    }
    script>
    

    computed与watch

    computed和watch都是Vue实例的选项,用来监听数据变化并执行相应的操作。

    computed

    computed:计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。这就意味着只要相关依赖没有发生改变,多次访问计算属性会立即返回之前的计算结果,而不必再次执行函数。计算属性默认只有getter,不过在需要时你也可以提供一个setter。
    示例:

    new Vue({
      el: '#app',
      data: {
        message: 'Hello'
      },
      computed: {
        reversedMessage: function () {
          return this.message.split('').reverse().join('')
        }
      }
    })
    

    watch

    watch:当你需要在数据变化时执行异步或开销较大的操作时,可以使用watch。watch选项允许我们执行异步操作(访问一个API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。
    示例:

    new Vue({
      el: '#app',
      data: {
        message: 'Hello'
      },
      watch: {
        message: function (newVal, oldVal) {
          console.log('message changed from', oldVal, 'to', newVal)
        }
      }
    })
    

    区别:

    • 计算属性是基于它们的依赖进行缓存的。只有在相关依赖发生改变时,计算属性才会重新求值。这意味着只要相关依赖没有发生改变,多次访问计算属性会立即返回之前的计算结果,而不必再次执行函数。相比之下,watch选项中的函数每次都会执行。
    • 计算属性通常用来计算一个值,这个值是基于它的依赖进行计算的。当你需要根据数据变化来改变数据时,可以使用计算属性。相比之下,watch选项通常用来执行异步操作或开销较大的操作。
    • 计算属性是响应式的,当它们的依赖发生改变时,它们会自动更新。相比之下,watch选项需要手动设置监听的数据。

    当你需要根据数据变化来改变数据时,可以使用计算属性;当你需要根据数据变化来执行异步操作或开销较大的操作时,可以使用watch。

    其他

    v-if和v-for同时使用在一个元素上的问题

    不建议在同一元素上同时使用v-for和v-if。当它们同时存在时,v-for的优先级比v-if更高,这意味着v-if将分别重复运行于每个循环的项上。这可能会导致性能问题,因为在渲染列表时会进行更多的计算。

    场景一:如果你想根据条件过滤列表并渲染过滤后的结果,可以将过滤后的结果计算为一个计算属性,然后在v-for中使用这个计算属性:

    <template>
      <ul>
        <li v-for="item in filteredItems" :key="item.id">
          {{ item.text }}
        li>
      ul>
    template>
    
    <script>
    export default {
      data() {
        return {
          items: [
            { id: 1, text: 'Item 1', show: true },
            { id: 2, text: 'Item 2', show: false },
            { id: 3, text: 'Item 3', show: true }
          ]
        }
      },
      computed: {
        filteredItems() {
          return this.items.filter(item => item.show)
        }
      }
    }
    script>
    

    场景二:如果你的目的是有条件地跳过循环的执行,那么可以将v-if放置在外层元素(如