前端学习——知识总结

506 阅读1小时+

css html

实现倒三角形

使宽高为 0,设置 border 的大小来改变三角形的大小,然后设置 border-bottom 的底部一个颜色,其余 border 为透明。

聊聊盒模型

  • 盒模型分为标准盒模型和 IE 盒模型,标准盒模型即 content、元素的内容就是其宽度,不包含 padding 和 border 的大小。而 IE 盒模型的宽度包含了该元素的 padding 和 border。
  • 在 css 中可以通过 box-sizing 来设置盒模型,content-box 对应标准模型,border-box 对应 IE 模型。

position 的属性和区别

1、列举

  • static 默认值
  • fixed
  • absolute
  • relative
  • sticky

2、说相对于谁进行定位

  • static 是默认值,遵循文档流。
  • fixed 是固定在我们可视窗口上的,不会随着滚动条的滚动而改变该元素在我们可视窗上的位置。
  • relative 是相对定位,是相对其原本遵循标准流时的位置。
  • absolute 是绝对定位,是相对于其有 relative 的父元素的定位,如果最近一级父元素没有 relative 属性,则再往上一级找,直至到 body 为止。我们可以通过改变 top、left、right、bottom 设置值来改变该元素的位置,但拥有该属性的元素不会占据空间,可能会与其他元素发生重叠。通常我们会用子绝父相的方式搭配使用。
  • sticky 是当滚动条在没有滚动到一定位置时,该元素不会随着滚动条的滚动而滚动,当滚动到一定位置时,该元素随着滚动条的滚动而滚动,在我们的可视窗范围内位置固定。通常我们会用来实现页面的点击返回到页面顶部。

CSS 布局,左边固定,右边自适应如何实现

两栏步局(多种方式)

水平垂直居中(多种方式)

margin 与 transform 实现水平垂直居中的区别 回流重绘

高度塌陷问题

CSS高度塌陷(或称为塌陷问题)是指在使用CSS布局时,某些元素的高度没有按照预期的方式显示或渲染的现象。这通常发生在使用浮动绝对定位内联块等属性时,导致父元素的高度无法自动适应其子元素的高度,从而造成视觉上的错乱或布局问题。

最常见的情况是当父元素只包含浮动元素时,父元素的高度会塌陷为零,而不会自动根据浮动元素的高度进行调整。这可能导致布局上的错乱,使得父元素无法完全包裹子元素,影响页面的外观和用户体验。

解决CSS高度塌陷问题的方法包括清除浮动、使用overflow属性、添加额外的占位符元素或使用Flexbox和Grid等现代布局技术。通过这些方法,可以确保父元素的高度可以根据其子元素的高度正确地进行调整,从而避免布局上的问题。

说说Flex

基本概念

  • 通常被称为flexbox,是一种一维的布局模型,给子元素提供了空间分布和对齐能力。它由(Flex Container容器/Flex item项目成员)构成。
  • 该布局模型的目的是提供一种更加高效的方式来对容器中的条目进行布局、对齐和分配空间。适用于不同尺寸屏幕中创建可自动扩展和收缩布局,通常可用于水平垂直居中两栏三栏布局等的场景里

Flex三个值

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

    • 如果所有项目的flex-grow属性相等(或都为1),将等分剩余空间,如果有一个为2,那么它占据的剩余空间将比其他项目
  • flex-shrink:项目的缩小比例,默认为1,即如果空间不足,该项目将缩小

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

  • flex属性是flex-grow,flex-shrinkflex-basis的简写,默认值为0 1 auto。后两个属性可选。该属性有两个快捷值auto (1 1 auto)none (0 0 auto)

  • 如果设置 flex:1,就等于 flex: 1 1 0;设置 flex:0;就等于 flex: 0 0 0;

子元素都设置 flex 1 宽度是否一样;

小提示:这个问题是对上述 Flex 的一个追问

Flex 1 相当于 flex: 1 1 0;项目占的主轴空间为0,所以平分;但是如果有padding属性的话,元素占的地方会被增加;设置margin的话,元素的宽度不变,但是content内容的宽度变小;

Flex 如何实现三栏布局

左右设置flex: 0 1 200px;中间设置flex:1;父元素flex布局。

Flex 1 的理解

如果设置 flex:1,就等于 flex: 1 1 0;设置 flex:0;就等于 flex: 0 0 0;

BFC

如何开启 BFC

  • 根元素
  • float属性不为none
  • position为absolute或fixed
  • overflow不为visible
  • display为inline-block,table-cell,table-caption,flex,inline-flex

BFC布局规则:

  1. 内部的Box会在垂直方向,一个接一个地放置。
  2. BFC的区域不会与float box重叠。
  3. 内部的box垂直方向的距离由margin决定。属于 同一个BFC的两个相邻 的BOX的margin会发生重叠
  4. 计算BFC高度时,浮动元素也会被计算在内(清除浮动haslayout)
  5. BFC就是页面上一个隔离的区域,外面不会影响到内部爱,内部也不会影响到外面

特性和应用

  • 阻止margin重叠:同一个 BFC 下外边距(margin)会发生折叠

  • 清除浮动 :清除内部浮动(清除浮动的原理是两个div都位于同一个 BFC 区域之中)

  • 自适应两栏布局:左float+右BFC,是利用了BFC 的区域不会与 float 的元素区域重叠的机制

聊聊 rem 与 em 和 px 的区别

  • em 是相对于当前对象内文本的字体尺寸,如果当前对行内文本的字体尺寸没有设置,则相对于浏览器的默认字体尺寸。
  • rem 是 css3 的新增字体单位,是相对于 HTML 根元素的。

在CSS中,emrempx 是用于定义长度和距离的单位,它们之间的主要区别如下:

  1. px(像素)

    • px 是最常见的长度单位,它代表屏幕上的一个像素点。
    • px 是一个绝对单位,其大小不会随着父元素的大小或字体大小的改变而改变。
    • 使用 px 可以确保元素的尺寸在不同设备上具有固定的大小,但可能会导致在不同分辨率的屏幕上显示不一致。
  2. em

    • em 是相对单位,它的值相对于父元素的字体大小来计算。
    • 如果应用在字体大小上,1em 等于当前元素的父元素的字体大小。
    • 如果应用在其他属性上,如宽度或高度,1em 等于当前元素的字体大小。
    • em 单位的使用可以使得页面更容易调整到不同的字体大小,同时也能实现一些相对于文字大小的布局效果。
  3. rem

    • rem 也是相对单位,但是相对于根元素(即 <html> 元素)的字体大小来计算。
    • em 不同,rem 的值不会受到父元素字体大小的影响,它始终是相对于根元素的字体大小。
    • rem 主要用于需要保持相对于整个页面的一致性的布局元素。

总结:

  • px 是绝对单位,适用于需要精确控制尺寸的情况。
  • em 是相对单位,适用于需要相对于父元素字体大小变化的情况。
  • rem 也是相对单位,但相对于根元素的字体大小,适用于需要相对于整个页面字体大小变化的情况。

说说移动端如何适配

响应式布局

简而言之,就是页面元素的位置随着屏幕尺寸的变化而变化,通常会用百分比来定位,而在设计上需要预留一些可被“压缩”的空间。

样式缩放

最省事的适配方法,直接用px为单位按视觉进行开发,然后通过计算屏幕与网页的宽高比,用transform:scale来对网页进行全局缩放。

不过此方法会有一个小问题,就是如果网页内有动画的话,缩放后会稍微降低页面性能,在低配的安卓机器上表现的比较明显,iOS上没发现有性能问题。

Rem缩放

通过Rem为单位来进行视觉开发,然后通过计算后改变html的front-size来对页面进行缩放。

懒加载

懒加载就是实现图片或视频的懒加载,当页面没有滑倒图片或视频所在位置时不加载该图片或视频,而是加载默认的图片。这是移动端性能优化的一种手段

这样可以大大的减少带宽。

vue2.0 时使用插件vue-lazyload插件 在vue3.0的时候可以使用一个叫vue3-lazy的插件来完成

js

聊聊 JS 数据类型

1、基本数据类型

string、number、undefined、null、boolen

2、复杂数据类型 Object、array、date、regexp

3、其他 symbol、bigint

判断数据类型

typeof Object.toString.call instanceof constructor

在JavaScript中,有几种方法可以用来判断数据类型,包括:

  1. typeof 操作符:

    typeof variable;
    

    typeof 操作符用于返回操作数的数据类型,返回的结果是一个字符串。常见的返回值包括 "undefined""object""boolean""number""string""function""symbol" 等。但是 typeof null 返回 "object",这是 JavaScript 的一个历史遗留问题。

  2. instanceof 操作符:

    variable instanceof Object;
    

    instanceof 操作符用于判断一个对象是否是某个构造函数创建的实例,返回布尔值。需要注意的是,instanceof 只能用于判断对象类型,不能用来判断基本数据类型。

  3. Object.prototype.toString.call() 方法:

    Object.prototype.toString.call(variable);
    

    这是一种通用的方法,可以精确地判断变量的类型,返回一个字符串,格式为 "[object 数据类型]"。例如,"[object Array]" 表示数组类型,"[object Object]" 表示对象类型,等等。

  4. Array.isArray() 方法:

    Array.isArray(variable);
    

    Array.isArray() 方法用于确定传递的值是否是一个数组,返回布尔值。

  5. NaN的特性:

    isNaN(variable);
    

    isNaN() 函数用于判断参数是否为非数字值,如果变量是 NaN,则返回 true,否则返回 false。需要注意的是,isNaN() 会尝试将参数转换为数字,因此非数字字符串也会返回 true

  6. 严格相等运算符(===) :

    variable === value;
    

    严格相等运算符 === 用于判断两个值是否严格相等,包括值和类型都要相等。这种方法可以用来判断基本数据类型的值。

这些方法可以根据需要选择合适的方式来判断数据类型

typeof instanceof constructor的区别

  1. typeof运算符

使用typeof运算符获取一个值的类型,可能的结果有5种:

undefined、boolean、number、string、object

注:typeof null //得到"object",typeof运算符对于null值会返回"object"。这实际上是JavaScript

  1. instanceof运算符

在引用类型值判断类型的时候,typeof运算符会出现一个问题,无论引用的是什么类型的对象,它都返回"object"。

示例:

function Person(){}

function User(){}

var u=new User;

console.log( u instanceof Person ); //false

console.log( u instanceof User ); //true

例子解析:

“变量u是否为Person对象的实例?得到false。

“变量u是否为User对象的实例?得到true。

因为typeof在判断对象类型的值的时候,有很大的局限性,

所以instanceof会是一个不错的选择,instanceof才能判断一个值具体是由什么构造函数构造出来的。

  1. Object.constructor属性

javascript中的所有对象都继承自Object。

constructor是Object的一个属性,他指向:创建对象的函数的引用(指针)。(可以理解为constructor指向对象的构造函数)

简单示例:

function User(){}

var u = new User;

console.log(u.constructor===User );//得到true,也就是说对象的constructor属性指向他的构造函数。

console.log(u.constructor.name );//得到User,也就是构造函数的名称

注:constructor属性并非一定指向构造函数,他也是可以修改、变更的。

聊聊堆栈内存

栈内存:

  1. 从电脑内存中分配一块出来,用来执行代码的内存,Stack
  2. 先创建变量,再做赋值操作
  3. 分配一个主线程来自上而下执行。(js单线程,浏览器多线程)

主要用来运行代码,和存储基本类型。

  1. 是变量的存储空间,存储创建的变量
  2. 值存储空间,存基本数据类型的值
  3. 一个值存储空间可以对应多个变量,一个变量只能对应一个值存储空间。

堆内存:

  1. 从内存中拿出来一块,用来存引用数据类型。
  2. 按照键、值分别存放,并关联起来。

聊聊 JS 的事件(捕获,响应,冒泡)

事件是文档和浏览器窗口中发生的特定的交互瞬间。

JavaScript事件处理模型涉及三个主要概念:捕获阶段、目标阶段和冒泡阶段。这些阶段描述了事件在DOM中传播和处理的顺序。

  1. 捕获阶段

    • 在捕获阶段,事件从最外层的父元素传播到目标元素。
    • 捕获阶段的主要目的是为了预先处理事件,例如在事件到达目标之前对其进行拦截或修改。
  2. 目标阶段

    • 当事件到达目标元素时,触发目标阶段。
    • 在目标阶段,事件会在目标元素上触发相应的事件处理程序。
  3. 冒泡阶段

    • 在目标阶段之后,事件开始从目标元素向外传播到最外层的父元素。
    • 冒泡阶段的主要目的是允许父级元素捕获或响应事件,以便在目标元素之外执行额外的处理。

聊聊事件代理、事件委托

什么是事件委托:通俗的讲,事件就是onclick,onmouseover,onmouseout等就是事件。委托,就是让别人来做,这个事件本来是加在某些元素上的,然而你却加到别人身上来做,完成这个事件。这样可以减少事件处理程序的数量,尤其在处理大量动态生成的子元素时非常有用。

也就是:利用冒泡的原理,把事件加到父级上,触发执行效果。

优点:

  1. 性能优化:减少了事件处理程序的数量,降低了内存消耗和页面加载时间。
  2. 简化代码:无需为每个子元素绑定事件处理程序,减少了重复代码量。
  3. 动态元素:对于动态生成的子元素,无需重新绑定事件处理程序,提供了更好的扩展性和灵活性。

例子:需要触发每个li来改变他们的背景颜色。 但是如果说我们可能有很多个li用for循环的话就比较影响性能,这时候可以在父级上绑定onclick事件进行处理。

DOM 事件有哪些

鼠标事件、键盘事件、框架/对象事件、表单事件、剪贴板事件、打印事件、拖动事件、多媒体事件、动画事件、过渡事件、其他事件

获取 DOM 事件的方法

要在JavaScript中获取DOM事件,您可以使用以下几种方法:

  1. addEventListener() 方法

    • 使用 addEventListener() 方法可以为DOM元素添加事件监听器,以便在事件触发时执行指定的函数。

    • 示例:

      element.addEventListener('click', function(event) {
          // 处理点击事件的代码
      });
      
  2. HTML 属性

    • 在HTML标签中直接添加事件属性,可以在事件发生时执行指定的JavaScript代码。

    • 示例:

      <button onclick="handleClick()">Click me</button>
      
  3. 内联事件处理程序

    • 直接在JavaScript代码中为元素的事件属性赋值一个函数,来作为事件处理程序。

    • 示例:

         <button onclick="alert('Hello, world!')">Click me</button>
      
  4. 通过事件对象

    • 在事件处理程序中,可以通过事件对象访问事件相关的信息,如事件类型、目标元素等。

    • 示例:

      element.onclick = function(event) {
          console.log('Event type:', event.type);
          console.log('Target element:', event.target);
      };
      
  5. 事件委托

    • 使用事件委托可以在父元素上监听事件,而不是在每个子元素上单独添加事件监听器。

    • 示例:

      parentElement.addEventListener('click', function(event) {
          if (event.target.tagName === 'BUTTON') {
              // 处理按钮点击事件的代码
          }
      });
      

这些是常用的获取DOM事件的方法,您可以根据实际情况选择适合您需求的方法来监听和处理事件。

事件绑定的方法同上

1、事件绑定的几种方式

在Javascript中,事件绑定一共有3种方式:

① 行内绑定

例:

<input type=’button’ onclick=’display()’ />

② 动态绑定

dom对象.事件 = 事件的处理程序(通常是一个匿名函数)

③ 事件监听

onclick 与 addEventListener 的区别

1.onclick 事件在同一时间只能指向唯一对象

2.addEventListener 可以给一个事件注册多个 listener,这些 listener 会依次执行。通过 onclick 给一个对象绑定多次事件的话,只会出现最后的一个绑定。

3.addEventListener 对任何 DOM 都是有效的,而 onclick 仅限于 HTML

4.addEventListener 可以控制 listener 的触发阶段,(捕获/冒泡)。对于多个相同的事件处理器,不会重复触发,不需要手动使用 removeEventListener 清除.

  1. onclick 绑定的事件不能通通过 removeEventListener 移除,只能通过使其等于 null 来移除。

注销事件绑定的方法

li.onclick = null ul.removeEventListener('')

let、const、var 的区别

  1. var声明的变量会挂载在window上,而let和const声明的变量不会
  2. var声明变量存在变量提升,let和const不存在变量提升
  3. let和const声明形成块作用域
  4. 同一作用域下let和const不能声明同名变量,而var可以
  5. const 一旦声明必须赋值,不能使用null占位,声明后不能再修改

call、apply、bind 的区别和使用

相同点: 都可以改变函数内部的 this 指向.

区别点:

  1. call 和 apply 会调用函数,并且改变函数内部 this 指向.
  2. call 和 apply 传递的参数不一样, call 传递参数 aru1, aru2..形式 apply 必须数组形式[arg]
  3. bind 不会调用函数,可以改变函数内部 this 指向.

主要应用场景:

  1. call 经常做继承.
  2. apply 经常跟数组有关系.比如借助于数学对象实现数组最大值最小值
  3. bind 不调用函数,但是还想改变 this 指向.比如改变定时器内部的 this 指向.

ES5

var 痛点: 变量提升 无块级作用域 for (var i = 0; i < 10; i++) { li.onclick = i }

ES6新特性

ES6的一些主要特性:

  1. 箭头函数(Arrow Functions):

    • 箭头函数提供了一种更简洁的语法来定义函数,可以减少代码量并改善代码可读性。
  2. let 和 const 声明

    • letconst 关键字用于声明变量,取代了 var 关键字,提供了块级作用域和更严格的变量声明方式。
  3. 模板字符串(Template Literals):

    • 模板字符串允许在字符串中插入变量和表达式,使得字符串拼接更加方便和直观。
  4. 解构赋值(Destructuring Assignment):

    • 解构赋值语法可以轻松地从数组或对象中提取数据并赋值给变量,使得代码更加简洁。
  5. 默认参数值(Default Parameters):

    • 可以为函数的参数指定默认值,简化了函数的定义和调用。
  6. 展开运算符(Spread Operator):

    • 展开运算符可以在数组或对象字面量中展开数组或对象,以方便地复制、合并或传递数据。
  7. 类和继承(Classes and Inheritance):

    • ES6引入了类的概念,使得面向对象编程更加直观和易用,并支持了类之间的继承关系。
  8. 模块化(Modules):

    • ES6提供了原生的模块化支持,可以使用 importexport 关键字来导入和导出模块,提高了代码的可维护性和可重用性。
  9. 箭头函数(Arrow Functions):

    • 箭头函数提供了一种更简洁的语法来定义函数,可以减少代码量并改善代码可读性。
  10. Promise

    • Promise 是一种用于异步编程的解决方案,ES6引入了原生的Promise对象,使得异步操作更加直观和易用。
  11. Generator

    • Generator 是一种异步编程的解决方案,ES6引入了原生的Generator函数,可以控制函数的执行流程,实现更灵活的异步编程。
  12. 新的数据结构(New Data Structures):

    • ES6引入了新的数据结构,如Map、Set、WeakMap、WeakSet等,提供了更多的数据处理方式和选择。
  13. 增强的对象字面量(Enhanced Object Literals):

    • ES6扩展了对象字面量的功能,支持了计算属性名、简写方法和属性的定义等。

这些是ES6的一些主要特性,它们使得JavaScript语言更加强大、灵活和易用。

聊聊 JS 线程

JavaScript 是一种单线程语言,这意味着它只能在一个主线程上执行代码。但是,JavaScript 环境通常不仅仅包含一个单独的线程。在浏览器环境中,除了主线程外,还有一些其他的线程,例如:

  1. UI 线程

    • 在浏览器环境中,UI 线程负责处理用户界面的渲染和交互。它会负责处理用户输入、执行 DOM 操作、处理事件等。
  2. 网络线程

    • 网络线程用于处理网络请求和响应。当 JavaScript 代码发起网络请求时,这些操作通常是在独立的线程中进行的,以防止阻塞主线程。
  3. 定时器线程

    • 定时器线程用于处理 setTimeout 和 setInterval 等定时器函数的调用。它会计算定时器的时间,并在指定的时间间隔后触发相应的回调函数。
  4. 事件循环线程

    • 事件循环线程负责管理 JavaScript 代码的执行顺序。它会维护一个事件队列,将任务逐个从队列中取出并执行。这个事件循环的机制决定了 JavaScript 的异步执行方式。
  5. 工作线程

    • 一些现代浏览器支持 Web Workers,它们允许 JavaScript 代码在独立的线程中运行,以执行一些耗时的计算或后台任务,而不会阻塞主线程。

尽管 JavaScript 是单线程的,但是利用以上这些额外的线程,使得 JavaScript 代码能够在后台进行网络请求、定时器操作、复杂计算等,从而实现了更为流畅和响应的用户体验。在编写 JavaScript 代码时,需要考虑到事件循环机制和异步执行方式,以避免阻塞主线程,从而提高页面性能和用户体验。

单线程

js为什么是单线程

JavaScript最初设计为单线程执行的原因有几个方面:

  1. 简单性和一致性:单线程模型使得JavaScript的执行模型更加简单和一致。它不需要考虑多线程并发执行所带来的复杂性,从而使得JavaScript的设计更加清晰和易于理解。
  2. 安全性:在Web浏览器中执行JavaScript代码时,JavaScript代码可以访问页面上的DOM结构和浏览器的API,如果允许多线程并发执行,可能会导致DOM操作的不一致性、竞态条件和安全漏洞。单线程执行可以避免这些问题,从而提高了代码的安全性。
  3. 浏览器的限制:早期的浏览器对多线程支持有限,单线程执行是为了兼容各种浏览器环境。即使现代浏览器支持了Web Workers等机制来进行多线程编程,但JavaScript本身仍然保持了单线程的特性。

尽管JavaScript是单线程执行的,但是通过异步编程模型(如事件循环机制、Promise、异步函数等),JavaScript可以在单线程的基础上实现并发执行,从而充分利用CPU和其他资源,提高了代码的性能和响应能力。

js 设计的初衷 让 dom 动起来 操作 dom 元素 bug

为什么要有异步?

异步任务时间长导致浏览器卡顿,

  • 因为 js 单线程,同一时间只能做一件事

  • 但是遇到等待 ( 网络请求、定时任务 ) 就会卡住,卡住的时候 CPU 就是空闲的,这种等待让人不甘心

  • 异步不会阻塞代码执行,页面就不会卡住,所以使用异步

  • 异步采用回调 callback 的形式

有哪些异步任务

1、定时器:settimeout setinterval 2、http 3、Js 事件触发的函数:onclick 这些 webworker

聊聊宏任务和微任务

宏任务(macrotask)和微任务(microtask)是与 JavaScript 异步编程相关的概念,它们有助于理解事件循环(event loop)的工作原理。

  1. 宏任务(Macrotask)

    • 宏任务是由浏览器或Node.js环境提供的任务,通常是一些异步操作,比如 setTimeout、setInterval、I/O 操作等。
    • 宏任务的执行顺序是按照任务队列中的顺序进行的,每个宏任务执行完毕后,会检查是否有微任务需要执行,然后再执行下一个宏任务。
  2. 微任务(Microtask)

    • 微任务是在当前任务(宏任务)执行完毕后立即执行的任务。
    • 微任务包括 Promise 的回调函数、MutationObserver 的回调函数等。
    • 微任务执行时机在宏任务执行完毕后、当前宏任务的事件循环结束前。

在事件循环中,宏任务和微任务的执行顺序是有规定的,典型的执行顺序是这样的:

  1. 当执行一个宏任务时,会检查是否有微任务需要执行,如果有,那么先执行所有微任务。
  2. 执行完所有微任务后,再执行下一个宏任务。
  3. 重复上述步骤。

这个执行顺序保证了微任务比宏任务更快执行,因为微任务是在当前宏任务执行完毕后立即执行的,而不需要等待下一个宏任务。这也是为什么在 Promise 的 then 方法中的回调函数会比 setTimeout 中的回调函数先执行的原因之一。

总的来说,宏任务和微任务的概念对于理解 JavaScript 异步编程中事件循环的工作原理非常重要,它们帮助开发者更好地控制代码的执行顺序,避免出现一些意外的结果。

js的事件循环

在解释事件循环之前首先先解释一下浏览器的执行线程:

浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程,其中浏览器渲染进程(浏览器内核)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等

其包含的线程有:GUI 渲染线程(负责渲染页面,解析 HTML,CSS 构成 DOM 树)、JS 引擎线程、事件触发线程、定时器触发线程、http 请求线程等主要线程

关于执行中的线程:

主线程:也就是 js 引擎执行的线程,这个线程只有一个,页面渲染、函数处理都在这个主线程上执行。

工作线程:也称幕后线程,这个线程可能存在于浏览器或js引擎内,与主线程是分开的,处理文件读取、网络请求等异步事件。

所有的任务可以分为同步任务和异步任务,同步任务,顾名思义,就是立即执行的任务,同步任务一般会直接进入到主线程中执行;而异步任务,就是异步执行的任务,比如ajax网络请求,setTimeout 定时函数等都属于异步任务,异步任务会通过任务队列的机制(先进先出的机制)来进行协调。

同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入任务队列。主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。

在事件循环中,每进行一次循环操作称为tick,通过阅读规范可知,每一次 tick 的任务处理模型是比较复杂的,其关键的步骤可以总结如下:

1.在此次 tick 中选择最先进入队列的任务( oldest task ),如果有则执行(一次)

2.检查是否存在 Microtasks ,如果存在则不停地执行,直至清空Microtask Queue

3.更新 render

4.主线程重复执行上述步骤

聊聊闭包,以及闭包的作用

  1. 闭包的定义:
  • 有权访问另一个函数作用域中变量的函数
  • 一个作用域可以访问另一个函数内部的局部变量
  • 闭包是一个对象
  • 闭包保存在内部函数中

2. 闭包的形成条件:

  • 一定要有函数嵌套
  • 内部函数引用外部函数的局部变量
  • 外部函数调用

3. 闭包的作用:

  • 延伸了变量作用域的范围
  • 延长外部函数局部变量的生命周期
  • 形成一个不销毁的私有作用域
  • 保护里面的私有变量不受外界干扰

4. 闭包的缺点:占内存,容易造成内存溢出

5. 使用闭包的注意事项: 及时清除闭包(让指向内部函数的外部变量指向null)

6. 闭包的使用场景:

  • 解决循环遍历加监听的问题
  • 将内部函数作为返回值返回

手写深拷贝

function deepCopy(newObj, targetObj) {
  for (var k in targetObj) {
    if (targetObj[k] instanceof Array) {   //由于数组也属于对象,因此要先把数组的判断放在前面
      newObj[k] = [];  
      deepCopy(newObj[k], targetObj[k]);
    } else if (targetObj[k] instanceof Object) {
      newObj[k] = {};
      deepCopy(newObj[k], targetObj[k]);
    } else {
      newObj[k] = targetObj[k];
    }
  }
}

ES6 去重

使用set方法

数组去重

Array.from(new Set(array))

防抖、节流原理

防抖

定义:- 对于短时间内连续触发的事件(上面的滚动事件),防抖的含义就是让某个时间期限(如上面的1000毫秒)内,事件处理函数只执行一次。

先说一个常见的功能,很多网站会提供这么一个按钮:用于返回顶部。

这个按钮只会在滚动到距离顶部一定位置之后才出现,那么我们现在抽象出这个功能需求-- 监听浏览器滚动事件,返回当前滚条与顶部的距离。但是,在运行的时候会发现存在一个问题:这个函数的默认执行频率很高

基于上述场景,首先提出第一种思路:在第一次触发事件时,不立即执行函数,而是给出一个期限值比如200ms,然后:

  • 如果在200ms内没有再次触发滚动事件,那么就执行函数
  • 如果在200ms内再次触发滚动事件,那么当前的计时取消,重新开始计时

效果:如果短时间内大量触发同一事件,只会执行一次函数。

实现:既然前面都提到了计时,那实现的关键就在于setTimeout这个函数,由于还需要一个变量来保存计时,考虑维护全局纯净,可以借助闭包来实现

应用场景: 频繁操作点赞和取消点赞的时候,search搜索联想,用户在不断输入值时,点击提交

节流

但是如果产品同学的期望处理方案是:即使用户不断拖动滚动条,也能在某个时间间隔之后给出反馈呢?

其实很简单:我们可以设计一种类似控制阀门一样定期开放的函数,也就是让函数执行一次后,在某个时间段内暂时失效,过了这段时间后再重新激活(类似于技能冷却时间)。

效果:如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新生效。

实现 这里借助setTimeout来做一个简单的实现,加上一个状态位valid来表示当前函数是否处于工作状态

  • 应用场景:一般在onresize/mousemove/onscroll等事件中,防止过多的请求造成服务器压力

什么是回流,什么是重绘,有什么区别?

html 加载时发生了什么

在页面加载时,浏览器把获取到的HTML代码解析成1个DOM树,DOM树里包含了所有HTML标签,包括display:none隐藏,还有用JS动态添加的元素等。
浏览器把所有样式(用户定义的CSS和用户代理)解析成样式结构体
DOM Tree 和样式结构体组合后构建render tree, render tree类似于DOM tree,但区别很大,因为render tree能识别样式,render tree中每个NODE都有自己的style,而且render tree不包含隐藏的节点(比如display:none的节点,还有head节点),因为这些节点不会用于呈现,而且不会影响呈现的,所以就不会包含到 render tree中。我自己简单的理解就是DOM Tree和我们写的CSS结合在一起之后,渲染出了render tree。

什么是回流

当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候,这时候是一定会发生回流的,因为要构建render tree。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。

什么是重绘

当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。

区别:

他们的区别很大:
回流必将引起重绘,而重绘不一定会引起回流。比如:只有颜色改变的时候就只会发生重绘而不会引起回流
当页面布局和几何属性改变时就需要回流
比如:添加或者删除可见的DOM元素,元素位置改变,元素尺寸改变——边距、填充、边框、宽度和高度,内容改变

dom但是异步更新的

Vue是异步执行dom更新的,一旦观察到数据变化,不会马上更新dom,而是Vue就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOm操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。

单页面应用(SPA)

单页Web应用(single page web application,SPA),就是只有一张Web页面的应用。单页应用程序 (SPA) 是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序。 浏览器一开始会加载必需的HTML、CSS和JavaScript,所有的操作都在这张页面上完成,都由JavaScript来控制。因此,对单页应用来说模块化的开发和设计显得相当重要。

速度:更好的用户体验,让用户在web app感受native app的速度和流畅,

MVC:经典MVC开发模式,前后端各负其责。

ajax:重前端,业务逻辑全部在本地操作,数据都需要通过AJAX同步、提交。

路由:在URL中采用#号来作为当前视图的地址,改变#号后的参数,页面并不会重载。

优点:

  1. 具有桌面应用的即时性、网站的可移植性和可访问性。
  2. 用户体验好、快,内容的改变不需要重新加载整个页面,web应用更具响应性和更令人着迷。
  3. 基于上面一点,SPA相对对服务器压力小。
  4. 良好的前后端分离。SPA和RESTful架构一起使用,后端不再负责模板渲染、输出页面工作,web前端和各种移动终端地位对等,后端API通用化。
  5. 对前端人员javascript技能要求更高,促使团队技能提升。

缺点:

1、分功能模块的鉴权不好实现。

2、不利于SEO,传统的SPA在SEO(搜索引擎优化)方面存在挑战,因为内容是动态生成的,搜索引擎的爬虫可能无法完全索引应用内容。不过,这个问题可以通过服务端渲染(SSR)或预渲染(Prerendering)技术来解决。。

3、初次加载耗时相对增多。

4、导航不可用,如果一定要导航需要自行实现前进、后退。

5、对开发人员技能水平、开发成本高。

CSS选择器有哪些

1.标签选择器

2.ID选择器

3.类选择器

4.组选择器

语法:选择器1,选择器2,选择器N{} , eg: #box1,.box2,p{} //为id为box1,class为box2和p的元素共同设置样式。

5、通配符选择器

语法:{} ,eg:{font-size:16px } //将整个页面字体大小设为16px。

6.后代选择器

语法:选择器1 选择器2{} , eg:p .aa{} //选中指定祖先元素p的指定后代.aa。

7.子元素选择器

语法:父元素>子元素

8.伪类选择器

伪类可以用来表示一些特殊的状态,如: :link - 未访问过的超链接。 :visited - 已访问过的超链接。 :hover - 鼠标经过的元素。 :active - 正在点击的元素。

CSS选择器的优先级顺序

当同一属性的不同值都作用到了同一个元素时,如果定义的属性之间有冲突,那么应该用谁的值的,这个时候就涉及到CSS的优先级顺序了。
1.在属性后面使用 !important 会覆盖页面内任何位置定义的元素样式。
2.作为style属性写在元素内的内部样式
3.id选择器
4.类选择器
5.标签选择器
6.通配符选择器
7.浏览器自定义或继承的

总结排序:!important > 内部样式>ID选择器 > 类选择器 > 标签选择器> 通配符选择器 > 继承 > 浏览器默认属性

聊聊 Promise

Promise 是 JavaScript 中处理异步操作的一种方式,它是 ES6 中新增的一个特性。Promise 提供了一种更加清晰和优雅的方式来处理异步代码,解决了传统的回调地狱问题,使得异步操作的链式调用更加直观和易于理解。

Promise 的主要特点包括:

  1. 状态(States):

    • Promise 有三种状态:待定(pending)、已完成(fulfilled)和已拒绝(rejected)。
    • 当 Promise 被创建时,它处于待定状态,可以转换为已完成或已拒绝状态。
    • 一旦 Promise 的状态发生变化,就不会再改变。
  2. then() 方法

    • Promise 实例提供了 then() 方法,用于指定当 Promise 对象的状态发生变化时,需要执行的回调函数。
    • then() 方法接收两个参数:一个用于处理 Promise 成功状态的回调函数和一个用于处理 Promise 失败状态的回调函数。
  3. catch() 方法

    • Promise 实例提供了 catch() 方法,用于捕获 Promise 对象的错误。
    • catch() 方法通常用于处理 Promise 中的拒绝状态,相当于 then(null, onRejected) 的简写形式。
  4. 链式调用

    • Promise 可以通过链式调用的方式处理异步操作,使得代码更加简洁和易于理解。
    • 可以在 then() 方法中返回一个新的 Promise 对象,从而实现链式调用。
  5. 异步操作

    • Promise 主要用于处理异步操作,如网络请求、文件读写、定时器等。
    • Promise 对象的状态会根据异步操作的结果发生改变。
  6. Promise.all() 方法

    • Promise.all() 方法接收一个包含多个 Promise 对象的可迭代对象,并返回一个新的 Promise 对象。
    • 当所有 Promise 对象都变为已完成状态时,返回的 Promise 对象才会变为已完成状态;如果有一个 Promise 对象变为已拒绝状态,则返回的 Promise 对象会立即变为已拒绝状态。
  7. Promise.race() 方法

    • Promise.race() 方法接收一个包含多个 Promise 对象的可迭代对象,并返回一个新的 Promise 对象。
    • 当可迭代对象中的任何一个 Promise 对象发生状态变化时,返回的 Promise 对象就会采用该 Promise 对象的状态。

Promise 的引入使得异步编程更加优雅和可控,避免了回调地狱的问题,使得代码更加清晰和易于维护。

有20个异步请求,如何保持同时三个的并发

使用 Promise.race 来控制并发请求数量需要一些额外的逻辑,因为 Promise.race 只会返回最先完成或拒绝的 Promise。但是我们需要保持同时三个的并发请求。下面是一种利用递归和 Promise.race 的方法来实现这个目标:

// 引入需要的库或模块
const axios = require('axios');

// 请求地址列表
const urls = [
  'url1',
  'url2',
  // 添加更多的请求地址
];

// 最大并发数
const maxConcurrency = 3;

// 递归函数,用于发起异步请求
const makeRequests = async (urls) => {
  // 如果请求列表为空,则结束递归
  if (urls.length === 0) return;

  // 从请求列表中取出前三个请求地址
  const batch = urls.slice(0, maxConcurrency);

  // 发起三个并发请求
  const promises = batch.map(url => axios.get(url));

  // 使用 Promise.race 来等待三个请求中的一个完成
  try {
    await Promise.race(promises);
  } catch (error) {
    console.error('One of the requests failed:', error);
  }

  // 继续递归,处理剩余的请求
  await makeRequests(urls.slice(maxConcurrency));
};

// 调用递归函数,开始发起请求
makeRequests(urls);

这段代码会保持最多三个并发请求,并在一个请求完成后,立即发起下一个请求,直到所有请求完成。在递归函数中,每次使用 Promise.race 来等待三个请求中的一个完成。

回调地狱

Promise 的 API 有哪些,他们的具体作用

Promise.all promise.race()

Promise应用场景 ** Promise 是异步编程的一种解决方案:从语法上讲,promise是一个对象,从它可以获取异步操作的消息;从本意上讲,它是承诺,承诺它过一段时间会给你一个结果。promise有三种状态: pending(等待态),fulfiled(成功态),rejected(失败态);状态一旦改变,就不会再变。创造promise实例后,它会立即执行。**

promise是什么? 1、主要用于异步计算

2、可以将异步操作队列化,按照期望的顺序执行,返回符合预期的结果

3、可以在对象之间传递和操作promise,帮助我们处理队列

4、Promise可以用来避免异步操作函数里的嵌套回调(callback hell)问题,因为解决异步最直接的方法是回调嵌套,将后一个的操作放在前一个操作的异步回调里,但如果操作多了,就会有很多层的嵌套。

为什么会有promise? 为了避免界面冻结(任务)

异步回调的问题: ● 之前处理异步是通过纯粹的回调函数的形式进行处理

● 很容易进入到回调地狱中,剥夺了函数return的能力

● 问题可以解决,但是难以读懂,维护困难

● 稍有不慎就会踏入回调地狱 - 嵌套层次深,不好维护

ES6里promise解决了什么问题? 1、promise本质上是给出了一种异步任务同步写法的解决方案,然后才是解决回调地狱问题;

2、其中还会涉及微任务宏任务问题;先执行宏任务,再执行微任务

3、anysc await 其实本质是promise.then的执行结果;但是其更完美的体现了异步任务同步执行的形式

数组 API(map、filter)

获取对象 key 值的方法(Object.keys)

for k in object 要获取 JavaScript 对象中的键(key)值,可以使用以下几种方法:

  1. 使用点语法

    • 如果知道对象的键名,可以直接使用点语法来获取对应的键值。
    const obj = { name: 'John', age: 30 };
    const name = obj.name; // 获取 name 属性的值
    const age = obj.age; // 获取 age 属性的值
    
  2. 使用中括号语法

    • 除了点语法,还可以使用中括号语法来获取对象的键值,这种方式可以动态获取键值,适用于键名是变量的情况。
    const obj = { name: 'John', age: 30 };
    const key = 'name';
    const name = obj[key]; // 获取 name 属性的值
    
  3. 使用 Object.keys() 方法

    • Object.keys() 方法返回一个包含对象自身可枚举属性的键的数组,然后可以遍历该数组来获取对应的键值。
    const obj = { name: 'John', age: 30 };
    const keys = Object.keys(obj); // 返回 ['name', 'age']
    for (const key of keys) {
        const value = obj[key]; // 获取键对应的值
        console.log(key, value);
    }
    
  4. 使用 Object.entries() 方法

    • Object.entries() 方法返回一个包含对象自身可枚举属性键值对的数组,然后可以遍历该数组来获取对应的键值。
    const obj = { name: 'John', age: 30 };
    const entries = Object.entries(obj); // 返回 [['name', 'John'], ['age', 30]]
    for (const [key, value] of entries) {
        console.log(key, value);
    }
    

这些方法可以根据需求选择合适的方式来获取对象的键值

new一个函数的时候发生了什么

  • 像普通函数执行一样,形成一个私有的作用域    
    • 形参赋值

    - 变量提升

  • 默认创建一个对象,让函数中的this执行这个对象,这个对象就是当前类的一个实例
  • 代码执行
  • 默认把创建的对象返回

手写new

function Dog(name) {
  this.name = name;
}
function _new(Fn, ...arg) {
  // 1.
  //创建一个空对象,让他的原型链指向Fn.prototype(作为Fn的一个实例)
  // let obj = {};
  // obj.__proto__ = Fn.prototype;

  // 2.
  let obj = Object.create(Fn.prototype);
  Fn.call(obj, ...arg);
  return obj;
}
Dog.prototype.saHi = function () {
  console.log("hi");
};
let sanmao = _new(Dog, "三毛");
sanmao.saHi();

JS 的基本数据类型

number string boolean undefined null

[1,2,3] + 3 会输出什么以及为什么

在JavaScript中,当你尝试使用加号(+)运算符将一个数组和一个数值相加时,数组首先会被转换成字符串,然后再与数值(也被转换为字符串)进行连接。这是因为+运算符在JavaScript中既可以用作数值的加法运算,也可以用作字符串的连接运算。JavaScript在执行这种操作时会进行类型转换,试图将操作数转换为一个共同的类型,以便可以应用加法或连接操作。

对于[1,2,3] + 3

  1. 数组[1,2,3]会首先被转换为字符串"1,2,3"
  2. 数值3也会被转换为字符串"3"
  3. 然后,这两个字符串会被连接在一起,得到最终的结果"1,2,33"

因此,[1,2,3] + 3的结果是字符串"1,2,33"

这个行为是JavaScript自动类型转换和强制类型转换的一个例子,是JavaScript中类型弱性的一个表现。这种类型的自动转换有时可能会导致意想不到的结果,因此在编写JavaScript代码时需要特别注意操作数的类型。

0.1 + 0.2 等于 0.3 吗?为什么?解决方案?

在JavaScript中,0.1 + 0.2并不等于0.3,而是等于0.30000000000000004。这个问题源于浮点数在计算机中的表示方式。JavaScript遵循IEEE 754标准,使用64位双精度浮点格式表示数字,这导致一些十进制小数不能被精确表示为二进制浮点数。

为什么会这样?

计算机使用二进制(基数为2)来存储数据,而某些十进制小数在转换为二进制表示时会出现无限循环小数。因为存储空间有限,这些无限循环小数必须在某一点被截断,这导致了精度损失。就像十进制无法精确表示1/3一样,二进制也无法精确表示某些十进制小数,例如0.10.2。当这些不精确的表示进行运算时,误差会累积,从而导致0.1 + 0.2不等于0.3

解决方案

解决这个问题的一种方法是在进行运算之前将小数点后的数值乘以一个因子(例如,将0.10.2乘以10,使它们变成整数,进行加法运算后再除以10),或者使用JavaScript提供的Number.prototype.toFixed()方法进行四舍五入,或者使用第三方库来处理精确的小数运算。

  1. 使用乘法和除法
const result = (0.1 * 10 + 0.2 * 10) / 10; // 将0.1和0.2先转换为整数进行运算
console.log(result); // 0.3
  1. 使用toFixed()方法

toFixed()方法可以将一个数四舍五入到指定的小数位数,并以字符串的形式返回结果。需要注意的是,toFixed返回的是字符串,如果需要将结果转换为数字,可以使用Number()或者一元加+操作符。

const result = +(0.1 + 0.2).toFixed(10); // toFixed返回的是字符串,+转换为数字
console.log(result); // 0.3
  1. 使用第三方库

对于复杂的数学运算,特别是需要高精度小数计算的场景,可以考虑使用第三方库,比如decimal.jsbig.jsbignumber.js,这些库提供了更精确的小数运算能力。

import Decimal from 'decimal.js';

const result = new Decimal(0.1).plus(0.2);
console.log(result.toString()); // '0.3'

通过这些方法,可以有效避免JavaScript中的浮点数精度问题。

聊聊原型与原型链

JavaScript 常被描述为一种基于原型的语言——每个实例对象( object )都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( __proto__ ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

javascript 查找机制

查找.jpg

① 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性,找到即返回,不再向上查找。

② 如果没有就查找它的原型(也就是__proto__ 指向的 prototype 原型对象)。

③ 如果还没有就查找原型对象的原型( Object 的原型对象)。

④ 依此类推一直找到 Object 为止( null ),返回 undefined。

proto对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。

原型对象中 this 指向

  • 在构造函数中,里面的 this 指向的是实例对象
  • 原型对象函数里面的 this,是谁调用这个函数,this 就指向谁,因此也是指向实例对象

如何判断一个对象是否在原型链上(instanceof)

在JavaScript中,可以通过几种方式来判断一个对象是否存在于另一个对象的原型链上。以下是一些常用的方法:

  1. 使用instanceof运算符instanceof可以检查一个对象是否是另一个对象的实例,它通过检查对象的原型链来确定这一点。

    // 示例
    function Constructor() {}
    var obj = new Constructor();
    console.log(obj instanceof Constructor); // 输出:true
    
  2. 使用Object.prototype.isPrototypeOf方法:这个方法可以用来检查一个对象是否存在于另一个对象的原型链上。

    // 示例
    var prototypeObj = {};
    var obj = Object.create(prototypeObj);
    console.log(prototypeObj.isPrototypeOf(obj)); // 输出:true
    
  3. 使用__proto__属性(非标准,但在很多环境中可用) :通过比较对象的__proto__属性,你可以手动遍历原型链来判断一个对象是否在另一个对象的原型链上。请注意,__proto__属性不是所有环境都支持的标准属性,使用时需要谨慎。

    // 示例
    var parent = {};
    var child = Object.create(parent);
    console.log(child.__proto__ === parent); // 输出:true
    
  4. 使用Object.getPrototypeOf方法:这是一个标准方法,用于获取对象的原型。通过比较返回的原型对象,可以判断原型链的关系。

    // 示例
    var prototypeObj = {};
    var obj = Object.create(prototypeObj);
    console.log(Object.getPrototypeOf(obj) === prototypeObj); // 输出:true
    

在实际应用中,推荐使用instanceof运算符或Object.prototype.isPrototypeOf方法,因为这两种方法更加标准化,且不需要直接操作对象的原型属性。

实现一下 instanceof

聊聊箭头函数

箭头函数和普通函数的区别:

  1. 箭头函数语法上比普通函数更加简洁
  2. 箭头函数没有自己的 this,它的 this 是继承函数所处上下文中的 this,。普通函数则是谁调用这个函数,this 就指向谁,并且可以通过 call、apply、bind 改变 this 指向,但箭头函数不可以。
  3. 箭头函数中没有 arguments(类数组),只能基于...arg 获取传递的参数集合(数组)
  4. 箭头函数不能被 new 执行(因为没有自己的 this 和 prototype)

当我们需要用到 this 的时候就好好好考虑要用哪一种形式的函数,假设我们给 html 元素绑定事件的时候使用箭头函数就会造成一个致命性错误,因为这里的 this 指向 window 而不是指向元素本身

箭头函数做构造函数会发生什么(报错)

在JavaScript中,箭头函数不能用作构造函数,尝试这样做会抛出一个错误。箭头函数相较于传统的函数表达式有几个关键的区别,其中之一就是它们没有自己的thissuperarguments、和new.target绑定。这意味着箭头函数不能用new关键字调用,因为new关键字在函数中是用来创建一个实例对象的,它依赖于函数的this绑定。

如果你尝试使用箭头函数作为构造函数,比如:

const MyConstructor = () => {};
const instance = new MyConstructor(); // 尝试使用箭头函数作为构造函数

你会遇到类似以下的错误信息:

TypeError: MyConstructor is not a constructor

这个错误表明MyConstructor(箭头函数)不能被用作构造函数。

解决方案:

如果你需要定义一个可以实例化的构造函数,你应该使用传统的函数声明或者函数表达式,而不是箭头函数:

function MyConstructor() {
    // 构造函数体
}

// 或者使用函数表达式
const MyConstructor = function() {
    // 构造函数体
};

const instance = new MyConstructor(); // 正确

这样定义的函数可以安全地与new关键字一起使用,用于创建新的实例对象。

总结来说,箭头函数因为没有自己的this绑定等特性,不能作为构造函数使用。当需要定义一个构造函数时,应该使用传统的函数声明或函数表达式。

聊聊 null 和 undefined

null:

null表示"没有对象",即该处不应该有值

(1) 作为函数的参数,表示该函数的参数不是对象。

(2) 作为对象原型链的终点。

undefined:

表示"缺少值",就是此处应该有一个值,但是还没有定义

(1)变量被声明了,但没有赋值时,就等于undefined。

(2) 调用函数时,应该提供的参数没有提供,该参数等于undefined。

(3)对象没有赋值的属性,该属性的值为undefined。

(4)函数没有返回值时,默认返回undefined。

聊聊作用域

  • 全局作用域 标签所在的区域就是所谓的全局作用域,全局作用域有一个全局的对象 我们所有定义的变量,函数,类,对象等等都是作用域 对象身上的 局部作用域(特指函数作用域)

  • 会产生一个局部作用域,这个作用域里面存储着这个局部作用域里面的所有变量定义,函数定义,对象定义等等。局部作用域是可以访问全局作用域的。

es6实现继承的子类中super()的位置能放在this后面吗

在ES6中,当你在子类的构造函数中使用super()调用父类的构造函数时,super()必须在使用this关键字之前调用。这是因为在子类的构造函数中,this对象是由父类构造函数创建的。如果你尝试在调用super()之前使用this,JavaScript会抛出一个引用错误。

原因: 在ES6类的背景下,super()负责初始化子类实例的this引用。如果没有调用super(),子类就没有机会初始化其this对象,从而无法正常访问this引用的属性或方法。这是为了确保子类能正确地继承和初始化从父类继承的所有属性。

js如何添加类名

要在 JavaScript 中添加类名,你可以使用以下方法:

  1. 使用 classList 属性

classList 属性提供了一组方法来操作元素的类名。

// 选择元素
let element = document.getElementById('myElement');

// 添加类名
element.classList.add('newClassName');

// 删除类名
element.classList.remove('oldClassName');

// 切换类名(如果存在则删除,不存在则添加)
element.classList.toggle('active');

// 检查类名是否存在
if (element.classList.contains('className')) {
  // 类名存在的处理
}
  1. 直接操作 className 属性

你也可以直接操作元素的 className 属性,但是这种方法不如 classList 方法灵活。

// 选择元素
let element = document.getElementById('myElement');

// 添加类名
element.className += ' newClassName';

// 删除类名
element.className = element.className.replace('oldClassName', '');

// 检查类名是否存在
if (element.className.includes('className')) {
  // 类名存在的处理
}

JS 的 Map 了解过吗

JavaScript 的 Map 是一种新的数据结构,它允许存储键值对(key-value pairs)。与传统的对象(Object)相比,Map 提供了一些显著的优势和特性:

  1. 键的多样性

Map 中,键可以是任意类型的值,包括函数、对象或任何原始值。而在传统的对象中,键只能是字符串或者 Symbol

  1. 元素顺序

Map 对象中的元素是按照插入顺序排序的。这意味着当你遍历 Map 时,键/值对会按照它们被添加的顺序返回,这与普通对象不同,普通对象的属性遍历顺序是不固定的。

  1. 大小易于获取

你可以通过 Mapsize 属性直接获取到映射中键/值对的数量,而在普通对象中,你通常需要手动计算属性的数量。

console.log(myMap.size); // 3
  1. 易于迭代

Map 是直接可迭代的,它有内建的迭代方法,如 map.keys()map.values()map.entries(),这些方法返回一个新的迭代器对象,可以用于迭代 Map 的键、值或键值对。

for (let [key, value] of myMap.entries()) {
  console.log(key + ' = ' + value);
}
  1. 性能

对于频繁增删键值对的场景,Map 的性能要优于对象,因为 Map 是为了动态增删键值对而优化的。

  1. 易于清空

使用 Mapclear() 方法可以一次性清空所有键值对,而在普通对象中,你可能需要手动遍历并删除每个属性。

myMap.clear();
console.log(myMap.size); // 0

js的Map和Weakmap

Map

  • Map 是一种常规的键值对集合,其中的键可以是任意值(包括原始值和对象引用),而值也可以是任意值。
  • Map 中的键是强引用,这意味着只要 Map 对象存在,其中的键的引用就会被保留,不会被垃圾回收器回收。
  • Map 中的键是有序的,可以通过 forEach 方法或迭代器遍历键值对。

let myMap = new Map();

let keyObj = {};
let keyFunc = function() {};

myMap.set(keyObj, 'value associated with keyObj');
myMap.set(keyFunc, 'value associated with keyFunc');

console.log(myMap.get(keyObj)); // 'value associated with keyObj'

WeakMap

  • WeakMap 也是键值对的集合,但是其中的键必须是对象。
  • WeakMap 中的键是弱引用,这意味着当键不再被其他对象引用时,它会被垃圾回收器回收,不会阻止键对象的垃圾回收。
  • WeakMap 没有提供遍历方法,因此不能像 Map 那样直接遍历其中的键值对。
let myWeakMap = new WeakMap();

let keyObj = {};
let keyFunc = function() {};

myWeakMap.set(keyObj, 'value associated with keyObj');
// Below line will throw a TypeError because keyFunc is not an object
// myWeakMap.set(keyFunc, 'value associated with keyFunc');

适用场景:

  • 使用 Map 当需要存储键值对,并且需要确保键不会被意外地清除时。
  • 使用 WeakMap 当需要存储临时的键值对,并且希望键对象可以被垃圾回收器回收时,通常用于存储对象的私有属性。

总的来说,MapWeakMap 都是用于存储键值对的数据结构,但其对键的引用类型和垃圾回收行为有所不同,因此适用于不同的场景。

Array的api中foreach和map的区别

在 JavaScript 中,forEach()map() 都是用于遍历数组的方法,但它们有一些区别:

  1. 返回值

    • forEach() 没有返回值,它只是对数组中的每个元素执行提供的回调函数,用于处理数组元素。
    • map() 返回一个新的数组,数组中的每个元素都是调用回调函数的结果。原始数组不会被改变。
  2. 使用场景

    • forEach() 用于对数组进行遍历,执行一些针对每个元素的操作,例如打印元素、修改元素等。适合在不需要返回新数组的情况下使用。
    • map() 用于对数组中的每个元素进行操作,并返回一个新数组,新数组的每个元素是对应元素经过回调函数处理后的结果。适合用于处理原始数组并生成新数组的情况。

示例:

// forEach 示例
let arr = [1, 2, 3, 4, 5];
arr.forEach(function(element) {
  console.log(element * 2); // 打印每个元素的两倍值
});

// map 示例
let doubled = arr.map(function(element) {
  return element * 2; // 返回新数组,每个元素是原始数组对应元素的两倍值
});
console.log(doubled); // [2, 4, 6, 8, 10]
  1. 返回的数组

    • forEach() 中,不会返回任何新数组,原数组不会被改变。
    • map() 中,会返回一个新数组,其中的元素是根据回调函数处理原数组元素后得到的。

总的来说,forEach() 主要用于遍历数组并对每个元素执行一些操作,而 map() 则用于遍历数组并生成一个新数组,新数组的每个元素是原数组元素经过回调函数处理后的结果。

js怎么监听对象中属性的变化

在 JavaScript 中,你可以使用以下方法来监听对象中属性的变化:

  1. 使用 Object.defineProperty

    Object.defineProperty 可以用来定义对象的新属性或修改对象的现有属性,并在属性值发生变化时触发回调函数。

    
    let obj = { value: 123 };
    
    Object.defineProperty(obj, 'value', {
      get() {
        return this._value;
      },
      set(newValue) {
        this._value = newValue;
        console.log('Value changed to: ', newValue);
      }
    });
    
    obj.value = 456; // 触发 setter,打印 "Value changed to: 456"
    

    通过这种方式,你可以在属性值被设置时执行额外的逻辑。

  2. 使用 Proxy 对象

    Proxy 对象用于定义基本操作的自定义行为(例如属性查找、赋值、枚举、函数调用等),可以用来监听对象的变化。

    let obj = { value: 123 };
    
    let handler = {
      set(target, key, value) {
        console.log(`${key} set to ${value}`);
        target[key] = value;
        return true;
      }
    };
    
    let proxy = new Proxy(obj, handler);
    
    proxy.value = 456; // 打印 "value set to 456"
    

    使用 Proxy 对象,你可以在属性被设置时执行自定义的操作。

Object.defineProperty和Proxy 有什么区别

Object.definePropertyProxy 都是用于监视对象属性的变化,但它们在实现方式和功能上有一些区别:

  1. 支持的属性范围

    • Object.defineProperty 只能用于定义单个属性,需要为每个要监视的属性单独调用。
    • Proxy 可以用于监视整个对象,无需为每个属性单独设置监视。
  2. 功能

    • Object.defineProperty 通过 getter 和 setter 方法来监视属性的读取和写入操作,因此只能监视到属性的赋值操作和读取操作。
    • Proxy 可以拦截更多的操作,如属性查找、删除、枚举等,以及一些其他的操作,如函数调用等。
  3. 支持程度

    • Object.defineProperty 是 ES5 中引入的特性,因此在一些老旧的环境中可能不被支持。
    • Proxy 是 ES6 中引入的特性,因此在一些较新的环境中得到了更好的支持,但在一些老旧的环境中可能不被支持。
  4. 兼容性

    • Object.defineProperty 在一些老旧的浏览器中存在兼容性问题,而且对于已存在的属性,Object.defineProperty 会导致属性的 writable 和 configurable 特性被改变,可能会影响现有代码的行为。
    • Proxy 的兼容性问题相对较少,但是在一些较老的浏览器中可能不被支持。

总的来说,Object.defineProperty 主要用于对单个属性进行监视,而 Proxy 则更适合用于对整个对象进行监视,且提供了更多的拦截操作,但需要考虑到兼容性和性能问题。在实际项目中,根据具体需求和目标平台选择合适的方案。

Vue

v-show 和 v-if 的区别

  1. v-show 不管条件是真还是假,第一次渲染的时候都会编译出来,也就是标签都会添加到 DOM 中。之后切换的时候,通过 display: none;样式来显示隐藏元素。可以说只是改变 css 的样式,几乎不会影响什么性能。

  2. 在首次渲染的时候,如果条件为假,什么也不操作,页面当作没有这些元素。当条件为真的时候,开始局部编译,动态的向 DOM 元素里面添加元素。当条件从真变为假的时候,开始局部编译,卸载这些元素,也就是删除。

watch 和 computed 的区别

computed 计算属性

  1. 在 computed 属性对象中定义计算属性的方法,和取 data 对象里的数据属性一样以属性访问的形式调用,即在页面中使用 {{ 方法名 }} 来显示计算的结果。 2. computed 一个重要的特点,就是 computed 带有缓存功能。依赖型数据发生改变,computed 才会重新计算。 3. 在 computed 中的属性都有一个 get 和一个 set 方法,当数据变化时,调用 set 方法。下面我们通过计算属性的 getter/setter 方法来实现对属性数据的显示和监视,即双向绑定。

watch 监听属性

  1. 主要用来监听某些特定数据的变化,从而进行某些具体的业务逻辑操作,可以看作是 computed 和 methods 的结合体;
  2. 可以监听的数据来源:data,props,computed 内的数据;
  3. watch 支持异步;
  4. 不支持缓存,监听的数据改变,直接会触发相应的操作;
  5. 监听函数有两个参数,第一个参数是最新的值,第二个参数是输入之前的值,顺序一定是新值,旧值。

1.如果一个数据依赖于其他数据,那么把这个数据设计为 computed 的

2.如果你需要在某个数据变化时做一些事情,使用 watch 来观察这个数据变化

Vue 生命周期、周期做了哪些事情

  1. 什么是 vue 生命周期?

Vue 实例从创建到销毁的过程,就是生命周期。也就是从开始创建、初始化数据、编译模板、挂载 DOM-渲染、更新-渲染、卸载等一系列的过程,我们称这是 Vue 的生命周期。

生命周期中有多个事件钩子,在控制整个 vue 实例的过程时更容易形成好的逻辑。

  1. 第一次页面加载会触发?

beforeCreate , created , beforeMount ,mounted 这几个钩子

  • beforeCreate:创建前,此阶段为实例初始化之后,this 指向创建的实例,此时的数据观察事件机制都未形成,不能获得 DOM 节点。

  • created:创建后,此阶段为实例已经创建,完成数据(data、props、computed)的初始化导入依赖项。

  • beforeMount:挂载前,虽然得不到具体的 DOM 元素,但 vue 挂载的根节点已经创建,下面 vue 对 DOM 的操作将围绕这个根元素继续进行。beforeMount 这个阶段是过渡性的,一般一个项目只能用到一两次。

  • mounted:挂载,完成创建 vm.$el,和双向绑定 完成挂载 DOM 和渲染,可在 mounted 钩子函数中对挂载的 DOM 进行操作。 可在这发起后端请求,拿回数据,配合路由钩子做一些事情。

  • beforeUpdate:数据更新前,数据驱动 DOM。 在数据更新后虽然没有立即更新数据,但是 DOM 中的数据会改变,这是 vue 双向数据绑定的作用。 可在更新前访问现有的 DOM,如手动移出添加的事件监听器。

  • updated:

  • activated

  • deactivated:组件被移除时使用。

  • beforeDestroy:销毁前

  • destroyed:销毁后,当前组件已被删除,销毁监听事件,组件、事件、子实例也被销毁。 这时组件已经没有了,无法操作里面的任何东西了。

vue 响应式原理

组件通信的方式

父子通信:

  1. 父组件 A 向 子组件 B 传递数据 通过 props 的方法 子组件 B 向 父组件 A 发送数据 通过 emit
  2. 子实例可以用 this.$parent 访问父实例 子实例被推入父实例的$children
  3. 父子、隔代、兄弟组件通信 $emit $on

路由

  • 前端 Router 基本功能 一个基本的前端路由至少应该提供以下功能:

  • 前端 Router 可以控制浏览器的 history,使的浏览器不会在 URL 发生改变时刷新整个页面。

  • 前端 Router 需要维护一个 URL 历史栈,通过这个栈可以返回之前页面,进入下一个页面。

  • 前端路由实现原理就是匹配不同的 url 路径,进行解析,然后动态的渲染出区域 html 内容。但是这样存在一个问题,就是 url 每次变化的时候,都会造成页面的刷新。那解决问题的思路便是在改变 url 的情况下,保证页面的不刷新。目前 Router 有两种实现方式 History 和 hash。

路由跳转的时候原来的页面去哪了

前端路由跳转是在单页应用程序(SPA)中常见的操作。在这种情况下,页面的实际切换是在客户端内部进行的,而不涉及服务器端的页面重新加载。当你从一个页面导航到另一个页面时,实际上是在同一个 HTML 页面内加载了不同的组件或视图。

当你进行前端路由跳转时,原来的页面并没有被销毁或去向不明。相反,通常是隐藏了当前的页面,并加载或渲染了新的页面内容。这样做是为了提供更流畅的用户体验,避免了整个页面的重新加载,同时在浏览器的历史记录中也可以保留相应的记录,以便用户能够回退到之前的页面状态。

在这个过程中,原来的页面状态通常会被保留在内存中,直到用户决定回退到该页面或者页面被浏览器的垃圾回收机制清理掉。

vue中data为什么是函数而不是对象

vue组件就是一个vue实例

JS中的实例是通过构造函数来创建的,每个构造函数可以new出很多个实例,那么每个实例都会继承原型上的方法或属性。

vuedata数据其实是vue原型上的属性,数据存在于内存当中

vue为了保证每个实例上的data数据的独立性,规定了必须使用函数,而不是对象。

因为使用对象的话,每个实例(组件)上使用的data数据是相互影响的,这当然就不是我们想要的了。对象是对于内存地址的引用,直接定义个对象的话组件之间都会使用这个对象,这样会造成组件之间数据相互影响。

使用函数后,使用的是data()函数,data()函数中的this指向的是当前实例本身

Vue 路由的两种实现

Vue.js是一款流行的前端JavaScript框架,用于构建用户界面和单页应用程序(SPA)。在Vue中,路由是用来管理不同页面之间导航的工具,它允许用户在不同的视图间切换而无需重新加载页面。Vue路由的实现通常依赖于Vue Router,这是Vue.js的官方路由库。Vue Router支持两种路由模式:hash模式history模式

1. Hash模式

Hash模式是Vue Router的默认模式。它使用URL的hash(#)来模拟一个完整的URL,从而实现页面的跳转而无需重新加载页面。

  • 原理:当URL的hash部分改变时(例如,从#home变为#about),页面不会重新加载,但JavaScript可以通过hashchange事件来监听这种变化,然后相应地加载不同的页面视图。

  • 特点

    • 不需要服务器配置特别的URL重写规则,可以在所有支持前端路由的服务器上工作。
    • 可以在旧版浏览器上工作,兼容性好。
    • URL会包含#符号,可能不是很美观。

2. History模式

History模式利用了HTML5的History API来实现前端路由,允许你创建一个类似于常规URL的路由,不再需要#符号。

  • 原理:使用history.pushState API来改变浏览器的地址栏,而不会发送请求到服务器。这样用户在应用内导航时,可以得到没有hash的干净URL。

  • 特点

    • URL看起来更美观,更像传统的服务器端路由。
    • 需要服务器配置支持。因为当用户直接访问一个深层链接或刷新页面时,浏览器会向服务器发送请求。此时需要服务器配置URL重写规则(所有的请求都返回同一个入口文件),以便于前端路由能够接管路径解析。
    • 不支持旧版浏览器。

3. 选择哪种模式?

  • 如果你的应用需要支持旧版浏览器,或者你无法控制服务器配置(例如在一些静态站点托管服务上),那么hash模式可能是更好的选择。
  • 如果你追求URL的美观,或者能够控制服务器的URL重写配置,history模式会是更合适的选项。

总之,选择哪种模式取决于你的项目需求和部署环境。

聊聊路由守卫

路由守卫是 Vue Router 提供的一种机制,用于在路由导航过程中进行权限验证、页面加载前后的处理等操作。通过路由守卫,我们可以在路由导航前、导航后以及导航被中断时执行相应的逻辑。

在 Vue Router 中,主要有三种类型的路由守卫:

  1. 全局前置守卫(Global Before Guards):

    • 全局前置守卫会在路由导航之前被触发,无论是通过编程式导航还是声明式导航都会触发这些守卫。

    • 常用的全局前置守卫有 beforeEach,可以在这里进行路由权限验证、页面加载前的处理等。

    • 示例:

      router.beforeEach((to, from, next) => {
        // 路由导航前的逻辑处理
        if (to.meta.requiresAuth && !auth.isAuthenticated()) {
          // 如果路由需要权限验证但用户未登录,则重定向到登录页面
          next('/login')
        } else {
          // 继续路由导航
          next()
        }
      })
      
  2. 路由独享守卫(Per-Route Guard):

    • 路由独享守卫是针对特定的路由配置的,它们会在进入路由之前被触发。

    • 通过在路由配置中的 beforeEnter 字段中定义路由独享守卫。

    • 示例:

      const router = new VueRouter({
        routes: [
          {
            path: '/admin',
            component: Admin,
            beforeEnter: (to, from, next) => {
              // 进入 admin 路由前的逻辑处理
              if (!auth.isAdmin()) {
                // 如果用户不是管理员,则重定向到首页
                next('/')
              } else {
                // 继续路由导航
                next()
              }
            }
          }
        ]
      })
      
  3. 组件内的守卫(In-Component Guard):

    • 组件内的守卫分为 beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave 三种。

    • beforeRouteEnter 在路由进入当前组件前被触发,但是此时组件实例尚未创建,无法通过 this 访问组件实例。

    • beforeRouteUpdate 在路由更新但是当前组件被复用时被触发,同样无法访问组件实例。

    • beforeRouteLeave 在离开当前路由时被触发,可以用于确认是否离开当前页面前需要进行数据保存等操作。

    • 示例:

      export default {
        beforeRouteEnter(to, from, next) {
          // 在路由进入当前组件前的逻辑处理
          // 注意:此时无法访问组件实例
          next()
        },
        beforeRouteUpdate(to, from, next) {
          // 在路由更新但组件被复用时的逻辑处理
          next()
        },
        beforeRouteLeave(to, from, next) {
          // 在离开当前组件前的逻辑处理
          next()
        }
      }
      

通过合理地使用这些路由守卫,我们可以对路由导航的各个阶段进行控制,实现路由权限验证、页面加载前的数据准备、路由变化的提示等功能。

v-for 的 key 值的作用

key的作用主要是为了高效的更新虚拟DOM。另外vue中在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让vue可以区分它们

$nextTick 用过吗

定义:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

所以就衍生出了这个获取更新后的DOM的Vue方法。所以放在Vue.nextTick()回调函数中的执行的应该是会对DOM进行操作的 js代码;

理解:nextTick(),是将回调函数延迟在下一次dom更新数据后调用,简单的理解是:当数据更新了,在dom中渲染后,自动执行该函数,

vuex

Vuex 是专门为 Vue.js 应用程序开发的状态管理模式

  • 它采用集中式存储管理应用的所有组件状态,并以相应的规则保证状态以一种可预测的方式发生变化

  • Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了注入零配置的 time-travel 调试、状态快找导入导出等高级调试功能

总而言之,Vuex 使某个状态能在多个组件中共享的插件,是响应式的

共享: 用户的登录状态、名称、头像等信息

商品的收藏、购物车的商品等

使用方法:

  • 在使用脚手架创建项目的时候选择 vuex
  • 在 src 内的 store 文件夹中的 index.js,state 是用来存储共享的数据的

调用方法:$store.state.被调用的数据

State 状态一定是通过 mutations 修改的,这样 Devtools 才会记录状态的修改日志,最终调错时才会更加方便

vuex中 Mutation 相应规则:

Vuex 的 store 中的 state 是响应式的,当 state 中的数据发生改变时, Vue 组件会自动更新.这就要求我们必须遵守一些 Vuex 对应的规则:

提前在 store 中初始化好所需的属性,已有的对象和属性的值的更改具有响应式,而对“对象的属性”或“属性”进行直接的增删,虽然能够使 state 里面的实际数据进行改变,但其不具有响应式,不能反映到页面中,因为这些数据没有被监听

当给 state 中的对象增删新属性时,使用下面的方式:

  • 方式一:使用 Vue.set(obj, 'newProp',123)
  • 方式二:用新对象给旧对象重新赋值
  • 方式三:Vue.delete(state.obj,deleteProp)

了解$set 吗

在vue2中,并不是任何时候数据都是双向绑定的,这是因为 Vue 2.x 使用的响应式系统是基于 ES5 的 Object.defineProperty 实现的,。

由于JavaScript 的限制,Vue不能检测以下数组的变动︰

  1. 当利用索引直接设置一个数组项时
  2. 修改数组的长度时

我们就需要使用Set

解决数据没有被双向绑定我们可以使用 vm.$set 实例方法,该方法是全局方法 Vue.set 的一个别名。

this.$set(原数组, 索引值, 需要赋的值)

在 Vue.js 中,$set 方法是一个全局API,用于向响应式对象添加一个属性,并确保这个新属性也是响应式的,即能够触发视图更新。这个方法特别有用,因为直接给对象赋值一个新属性(例如 obj.newProp = value)在默认情况下不会触发视图的重新渲染。

用法

$set 方法通常有三个参数:

  1. 目标对象(Object | Array):要添加属性的对象或数组。
  2. 键名(string | number):要添加的属性名称或数组的索引。
  3. (any):属性的值。

示例

假设我们有一个 Vue 组件的数据对象,其中包含一个对象 item,我们想要在这个对象中添加一个新的属性 newProp,并且希望 Vue 能够跟踪这个变化,以便更新视图:

new Vue({
  el: '#app',
  data: {
    item: {
      existingProp: 'I exist'
    }
  },
  mounted() {
    // 使用 $set 添加新属性
    this.$set(this.item, 'newProp', 'I am new here');
  }
});

在这个例子中,$set 方法将 newProp 属性添加到 item 对象中,并确保这个属性的变化能够触发视图的更新。

Vue 3 中的变化

在 Vue 3 中,由于引入了基于 Proxy 的响应式系统,直接给对象赋值一个新属性已经可以自动被检测到,因此在大多数情况下不再需要使用 $set。然而,Vue 3仍然提供了 set 方法作为全局API的一部分,主要用于兼容性和特殊情况下的使用,但它不再是必需品。

总之,$set 是 Vue 2 中处理响应式数据更新的一个重要方法,尤其是在动态添加对象属性或数组元素时。而在 Vue 3 中,新的响应式系统提供了更自然和直接的方式来处理这类场景。

vue2和vue3的区别

juejin.cn/post/718544…

虚拟DOM

(Virtual DOM)是一种编程概念,用于提高网页的渲染效率。它是对真实DOM(Document Object Model)的一种内存中的轻量级表示。虚拟DOM允许开发者通过JavaScript对象来描述界面,而无需直接操作耗费性能的真实DOM。

工作原理:

  1. 创建虚拟DOM树:应用的初始渲染阶段,虚拟DOM以JavaScript对象的形式创建一个当前DOM结构的轻量级复制品。
  2. 检测变化:当应用状态发生变化时,会创建一个新的虚拟DOM树。系统会比较新旧两个虚拟DOM树,以识别实际变化的部分。
  3. 差异计算(Diffing) :通过对比新旧虚拟DOM树,算法计算出具体哪些元素发生了变化。
  4. 更新真实DOM:只有在虚拟DOM中识别出真实变化的部分,才会在真实DOM上进行更新操作,而不是重新渲染整个DOM,这大大提高了渲染效率。

优点:

  • 性能提升:因为虚拟DOM减少了对真实DOM的操作次数,这些操作是Web开发中性能消耗最大的部分之一。
  • 跨平台兼容性:虚拟DOM本质上是JavaScript对象,这使得它能够在任何可以运行JavaScript的环境中工作,为跨平台应用(如React Native)提供了基础。
  • 简化程序复杂度:开发者可以专注于数据的状态,而不是如何操作DOM来反映这些状态的变化,简化了开发过程。

虚拟DOM不是没有缺点,它需要额外的内存来维护两个DOM树的状态,并且在某些情况下,虚拟DOM的性能优势可能不如预期显著。然而,在大多数现代Web应用中,虚拟DOM提供了明显的性能优势和开发效率。

vue是如何将html标签变成dom的

在 Vue.js 中,HTML 标记会被解析成虚拟 DOM(Virtual DOM),然后被转换为实际的 DOM 元素。

  1. 模板解析:Vue.js 通过模板解析器将模板(通常是 HTML)解析为抽象语法树(AST)。
  2. 虚拟 DOM 创建:根据解析得到的 AST,Vue.js 创建一个虚拟 DOM 树。虚拟 DOM 是一个 JavaScript 对象,它以树形结构表示整个页面的结构,但不包含真实的 DOM 元素。
  3. 渲染到真实 DOM:Vue.js 通过虚拟 DOM 中的节点和标记创建真实的 DOM 元素并将其渲染到页面上。这一过程通过使用底层的浏览器 API 来实现。
  4. 数据绑定:Vue.js 会监听数据的变化。当数据发生变化时,Vue.js 会重新计算虚拟 DOM,并将新的虚拟 DOM 与旧的虚拟 DOM 进行对比,找出差异。然后,只更新真实 DOM 中需要更新的部分,而不是重新渲染整个页面。这个过程是高效的,因为操作真实 DOM 是相对较慢的。

总的来说,Vue.js 通过模板解析器将 HTML 模板转换为虚拟 DOM 树,然后通过虚拟 DOM 将页面渲染到真实的 DOM 上。数据变化时,Vue.js 可以根据新的数据计算出新的虚拟 DOM,并将差异更新到真实 DOM 上,从而实现页面的动态更新。

vue双向绑定的原理

Vue.js 中双向数据绑定的核心原理是基于响应式系统指令(Directives) 。具体到 v-model 指令,它是实现表单输入和应用状态之间双向绑定的语法糖。双向绑定的实现机制主要涉及三个部分:数据劫持(或观察者模式)、依赖收集与派发更新、以及事件监听。下面分步骤解析这一过程:

1. 数据劫持(响应式系统)

Vue.js 使用 Object.defineProperty() (在 Vue 2.x 中)或 Proxy(在 Vue 3.x 中)来实现数据的响应式。这些方法允许Vue监视数据对象的属性变化。

  • Vue 2.x 中,对于每个组件实例的数据,Vue会遍历其所有属性,利用 Object.defineProperty() 将这些属性全部转为 getter/setter。Getter 用于依赖收集(在属性被访问时收集当前依赖),setter 用于派发更新(在属性被修改时通知变化)。
  • Vue 3.x 中,采用了更现代的 Proxy API,它可以直接监听对象和数组的变化,而不需要为每个属性设置 getter 和 setter。这改进了性能,并且可以监听到更多类型的变化,比如属性的添加和删除。

2. 依赖收集与派发更新

当组件进行渲染时,组件中的响应式数据会被访问,此时通过getter进行依赖收集,将当前的组件(观察者)添加到这个数据(被观察者)的依赖列表中。当响应式数据变化时,setter 会被触发,进而通知所有依赖于这个数据的观察者进行更新。

3. 事件监听

v-model 实际上是一个语法糖,它背后做了两件事:

  • 数据绑定:通过 v-bind 绑定一个值到元素上。
  • 事件监听:通过 v-on 监听元素的输入事件(如 input、change),当用户与输入框交互时,事件处理器会被触发,然后更新 Vue 实例的数据。

结合过程

  • 用户输入:当用户在表单输入字段中输入数据时,触发输入事件。
  • 更新数据v-model 监听到输入事件,更新相应的数据。
  • 响应数据变化:数据被修改后,触发setter,Vue 检测到数据变化,通知依赖于这个数据的所有地方进行更新。
  • 视图更新:组件重新渲染,将新的数据展示在界面上。

这个过程形成了双向绑定:数据的改变驱动视图更新,视图的改变反过来又驱动数据更新。这一机制使得Vue开发变得高效且直观。

vue中双向绑定的方法

Vue.js 中实现数据的双向绑定主要依赖于 v-model 指令。这个指令在表单元素上创建了数据的双向绑定:视图到模型(用户输入更新数据)和模型到视图(数据更新反映在界面上)。v-model 本质上是一个语法糖,在不同的表单元素上它背后做的事情略有不同,但总体上它结合了 v-bind 用于数据绑定和 v-on 用于监听用户输入事件来实现双向绑定。

vue中如何让一个数据双向绑定失败

在Vue中,双向绑定是通过v-model指令实现的,它基于Vue实例的数据响应系统。要让一个数据的双向绑定失败,即不让视图的改变更新到数据,或者不让数据的改变反映到视图上,可以通过以下几种方式实现:

  1. 不使用v-model指令:简单地不使用v-model,而是使用v-bind绑定数据到视图,然后手动监听事件来更新数据。这样,你可以控制数据到视图的单向绑定,而视图到数据的部分则可以手动控制,包括不控制。
  2. 自定义v-model的行为:通过自定义组件时改写v-model的行为,可以在组件内部处理输入,决定是否以及如何更新数据。这样,你可以拦截并修改或者完全忽略视图到模型的更新。
  3. 使用计算属性和监听器(Watchers)来细粒度控制:虽然这不是直接使v-model失效,但通过计算属性的setter函数,你可以控制当视图试图更新模型时发生的行为。同时,你也可以使用监听器(watchers)来决定何时以及是否更新视图。
  4. 条件渲染:通过条件渲染,可以在某些条件下不渲染使用v-model的元素,这样即便数据变化,也不会更新到视图上。
  5. 使用readonlydisabled属性:在表单元素上使用readonlydisabled属性,可以阻止用户更改输入,从而阻止v-model同步更新数据。
  6. 操作DOM来避免更新:通过直接操作DOM(例如,使用document.getElementById或Vue的ref),可以在某些情况下改变输入字段的值而不触发Vue的更新机制。这种方法不推荐,因为它违反了Vue的数据驱动视图的原则,可能导致不可预见的问题。

请记住,Vue的设计哲学是数据驱动视图,故意去破坏这一机制可能会导致应用行为变得不可预测和难以维护。在实际应用中,如果你发现需要阻止双向绑定,可能需要重新考虑你的应用架构或状态管理策略。

vue是如何获取到某个数据的变化的,是如何确定某个变化就是这个数据的

Vue 获取到某个数据变化并确定该变化确实来源于这个数据的过程,涉及到响应式系统中的几个关键概念:依赖收集观察者模式、以及在 Vue 3 中的**响应式代理(Proxy)或在 Vue 2 中的数据劫持(Object.defineProperty)**技术。下面分别解释这些概念是如何工作的,首先以 Vue 3 的实现为主进行说明,然后简要对比 Vue 2 的不同。

Vue 3.x

一、 响应式代理(Proxy)

Vue 3 使用 Proxy 对象重写了响应式系统。Proxy 允许 Vue 拦截并监视对象的各种操作,如属性访问(get)、属性设置(set)、属性删除(deleteProperty)等。

  1. 初始化阶段:当组件的状态(data、props等)被初始化时,Vue 通过 Proxy 将这些状态转换为响应式数据。这意味着当这些状态被读取或修改时,Vue 可以捕捉到这些操作。
  2. 依赖收集:当组件渲染或计算属性被访问时,它们会读取响应式数据。这个读取操作会触发 Proxy 的 get 处理函数,Vue 在这一步进行依赖收集,即记录下当前正在渲染的组件或计算属性作为依赖。
  3. 派发更新:当响应式数据被修改(set)时,会触发 Proxy 的 set 处理函数。Vue 此时知道哪个数据被修改,并通知之前在依赖收集阶段收集的所有依赖(组件、计算属性等),让它们重新渲染或重新计算。

二、 如何确定变化来源

Vue 通过维护一个依赖关系图来跟踪哪些组件依赖于哪些数据。当数据变化时,Vue 知道需要通知哪些依赖进行更新。这个过程是自动化的,开发者无需手动管理依赖关系。

Vue 2.x

一、 数据劫持(Object.defineProperty)

Vue 2 使用 Object.defineProperty 对组件的数据进行劫持,为数据的每个属性定义自定义的 getter 和 setter。

  1. 初始化阶段:Vue 遍历组件的数据对象,对每个属性使用 Object.defineProperty 定义 getter 和 setter。
  2. 依赖收集:当数据被读取时(访问 getter),Vue 会记录当前正在评估的组件或计算属性作为依赖。
  3. 派发更新:当数据被修改时(触发 setter),Vue 会通知所有依赖这个数据的组件或计算属性重新评估或渲染。

二、如何确定变化来源

和 Vue 3 类似,Vue 2 通过依赖关系图来跟踪和管理组件与数据之间的依赖关系。每当数据发生变化,通过 setter 捕获的修改操作允许 Vue 确定变化的来源,并通知所有依赖该数据的相关部分进行更新。

总结

Vue 内部通过一套精巧的响应式系统来跟踪数据的变化及其消费者(即依赖)。无论是使用 Vue 2 的 Object.defineProperty 还是 Vue 3 的 Proxy,Vue 都能够准确地捕获数据变化并确定变化来源,从而确保只有依赖变化数据的组件和计算属性被更新,保持界面和数据的同步。

vue从data改变到页面渲染的过程

Vue.js 使用一种响应式系统来确保当数据改变时,视图能够自动更新。从 Vue 2.x 到 Vue 3.x,虽然实现细节有所不同(Vue 2.x 使用 Object.defineProperty,而 Vue 3.x 使用 Proxy API),但基本原理保持一致。下面是 Vue 中从数据(data)变化到页面渲染的过程的一般描述:

Vue 2.x

  1. 响应式系统初始化:在 Vue 实例初始化时,Vue 会遍历 data 选项中的属性,并使用 Object.defineProperty 将它们转换为 getter/setter,从而使数据变得响应式。这一过程在 Vue 实例的初始化阶段完成,为每个属性创建了一个依赖收集器(Dep)。
  2. 依赖收集:当模板被编译成渲染函数时,它会被执行以生成虚拟 DOM。在这个过程中,渲染函数会读取响应式数据,这时 Vue 会自动将当前的组件实例(观察者)添加到数据的依赖收集器中,建立数据和视图之间的依赖关系。
  3. 侦测变化:当响应式数据的 setter 被触发(即数据被修改)时,Vue 会通知所有订阅了该数据变化的依赖收集器(Dep),告诉它们数据已经改变。
  4. 更新视图:依赖收集器接收到数据变化的通知后,会通知所有依赖于这个数据的组件实例(观察者)执行更新函数。Vue 会重新执行组件的渲染函数来生成新的虚拟 DOM,并通过虚拟 DOM 的对比(diff 算法),找出与上一个虚拟 DOM 之间的差异。最后,Vue 会将这些差异应用到实际的 DOM 上,完成视图的更新。

Vue 3.x

Vue 3.x 的响应式系统重构使用了 ES6 的 Proxy 特性,使整个过程更加高效和灵活,但基本原理相同:

  1. 响应式系统初始化:Vue 3.x 通过 Proxy 包装 data 对象,拦截对对象的所有操作,包括属性的读取和写入等。
  2. 依赖收集:当组件渲染时,读取响应式数据,Vue 3.x 会自动将当前活动的效果(effect,如组件的渲染函数)注册为依赖。
  3. 侦测变化:当响应式数据被修改时,通过 Proxy 拦截到的操作会触发变化通知。
  4. 更新视图:Vue 3.x 使用基于 Proxy 的响应式系统来追踪依赖,并在数据变化时重新执行对应的效果(effect)。然后,通过虚拟 DOM 的对比和实际 DOM 的更新,完成视图的渲染过程。

总的来说,Vue 的响应式系统通过建立数据和视图之间的依赖关系,并在数据发生变化时自动更新视图,从而简化了数据到视图渲染的复杂性。Vue 3.x 的响应式系统相较于 Vue 2.x 在性能和灵活性上都有所提升。

Vue 如何实现事件绑定

Vue.js 使用 v-on 指令来实现事件绑定,这是其模板语法的一部分。通过这种方式,Vue 允许开发者在其单文件组件或模板中直接绑定事件监听器到 DOM 元素上。当这些 DOM 事件被触发时,Vue 实例会执行对应的方法。

以下是 Vue 事件绑定的基本用法示例:

<!-- 在模板中绑定一个点击事件 -->
<button v-on:click="handleClick">Click me</button>

在 Vue 的 JavaScript 部分,你会定义一个 handleClick 方法来处理点击事件:

export default {
  methods: {
    handleClick() {
      console.log('Button was clicked!');
    }
  }
}

Vue 在内部使用事件委托来处理这些绑定,它不仅减少了需要直接绑定到元素上的事件监听器数量,还提高了性能和管理的便捷性。Vue 的事件绑定机制支持绑定原生 DOM 事件,并且也可以通过 .native 修饰符在组件上监听原生事件(Vue 2.x 中)。Vue 3.x 通过为组件自定义事件使用 v-on 或者使用特定的监听器属性来进一步简化事件的处理。

此外,Vue 还提供了事件修饰符来处理常见的 DOM 事件行为,例如 .stop 阻止事件冒泡、.prevent 阻止默认事件行为等,使得事件处理更加灵活和强大。

.vue文件经过加工之后会生成什么内容

.vue 文件是 Vue.js 框架中的单文件组件(Single-File Components,SFCs),它们通常包含模板(template)、脚本(script)和样式(style)三部分。这些文件不能直接被浏览器理解,因此需要经过特定的构建步骤(如由 Webpack 或 Vite 等构建工具处理)来转换成浏览器可执行的代码。下面概述了 .vue 文件加工后的主要内容:

1. JavaScript 模块

.vue 文件被编译成 JavaScript 模块,这是最主要的产出。编译过程大致包括以下几个步骤:

  • 模板编译:模板(HTML 部分)会被编译成 JavaScript 渲染函数(Render Functions)。Vue 提供了一个模板编译器,可以将 HTML 模板转换成纯 JavaScript 的渲染函数。这个过程涉及虚拟 DOM 的概念,渲染函数返回的是虚拟 DOM 节点(VNodes)。
  • 脚本处理:脚本部分(JavaScript)被处理成一个组件选项对象,这个对象包括了组件的所有逻辑,如数据(data)、方法(methods)、生命周期钩子(lifecycle hooks)等。
  • 样式处理:样式(CSS)可以通过配置被注入到 JavaScript 中,或者编译成单独的 CSS 文件。这取决于构建工具的配置(如 Webpack 配置中的 style-loader、css-loader、vue-style-loader 等)。

2. 静态资源

如果 .vue 文件中引用了图片、字体或其他静态资源,这些资源通常会被构建工具处理:

  • URL 转换:静态资源的路径可能会被转换成最终的发布路径,并且资源可能会被复制到输出目录中。
  • 优化处理:图片和其他媒体文件可能会经过压缩和优化。

3. 单独的 CSS 文件(可选)

根据配置,样式可以被提取成单独的 CSS 文件,而不是打包进 JavaScript。这通常通过插件或 loader 实现,如 MiniCssExtractPlugin。

4. Source Maps(可选)

构建过程还可以生成 source maps,这些是调试时用来映射编译后代码回源代码的文件。它们对于在浏览器中调试经过编译的 .vue 文件非常有用。

总结

经过加工后,.vue 文件最终会生成一个或多个 JavaScript 模块,可能会生成单独的 CSS 文件(如果样式被提取出来的话),以及相关的静态资源文件和 source maps。这些内容是为了让浏览器能够理解和执行,同时支持更快的加载速度和更方便的调试。构建过程的具体细节和结果取决于使用的构建工具和配置。

加载一个大文件的话,为什么要避免加载时间过长,以及如何避免

加载一个大文件时,如果加载时间过长,可能会带来以下不利影响:

  1. 用户体验:长时间的加载导致用户等待,会显著降低用户体验和满意度。在快节奏的互联网环境中,用户对等待时间非常敏感,过长的加载时间可能导致用户流失。
  2. SEO影响:搜索引擎(如Google)在排名算法中考虑了页面的加载速度。较慢的加载时间会负面影响网站的搜索引擎排名,从而减少网站的可见性和流量。
  3. 性能瓶颈:大文件加载需要占用更多的带宽和资源,可能导致网站其他部分的性能下降,尤其是在带宽有限的环境下更为明显。

为了避免这些问题,可以采取以下措施优化大文件的加载时间:

1. 压缩和优化文件

  • 图像和视频:使用工具压缩图像和视频文件,减少它们的文件大小,同时尽量保持质量。对于图像,可以考虑使用现代的格式(如WebP)来进一步减小文件大小。
  • CSS和JavaScript:使用工具压缩CSS和JavaScript文件,移除不必要的空格、注释和代码。使用如Terser、UglifyJS、或CSSNano等工具。

2. 分割加载

  • 代码分割:对于大的JavaScript文件,可以使用Webpack、Rollup或其他模块打包工具来实现代码分割(Code Splitting),仅在需要时才加载特定的代码块。
  • 懒加载:对于图像、视频和其他非关键资源,可以实现懒加载,即只有当这些资源出现在视口(viewport)中时才开始加载它们。

3. 使用内容分发网络(CDN)

将文件存储在CDN上可以缩短加载时间,因为CDN可以将内容缓存在全球多个节点上,确保用户从最近的服务器加载资源,从而减少延迟。

4. 缓存策略

  • 浏览器缓存:通过设置合理的HTTP缓存头(如Cache-Control),可以使浏览器缓存已加载的资源,避免用户再次访问网站时重新下载相同的资源。
  • 服务端缓存:对于从服务器动态生成的资源,可以在服务器端实现缓存,减少服务器处理时间和响应时间。

5. 预加载

对于一些关键资源,可以使用<link rel="preload">标签提前加载这些资源,即使它们还没有被页面直接引用。这样可以确保当这些资源被需要时,它们已经被加载并缓存。

6. 优化渲染路径

减少关键渲染路径上的资源数量和大小,确保页面能尽快完成首次渲染。这可能涉及到调整CSS和JavaScript的加载顺序,以及优化页面结构。

通过采取上述措施,可以显著减少大文件的加载时间,改善用户体验,提高网站性能,并有利于SEO。

html解析过程是怎样的,html解析过程会影响到加载速度,如何避免

HTML解析是浏览器将HTML文档转换成可呈现的网页的过程。这个过程对页面加载速度有直接影响,因为它决定了页面开始渲染的时间。HTML解析过程大致可以分为以下几个步骤:

HTML解析过程:

  1. 字节到字符:浏览器首先将接收到的字节数据根据指定的编码(如UTF-8)转换成字符。
  2. 字符到令牌(Tokens) :然后,浏览器将字符数据解析成一系列的令牌,这些令牌包括开始标签、结束标签、属性名和属性值等。
  3. 令牌到节点(Nodes) :浏览器将令牌转换成DOM节点,每个节点代表文档中的一个元素或内容片段。
  4. 构建DOM树:浏览器将节点组织成DOM树,DOM树是一个层次化的节点树,表示文档的结构。
  5. CSSOM树构建:同时,浏览器会解析CSS文件和<style>标签中的样式信息,构建CSS对象模型(CSSOM)树。
  6. 渲染树构建:DOM树和CSSOM树结合后,浏览器构建渲染树,渲染树只包含需要显示的节点和它们的样式信息。
  7. 布局(Reflow) :浏览器计算渲染树中每个节点的位置和大小。
  8. 绘制(Paint) :最后,浏览器根据渲染树和布局信息在屏幕上绘制页面。

影响加载速度的因素:

  • 大量的HTML:大量的HTML会增加解析时间,延迟页面渲染。
  • 外部CSS和JavaScript的阻塞行为:浏览器在解析HTML并构建DOM的过程中,如果遇到外部的CSS或同步加载的JavaScript,它会停止HTML解析直到外部文件被下载和处理完毕,因为CSS可能会改变DOM的样式,而JavaScript可能会修改DOM树本身。

如何避免影响:

  1. 优化HTML结构:精简HTML,移除不必要的嵌套和元素,减少DOM树的深度。
  2. 异步加载JavaScript:使用asyncdefer属性加载JavaScript脚本,以避免阻塞HTML解析。
  3. 优化CSS的加载:确保关键的CSS尽可能早地加载和解析,可以使用<link>标签在<head>部分加载,以避免重绘和回流。
  4. 利用浏览器缓存:通过缓存策略使得返回用户能够重用先前加载的资源。
  5. 压缩资源:使用GZIP或Brotli等工具压缩HTML、CSS和JavaScript文件,减少传输大小。
  6. 利用CDN:通过内容分发网络(CDN)分发资源,减少资源加载时间。

通过优化这些环节,可以显著提高页面的加载速度和响应能力,改善用户体验。

服务端渲染

服务端渲染为什么快,服务端渲染一定比客户端渲染快吗

服务端渲染(SSR)与客户端渲染(CSR)的性能和速度比较取决于多种因素,包括应用的具体情况、用户的网络条件、服务器的性能等。理解它们之间的差异以及为什么在某些情况下服务端渲染看起来更快,需要考虑以下几个关键点:

服务端渲染的:

  1. 内容生成:在服务器上生成完整的 HTML 页面,并将其直接发送到客户端(用户的浏览器)。这意味着页面的大部分内容在到达浏览器之前就已经被渲染好了。

  2. 更快的首屏加载时间:服务端渲染可以直接生成并发送包含了最终HTML的页面到浏览器,这样浏览器可以立即开始渲染页面,而不需要等待所有的JavaScript被下载并执行以构建页面内容。这对于首屏加载速度是一个重大的优势,特别是在网络连接慢或是服务器响应快的情况下。

  3. 搜索引擎优化(SEO) :服务端渲染的页面因为是预先生成的,所以对搜索引擎更友好。搜索引擎爬虫可以直接抓取和索引渲染后的页面内容,这对于需要良好SEO的应用来说至关重要。

  4. 减少白屏时间:由于浏览器直接接收到了完整的页面内容,用户几乎可以立即看到页面的布局和内容,减少了在应用加载过程中出现白屏的时间。

客户端渲染:

  1. 内容生成:浏览器下载一个最小的 HTML 骨架和必需的 JavaScript 文件,页面的内容通过 JavaScript 在浏览器中动态生成。
  2. 富交互性:客户端渲染应用(如单页面应用,SPA)能够提供更丰富的交互性和更流畅的用户体验,因为页面的大部分内容无需重新加载即可更新。
  3. 减轻服务器负担:在客户端渲染中,页面的渲染工作由客户端(用户的浏览器)完成,这减轻了服务器的负担,特别是在高流量的情况下。
  4. 利用浏览器缓存:一旦应用的框架和资产被加载,它们可以被浏览器缓存,减少了后续请求的加载时间。

服务端渲染是否总是比客户端渲染快?

并非总是如此。服务端渲染虽然可以提供更快的首屏渲染时间,但它也需要服务器生成页面内容,并且在用户交互时可能需要重新加载整个页面或向服务器请求更多的数据,这可能会导致额外的延迟。而客户端渲染一旦完成初始加载,页面的交互响应时间通常会比服务端渲染快,因为数据更新可以直接在客户端处理,无需等待服务器响应。

在什么情况下客户端渲染比服务端渲染快

客户端渲染(Client-Side Rendering, CSR)比服务端渲染(Server-Side Rendering, SSR)快的情况通常涉及以下几个方面:

1. 后续页面交互和导航

在单页面应用(SPA)中,客户端渲染可以提供更快的页面交互和导航体验。一旦初始的JavaScript代码和应用框架被加载和执行,页面内的导航可以通过客户端路由实现,无需从服务器加载新的页面。这样,用户在应用内部切换页面时,感觉几乎是瞬时的,因为只有数据请求(而不是整个页面)需要通过网络加载。

2. 利用浏览器缓存

客户端渲染的应用可以更有效地利用浏览器缓存来存储静态资源(如JavaScript、CSS文件和图片)。在用户第一次访问应用时,这些资源会被下载并缓存。在后续访问或页面导航中,如果这些资源没有更新,浏览器可以直接从缓存中加载,而不是再次从服务器请求,这可以显著减少加载时间。

3. 减少服务器负载

在客户端渲染中,页面的渲染工作是在用户的浏览器中完成的,这意味着服务器不需要为每个请求生成HTML,从而减少了服务器的负载。在高并发访问的情况下,这可以提高应用的整体性能,因为服务器主要处理API请求,而不是页面渲染。这种减轻服务器负担的优势可能在资源有限的服务器上更为明显。

4. 适用于动态内容和高度交互的应用

对于需要频繁更新内容和具有高度交互性的应用,客户端渲染提供了更好的性能和用户体验。因为数据可以通过Ajax或Fetch API异步从服务器检索,然后通过JavaScript动态更新页面内容,无需重新加载整个页面。这种方法对于实时应用(如在线聊天应用)、复杂的单页应用(SPA)和需要即时反馈的界面尤其有效。

结论

虽然服务端渲染可以提供更快的首屏加载时间和更好的SEO优化,但客户端渲染在提供丰富交互性、减轻服务器负担、利用浏览器缓存和优化后续页面交互方面具有明显优势。因此,在需要这些特点的应用场景中,客户端渲染可能会比服务端渲染表现得更快、更流畅。选择最合适的渲染策略取决于具体的项目需求、目标用户群体和应用的性能目标。

总结

是否选择服务端渲染或客户端渲染取决于应用的特定需求。对于需要快速首屏渲染和良好SEO的应用,服务端渲染可能是更好的选择。而对于那些需要高度交互和动态内容更新的应用,客户端渲染或是结合使用服务端渲染和客户端渲染的混合模式(如Next.js或Nuxt.js)可能更加合适。在决定最佳方案时,考虑应用的目标、用户体验需求以及开发和维护成本是非常重要的。

为什么服务端渲染有利于seo。

服务端渲染 (Server-Side Rendering, SSR) 对于搜索引擎优化 (SEO) 特别有利,原因在于它改善了网站的可索引性、加载时间以及用户体验,这些因素都是搜索引擎排名的重要考量。以下是服务端渲染对 SEO 有利的几个主要原因:

1. 提升内容的可索引性

  • 完整的 HTML 页面:服务端渲染生成的是完整的 HTML 页面,当搜索引擎爬虫访问网站时,它们能够立即看到完整的页面内容。这与客户端渲染不同,在客户端渲染中,爬虫可能只看到一个基本的 HTML 框架和 JavaScript 代码,实际内容需要在浏览器运行 JavaScript 后才能显示。
  • 更快的内容可见:由于页面是在服务器上渲染的,用户和搜索引擎爬虫不需要等待所有的 JavaScript 都执行完毕才能看到页面的内容。这有助于搜索引擎爬虫更高效地抓取和索引网站内容。

2. 改善加载时间

  • 更快的首次渲染:服务端渲染的页面能够更快地被用户看到,因为页面的 HTML 是预先生成的。对于搜索引擎来说,加载时间是一个重要的排名因素,因为它直接影响到用户体验。
  • 减少白屏时间:相对于客户端渲染,SSR 减少了页面的初始加载时间,即减少了白屏时间。这对用户体验极其重要,也是搜索引擎评估的一个重点。

3. 改善用户体验

  • 更好的性能:提供快速的页面加载时间和即时的内容显示,从而提升了用户体验。良好的用户体验会降低跳出率,增加页面停留时间,这些用户行为信号可以被搜索引擎用作排名的参考。
  • 更广泛的兼容性:服务端渲染生成的页面可以在不支持 JavaScript 或者 JavaScript 被禁用的环境中查看,这意味着它能够为更广泛的用户提供服务。

4. 社交媒体共享优化

  • 元数据渲染:服务端渲染还可以确保当网页被分享到社交媒体平台时,所有的元数据(如标题、描述、缩略图等)都已经嵌入到 HTML 中。这对于提高内容在社交媒体上的可见性和吸引点击至关重要。

总之,服务端渲染通过提供完整的 HTML 页面、缩短加载时间、改善用户体验和优化社交分享,对 SEO 极为有利。这些因素共同作用,帮助网站在搜索引擎中获得更好的排名,吸引更多的访问者。

非服务端渲染能做seo吗,浏览器爬虫原理是什么。

非服务端渲染的网站,特别是那些主要依赖于客户端 JavaScript 来动态生成内容的单页面应用程序 (SPA),同样可以实施 SEO 策略以提升搜索引擎排名。尽管服务端渲染(SSR)在某些方面对 SEO 更为有利,但现代搜索引擎(尤其是 Google)已经能够更好地处理和索引客户端渲染的内容。以下是非服务端渲染网站可以采取的一些 SEO 优化措施:

1. 使用预渲染 (Pre-rendering)

预渲染是一种技术,它为搜索引擎提供了静态的 HTML 快照。这意味着当搜索引擎爬虫访问网站时,它们会接收到一个已经渲染好的页面版本,而不是原始的 JavaScript 代码。这样可以确保搜索引擎能够看到和索引网站内容,即使它们不执行 JavaScript。

2. 使用搜索引擎优化的框架和库

许多现代 JavaScript 框架和库(如 Vue.js、React 和 Angular)提供了用于改善 SPA SEO 的工具和插件。例如,Vue.js 的 vue-meta 可以管理页面元数据,而 React 的 react-helmet 可以动态更新网页头部信息。

3. 确保核心内容直接在 HTML 中

尽量保证网站的核心内容(或至少是其概要)能够直接通过 HTML 提供,而不完全依赖于 JavaScript 渲染。这有助于确保内容即使在 JavaScript 未执行的情况下也能被爬虫索引。

4. 使用 Google Search Console

利用 Google Search Console 等工具可以帮助你理解 Google 如何抓取和索引你的网站,以及是否存在任何抓取或索引问题。这些工具还可以提供有关网站性能的宝贵反馈。

5. 关注网站性能

网站的加载速度和性能对 SEO 至关重要,尤其是在移动设备上。优化图片大小、利用浏览器缓存、减少 JavaScript 文件大小等措施可以帮助提高网站速度。

浏览器爬虫的原理

搜索引擎爬虫(也称为蜘蛛或机器人)的工作是访问网页,读取它们的内容,然后将这些信息存储在搜索引擎的索引数据库中。传统的爬虫主要读取 HTML 和网页上的链接,但现代爬虫(如 Googlebot)也能执行 JavaScript,这意味着它们可以抓取和索引由客户端脚本动态生成的内容。不过,JavaScript 的执行可能会消耗更多资源,导致抓取和索引过程变慢,因此优化这些过程依然十分重要。

尽管如此,对于高度动态的内容和复杂的交互逻辑,直接通过 HTML 提供内容的方法(如服务端渲染或预渲染)在 SEO 方面通常更为有效。

Ts

TypeScript 中 any、never、unknown、null & undefined 和 void 有什么区别?

any: 动态的变量类型(失去了类型检查的作用)。
never: 永不存在的值的类型。例如:never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。
unknown: 任何类型的值都可以赋给 unknown 类型,但是 unknown 类型的值只能赋给 unknown 本身和 any 类型。
null & undefined: 默认情况下 null 和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给 number 类型的变量。当你指定了 --strictNullChecks 标记,null 和 undefined 只能赋值给 void 和它们各自。
void: 没有任何类型。例如:一个函数如果没有返回值,那么返回值可以定义为void。

juejin.cn/post/699998…

网络

浏览器从输入 url 到页面展示经历了什么

当你在浏览器中输入URL并按下回车时,发生了一系列复杂的步骤,这些步骤共同完成了从请求资源到展示页面的过程。这个过程可以分解为以下几个主要步骤:

  1. 解析URL:首先,浏览器需要解析你输入的URL,这包括确定协议(例如HTTP或HTTPS)、域名(例如www.example.com)以及路径(例如/path/to/resource)。
  2. DNS查询:解析域名需要通过DNS(域名系统)将域名转换为IP地址,这样浏览器才能找到它想要联系的服务器。如果域名的IP地址没有缓存,则需要执行DNS查询。
  3. 建立连接:浏览器使用解析得到的IP地址与服务器建立连接。如果URL使用HTTPS协议,这个过程还将包括TLS握手过程,以确保连接的安全。
  4. 发送HTTP请求:一旦建立了连接,浏览器就会构建并发送一个HTTP请求给服务器。这个请求包括了所请求资源的路径,以及浏览器可以接受的内容类型、编码等信息。
  5. 服务器处理请求并响应:服务器接收到浏览器的请求后,会处理这个请求,并将响应发送回浏览器。响应通常包括一个状态码(例如200表示成功),请求的资源内容(如HTML文档),以及其他元信息。
  6. 浏览器处理服务器响应:浏览器接收到服务器的响应后,会根据响应的内容类型决定如何处理。对于HTML文档,浏览器会解析HTML,并可能进一步请求页面上引用的CSS、JavaScript文件、图片等资源。
  7. 渲染页面:浏览器将HTML文档解析成DOM(文档对象模型),然后结合CSS信息构建渲染树,最后绘制到屏幕上。JavaScript将在适当的时间被执行,可能会修改DOM或改变最终呈现的页面。
  8. 用户交互:一旦页面被加载和渲染完成,用户就可以与页面进行交互了,如点击链接、填写表单等。JavaScript代码可以监听和响应用户操作,实现动态的页面功能。

url解析出哪些东西?是唯一的吗?

image.png

https

超文本传输安全协议是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,但利用SSL/TLS加密数据包。HTTPS开发的主要目的,是提供对网站服务器的身份认证,保护交换资料的隐私与完整性

http和tcp

TCP协议对应于传输层,而HTTP协议对应于应用层,从本质上来说,二者没有可比性。

Http协议是建立在TCP协议基础之上的,当浏览器需要从服务器获取网页数据的时候,会发出一次Http请求。

Http会通过TCP建立起一个到服务器的连接通道,当本次请求需要的数据完毕后,Http会立即将TCP连接断开,这个过程是很短的。所以Http连接是一种短连接,是一种无状态的连接。所谓的无状态,是指浏览器每次向服务器发起请求的时候,不是通过一个连接,而是每次都建立一个新的连接。如果是一个连接的话,服务器进程中就能保持住这个连接并且在内存中记住一些信息状态。而每次请求结束后,连接就关闭,相关的内容就释放了,所以记不住任何状态,成为无状态连接。

HTTP 状态码有哪些

1** 信息,服务器收到请求,需要请求者继续执行操作

2** 成功,操作被成功接收并处理

3** 重定向,需要进一步的操作以完成请求

4** 客户端错误,请求包含语法错误或无法完成请求

5** 服务器错误,服务器在处理请求的过程中发生了错误

请描述⼀下 cookies , sessionStorage 和 localStorage 的区别?

  • cookie 是⽹站为了标示⽤户身份⽽储存在⽤户本地终端(Client Side)上的数据(通常 经过加密)

  • cookie数据始终在同源的http请求中携带(即使不需要),记会在浏览器和服务器间来回 传递

  • sessionStoragelocalStorage 不会⾃动把数据发给服务器,仅在本地保存

  • 存储⼤⼩:

    • cookie 数据⼤⼩不能超过4k
    • sessionStoragelocalStorage 虽然也有存储⼤⼩的限制,但⽐ cookie ⼤得 多,可以达到5M或更⼤
  • 有期时间:

    • localStorage 存储持久数据,浏览器关闭后数据不丢失除⾮主动删除数据
    • sessionStorage 数据在当前浏览器窗⼝关闭后⾃动删除
    • cookie 设置的 cookie 过期时间之前⼀直有效,即使窗⼝或浏览器关闭

cookie和session

CookiesSession 是两种在Web开发中用来跟踪用户状态和数据的技术。它们通常被用于识别用户、管理用户会话、实现登录机制等,但它们的工作原理和使用场景有所不同。

1. Cookies

  • 客户端存储:Cookies是小的文本文件,存储在用户的浏览器上。它们包含了一些键值对,用来记录用户的信息和偏好设置。
  • 大小限制:每个Cookie的大小限制大约为4KB,而且每个域名下存储的Cookies数量也有限制(具体限制取决于浏览器)。
  • 发送到服务器:Cookies随着每个对同一域的HTTP请求自动发送到服务器,因此可以用来持久化用户的状态信息(如登录状态)。
  • 生命周期:Cookies可以设置过期时间。如果没有设置过期时间,它们将作为会话Cookies存在,即浏览器关闭时被删除。如果设置了过期时间,Cookies会一直存储到过期时间。

2. Session

  • 服务器端存储:Session数据存储在服务器上。每个用户会话都有一个唯一的Session ID,用于区分不同的用户。
  • 容量:理论上,Session可以存储大量数据,只受限于服务器的内存容量。
  • 生命周期:Session的生命周期通常由服务器控制,一般是基于用户的活动时间。用户在一定时间内没有活动,Session就会过期并被销毁。
  • 安全性:由于Session数据存储在服务器上,因此相对于存储在客户端的Cookies来说,更加安全。用户无法直接访问或修改存储在服务器上的Session数据。

3. Cookies 和 Session 的工作方式 通常情况下,当用户访问一个网站并进行登录时,网站服务器会创建一个Session,并为该Session生成一个唯一的Session ID。这个Session ID会被发送到用户的浏览器并存储为一个Cookie。之后,用户每次请求网站时,这个Session ID都会被发送到服务器,服务器通过Session ID来识别用户,并提供相应的用户数据。

总结

  • Cookies主要用于在客户端存储用户的偏好设置、跟踪用户行为等。
  • Session主要用于在服务器端存储用户相关的数据,如用户认证后的身份信息,提供一个跨多个页面请求的用户会话。
  • 使用Cookies和Session的主要区别在于存储位置、安全性以及数据容量。在实际应用中,它们往往被结合使用来管理用户的登录状态和数据

浏览器缓或http缓存

HTTP在Web开发中被用于减少网络带宽消耗、提高网页加载速度和改善用户体验。这种缓存机制主要通过存储已请求资源的副本来实现,使得在后续相同资源的请求中,可以直接从缓存中读取数据,而不必重新从服务器下载。下面详细介绍浏览器(HTTP)缓存的工作原理和主要类型。

工作原理

当浏览器发起一个请求时,它会首先检查本地缓存中是否有该请求的副本以及该副本是否仍然有效(根据缓存规则)。如果有效,浏览器将直接从缓存中加载资源,而不是从服务器请求。如果缓存的副本过期或不存在,浏览器则会向服务器发起请求,并将响应的资源存储到缓存中,供未来请求使用。

主要类型

  1. 强缓存(Strong Caching)

    • 直接根据缓存规则判断资源是否有效,无需与服务器通信。
    • 控制强缓存的HTTP头包括ExpiresCache-ControlExpires是HTTP/1.0的产物,指定了资源的具体过期时间。Cache-Control是HTTP/1.1引入的,提供了更多选项,如max-age(资源的最大有效时间)。
  2. 协商缓存(Negotiated Caching)

    • 当强缓存失效时,浏览器会与服务器进行通信,确认缓存的副本是否仍然有效。
    • 控制协商缓存的HTTP头包括Last-Modified/If-Modified-SinceETag/If-None-MatchLast-ModifiedIf-Modified-Since通过时间戳判断资源是否更新;ETagIf-None-Match提供资源的唯一标识(版本号),更精确地控制缓存。

实践

  • 使用场景:静态资源(如样式表、脚本、图片等)是缓存的理想对象,尤其是那些不经常变更的资源。
  • 缓存策略:为了有效利用缓存,开发者应该合理配置HTTP响应头。例如,对于经常变更的资源,可以设置较短的Cache-Control: max-age或使用协商缓存。对于不经常变动的资源,可以设置较长的max-age或将其标记为immutable

总结

浏览器缓存(HTTP缓存)是一种重要的性能优化技术,它可以减少网络延迟,降低服务器负载,快速呈现网页内容。通过合理配置缓存策略,开发者可以在保证内容更新及时的同时,提供更加流畅的用户体验。 360189171b3d06dbb148a2c2daa6347.png

get和post的区别

最直观的区别就是 GET 把参数包含在 URL 中,POST 通过 request body 传递参数。

  • GET在浏览器回退时是无害的,而POST会再次提交请求。
  • GET产生一个TCP数据包;POST产生两个TCP数据包。
  • GET产生的URL地址可以被Bookmark,而POST不可以。
  • GET请求会被浏览器主动cache,而POST不会,除非手动设置。
  • GET请求只能进行url编码,而POST支持多种编码方式。
  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  • GET请求在URL中传送的参数是有长度限制的,而POST么有。
  • 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
  • GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。

聊聊跨域

为什么要跨域?

跨域是浏览器的一个特性,就是浏览器从一个“域”向另一个“域”的服务器发出请求,来访问另一个“域”上的资源。但是,由于请求的文件可能会存在恶意攻击,浏览器并不允许直接访问另一个“域”上的资源,只能访问同一个“域”上的资源,这个就是“同源策略”。而所谓的“同源”,指的是“协议、域名、端口号”一致,比如:

domain-a.com可以访问domain-a.com/data.json的内容,但是不能访问:

TCP和UDP的区别

TCP与UDP区别总结:

1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接

2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付

3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的

UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)

4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信

5、TCP首部开销20字节;UDP的首部开销小,只有8个字节 6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道

三次握手

TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接。 一个TCP连接必须要经过三次“对话”才能建立起来,其中的过程非常复杂, 只简单的描述下这三次对话的简单过程:

第一次握手:主机A通过向主机B 发送一个含有同步序列号的标志位的数据段给主机B,向主机B 请求建立连接,通过这个数据段, 主机A告诉主机B 两件事:我想要和你通信;你可以用哪个序列号作为起始数据段来回应我。

第二次握手:主机B 收到主机A的请求后,用一个带有确认应答(ACK)和同步序列号(SYN)标志位的数据段响应主机A,也告诉主机A两件事:我已经收到你的请求了,你可以传输数据了;你要用那个序列号作为起始数据段来回应我

第三次握手:主机A收到这个数据段后,再发送一个确认应答,确认已收到主机B 的数据段:"我已收到回复,我现在要开始传输实际数据了,这样3次握手就完成了,主机A和主机B 就可以传输数据了。

四次挥手

TCP建立连接要进行3次握手,而断开连接要进行4次

第一次: 当主机A完成数据传输后,将控制位FIN置1,提出停止TCP连接的请求 ;

第二次: 主机B收到FIN后对其作出响应,确认这一方向上的TCP连接将关闭,将ACK置1;

第三次: 由B 端再提出反方向的关闭请求,将FIN置1 ;

第四次: 主机A对主机B的请求进行确认,将ACK置1,双方向的关闭结束.。

\

由TCP的三次握手和四次断开可以看出,TCP使用面向连接的通信方式, 大大提高了数据通信的可靠性,使发送数据端和接收端在数据正式传输前就有了交互, 为数据正式传输打下了可靠的基础。

聊聊 Cache Control

Cache-Control 是一个 HTTP 头部字段,用于在HTTP请求和响应中定义缓存策略。通过这个字段,服务器可以指示浏览器或其他中间缓存(如代理服务器)如何处理特定资源的缓存,包括它们可以缓存多久,以及在什么条件下可以重新使用缓存的资源。

  1. 缓存指令

Cache-Control头部可以包含多种指令,主要分为两类:请求指令和响应指令。

  1. 常见的响应指令:
  • public:指示响应可以被任何缓存区缓存。
  • private:指示响应只为单个用户缓存,不能由共享缓存缓存。
  • no-cache:强制所有缓存在使用已缓存的数据之前提交给原始服务器进行验证。
  • no-store:禁止缓存响应的任何内容。
  • max-age=<seconds>:设置资源可以被缓存的最大时间(秒),超过这个时间缓存将被认为过期。
  • s-maxage=<seconds>:覆盖max-age或者Expires头部,但仅适用于共享缓存。
  1. 常见的请求指令:
  • no-cache:强制缓存将请求提交给原始服务器进行验证。
  • no-store:禁止缓存请求的任何内容。
  • max-age=<seconds>:设置客户端愿意接收的响应的最大年龄。
  • min-fresh=<seconds>:指示客户端希望在指定的时间内获取最新的响应。

示例

在HTTP响应头中设置Cache-Control

arduinoCopy code
Cache-Control: public, max-age=3600

这表示响应可以被任何缓存(无论是私有的还是公共的)缓存,并且在接下来的3600秒(1小时)内,客户端无需再次请求,直接使用缓存的版本。

  1. 为什么使用 Cache-Control

使用Cache-Control头部可以有效地控制网站的缓存策略,减少服务器负担,减少延迟,并提高用户体验。它可以确保用户获取到的内容是最新的,同时也可以减少不必要的网络带宽消耗。

  1. 缓存策略的选择

选择正确的缓存策略依赖于你的应用和内容的特性。对于经常变化的内容,使用no-cache或较短的max-age值可以确保用户总是获取到最新内容。对于不经常更改的静态资源,如图片、CSS文件和JavaScript文件,使用较长的max-age值或immutable指令(表示资源不会改变)可以提高性能。

聊聊什么是无状态协议

无状态协议是指协议在两次请求之间不保留任何状态信息的通信协议。这意味着每个请求都是独立的,服务器不会根据之前的请求来改变它对当前请求的响应。最典型的无状态协议例子是HTTP(超文本传输协议),它是互联网上应用最广泛的协议之一。

  1. 特点
  • 独立性:每个请求都包含了处理该请求所需的所有信息。服务器不需要依赖之前的请求或响应来处理当前的请求。
  • 简单性:由于不需要在服务器上保持状态,实现无状态协议的服务器相比于需要跟踪状态的服务器来说,通常更简单、更易于维护。
  • 可伸缩性:不保持状态信息意味着服务器可以更容易地扩展,因为每个请求都是独立的,处理请求的服务器之间几乎不需要同步状态信息。
  1. 缺点
  • 效率问题:对于需要多次交互才能完成的操作,每个请求都必须包含足够的信息来让服务器理解请求的上下文,这可能导致数据的重复发送,增加了通信的开销。
  • 功能限制:某些应用场景,如用户会话管理,需要服务器能够识别连续的请求是否来自同一个客户端。在无状态协议中,实现这样的功能需要额外的机制,如Cookies。
  1. 应用

HTTP协议的无状态性允许Web服务器处理大量并发的客户端请求,而无需维护每个客户端的连接状态,这极大提高了Web应用的可伸缩性。但是,为了克服无状态协议的局限性,通常会使用各种技术(如Cookies、Session、Token)来在无状态协议之上构建有状态的会话层,以支持如登录状态保持、购物车管理等需要状态维护的功能。

  1. 结论

无状态协议是构建可伸缩性强、高效的网络应用的关键技术之一。通过理解和合理利用无状态协议的特点,可以设计出既能满足性能要求又能提供丰富用户体验的网络服务。

同源的不同页面间如何传递数据

前端同源的不同页面(或标签)之间传递数据的方法主要包括以下几种:

  1. 使用LocalStorage或SessionStorage

LocalStorageSessionStorage 提供了在同源的不同页面间共享数据的能力。两者都存储键值对数据,但作用域和生命周期有所不同。LocalStorage 数据保存直到明确地删除,适合持久化数据;而SessionStorage 数据在页面会话结束时(例如关闭标签页)被清除,适合临时保存数据。

  • 设置数据localStorage.setItem('key', 'value');
  • 获取数据localStorage.getItem('key');
  1. 使用Cookies

Cookies也可以在同源的不同页面间共享数据,但由于大小限制(每个域名下约4KB)和每次HTTP请求都会携带Cookie数据,导致额外的网络开销,因此更适合存储小量数据。

  • 设置Cookiesdocument.cookie = "username=John Doe";
  • 读取Cookies:通过document.cookie可以读取当前页面可访问的所有Cookies。
  1. 使用BroadcastChannel API

BroadcastChannel API 允许同源的不同页面(包括标签页、iframe或窗口)之间通信。通过创建相同名称的BroadcastChannel对象,不同页面可以相互发送和接收消息。

// 创建一个BroadcastChannel
const channel = new BroadcastChannel('channel_name');

// 监听消息
channel.onmessage = function(event) {
  console.log('Received', event.data);
};

// 发送消息
channel.postMessage('Hello from another page');
  1. 使用Window.postMessage

window.postMessage方法可以安全地实现跨源通信。虽然它主要用于不同源之间的通信,但也可以在同源的不同页面间使用。你需要有目标窗口的引用,例如,如果是通过window.open打开的窗口,或者是父子页面之间的通信。

// 在发送方
targetWindow.postMessage(message, targetOrigin);

// 在接收方
window.addEventListener("message", receiveMessage, false);

function receiveMessage(event) {
  if (event.origin !== "http://example.org:8080")
    return;

  // 处理event.data
}
  1. 使用SharedWorker

SharedWorker是一个可以被多个脚本(即使是在不同的窗口、标签、iframe中)共享的后台任务。它可以用来在这些上下文之间共享数据和消息。

// 在主线程中
const worker = new SharedWorker('sharedworker.js');

worker.port.start();

worker.port.postMessage('hello');

// 在SharedWorker中
self.onconnect = function(e) {
  const port = e.ports[0];

  port.onmessage = function(e) {
    // 处理来自主线程的消息
  };
};

选择适合的方法

选择哪种方法取决于你的具体需求,如数据的大小、是否需要持久存储、以及是否跨标签页等。对于简单的数据共享,LocalStorageSessionStorage 通常是最简单的选择;对于需要实时通信的应用,BroadcastChannel APISharedWorker 可能更适合。

HTTP(超文本传输协议)和HTTPS(安全超文本传输协议)都是用于互联网上的信息传输的协议,但它们在安全性方面有本质的区别。下面是这两种协议的主要区别:

HTTP和HTTPS

1. HTTP

  • 不安全:HTTP在客户端和服务器之间传输数据时不进行加密,这意味着数据在传输过程中可能被第三方截获或篡改,例如通过中间人攻击(MITM)。
  • 端口:默认情况下,HTTP通信使用80端口。
  • 性能:因为不涉及加密过程,所以理论上讲HTTP请求的处理速度比HTTPS要快一些。

2. HTTPS

  • 安全:HTTPS在HTTP的基础上添加了SSL/TLS(安全套接字层/传输层安全)协议,对数据进行加密。这确保了数据在传输过程中的安全性和完整性,防止了数据被第三方截获或篡改。
  • 端口:默认情况下,HTTPS通信使用443端口。
  • 性能:由于加密和解密数据需要额外的处理过程,HTTPS在性能上可能稍微慢一些。然而,随着技术的进步,如HTTP/2的推广,这一差距已经大大缩小。
  • 信任:HTTPS需要网站拥有一份由认证机构(CA)签发的证书。这提供了额外的信任层次,用户可以通过浏览器的安全锁标志来验证网站的身份。

总结

HTTPS提供了比HTTP更高的安全性,是现代互联网中推荐使用的协议。随着互联网安全意识的提高,越来越多的网站和应用转向了HTTPS,以保护用户数据免受窃取和篡改,尤其是在处理敏感信息(如登录凭证、支付信息等)时。尽管HTTPS在性能上可能稍有损失,但其带来的安全好处远远超过了这一点小小的代价

聊聊 XSS 攻击和 CSRF 攻击

XSS(跨站脚本攻击)

XSS攻击,全名Cross-Site Scripting,是一种网站应用程序的安全漏洞攻击,攻击者通过在目标网站上注入恶意脚本,当用户浏览网站时执行这些脚本,以此来盗取用户信息、操作用户会话或者篡改网站内容。

XSS攻击的类型:

  1. 反射型XSS:恶意脚本来自用户的请求,通过URL参数等方式传入,服务器直接将脚本内容反射到响应中,浏览器执行该脚本。
  2. 存储型XSS:恶意脚本被存储在目标服务器上(如数据库、消息论坛、访客留言等),当其他用户浏览相关页面时,脚本被执行。
  3. DOM型XSS:脚本通过DOM修改客户端页面内容实现的攻击,不涉及服务器端的处理,纯粹是客户端的问题。

防御措施:

  • 对输入内容进行验证和过滤,避免直接在页面上输出不可信的内容。
  • 使用CSP(内容安全策略)限制资源获取。
  • 对输出内容进行转义,特别是在插入HTML、属性、JavaScript、CSS等位置时。
  • 使用HTTP-only Cookie防止脚本访问敏感Cookie。

CSRF(跨站请求伪造)

CSRF攻击,全名Cross-Site Request Forgery,是一种诱使用户在已经认证的网站上执行非预期的操作的攻击。攻击者诱使用户访问第三方网站,在不知情的情况下,以用户的身份提交请求给目标网站。

CSRF攻击的典型场景:

  • 用户登录网银后没有退出,然后访问了一个恶意网站,该网站含有一个请求,指向网银的转账接口,用户不知不觉中完成了转账操作。

防御措施:

  • 使用Token:服务器生成一个唯一的CSRF Token,用户每次提交请求时必须携带这个Token,服务器验证Token的有效性。
  • 验证Referer:检查请求是否来自合法的源。
  • 双重Cookie验证:将CSRF Token存储在Cookie中,请求时从Cookie中读取Token并提交,服务器进行验证。
  • 使用自定义请求头:利用浏览器的同源策略,只有同源的请求才能读写自定义头部,通过验证头部信息来防御CSRF。

XSS攻击利用的是用户对指定网站的信任,CSRF攻击利用的是网站对用户网页浏览器的信任。两者都是常见的网络安全威胁,需要开发者采取有效的安全措施来防范。

主流浏览器之间的差异

主流浏览器之间的差异主要体现在以下几个方面:

  1. 性能和速度:不同的浏览器在处理网页的速度和效率上有所差异,这通常取决于它们的渲染引擎和JavaScript引擎。例如,Chrome使用Blink渲染引擎和V8 JavaScript引擎,而Firefox使用Gecko渲染引擎和SpiderMonkey JavaScript引擎。
  2. 用户界面:各个浏览器的用户界面(UI)设计有所不同,包括标签页的布局、收藏夹/书签的管理方式、菜单的组织结构等。
  3. 隐私和安全性:浏览器在隐私保护和安全措施上也有所差异。例如,一些浏览器可能提供更强大的追踪防护、更安全的浏览模式或更严格的安全警告。
  4. 兼容性和标准支持:虽然大多数现代浏览器都致力于支持最新的Web标准,但在支持的程度和方式上仍然存在差异。这可能影响到某些网页或Web应用在不同浏览器中的显示和功能。
  5. 扩展和插件生态系统:不同浏览器支持的扩展和插件数量及质量不同。Chrome和Firefox因其庞大的扩展库而闻名,而Safari和Edge的扩展库则相对较小。
  6. 更新频率和政策:浏览器的更新频率和政策也不尽相同,这直接影响到它们对新技术的支持速度以及安全漏洞的修复速度。
  7. 资源消耗:不同的浏览器对系统资源的消耗也不同,这包括内存使用、CPU占用等。某些浏览器可能对旧硬件更为友好,而另一些则可能需要更多资源以提供更快的性能。

了解这些差异有助于用户根据个人需求和偏好选择最合适的浏览器

前端体系

  • 浏览器
  • 计算机网络
  • 前端基础(html、css、js)
  • node
  • webpack
  • 数据结构和算法
  • web安全
  • 前端工程化(编译原理的应用)
  • 渲染优化
  • 性能监控

性能优化方面是怎么做的

  • 重定向优化:

    • 避免重定向
  • DNSTCP优化:

    • 预解析;也就是用<link rel="preconnect" href="//``sample.com``" crossorigin>
    • 使用preconnect时,同时也使用 dns-prefetch作为兼容手段,或者凸显关键资源;
  • 请求优化:

    • 减少HTTP请求数量:多个压缩成一个;
    • 使用HTTP2:头部压缩
    • 减少cookie大小:http请求会携带cookie
  • DOMCSS解析优化:

    • CSS放在头部
    • 优化CSS选择器结构:浏览器解析CSS选择器是从右到左;
    • JS放在尾部,或者标注deferasync避免阻塞解析;
    • 精简DOM结构
    • 提取critical css:也就是将首屏需要用到的CSS放在Head里加载,其余的异步加载;
  • 渲染优化:

    • 减少DOM操作的次数,减少重绘重排

    • 减少DOM数量:大数据量分页、虚拟列表

    • 使用GPU加速:比如transforms opacity两个属性;

      • 要慎用,低端机GPU差,占用较多内存卡顿,因此什么时候开启硬件加速,给多少元素开启硬件加速,需要用测试结果说话
    • 尽量使用requestAnimationFrame来实现视觉变化

    • 使用loading图:能较早的有视觉变化

  • 三方资源、图片优化

    • 第三方资源、库使用CDN
    • 压缩图片体积,减少图片大小;
    • 图片懒加载:使用IntersectionObserver或者监听scroll滚动的距离
    • 图片使用webp格式:体积会比jpg小;
    • 去掉图片中的metadata(拍照时的时间地点等)
    • 使用 preload 预先加载css文件或者字体文件js文件等;从而更不易阻塞页面的初步渲染,进而提升性能
    • 还可以关注一下HTTP2服务端推送或者HTTP103状态码
  • 其余的性能优化

    • 合理使用协商缓存
    • 善于使用事件委托
    • 避免死循环(死循环导致页面卡死)
    • 慎用闭包、全局状态(导致内存泄漏)
    • 使用Web Workers处理纯数据,或者与浏览器 UI 无关的长时间运行脚本
    • 保持代码设计的优雅性,减少冗余代码;
    • 灵活使用防抖节流;
    • 避免频繁JS获取Layout属性值造成的回流;

堆和栈

在计算机科学中,堆(Heap)和栈(Stack)是两种常见的数据结构,它们在内存管理中起着不同的作用。

  1. 栈(Stack)

    • 栈是一种后进先出(LIFO,Last In First Out)的数据结构,类似于一摞盘子,最后放入的元素首先被取出。
    • 栈的操作主要包括入栈(push)和出栈(pop)。
    • 在程序中,栈通常用于存储局部变量、函数调用和参数传递等信息。每个线程都会有自己的栈,用于存储函数调用过程中的局部变量和返回地址等信息。
    • 栈的大小在程序运行前就已经确定,因此栈的分配和释放是自动的,不需要手动管理。
  2. 堆(Heap)

    • 堆是一种动态分配内存的数据结构,存储的数据在内存中的位置不固定,可以动态增长和收缩。
    • 堆中的内存分配由程序员手动控制,需要手动申请内存(如使用 new 操作符)和释放内存(如使用 delete 操作符)。
    • 堆的生存期由程序员控制,当程序员不再需要某个内存块时,需要手动释放该内存块,否则可能会导致内存泄漏问题。
    • 堆通常用于存储动态分配的对象、数据结构和大型数据等。

在许多编程语言中,包括C、C++、Java等,都有堆和栈的概念,并且在内存管理中起着重要作用。了解堆和栈的区别以及它们在内存管理中的作用,有助于编写高效、健壮的程序。

前端优化对于 SEO

前端优化对于 SEO(搜索引擎优化)至关重要,因为搜索引擎需要能够有效地抓取和索引您的网站内容。以下是一些前端优化方面可以提升 SEO 的方法:

  1. 网站速度优化

    • 加载速度是搜索引擎排名的一个关键因素。通过减少页面的加载时间,可以提高用户体验并提升排名。
    • 使用压缩和缓存静态资源(如 CSS、JavaScript 和图片),以减少页面加载时间。
    • 通过使用内容分发网络(CDN)来加速静态资源的传输。
  2. 响应式设计

    • 使用响应式设计确保您的网站在各种设备上都能够良好地显示,包括桌面电脑、平板电脑和手机。这有助于提升用户体验,并可能提升您的排名,因为搜索引擎更喜欢提供适应性更强的网站。
  3. 语义化 HTML

    • 使用语义化的 HTML 标记结构可以让搜索引擎更好地理解您的页面内容。使用适当的标题(h1、h2、h3 等)和语义化的标签(如 <nav><article><section> 等)来组织页面内容。
  4. 优化页面元数据

    • 使用适当的标题(title)和描述(meta description)标签来描述页面内容,这有助于搜索引擎理解您的页面主题,并在搜索结果中显示有吸引力的摘要。
    • 使用适当的关键字和标签来描述页面内容,以帮助搜索引擎索引您的页面。
  5. 友好的 URL 结构

    • 使用简洁、描述性和友好的 URL 结构,以提高页面的可读性和可理解性,例如使用包含关键字的 URL 而不是随机的参数串。
  6. 图片优化

    • 使用适当的 alt 属性描述图像内容,这不仅有助于搜索引擎了解图像内容,还提高了页面的可访问性。
    • 优化图像大小和格式,以减少页面加载时间。
  7. 合理的页面结构

    • 使用合理的页面结构,包括正确的标题、段落和列表等元素,以提高搜索引擎对页面内容的理解。
  8. 网站可访问性

    • 确保您的网站对残障用户友好,包括正确使用语义化的 HTML 标记和提供键盘导航。

通过实施这些前端优化措施,您可以提升您网站的 SEO 效果,使其更容易被搜索引擎发现和排名。

代码

19、--------------------------------------

let a = 10
function fn () {
    console.log(a)
    let a = 20
}
fn ()

---

function fn() {
console.log(i)
for (var i = 1; i < 2; i++) {
console.log(i)
}
}
8、--------------------------------------
const showThis = () => {
console.log(this)
}
var obj = {
showThis: showThis
}
showThis()
obj.showThis()
3、--------------------------------------
var a = 0, b = 0
function A(a) {
A = function (b) {
console.log(a + b++)
}
console.log(a++)
}
A(1)
A(2)

23、--------------------------------------
let a = 1
let obj = {
a: 2,
b: function () {
return () => {
console.log(this.a)
}
}
}
let bb = obj.b()
bb()
24、--------------------------------------
let a = 1
let obj = {
a: 2,
b: function () {
return function () {
console.log(this.a)
}
}
}
let bb = obj.b()
bb()

20、--------------------------------------
setTimeout(() => {
console.log(2)
},0)
Promise((resolve, reject) => {
console.log(1)
resolve()
}).then(res => {
console.log(3)
})

9、--------------------------------------
new Promise(function (resolve, reject) => {
console.log('a')
setTimeout(function(){
console.log('b')
})
resolve()
}).then(() => {
console.log('c')
})
setTimeout(function () {
console.log('d')
})
console.log('e')

1、--------------------------------------
Promise.reject(2).catch(e => e).then(d => {
console.log(d)
}

12、--------------------------------------
const p1 = new Promise((resolve, reject) => {
resolve('hello')
}).then(result => result)
.catch(e => e)

const p2 = new Promise((resolve, reject) => {
throw new Error('报错了')
}).then(result => result)

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e))

13、--------------------------------------
const p1 = new Promise((resolve, reject) => {
resolve('hello')
}).then(result => result)
.catch(e => e)

const p2 = new Promise((resolve, reject) => {
throw new Error('报错了')
}).then(result => {
result
})
.catch(e => e)

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e))

14、--------------------------------------
function Foo() {
getName = function () {
alert(1);
};
return this;
}
Foo.getName = function () {
alert(2);
};
Foo.prototype.getName = function () {
alert(3);
};
var getName = function () {
alert(4);
};

function getName() {
alert(5);
}
Foo.getName()
getName();
Foo().getName()

11、--------------------------------------
var bar = {
myName: "bar",
printName: function () {
console.log(myName)
console.log(this)
}
}
function foo() {
let myName = "foo"
return bar.printName
}
let myName = "global"
let \_printName = foo()
\_printName()
bar.printName()