一、HTML+CSS
1、 什么是html语义化
html语义化是指通过HMTL元素来传达文档结构和内容含义,使得网站更容易被搜索引擎理解、索引、呈现。有助于提高网页的可访问性、可维护性、可重用性。以下是几个关于html语义化的概念
-
语义元素:html元素分为两类:语义元素和非语义元素。语义元素是指那些明确表达文档结构和内容含义的元素,如
<header>、<nav>、<main>、<footer>等。而非语义性元素则没有明确的语义,如<div>、<span>等 -
语义化的好处
- 更好的被搜索引擎理解:搜索引擎可以更好的识别网页内容和结构,从而提高搜索排名
- 提高可访问性:对于视障用户,语义化的html可以帮助屏幕阅读器更好的理解网页内容
- 提高可维护性:语义化的html使得网页结构更加清晰,代码更易于理解和维护
- 提高可重用性:使得网页各个部分可以被重用,提高了网页的复用性
示例:
- 使用
<header>标签来表示网页的头部。- 使用
<nav>标签来表示网页的导航菜单。
2、script 标签中的 async 和 defer 属性 (相关链接)
在 HTML 中会遇到以下三类 script:
<script src='xxx'></script>浏览器在解析 HTML 的时,遇到script标签会暂停解析HTML,会先发送网络请求获取该 JS 脚本,获取成功后让 JS 引擎执行该脚本,执行完毕后才会继续解析HTML(如果获取 JS 脚本的网络请求迟迟得不到响应,或者 JS 脚本执行时间过长,都会导致白屏,用户看不到页面内容。)<script src='xxx' async></script>浏览器解析HTML时,会异步下载js脚本,但是下载完成后如果HTML还没解析完成的话,会暂停解析HTML,先执行js脚本后,才继续解析HTML(可能会阻碍HTML解析)<script src='xxx' defer></script>浏览器解析HTML时,异步下载js脚本,下载完成后如果HTML还没解析完成,会等待HTML解析完成再执行脚本
什么情况下使用不同script
- 如果脚本是模块化的并且不依赖于任何脚本,则使用
async - 如果脚本依赖于其他脚本或被另一个脚本依赖,则使用
defer - 如果脚本很小,并且被异步脚本依赖,则在异步脚本之上使用没有属性的内联脚本。
3、 cookie、sessionStorage 和 localStorage 的区别。
都是浏览器在客户端存储数据的机制,但各自有不同的特性:
- cookie: 由服务器发送到浏览器并保存在浏览器中的小文本文件(数据片段),每次http请求都会自动将cookie发送到服务器,增加了网络传输量。
- sessionStorage: 是由浏览器提供的一个内置对象,它用于存储会话级别的数据,前端可通过相关API可创建或修改
- localStorage: 是由浏览器提供的一个内置对象,它用于存储持久化的本地数据,前端可通过相关API可创建或修改
| cookie | sessionStorage | localStorage | |
|---|---|---|---|
| 生命周期 | 根据设置的过期时间(Expires 或 Max-Age 属性)来决定,如果不设置,则默认为会话级别(浏览器关闭) | 仅在当前浏览器窗口或标签页的会话期间有效,一旦窗口或标签页关闭,数据就会被销毁。 | 数据是永久存储的,除非手动删除,否者数据不会过期 |
| 容量(每个域名) | 4KB | 5MB | 5MB |
| 作用域 | 同源策略(协议、域名以及端口号相同)所有窗口 | 当前页面窗口 | 同源策略下所有窗口 |
| 是否随着每个 HTTP 请求发送给服务器 | 通过 Cookie 请求头,自动发送给服务器 | 否,仅为本地浏览器所用 | 否,仅为本地浏览器所用 |
总结来说,cookie主要服务于需要在客户端和服务器之间来回传递信息的情况,如保持用户登录状态;而localStorage和sessionStorage则主要用于在客户端存储应用相关的数据,其中localStorage用于长期存储,sessionStorage用于临时且会话级别的存储
4、 HTTP
4.1 从浏览器地址栏输入 url 到请求返回发生了什么(相关链接)
浏览器会执行以下步骤来请求该资源:
- 解析URL:浏览器会解析URL,将其分解成协议、主机名和路径等组成部分
- 构建请求:浏览器会构建一个HTTP请求报文,包括请求行(请求方法、请求路径、协议版本)、请求头和空行
- 发送请求:浏览器会将请求报文发送给服务器
- 接收响应:服务器接收到请求后,会处理请求并返回一个响应报文,包括响应行(协议版本、状态码、原因短语)、响应头和响应体
- 解析响应:浏览器会解析响应报文,提取响应头和响应体
- 渲染页面:如果响应体是HTML文档,浏览器会解析HTML并渲染页面
这个过程涉及的主要组件包括
- 浏览器:负责解析URL、构建请求、发送请求、接收响应和渲染页面
- 服务器:负责接收请求、处理请求并返回响应
- 网络:负责传输请求和响应
总是要问:为什么需要三次握手?其实这是由 TCP 的自身特点可靠传输决定的。客户端和服务端要进行可靠传输,那么就需要确认双方的接收和发送能力。第一次握手可以确认客户端的发送能力,第二次握手,确认了服务端的发送能力和接收能力,所以第三次握手才可以确认客户端的接收能力。不然容易出现丢包的现象。
这个过程涉及到的协议包括:
- DNS协议:用于解析域名转化为IP地址
- TCP/IP协议:用于建立和维护网络连接
- HTTP/HTTPS协议:用于发送和接收数据
4.2 http状态码
状态码分类
- 1xx:服务器收到请求
- 2xx:请求成功,如200
- 3xx:重定向,如302
- 4xx:客户端错误,如400
- 5xx:服务器端错误,如500
常见状态码:
- 200:表示请求成功
- 301:永久重定向,表示资源已被永久移动到新位置
- 302:临时重定向,表示资源被临时移动到新位置
- 304:资源未被修改
- 400:表示请求无效
- 401:表示请求未经授权,需提供有效身份验证凭证
- 403:表示请求被拒绝,无权限
- 404:请求的资源不存在
- 500:服务器内部错误
- 504:网关超时
4.3 简述http缓存
http缓存是一种机制,允许浏览器存储从服务器下载的资源(如HTML、CSS、javaScript文件、图片等),以便在后续请求中重用这些资源,从而减少网络流量和提高网页加载速度
- 强制缓存 当浏览器发起一个资源请求时,首先检查是否存在对该资源的强缓存。如果存在且未过期,浏览器会直接从本地缓存中获取资源,而无需向服务器发送请求
强缓存的启用和有效期由服务器在响应头中设置的特定指令决定
Cache-Control:
- 在 Response Headers 中。
- 控制强制缓存的逻辑。
- 例如 Cache-Control: max-age=3153600(单位是秒)
Cache-Control 有哪些值:
- max-age:缓存最大过期时间。
- no-cache:可以在客户端存储资源,每次都必须去服务端做新鲜度校验,来决定从服务端获取新的资源(200)还是使用客户端缓存(304)。
- no-store:永远都不要在客户端存储资源,永远都去原始服务器去获取资源。
Expires:HTTP/1.0中定义的较早的字段,以绝对事件(GMT格式的日期)表示资源过期事件。当请求事件小雨Expires指定的时间时,浏览器认为缓存有效
- 协商缓存(对比缓存):如果资源在本地缓存中可用,但已过期,浏览器会向服务器发送一个条件GET请求,询问服务器是否需要更新资源。如果服务器确认资源未更改,浏览器将继续使用本地缓存中的副本;否则,服务器会返回新的资源。
- 服务端缓存策略。
- 服务端判断客户端资源,是否和服务端资源一样。
- 一致则返回 304,否则返回 200 和最新的资源
资源标识:
- 在 Response Headers 中,有两种。
- Last-Modified:资源的最后修改时间。
- Etag:资源的唯一标识(一个字符串,类似于人类的指纹)。
5、盒模型
盒模型:内容(content)、内边距(padding)、边框(border)、外边距(margin)、
标准盒模型: box-sizing 为content-box
实际宽高:width/height + padding + border + margin
IE盒模型(怪异盒模型): box-sizing 为border-box
实际宽高:width/height + margin (即width/height包含了padding和border)
6、css选择器及优先级
常用css选择器:
- 内联选择器(直接写在标签上用style)
- id选择器(#id名)
- class类选择器(.class名)
- 标签选择器(p、div)
- 后代选择器(利用空格隔开)
- 群组选择器(用逗号隔开)
- 通配符(*)
优先级: !important>内联>id>(伪)类>标签>通配符>继承性
7、BFC是什么
BFC即块级格式化上下文,它是页面中的一块独立的渲染区域,内部的子元素不会影响到外部的元素,并且有一套自己的渲染规则
BFC内部的块级盒会在垂直方向上一个接一个排列- 同一个
BFC下的相邻块级元素可能发生外边距折叠,创建新的BFC可以避免的外边距折叠 - 每个元素的左
margin值和容器的左border相接触(从右向左的格式化,则相反)。即使浮动元素也是如此 BFC区域不会与浮动的容器发生重叠。- 计算
BFC的高度时,浮动子元素也会参与计算
以下元素会创建 BFC:
- 根元素(
<html>) - 浮动元素(
float不为none) - 绝对定位元素(
position为absolute或fixed)。 - 行内块元素,即
display为inline-block。 - 表格的标题和单元格(
display为table-caption,table-cell) - 匿名表格单元格元素(
display为table或inline-table) - 弹性元素(
display为flex或inline-flex的元素的直接子元素) - 网格元素(
display为grid或inline-grid的元素的直接子元素) overflow的值不为visible的元素
BFC 的应用:
- 防止
margin重叠:创建新的BFC,让相邻的块级盒位于不同BFC下可以防止外边距折叠 - 清除内部浮动(高度塌陷):
BFC内部的浮动元素也会参与高度计算,可以清除BFC内部的浮动 - 自适应多栏布局:中间栏创建
BFC,左右栏宽度固定后浮动。由于盒子的margin的左边和包含块border box的左边相接触,同时浮动盒的区域不会和BFC重叠,所以中间栏的宽度会自适应
8、定位有哪些,有什么区别
position:static;静态定位,元素按照正常的文档流排列position:relative; 相对定位,元素相对于其正常位置进行定位,仍保留在文档流中position:absolute;绝对定位,元素相对于最近的非静态定位的父元素进行定位,如没有则相对浏览器窗口定位position:fixed;固定定位,元素相对于浏览器窗口进行定位position:sticky;粘性定位,元素在达到特定阈值之前为相对定位,然后变为固定定位
9、em/px/rem/vh/vw的区别
px:绝对单位,页面按精确像素展示em:相对单位,基准点为父节点字体的大小,如果自身定义了font-size按自身来计算,整个页面内1em不是一个固定的值rem:相对单位,可理解为root em,相对根节点html的字体大小来计算vh、vw:主要用于页面视口大小布局
10、哪些方式可以隐藏页面元素及区别
display:none元素不可见,不占据空间,无法响应点击事件,元素彻底消失。会导致浏览器的重排和重绘visibility:hidden元素不可见,占据页面空间,无法响应点击事件,不会触发重排但会触发重绘opacity:0改变元素透明度,占据页面空间,可以响应点击事件,不会引发重排,一般情况下会引发重绘
11、回流(重排)和重绘
重排和重绘是浏览器关键渲染路径上的两个节点。 浏览器的关键渲染路径就是DOM和CSSOM生成渲染树 然后根据渲染树通过一个Layout的步骤来计算和确定页面上所有内容的大小和位置,确定布局后将像素绘制到屏幕上
重排:布局引擎会根据各种样式计算每个元素在页面上的大小和位置。当元素位置/大小发生变化时,浏览器会重新执行Layout这个步骤,来重新确定页面上内容的大小和位置,确定完后会重新绘制到屏幕上,所以重排一定会导致重绘
重绘:如果元素位置没有发生变化,只是样式变化。此时浏览器重新渲染的时候会跳过Layout这个步骤,直接进入绘制步骤。所以重绘不一定会导致重排
重排的代价更高,耗时更长,对性能的影响更大。而重绘的影响相对较小,因为它只需要重新绘制元素的样式,不需要重新计算布局信息。
二、 js基础
1、数据类型
1.1 数据类型介绍
- 原始/基础/值类型:6个,直接存储在栈内存中
Undefined、Null、Boolean、Number、String、BigInt(ES6 新增的数据类型:BigInt可以表示任意大小的整数。) - 引用类型:
JavaScript中的引用类型是那些值不是直接存储在栈内存中,而是存储在堆内存中的对象。引用类型的值实际上是指向对象的引用或指针,而非对象本身。常见的引用类型包括:
- Object: 最基本的引用类型,所有的对象都是 Object 类型的实例,包括数组、函数以及其他内置对象等。
- Array: 数组类型,用于存储有序的数据集合,可以通过索引访问元素。
- Function: 函数也是引用类型的一种,可以被调用执行,并可拥有内部状态和方法。
- Date: 表示日期和时间的对象,提供了日期相关的各种操作方法。
- RegExp: 正则表达式类型,用于匹配、替换和提取文本中的模式。
- Error:
错误类型,包括
Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError和URIError等,用于抛出和捕获错误信息。 - Symbol: 代表独一无二的值,最大的用法是用来定义对象的唯一属性名。
- Typed Arrays:
类型化数组,如
Int8Array、Uint8Array等,用于高效操作特定类型的二进制数据。 - Map/Set: ES6 中引入的新数据结构,用于存储键值对(Map)或唯一值集合(Set)。
- Proxy: 代理对象,用于定制对目标对象的访问。
- Promise: 用于异步编程的对象,表示一个异步操作的最终完成(resolve)或失败(reject)及其产生的结果值。
- Class: ES6 引入了类语法,但它本质上是对原型继承模式的封装,类的实例仍然是引用类型。
引用类型的值在赋值、传递或复制时,实际上是在复制指向对象的引用,而不是复制整个对象内容。这意味着当一个引用类型的变量被赋值给另一个变量时,两者都指向同一个内存地址,即同一个对象,因此修改一个变量会影响到另一个变量。
值类型的赋值变动过程如下:
javascript
复制代码
let a = 100;
let b = a;
a = 200;
console.log(b); // 100
值类型是直接存储在**栈(stack)**中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
引用类型的赋值变动过程如下:
javascript
复制代码
let a = { age: 20 };
let b = a;
b.age = 30;
console.log(a.age); // 30
引用类型存储在堆(heap) 中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;
1.2 数据类型判断
- typeof:能判断出基础数据类型,函数。不可对
null、对象、数组进行精确判断,因为都返回object - instanceof:可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型,其内部执行机制是顺着原型链去找,看能否找到该类型的原型,找到即返回
true,反之为false - Object.prototype.toString.call(): 所有原始数据类型都能判断,还有 Error 对象,Date 对象等。
示例:Object.prototype.toString.call({}) //"[object Object]"
Object.prototype.toString.call(2) //"[object Number]"
1.3 深拷贝和浅拷贝
浅拷贝:如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,则拷贝的是内存地址,即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址
1.使用Object.assign()函数:
const obj={a:1,b:{age:2}}
const shallowCopy=Object.assign({},obj)
2.使用扩展运算符:
const obj={a:1,b:{age:2}}
const shallowCopy={...obj}
深拷贝:开辟一个新的栈,两个对象属性完全相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
1. 使用JSON.stringify()和JSON.parse
const obj={a:1,b:{age:2}}
const deepCopy=JSON.parse(JSON.stringify(obj))
注意:使用此方法存在弊端,会忽略undefined、symbol和函数
2.手动递归
function deepCopy(obj){
if(typeof obj!=='object'||obj===null) return obj; //复制基本类型
let copy;
if(Array.isArray(obj)){
copy=[]; //复制数组
}else{
copy={}; //复制对象
}
for(let key in obj){
copy[key]=deepCopy(obj[key]); //递归复制属性
}
return copy
}
3.使用库函数。如lodash的_.cloneDeep():
const _=require('lodash');
const obj={a:1,b:{age:2}};
const deepCopy=_.cloneDeep(obj)
1.4 根据 0.1+0.2 ! == 0.3,讲讲 IEEE 754 ,如何让其相等?
原因:js在做数字运算时,会转换为二进制进行运算,对于十进制小数如 0.1、0.2 和 0.3,它们在二进制中往往不是有限位数就能精确表示的,而是无限循环或无穷小数。但是 js 采用的 IEEE 754 二进制浮点运算,最大可以存储 53 位有效数字,于是大于 53 位后面的会全部截掉,将导致精度丢失。
解决:
- 转换为整数运算
- 使用Number.EPSILON误差范围
function isEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
console.log(isEqual(0.1 + 0.2, 0.3)); // true
在某些情况下,`Number.EPSILON` 可能过于严格或宽松,可以根据实际需求乘以一个系数来调整比较的精度:
const customEpsilon = Number.EPSILON * 100; // 假设需要更宽松的比较条件
const areCloseEnough = Math.abs(a - b) < customEpsilon;
Number.EPSILON 的实质是一个可以接受的最小误差范围,约等于 2^-52,即 2.220446049250313080847263336181640625e-16 。
2、原型和原型链
原型:每个javaScript对象创建的时候,都会与之关联另一个对象,这个对象就是原型对象,每个对象都会从原型继承属性,其实就是 prototype 对象。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会在搜寻该对象的原型,以及该对象原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性,或者到达原型链的末尾
原型链:由相互关联的原型组成的链状结构就是原型链,原型对象也可能拥有原型,并从中继承方法和属性,一层一层向上追溯,直到到达原型链的顶端--Object.prototype(其Prototype指向null),标志着原型链的结束.
3、== 和 ===区别
相等操作符(==) 会做隐式类型转换,再进行值的比较。对象和非对象之间,尝试调用对象的valueOf()和toString()方法转换为基础类型进行比较;null和undefined相等;存在NaN则返回false
全等操作符(===) 在比较时不仅要求两个操作数的值相等,而且类型也必须相同
4、作用域与作用域链
作用域:即变量(变量作用域又称上下文)和函数的有效范围,或者说代码中能够访问变量和函数的区域。有两种主要的作用域类型:
- 全局作用域: 在任何位置都可以访问/使用,例如全局变量和全局函数
- 局部/函数作用域: 只有在当前【函数调用时内部可用】,局部变量(1.直接在函数作用域中创建的变量 2.行参)和局部函数
- 块级作用域(ES6引入):
通过
let和const关键字声明的变量拥有块级作用域,他们只在相应的代码块(如一对{}中有效)
作用域链:有了作用域才有变量的使用规则,使用一个变量的时候,首先在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。如果在全局作用域中仍未找到,那么查找过程结束,并返回undefined。
5、声明提前
在程序执行之前,悄悄的将var声明的变量和function声明的函数,提前到当前作用域的顶部,但是赋值留在原地,变量比函数更轻。程序员看不到但是会悄悄执行
6、this的指向
this关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象
- 单个元素绑定事件时-->当前元素
- 多个元素绑定事件时-->当前触发事件的元素
- 箭头函数-->外层的
this对象(固定,不可以更改) - 直接调用
this-->全局对象(在浏览器环境中全局对象是Window,在Node.js环境中是global对象。在ES6严格模式下是undefined) - 定时器中使用-->
window - 当函数作为某个对象的方法被调用-->调用此函数的对象
const obj = {
method: function() {
console.log(this); // 这里 this 指向 obj
}
};
obj.method();
- 构造函数中使用-->正在创建的对象
function Person(name) { this.name = name; }
const person = new Person('Alice');
console.log(person.name); // 'Alice',这里的 this 指向新创建的 person 对象
7、执行上下文
执行上下文是javaScript引擎在执行代码时创建的一个抽象概念,它包含了当前代码执行的环境信息,主要包括以下几个方面:
- 变量环境:存储当前上下文中定义的所有变量和函数声明。变量环境是一个词法环境,它由两部分组成:
- 环境记录:存储变量和函数声明的实际信息,如变量名、值、函数的引用等
- 外部环境引用:指向包含当前环境的外部词法环境,形成作用域链,用于变量查找
- 词法环境:在ES6规范中,变量环境被更通用的词法环境替代。词法环境同样包含环境记录和外部环境引用,它不仅管理变量和函数声明,还负责处理块级作用域(
let\const\class\import等) - 作用域链:作用域链是当前执行上下文中的变量环境(或词法环境)和外部环境(包括全局环境)的有序列表。当查找变量时,
javaScript会沿着作用域链从当前环境开始向上查找,直到全局环境或找到变量为止 this值:执行上下文中包含当前调用位置的this值。this的值取决于函数调用方式。- 闭包:当一个函数在执行上下文被创建后,如果内部定义了嵌套函数,并且这个嵌套函数被外部作用域访问到(如作为返回值返回),那么这个嵌套函数就形成了闭包。闭包使得内部函数可以访问到其外部函数的词法环境,即使外部函数已经执行完毕。
执行上下文的创建和管理是javaScript引擎内部的工作,对开发者而言通常是透明的,但在理解变量作用域、函数作用域、this的指向以及闭包等概念时,理解执行上下文至关重要,每当代码执行流程进入一个新的作用域(如全局作用域、函数作用域、eval作用域等),javaScript引擎就会创建对应的新执行上下文
8、闭包
目的:保护一个可以【反复使用的局部变量】的一种词法结构.(示例:防抖节流) 使用场景:
- 创建私有变量
- 延长变量的生命周期
一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的 即 外层函数已经执行完毕,返回的内层函数(即闭包)仍然可以访问并更新外层函数中的变量
唯一的缺点:受保护的变量永远不能释放,用多了会造成内存泄漏 固定语法:
- 创建一个外层函数
- 在其中创建一个受保护的局部变量
- 外层函数调用要返回内层函数,此内层函数在操作受保护的变量
固定语法:
function 外层函数(){
受保护的变量;
return function(){
不断操作受保护的变量
return 结果;
}
}
var 函数名=外层函数;
函数名();
9、bind、call、apply 区别
call、apply、bind作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向
apply:临时替换了函数中的this
apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入 改变this指向后原函数会立即执行,且此方法只临时改变this指向一次
用法:方法名.apply(借用的对象,[实参,...]) --apply自动打散数组
示例:
Object.prototype.toString.call/apply( xx )是用于获取数据类型的通用方法Math.max/min.apply(Math,arr); 用它来获取数组中最大/小的一项。
call:临时替换了函数中的this
call方法的第一个参数也是this的指向,后面传入的是一个参数列表 跟apply一样,改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次
用法:方法名.call(借用的对象,实参,...)
示例:
lis=Array.prototype.slice.call(lis);
将类数组转为普通数组
bind:永久替换函数中的this
bind方法和call很相似,第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入) 改变this指向后不会立即执行,而是返回一个永久改变this指向的函数,并且需要稍后调用,才会执行
1.创建了一个函数功能和原函数完全一样
2.将新函数的this永久绑定为了你指向的对象
3.将新函数中的部分固定参数提前永久绑定
用法:var 新方法名=方法名.bind(永久绑定的对象,永久绑定的实参,...); -需调用
总结:从上面可以看到,apply、call、bind三者的区别在于:
- 三者都可以改变函数的
this对象指向 - 三者第一个参数都是
this要指向的对象,如果参数为undefined或null,则默认指向全局window - 三者都可以传参,但是
apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入 bind是返回绑定this之后的函数,apply、call则是立即执行
10、new操作符具体都干了什么?
- 创建一个新的空对象obj
- 将新空对象与构建函数通过原型链连接起来,设置空对象的
__proto__为构造函数的prototype。 - 将构造函数中的this绑定到obj上,执行构造函数的代码(为这个新对象添加属性)。
- 根据构造函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,则返回这个对象
手写new操作符
function mynew(Func, ...args) {
// 1.创建一个新对象
const obj = {}
// 2.新对象原型指向构造函数原型对象
obj.__proto__ = Func.prototype
// 3.将构建函数的this指向新对象
let result = Func.apply(obj, args)
// 4.根据返回值判断
return result instanceof Object ? result : obj
}
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function () {
console.log(this.name)
}
let p = mynew(Person, "huihui", 123)
console.log(p) // Person {name: "huihui", age: 123}
p.say() // huihui
11、同步和异步
同步和异步是计算机程序执行的两种模式
- 同步模式是指程序按照顺序执行,一个任务必须在前一个任务完成后才能开始执行
- 异步模式是指进程不需要等待,而是继续执行下面的操作。程序可以同时执行多个任务,且执行顺序并不固定,提高程序的执行效率
12、async/await 和 Promise 的关系
Promise 提供了一种处理异步操作的方式。避免了回调地狱,并可以表示异步操作的最终结果。Promise 是一个对象,它可以处于三种状态之一:pending(等待中)、fulfilled(已完成)或rejected(已拒绝)。当异步操作完成时,Promise 的状态会被设置为 fulfilled 或 rejected,并且可以传递一个结果值。
用法:
Promise对象是一个构造函数,用来生成Promise实例
const promise=new Promise(function(resolve,reject) {})
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject
resolve函数的作用是,将promise对象的状态从pending变为fulfilledreject函数的作用是,将promise对象的状态从pending变为rejectedpromise的优点:
- 易于理解和使用:
promise提供了一种清晰的结构来处理异步操作的完成或失败,使得代码更容易理解,增强可读性 - 支持链式调用:
promise支持链式调用,降低了编码难度
async/await 是基于Promise的一种语法糖,提供了一种更直观和同步化的方式来编写异步代码。通过使用async关键字声明一个函数为异步函数,并使用 await 关键字来等待异步操作的完成。await 关键字后面跟着一个 Promise 对象,它会等待这个 Promise 对象的状态变为 fulfilled 或 rejected。
async 函数返回一个 Promise 对象,它会自动将函数的返回值包装在一个已解决(resolved)的Promise中,这意味着我们可以在async函数中使用await来等待其他Promise的完成,并以同步的方式处理它们的结果
总的来说,async/await 是一种更简洁、更易于理解的异步编程方式,它使得异步编程更加直观和易于阅读。而 Promise 是一种更底层的异步编程方式,它是 async/await 的基础。
13、数组的常用方法有哪些?
增:前3个改变原数组
push:从数组末尾增加任意数量值,返回新数组长度unshift:从数组开头增加任意数量值,返回新数组长度splice:第一个参数表示起始位置,第二个参数表示删除个数,后面参数表示添加的项,返回空数组concat:从数组末尾增加任意数量值,返回新数组
删:前三个改变原数组
shift:删除数组开头一项,返回被删除的项pop:删除数组最后一项,返回被删除的项spliceslice:截取,arr.slice(starti,endi+1)返回被截取的新数组,第二实参也可以省略:从starti截到末尾 第一实参也可以省略:从头截到尾
查:
indexOf:返回第一项的坐标,如没找到返回-1find:返回找到的第一项includes:找到返回true,反之false
排序方法:
reverse:将数组元素顺序翻转sort:接收一个比较函数,用于判断哪个值应该排在前面
arr.sort((a,b)=>{ //a前一个数,b后一个数
return a-b
})
- 扩展-冒泡排序:把数组的每一项与前一项比较,如果前一项大则交换位置
var arr=[22,11,44,7,99,89,23,45,67,12]
for(let j=0;j<arr.length-1;j++){
for(let i=0;i<arr.length-(j+1);i++){
if(arr[i]>arr[i+1]){
let m=arr[i];
arr[i]=arr[i+1];
arr[i+1]=m;
}
}
}
console.log(arr)
转换方法:
join:接收一个参数,拼接数组元素为字符串
判断:
some:只要有一项满足条件则返回trueevery:数组元素必须全部满足条件才返回true
遍历:把数组中的每个元素取出来执行相同或相似的操作
forEach:直接修改原数组map:不修改原数组,返回一个新数组
汇总和过滤:
- 过滤
filter:筛选出符合条件的元素,创建一个新数组 - 汇总
reduce:将所有的数组的元素进行+-*/
14、字符串的常用方法
拼接:
concat:用于将一个或多个字符串拼接成一个新字符串
截取:
slice:str/arr.slice(starti.endi+1);substring:str.substring(starti,endi+1);//不支持负数参数substr:str.substr(starti,n);
替换:
replace:str.replace("关键字"/正则表达式,"新内容");
改:
split:str.split("自定义切割符"),切割字符串/转换为数组;toLowerCase:转换为小写toUpperCase:转换为大写trim、trimLeft、trimRight:删除前、后或前后所有空格符,再返回新的字符串repeat:表示要将字符串复制多少次,然后返回拼接所有副本后的结果padStart:填充指定内容到字符串前面到指定长度padEnd:填充指定内容到字符串尾部到指定长度
查:
indexOf:返回第一项的坐标,如没找到返回-1chatAt:返回查询坐标的字符includes:找到返回true,反之false
15、浏览器的垃圾回收机制
在javaScript中,内存管理是由浏览器的垃圾回收机制来处理的,垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存
垃圾回收机制主要由以下2种:
- 标记清除:这是目前最常用的垃圾回收机制,首先垃圾回收器会给所有活动对象打上标记,然后清理掉没有标记的对象
- 引用计数:每个对象都有一个引用计数,当一个对象被引用时,他的引用计数+1;当一个对象的引用被解除时,它的引用计数-1。当它的引用计数变为0时,说明这个对象不再被使用,垃圾回收器就会回收它
16、var、let、const之间的区别
- 作用域:
var声明的变量的作用域是当前函数或全局作用域let和const声明的变量的作用域是当前块级作用域
- 变量提升:
var声明的变量会被提升到当前作用域的顶部,这意味着可以在声明之前调用,值为undefinedlet和const声明的变量不存在变量提升,只能在声明后调用否则报错
- 重复声明:
var可以在同一作用域内多次声明同一个变量let和const不允许在同一作用域重复声明
- 修改:
var和let声明的变量可以被修改const声明的变量不能修改,但如果是对象或数组可修改其属性或元素
17、防抖节流
防抖:
function fdjl(fn,delay,immediate){
let timer=null;
return function(){
const tis=this;
const arg=arguments;
if(timer){clearTimeout(timer)}
if(immediate){
const now=!timer;
timer=setTimeout(()=>{
timer=null;
},delay)
if(now){
fn.apply(tis,arg);
}
}else{
timer=setTimeout(()=>{
fn.apply(tis,arg)
},delay)
}
}
}
节流:
时间戳写法:
function jl1(fn,delay){
let oldTime=Date.now();
return function(...args){
let newTime=Date.now();
if(newTime-oldTime>=delay){
fn.apply(null,args);
oldTime=Date.now
}
}
}
定时器写法:
function jl2(fn,delay){
let timer=null;
return function(...args){
if(!timer){
setTimeout(()=>{
fn.apply(null,args)
timer=null;
},delay)
}
}
}
区别:都是为了优化性能,用于减少频繁操作带来的性能开销
- 防抖是指在一个事件被触发后,延迟一段时间执行相应的操作,如果在这段时间内再次触发该事件,则会取消之前的任务重新开始计时,可以确保在短时间内连续触发的事件只执行最后一次操作。常用于
elem.onmousemove- 鼠标移动事件,每次移动就会触发input.oninput-input每次修改内容就会触发window.onresize- 屏幕每次改变大小就会触发
- 节流是指在一个事件被触发后,每隔一定时间执行一次相应的操作,可以确保短时间内连续触发的事件只执行有限次操作。常用于滚动事件的处理、定时刷新数据等场景
18、事件冒泡及事件委托
- 事件冒泡:当一个事件发生时,事件会依次经过目标元素的所有父元素,直到找到一个处理该事件的事件处理器为止。即会触发父元素的click事件处理器
- 事件委托:事件委托是一种优化性能的技术,用于处理大量元素的事件绑定。它基于事件冒泡原理,将事件绑定到父元素上,然后通过冒泡过程捕获子元素的事件。这样可以减少事件绑定的数量,提高性能
- 事件捕获:事件捕获阶段发生在事件的目标元素之前,即事件从文档的根元素开始,经过所有祖先元素,最终到达目标元素。这意味着,如果一个元素的祖先元素有一个事件监听器,那么这个事件监听器会在目标元素的事件监听器之前被触发。
19、get和post区别
get和post是http协议中两种请求方法
- get参数会放在url中,参数长度有限制(大多数浏览器限制在2048个字符),不适合传递大量数据,post方法参数放在请求体body中,可以传递大量数据,没有长度限制。
- get参数放在url上所以隐私性及安全性较差
- get请求可以被缓存,可以被收藏为书签,post请求不会被缓存也不能被收藏为书签
- get请求会保存在浏览器历史记录中,post不会
- get请求可以被重定向,post不能
- get请求刷新服务器或回退没有影响,post请求回退会重新提交数据请求
- get通常用于获取数据,而post通常用于提交数据
20、跨域及解决方案
跨域是由于浏览器的同源策略,浏览器会阻止在不同的域名、协议和端口之间进行数据请求。解决方法如下:
- JSONP(JSON with Padding):利用浏览器允许从不同源加载脚本的特性,JSONP通过在页面插入一个script标签,像指定服务器发送请求,服务器返回一个包含回调函数和数据的JSON响应。客户端的脚本会调用这个回调函数,从而获取数据
- CORS(Access-Control-Allow-Origin):服务器设置
Access-Control-Allow-Origin响应头信息,指定哪些源可以访问资源。客户端的浏览器会检查这个头信息,如果允许就可以请求 - 代理服务器:代理服务器位于客户端和服务器之间,可以接收客户端的请求,然后转发给服务器,服务器返回的数据通过代理服务器返回给客户端
- 客户端代理:在vue中前端在vue.config中设置devServer和proxy
module.exports = {
devServer:{
proxy:{
'/hehe':{
taget:'目标地址',
pathRewrite:{
'^/hehe':''
}
}
}
}
}
- postMessage API:此API允许不同源窗口之间进行通信,可以使用这个API来发送和接收消息
21、undefined和null的区别
undefined表示一个变量没有被定义或者没有被赋值null表示一个变量被显式的设置为空值,null是一个对象,但是它没有任何属性或方法。
22、ES5、ES6、ES7
ES5、ES6、ES7分别代表JavaScript的不同版本,每一代都有其独特的特性和区别:
ES5(ECMAScript 5th Edition,2009年发布)
ES5是JavaScript语言发展过程中的一个里程碑,它引入了一系列关键特性和标准化:
-
严格模式("use strict"):通过在脚本或函数顶部添加特定指令,启用更严格的错误检查和语法规则,有助于消除潜在的编程错误和不安全行为。
-
Array新增方法:如
map(),filter(),reduce(),forEach()等,增强了对数组数据处理的能力。 -
Object新增方法:如
Object.create()用于创建新的对象并指定其原型,Object.keys()、Object.getOwnPropertyNames()用于获取对象的所有属性名。 -
JSON对象:原生支持JSON序列化(
JSON.stringify())和解析(JSON.parse())。
ES6(ECMAScript 2015,2015年发布)
ES6是一次重大的语言升级,引入了许多新特性以提升开发效率和代码可维护性:
-
类(Class):语法糖层面上支持面向对象编程,简化了构造函数、原型链的使用。
-
箭头函数:简化函数定义,自动绑定
this上下文。 -
模板字符串:支持多行字符串和字符串插值,替代复杂的字符串拼接。
-
解构赋值:可以从数组或对象中直接提取值赋给变量。
-
let和const:引入块级作用域变量声明和常量声明。
-
扩展运算符:用于展开数组或对象,简化数组操作和函数参数传递。
-
Promise:用于处理异步操作,提供统一的错误处理和链式调用。
-
模块系统:引入
import和export,支持模块化编程。 -
生成器(Generator):通过
function*和yield实现可暂停的迭代器。 -
其他特性:如Symbol、Proxy、Reflect、Map、Set、WeakMap、WeakSet等。
ES7(ECMAScript 2016,2016年发布)
ES7虽然相较于ES6改动较少,但也包含了一些重要的新功能:
-
数组includes()方法:检测数组是否包含指定的值,返回布尔值。
-
指数运算符():用于计算幂运算,如
2 ** 3等于8。 -
二进制和八进制字面量:增加了前缀
0b和0o分别表示二进制和八进制数值。 -
Async/Await:基于Promise的异步编程模型,提供了更简洁、同步风格的异步代码编写方式。
-
对象属性简写:在对象字面量中可以省略冒号和值,如果变量名与属性名相同。
-
Trailing commas in function parameter lists and calls:允许函数参数列表和调用时尾部的逗号,提高了代码可读性和可维护性。
后续的ECMAScript版本(ES8及以后)继续引入更多新特性,如异步迭代器、共享内存与原子操作、可选链、空值合并运算符、BigInt、可选捕获组等,不断丰富和完善JavaScript语言。随着新特性的普及,现代JavaScript开发通常会采用较新版本的ES标准,同时借助编译工具(如Babel)确保代码能够在较旧的环境中运行。
23、宏任务和微任务
宏任务和微任务
- 宏任务:
setTimeout、setInterval、网络请求 - 微任务:
promise、async await、mutationObserver - 微任务在下一轮
DOM渲染之前执行,宏任务在DOM渲染之后执行
Event Loop
浏览器的Event loop
- 当异步任务开始执行时,优先执行
微任务 - 直到
微任务队列为空,再执行宏任务 - 无论是执行宏任务还是微任务
- 每次执行完成后检查先还有没有
微任务,有就优先执行微任务(继续回到第一步)
24、 JavaScript 中实现继承的主要方式
-
原型链继承 (Prototype Chain Inheritance)
- 这是最基本的继承方式,通过将父类的实例赋值给子类的原型(
prototype)属性,子类的实例就可以访问到父类原型上的方法和属性。但是,所有子类实例共享父类实例的属性,可能导致意料之外的数据共享。
- 这是最基本的继承方式,通过将父类的实例赋值给子类的原型(
-
构造函数继承 (Constructor Inheritance / Classical Inheritance)
- 也称为假冒构造函数或借用构造函数,通过在子类构造函数内部调用父类构造函数(
Parent.call(this)),并传递this,使得每个子类实例都能拥有父类的属性副本。这种方法解决了数据共享的问题,但不能继承父类原型上的方法。
- 也称为假冒构造函数或借用构造函数,通过在子类构造函数内部调用父类构造函数(
-
组合继承 (Combination Inheritance)
- 结合了原型链继承和构造函数继承的优点,先通过构造函数继承为子类实例添加属性,再通过原型链继承父类的方法。这是JavaScript中最常用的继承模式。
-
原型式继承 (Prototypal Inheritance)
- 使用
Object.create()方法创建一个新对象,其原型指向父对象。这种方式简单直接,但不常见于传统的类式继承结构中,更多用于对象之间的简单复制和扩展。
- 使用
-
寄生式继承 (Parasitic Inheritance)
- 创建一个实现继承的新对象,通过某种方式增强对象,最后返回这个对象。这种方式也是基于原型链,但会在返回新对象前对其进行增强。
-
寄生组合继承 (Parasitic Combination Inheritance)
- 一种优化的组合继承方式,通过借用构造函数继承属性,并修复原型链以避免调用两次父类构造函数。这是《JavaScript高级程序设计》一书中推荐的继承模式,效率较高。
-
ES6 Class继承
- 虽然本质上还是基于原型,但ES6引入了更接近传统面向对象语言的
class语法糖,使得继承更加直观。class Child extends Parent的形式清晰表达了继承关系,背后仍然是原型链和构造函数的组合。
- 虽然本质上还是基于原型,但ES6引入了更接近传统面向对象语言的
每种继承方式都有其适用场景和优缺点,实际应用时需根据具体需求选择合适的继承策略。
三、Vue
1、说说你对vue的理解?
1.1、vue是什么?
vue是一个用于构建用户页面的开源JavaScript框架,也是一个用于创建单页面应用的web应用框架,能方便的获取数据更新,并通过组件内部的特有方法实现模型与视图的交互
1.2、Vue的核心特性
1、 数据驱动(mvvm)
MVVM(Model View ViewModel)由 Model,View,ViewModel三部分构成
Model:数据模型层,负责处理数据和业务逻辑与服务器端进行交互View:视图层,应用的展示效果,各类UI组件,简单理解就是HTML页面ViewModel:视图模型层,通过双向数据绑定来连接模型层和视图层,是他们之间的通信桥梁。当模型层的数据发生改变时,ViewModel会自动更新视图层,反之亦然。
2、 组件化
将一个页面拆分为多个功能模块,每个功能模块完成属于自己部分独立的功能,在vue中每个.vue文件都可以视为一个组件。使得页面的管理和维护变得容易
组件化优点:
- 重用性:组件可以被多次复用,使得代码的复用性大大提高
- 模块化:组件化使代码更加模块化,每个组件有自己的职责和功能,可以独立开发和测试,降低了代码的耦合度,提高了代码的可维护性
- 灵活性:组件可以根据需要进行组合和扩展,可以轻松实现复杂ui界面
- 可扩展性/可维护性:每个组件有自己的生命周期和状态管理,可以独立开发扩展,不影响其他部分的代码,方便维护
- 可测试性:可以针对每个组件进行单元测试,确保代码的质量和稳定性
3、指令系统
指令是带有v-前缀的特殊属性 作用:当数据发生变化时,可以相应的作用于DOM 常用指令:
- v-if:条件渲染指令
- v-for:列表渲染指令
- v-on:事件绑定指令
- v-bind:属性绑定指令
- v-model:数据双向绑定指令
1.3、vue与react的对比
相同点:
- 都是基于组件化的开发模式:可以将应用程序分解成可复用的组件
- 都支持数据驱动视图:通过将数据和视图绑定在一起,实现数据驱动
- 都支持服务器端渲染
服务器端渲染:(Server-Side Rendering,简称SSR)是指在服务器端将应用程序的视图渲染成HTML字符串,然后将这些HTML字符串发送给客户端浏览器,客户端只负责解析HTML并显示出来;
客户端渲染:(Client-Side Rendering,简称CSR)是指在客户端浏览器中,通过JavaScript运行时环境来渲染应用程序的视图
不同点:
- 数据变化的实现方式不同:react使用的是不可变数据,即数据一旦被创建,就不能再被修改;而vue.js使用的是可变数据,即数据可以被修改
- 组件化的方式不同:react的组件化是基于类的,每个组件都是一个类,通过实例化这个类来创建组件;而Vue.js的组件化是基于函数的,每个组件都是一个函数,通过调用这个函数来创建组件
- 路由的实现方式不同:react的路由是通过第三方库来实现的,例如React Router;而vue的路由是内置的,可以直接使用
- 性能优化的方式不同:react通过虚拟DOM技术来实现视图的渲染,通过比较虚拟DOM和真实DOM的差异,来最小化DOM操作,提高了性能。而Vue通过响应式数据绑定机制来实现数据的更新,通过观察者模式来监听数据的变化,当数据发生变化时,Vue.js会自动更新视图
2、虚拟DOM和diff算法
虚拟dom:是用来表示真实DOM的一个树形结构对象,主要作用是在数据发生变化时,通过与上一次渲染的虚拟DOM进行对比,找出发生变化的部分,并最小化的更新实际DOM。通过在内存中操作虚拟DOM树来减少实际DOM操作的次数,从而提高页面渲染的性能和效率
虚拟DOM工作原理:
- 当数据发生改变时,
JavaScript代码会生成一个新的虚拟Dom树 虚拟DOM比较新旧两棵树,找出最小变化集- 根据这些变化,
JavaScript代码会生成新的真实DOM树,然后将这个新树替换掉旧的DOM树 - 最后,浏览器会重新渲染整个页面,以反映出最新的DOM结构
diff算法:比较同层级之间的新旧虚拟DOM节点之间差异性的算法。对比出时哪个虚拟节点更改了,找出这个虚拟节点并只更新这个虚拟节点所对应的真实节点,而不用更新其他数据没发生改变的节点,实现精准的更新真实DOM,进而提高效率
双端diff算法在进行比较有四个指针:新节点首尾指针newS/newE、旧节点首尾指针oldS/oldE。在diff比较的过程中,循环从两边向中间比较
总共有五种比较情况:
oldS 和 newS进行比较,sameVnode(oldS, newS)oldS 和 newE进行比较,sameVnode(oldS, newE)oldE 和 newS进行比较,sameVnode(oldE, newS)oldE 和 newE进行比较,sameVnode(oldE, newE)当上述4种都不符合的时候就会执行,在oldVnodes里面寻找跟newS一样的节点然后位移到oldS,若没有找到在就oldS创建一个
- 执行过程是一个循环,在每次循环里,只要执行了上述的情况的五种之一就会结束一次循环
循环结束的收尾工作:直到oldS>oldE || newS>newE(代表旧节点或者新节点已经遍历完)
3、key的作用
key这个特殊的属性,用于标识每个节点的唯一性,主要作为Vue的虚拟Dom算法提示,在比较新旧节点列表时可以根据key,更准确更快的找到对应的vnode节点。优化diff算法,提高更新DOM树的效率
在比较新旧两个虚拟DOM树时
- 如果不用key,Vue默认会采用
就地复用原则:如果数据项的顺序被改变,Vue不会移动DOM元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。 - 如果使用了key,会根据key属性判断两个节点是否相同,如果key属性相同会认为是相同节点,不需要进行DOM操作。如果key不同Vue.js会认为是不同的节点,需要进行DOM操作,它会基于key的变化重新排列元素顺序,并且会移除key不存在的元素
4、vue中data为何是一个函数不是个对象
- 根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况
- 组件实例对象data必须为函数。如采用对象的形式,多个组件实例对象之间共用一个data,即共用了同一个内存地址,实例A修改的内容,同样对实例B产生了影响,而采用函数形式返回的对象内存地址并不相同。即返回一个全新data对象,使每个实例对象的数据不会受到其他实例对象数据的污染
5、说说你对Spa单页面的理解,它的优缺点是什么
SPA仅在web页面初始化时,加载相应的html、css、javaScript。它允许用户在不刷新整个页面的情况下,通过路由机制在不同的视图之间切换。SPA的核心思想是将整个应用程序作为一个单一的页面,通过异步加载数据和动态更新视图来模拟多页面应用程序的行为
SPA的优点包括:
- 更快的用户体验:由于SPA只有一个页面,因此在首次加载时,可以一次性加载所有必要资源,在后续导航中内容的改变不需要重新加载,因此可以提供更快的响应速度
- 减少服务器的压力
缺点:
- 首次渲染速度相对较慢:由于SPA 在首次加载时需要一次性加载所有必要的资源,因此初次加载时间可能会较长。
- 不利于搜索引擎的抓取
6、v-show和v-if有什么区别
都能控制元素的显示和隐藏
- v-if是真正的条件渲染指令,因为它会确保条件块内的事件监听器和子组件被销毁或者重建,当渲染条件为假时不会做操作,当条件为真时才会渲染条件块
- v-show不管初始条件时什么,元素总是会被渲染,并且只是通过css的属性display来进行切换
- v-if适用于运行时很少改变条件,不需要频繁变化条件的场景;v-show适用于频繁改变条件的场景
7、vue实例挂载的过程中发生了什么
- 创建实例:
New vue的时候会调用_init方法,初始化实例的各种属性和方法,包括数据、生命周期钩子、指令等 - 挂载实例:调用
$mount进行页面的挂载,在$mount方法内部,会调用mountComponent方法,进行一系列的准备工作,包括编译模版、创建组件实例、生成渲染函数等 - 更新组件:在
mountComponent方法中,定义了updateComponent更新函数,用于在数据变化时更新组件,当数据发生变化时,会调用此函数,执行render方法生成新的虚拟DOM树,然后调用_update方法将虚拟DOM树转化为真实DOM树,并渲染到页面中
8、描述对生命周期的理解,在created和mounted生命周期中请求数据有什么区别?
vue中实例从创建到销毁的过程就是生命周期,即从创建、初始化数据、编译模版、挂载dom=>渲染、更新=>渲染、销毁等一系列过程 vue生命周期可以分为8个阶段
beforeCreate组件实例初始化之前,data、method未初始化created组件实例初始化完成、data、method属性可用,但真实dom还没有生成,$el(指明vue实例的挂载目标)还不可用beforeMount组件挂载之前mounted组件挂载到实例之后beforeUpdate组件数据更新之前updated组件数据更新之后beforeDestroy组件实例销毁之前destroyed组件实例销毁之后
一些特殊场景的生命周期
activated: keep-alive缓存的组件激活时deactivated: keep-alive缓存的组件停用时调用errorCaptured:捕获一个来自子孙组件的错误时被调用
调用异步请求可以在钩子函数
created、beforeMount、mounted中进行调用,因为在这三个钩子函数中,data已经创建,可以将服务器端返回的数据进行赋值
created是在组件实例一旦创建完成后立即调用,此时页面dom节点并未生成、Mounted是在页面dom节点渲染完毕之后立即执行。在触发时机上created在mounted之前,如果我们的请求不需要依赖DOM,这时请求可以放在Created。大多数情况下推荐在 mounted 钩子中发起数据请求,以确保正确操作 DOM 或者更新组件的状态。但在mounted里面请求数据可能会导致页面闪动(此时dom结构已经生成)
9、v-if和v-for的区别、优先级
v-if是条件渲染指令,控制元素的显示和隐藏,只有表达式为true时,才渲染v-for指令用于遍历数组或对象并渲染成多个元素v-for的优先级比v-if高 不能把v-if和v-for作用于同一个元素。带来性能上的浪费,每次渲染都会先循环在进行条件判断,遇上这种问题可以在外层加上template(不渲染dom),进行v-if渲染,在内层进行v-for。 vue3.x则v-if优先级更高
10、减低页面加载时间的方法
页面加载时间:浏览器从响应用户输入的网址地址,到页面的渲染完成的时间
方法:压缩js、css代码、优化图片、减少http请求、减少dom操作等
11、如何实现双向数据绑定
双向数据绑定:指视图变化更新数据、数据变化更新视图
其中视图view更新数据data可以通过事件监听实现,所以vue的双向绑定个主要是实现data变化更新view
vue主要通过4个步骤实现双向数据绑定:
- 实现一个监听器
observer:对数据对象进行遍历,利用object.defineProperty()对属性都加上setter和getter,当给这个对象的某个属性赋值就会触发setter,这样就监听到了数据变化 - 实现一个解析器
compile:解析vue指定模版,找到其中动态绑定的值,从data中获取并初始化视图,给每个指令对应的节点绑定更新函数 - 定义
Watcher:当observer中的属性值变化,触发compile中对应的更新函数(对应数据变化时Watcher会调用更新函数) - Dep:由于data中的某个
key值有可能在一个视图中出现多次,所以每个key需要一个dep管家管理多个watcher。当data中的值发生变化首先会找到对应的dep,通知所有watcher执行更新函数
流程图如下:
12、如何理解$nextTick
Vue在执行更新dom时是异步执行的,当数据发生变化,vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,在统一进行更新。而且如果我们重复修改相同的数据,异步操作队列还会进行去重,等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿出来处理,进行DOM的更新
如果想要在数据更新操作之后得到最新的dom结构,就可以在数据变化后立即使用Vue.nextTick(callback);这样回调函数会在DOM更新完成之后被调用,就可以拿到更新的dom元素
13、Object.definproperty为什么监听不到数组,vue2怎么实现对数组的监听
Object.defineProperty 本身并不直接用于监听数组的变化,因为它是设计来劫持和监听对象属性的读取和写入操作的。当我们使用 Object.defineProperty 对对象的属性进行劫持时,我们实际上是在设置属性的 getter 和 setter,这样每当属性被访问或修改时,我们就可以执行自定义的逻辑,比如通知视图进行更新。然而,当应用到数组上时,存在几个核心限制使得 Object.defineProperty 无法有效地监听数组的变化:
- 数组索引的修改
当通过索引直接修改数组(如 arr[0] = 'new value')时,这实际上是一个属性赋值操作。虽然理论上可以对数组的每个索引使用 Object.defineProperty 来监听变化,但这在实践中是不可行的,因为:
性能问题:数组可能非常大,为每个索引设置 getter 和 setter 会极大地影响性能。
动态性问题:数组长度是动态变化的,每次数组变化时都需要重新为新的索引设置劫持,这在技术上是复杂且低效的。
- 修改数组长度
直接修改数组的 length 属性(例如,通过设置 arr.length = 0 来清空数组),这种操作同样无法被 Object.defineProperty 直接侦测到。这是因为 length 属性的变化不会触发索引属性的 setter。
- 使用数组方法
数组的方法(如 push、pop、splice 等)可以修改数组的内容或结构。这些操作不仅改变数组元素,有时还会改变数组的长度。Object.defineProperty 无法直接拦截这些方法调用,因为它们是数组原型上的方法,而不是数组实例上的直接属性。
Vue 2 如何实现数组的响应式
正因为上述限制,Vue 2 选择了一种不同的方式来实现对数组的响应式监听:
重写数组方法:Vue 2 通过修改数组实例的原型,将数组的一些方法(如 push、pop 等)重写为可以触发视图更新的版本。当这些重写的方法被调用时,Vue 可以捕获到数组的变动并触发相应的更新。
总结来说,Object.defineProperty 由于其内在的机制和限制,并不能直接用于有效监听数组的变化。Vue 2 通过一种巧妙的方式绕过了这些限制,能够实现对数组操作的响应式更新。
14、说说你对keep-alive的理解
keep-alive是vue中的内置组件,能在组件切换过程中将组件状态保留在内存中,防止重复渲染DOM,从而提高性能
- 一般结合路由和动态组件一起使用,用于缓存组件
- 提供
include和exclude属性,都支持字符串和正则表达式,include表示只有名称匹配的组件才被缓存,exclude表示所有名称匹配的组件都不会被缓存,其中exclude的优先级比include高 - 设置了
keep-alive缓存的组件会多出两个生命周期钩子函数,activated和deactivated,当组件被激活时,触发activated函数,当组件被移除时,触发deactivated钩子函数
15、vue常见的修饰符有哪些
- .stop:阻止事件冒泡
- .prevent:阻止默认事件
- .once:只执行一次
- .capture:用于事件捕获
- .keyCode:监听特定键码
- .self:将事件绑定在自身上,相当于阻止事件冒泡
- .native:绑定原生事件
- .right:鼠标右键
16、
17、computed和watch的区别和应用场景
Computed是计算属性,依赖其他属性值,并且computed的值有缓存,只有它依赖的属性值发生变化,下一次获取computed的值才会进行重新计算。不支持异步
watch:监听属性,不支持缓存,监听的数据发生变化会直接触发相应的操作,支持异步操作
运用场景:
- 当某个值需要通过一个或多个数据计算得到时,应该使用
computed,而且可以利用computed的缓存特性,避免每次获取值时都要重新计算。 - 当我们需要在数据变化时执行异步或者开销较大的操作时,使用
watch
18、vue组件之间的通信方式有哪些?
Vue组件之间的通信主要有:父子组件通信、兄弟组件通信、隔代组件通信
8种常见的通信方案:props、$emit、ref、EventBus、$parent或$root、vuex、attires、与listeners、provide、inject
props/$emit/ref/$parents/$children适用于父子组件通信ref如果在普通dom上使用、引用指向就是dom元素,如果在子组件上使用,引用指向就是组件实例$parents/$children:访问父/子组件实例EventBus($emit/$on):通过一个空的vue实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信vuex:复杂组件之间通信。vuex作用相当于一个用来存储共享变量的容器provide和inject用来实现非父子组件间的通信。provide可以在一个组件中提供一个值,而inject可以在另一个组件中注入这个值$root:子组件可以通过$root属性访问根组件,但不能直接改变根组件的数据。
19、vue中给对象添加新属性,页面不刷新?
在Vue.js中给对象添加新属性并不会立即刷新页面,Vue是通过object.defineProperty实现数据响应式,当我们访问或修改已有的对象属性值时都能触发getter或setter。但当我们为对象添加新属性或删除时无法触发事件属性的拦截,因为一开始此obj属性就被设置成了响应式数据。而给此对象添加新的属性时,这个属性不会通过Object.defineProperty设置成响应式数据
解决:使用Vue.set( target, propertyName/index, value )或$set()方法显式的告诉Vue.js这个属性需要被追踪
20、如何理解vue单项数据流?
指数据一般从父组件传给子组件,子组件没有权利直接修改父组件传来的数据,即子组件从props中直接获取的数据,只能请求父组件修改数据在传给子组件
父组件发生更新时,子组件中所有的prop都会刷新为最新的值,子组件想修改时只能通过$emit派发一个自定义事件,父组件接收后,由父组件修改
有两种常见的试图改变一个prop的情形:
-
这个
prop用来传递一个初始值;这个子组件希望将其作为一个本地的prop数据来使用;在这种情况下,最好定义一个本地的data属性并将这个prop用作其初始值 -
这个
prop以一种原始的值传入且需要进行转换,这种情况下,最好使用这个prop的值来定义一个计算属性
21、父组件和子组件生命周期钩子函数执行顺序
vue的父组件和子组件生命周期钩子函数执行顺序可以归类为以下四部分
- 加载渲染过程:
父
beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted - 子组件更新过程:父
beforeUpdate->子beforeUpdate->子updated->父updated - 父组件更新过程:父
beforeUpdate->父updated - 销毁过程:父
beforeDestroy->子beforeDestroy->子destroyed->父destroyed
22、v-model原理
v-model本质上是语法糖,它简化了在表单控件(如 input、select、textarea)上双向绑定数据的过程,v-model实际上是将v-bind和v-on两个指令组合在一起,实现了数据的双向绑定。
在vue2中,分别使用v-bind:value和v-on:input或v-bind:select和v-on:change来实现双向绑定,v-model在内部为不同的输入元素使用不同的属性并抛出不同的事件,:
input和textarea元素使用value属性和input事件checkbox和redio使用checked属性和change事件select使用value属性和change事件
以input表单元素为例:
<input v-model='xx'>
相当于
<input v-bind:value='xx' v-on:input='xx=$event.target.value'>
$event指代当前触发的事件对象,$event.target指代当前触发的事件对象的dom;
$event.target.value就是当前dom的value值
其核心就是,一方面model层通过defineProperty来劫持每个属性,一旦监听到变化通过相关的页面元素更新。另一方面通过编译模版文件,为控件的v-model绑定input事件,从而页面输入能实时更新相关data属性值
23、单项数据流和双向数据流
数据流是指数据在应用程序中流动的方式。数据流有两种主要类型:单项数据流和双向数据流。
- 单项数据流:单项数据流是指数据只能从父组件传递给子组件,而不能从子组件传递回父组件。这意味着子组件只能读取父组件提供的数据,而不能改变这些数据。这种数据流方式可以避免子组件之间的相互影响,提高数据的一致性和可靠性。
- 双向数据流:双向数据流是指数据可以从父组件传递给子组件,也可以从子组件传递回父组件。这意味着子组件不仅可以读取父组件提供的数据,还可以改变这些数据。这种数据流方式可以实现数据的实时更新和交互,但可能会导致数据的混乱和不一致性。
在React中,数据流通常是单项的。React使用 props 来传递数据,子组件只能读取 props ,而不能改变它们。这种设计可以确保数据的一致性和可靠性,避免子组件之间的相互影响。
在Vue中,数据流默认也是单项的。Vue使用props来传递数据,子组件只能读取props,而不能改变它们。但是,Vue也提供了一些机制来实现双向数据流,例如v-model指令。v-model指令可以将子组件的输入元素与父组件的数据绑定起来,实现双向数据流。
24、vuex中重要核心属性有哪些
Vuex是一个状态管理器用来存放一些需要跨组件共享的数据
State:存放数据mutations:用于同步修改state中的数据,且只能通过提交commit方法来触发actions:异步修改数据,但是会通过commit方法触发mutations来改变数据getters:数据过滤器,类似于 Vue 的计算属性,可以根据state中的数据进行计算,返回一个新的数据对象modules:用于组织和拆分状态。当数据过多和复杂时,将数据拆分成模块,在往文件中用modules引用
25、vue-router的路由模式有几种
vue-router有三种路由模式:hash、history、abstract
- 哈希模式(
hash):默认情况下,Vue Router使用hash模式。此模式下,URL中的哈希值(#)后面的内容会被用来匹配路由(例如:http://example.com/#/home)。此模式支持所有浏览器。 - 历史模式(
history):URL中的路径部分会被用来匹配路由(例如:http://example.com/home中/home部分用来匹配路由)。此模式优点是URL更加美观。但需要服务器端配合设置,以支持HTML5 History API。更适合现代浏览器环境 - 抽象模式(
abstract):此模式下,Vue Router不依赖于具体的URL结构,而是使用一个抽象的路径来匹配路由。支持所有JavaScript运行环境,如Node.js服务器端,如果发现没有浏览器的API,路由强制进入这个模式
26、vue-router中常用的hash和history路由模式实现原理
- hash模式的实现原理
Vue Router默认使用hash模式。早期的前端路由的实现就是基于location.hash来实现的。location.hash的值就是URL中#后面的内容
hash路由模式的实现主要基于下面几个特性:
URL中hash值只是客户端的一种个状态,也就是说当向服务器发出请求时,hash部分不会被发送;hash值的改变,都会在浏览器中的访问历史中增加一个记录。因此我们能通过浏览器的前进和后退控制hash的切换- 可以通过
a标签,并设置href属性,当用户点击这个标签后,URL的hash值会发生改变;或者使用JavaScript来对location.hash进行赋值,改变URL的hash值; - 我们可以使用
hashchange事件来监听hash值的变化,从而对页面进行跳转(渲染)
history模式的实现原理:HTML5提供了history API来实现URL的变化。其中最主要的API有两个:history.pushState()和history.replaceState().这两个API可以在不进行刷新的情况下,操作浏览器的历史记录。前者是新增历史记录,后者是直接替换当前的历史记录
history路由模式的实现主要基于下面几个特性:
pushState和replaceState两个API来操作实现URL的变化- 可以使用
popstate事件来监听URL的变化,从而对页面进行跳转(渲染) history.pushState()或history.replaceState()不会触发popstate事件,这时我们需要手动触发页面跳转(渲染)
27、如何理解单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点,这样的模式就叫做单例模式。
一般情况下,当我们创建了一个类(本质是构造函数)后,可以通过new关键字调用构造函数进而生成任意多的实例对象。而单例模式想要做到的事,不管我们尝试去创建多少次,它都只返回第一次所创建的那唯一的一个实例(要做到这一点,就需要构造函数具备判断自己是否已经创建过一个实例的能力。案例:Vuex中store是一个‘假单例’)
单例模式的主要优点是:
- 全局访问点:通过全局访问点,可以方便地在程序的任何地方获取到单例对象,无需关心它的创建过程。
- 资源节约:由于只存在一个实例,可以避免资源的浪费,例如内存、数据库连接等。
- 性能优化:由于只存在一个实例,可以减少不必要的对象创建和销毁过程,提高程序的运行效率。
然而,单例模式也有一些缺点,例如:
- 难以扩展:由于单例模式限制了类的实例化,因此很难在不破坏原有代码的情况下扩展类的功能。
- 难以测试:由于单例模式限制了类的实例化,因此很难在单元测试中模拟不同的实例行为。
28、vue2和vue3的区别
- 组合式API和选项式API:
- 在vue2中采用选项式API,将数据和函数集中起来处理,将功能点切割了,当逻辑复杂的时候不利于代码阅读和维护。
- 在vue3中采用组合式API,将同一功能的代码集中起来处理,使得代码更加有序利于阅读和维护,同时也支持选项式API
- 生命周期的变化:
- 组件实例初始化前:beforeCreate->使用setup()
- 初始化完成:created->使用setup
- 挂载前:beforeMount->onBeforeMount
- 挂载后:mounted->onMounted
- 更新前:beforeUpdate->onBeforeUpdate
- 更新后:updated->onUpdated
- 销毁前:beforeDestroy->onBeforeUnmount
- 销毁后:destroyed->onUnmounted
- 异常捕获:errorCaptured->onErrorCaptured
- keep-alive缓存的组件被激活时:activate->onActivated
- keep-alive缓存的组件被切换时:deactivated->onDeactivated
- v-if和v-for的优先级:
- vue2中v-for的优先级高于v-if,可以放在一起使用但不建议,会带来性能上的浪费。
- vue3中v-if的优先级高于v-for,一起使用会报错。可以通过在外部添加标签解决
- diff算法不同:
- vue2的diff算法:使用的是双向指针的算法来比较新旧虚拟DOM树,通过同时从头和尾部遍历两个子节点列表,寻找可复用的节点(遍历每个虚拟节点进行对比,并返回一个patch对象用来存储两个节点不同的地方。用patch记录的消息去更新dom。)
- vue3的diff算法:采用单向链表的方式,并结合了更高效的‘Map’数据结构来优化key值比较,提高了查找和匹配的效率。引入了‘静态提升’的概念,编译时会识别出不会变化的静态节点并将其从diff过程中移除,避免运行时比较静态节点,而且对带有key的节点进行源码缓存,更有效的追踪动态节点的变化,仅对实际变化的节点进行diff.从而减少计算负担(在初始化的时候会给每个虚拟节点添加一个patchFlages,是一种优化的标识,只会比较patchFlage发生变化的节点,进行视图更新。没有变化的元素作为静态标记,在渲染时直接复用)
- 综上所述:vue3的diff算法在设计上更注重性能优化,通过多种策略减少了比较和计算操作,使得组件更新更加高效
- 响应式原理不同:
- vue2:通过Object.defineProperty()的get()和set()来做数据劫持、结合发布订阅者模式来实现。Object.definedProperty()会遍历每个属性,并添加getter(读取)和setter(设置)方法来劫持属性,但不能监听数组索引的变化和对象属性的添加/删除。解决这个问题可以用Vue.set(或this.delete),用来显式的添加或删除属性
- vue3:通过使用Proxy监听整个对象,那么整个对象的所有操作都会进入监听操作。可以理解为在目标对象之前架设一层“拦截”,外界对该对象的访问都必须通过这一层拦截。不需要遍历会自动监听所有属性,有利于性能的提升
- 更好支持TypeScript:Vue 3 的代码库已经全面采用 TypeScript 重写,提供了更好的类型推断和类型提示。
- 更小的体积:Vue的运行时核心相比Vue2更小,且更容易被Tree-shaking优化,更好的剔除不需要的代码,意味着更小的打包体积,减少了前端加载时间。
- 3、模板改进: Vue 3 支持模板中的新特性,如 Fragments,使得组件可以有多个根节点,提高了灵活性。 引入 、 等新组件,分别用于在DOM的不同位置插入内容和延迟渲染直到异步数据加载完成。
29、MVC和MVVM区别
MVC(Model View Controller)由Model,View,Controller三部分构成
Model:数据模型层,是应用程序中用于处理数据逻辑的部分,通常模型对象负责在数据库中存取数据View:视图层,用户界面显示,通常视图是根据模型数据创建的Controller:控制器,是应用程序中处理用户交互的部分,控制器负责从视图读取数据,控制用户输入,完成业务逻辑后将数据发送给模型,然后Model将新的数据发送到View,用户得到反馈
MVC所有通信都是单向的。
MVVM(Model View ViewModel)由 Model,View,ViewModel三部分构成
Model:数据模型层,负责处理数据和业务逻辑与服务器端进行交互View:视图层,应用的展示效果,各类UI组件,简单理解就是HTML页面ViewModel:视图模型层,通过双向数据绑定来连接模型层和视图层,是他们之间的通信桥梁。当模型层的数据发生改变时,ViewModel会自动更新视图层,反之亦然。
MVC和MVVM都是一种设计思想。最大的区别是:MVVM实现了View和Model的自动同步。而MVC需要手动编写代码来同步模型和视图层