# 前端面经
温故而知新,可以为师矣。——《论语・为政》
# js 部分
# 1.var、let、const 之间的区别
作用域
var
声明的变量的作用域是函数作用域或全局作用域的。如果在全局环境中声明,它将成为全局变量;如果在函数内部声明,它将成为该函数的局部变量。let
声明的变量的作用域是块级作用域。let
声明的变量只在它声明的块(比如{}
大括号内部)中可见。const
声明常量,也具有块级作用域,类似于let
。
var
允许在同一作用域内重复声明同一个变量,而 let
不允许。
var
声明的变量存在变量提升,可以在声明之前使用。let 和 const 没有传统意义上的变量提升,存在于暂时性死区,在声明变量之前访问会出现 ReferenceError
。
补充
块级作用域
块级作用域(Block Scope)是指在特定的代码块(如 {}
之间)内部定义的变量、函数或常量,只在这个代码块内部可访问和使用,在代码块外部则不可访问。
变量提升
所有的变量声明(使用 var
)和函数声明(使用 function
)都会被提升到它们所在的作用域顶部。这就使得即使在声明之前访问这些变量或调用函数,也不会抛出错误。
# 2.=== 和 == 有什么区别
-
==
运算符会在比较前先进行类型转换(type coercion),然后再进行比较。注意:
null
和undefined
在==
的判断下,它们相等,但不等于任何其他值。如果两个值类型不同,JavaScript 会尝试将它们转换成相同的类型,然后再进行比较。
-
===
运算符不会进行类型转换,只会在两个值和类型相同且值相等时才返回true
。
== 的转换规则
- 如果两个值的类型相同:直接比较它们的值。
- 如果其中一个值是 null,另一个是 undefined:
null == undefined
结果为true
。 - 如果一个值是数字,一个是字符串:将字符串转换为数字,然后进行比较。
- 如果一个值是布尔值:将布尔值转换为数字(
true
转换为1
,false
转换为0
),然后进行比较。 - 如果其中一个是对象,另一个是原始值(数字、字符串或布尔值):将对象转换为其原始值,再进行比较。通常对象会通过
valueOf()
或toString()
方法转换为字符串或数字。
其他情况:返回 false
。
# 3.js 中的数据类型
分为基本类型和引用类型,区别在于存储位置不同
基本类型 7 种 3+2+2
- String
- Number
- Boolean
- Undefined
- Null
- Bigint
- Symbol
引用类型
Object,Array,function 等
存储位置
- 基本类型数据保存在在栈内存中。
- 引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中。
转 boolean 为空的值
0,false,' ',Nan,undefined,null
关于 Symbol
Symbol
用于生成唯一的标识符。可以用于 **“隐藏” 对象属性 **。JavaScript 使用了许多系统 symbol,这些 symbol 可以作为 Symbol.*
访问。我们可以使用它们来改变一些内建行为。
# 4. 数组常用的方法
# 静态方法
Array.isArray()
- 用于检查一个值是否是数组。
Array.from()
- 用于将类数组对象或可迭代对象转换为数组。
Array.of()
- 用于根据传入的参数创建一个新的数组。
# 基本操作
增
push()
push()
方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度。
unshift()
unshift()
在数组开头添加任意多个值,然后返回新的数组长度。
splice(start,deleteCount,item1, item2, ...)
参数
- start:必选。指定修改开始的位置(从 0 开始计数)。如果
start
大于数组的长度,则从数组末尾开始添加内容。如果是负数,则表示从数组末尾开始的第start
个位置。 - deleteCount:可选。整数,表示要移除的数组元素的个数。如果
deleteCount
为 0,则不移除元素。这种情况下,至少应添加一个新元素。如果未指定deleteCount
,则从start
到数组末尾的所有元素都会被删除。 - item1, item2, ...:可选。要添加到数组中的新元素。如果未指定,则
splice()
只删除数组元素。
concat()
首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组
删
pop()
pop()
方法用于删除数组的最后一项,同时减少数组的 length
值,返回被删除的项
shift()
shift()
方法用于删除数组的第一项,同时减少数组的 length
值,返回被删除的项
splice()
见上,增部分有说明。传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组
slice(beginIndex, endIndex)
从数组中提取部分元素,返回一个新的数组,包含从 beginIndex
到 endIndex
(但不包括 endIndex)的所有元素。
改
splice()
见上,增部分有说明。传入三个参数,分别是开始位置,要删除元素的数量,要插入的任意多个元素,返回删除元素的数组,对原数组产生影响。
查
indexOf()
返回要查找的元素在数组中的位置,如果没找到则返回 -1
includes()
返回要查找的元素在数组中的位置,找到返回 true
,否则 false
find()
返回第一个匹配的元素
# 排序方法
reverse()
转数组中元素的顺序,即第一个元素变成最后一个,最后一个变成第一个。该方法会修改原数组
sort([compareFunction]);
对数组中的元素进行排序。可以设置比较方法
# 迭代方法
some()
对数组每一项都运行传入的测试函数,如果至少有 1 个元素返回 true ,则这个方法返回 true
every()
对数组每一项都运行传入的测试函数,如果所有元素都返回 true ,则这个方法返回 true
forEach()
对数组每一项都运行传入的函数,没有返回值
不返回任何值,即返回 undefined
。它只是对数组的每个元素执行一次回调函数,用于执行某种操作或副作用
不生成新数组,只是遍历数组,对每个元素执行操作
filter()
对数组每一项都运行传入的函数,函数返回 true
的项会组成数组之后返回
map()
对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组
会生成并返回一个新的数组,这个新数组中的元素是根据回调函数的返回值来创建的
reduce()
将数组中的所有元素通过指定的函数合并为一个单一的值。
补充
实现一个 reduce
array.reduce(callback(accumulator, currentValue, currentIndex, array), initialValue) |
Array.prototype.myReduce = function (callback, initialValue) { | |
let accumulator = initialValue; // 保存初始值 | |
let startIndex = 0; | |
// 如果没有提供初始值,则将数组的第一个元素作为初始值 | |
if (initialValue === undefined) { | |
accumulator = this[0]; | |
startIndex = 1; | |
} | |
for (let i = startIndex; i < this.length; i++) { | |
accumulator = callback(accumulator, this[i], i, this); | |
} | |
return accumulator; | |
}; |
# 5.for...in 和 for..of 的区别
-
for...in
-
用途:遍历对象的可枚举属性(包括继承的可枚举属性)。
-
遍历的内容:遍历对象的键名包括数组的索引(但是如果数组添加了自定义属性或方法也会被遍历,所以不推荐遍历数组)。
-
-
for...of
- 用途:遍历可迭代对象(如数组、字符串、Map、Set 等)。
- 遍历的内容:遍历对象的元素值(对于数组,遍历元素本身)。
如果需要遍历对象的属性,使用 for...in
;如果需要遍历数组或其他可迭代对象的元素,使用 for...of
。
为什么有的可以被遍历为什么有的不行
for in 对象上没有 原型链上有? 可枚举?迭代器协议
# 6. 深拷贝和浅拷贝区别
浅拷贝
浅拷贝只复制对象或数组的第一层属性,如果属性是一个对象或数组,浅拷贝只会复制它们的引用,而不会复制它们的内容。因此,修改浅拷贝对象中的嵌套对象或数组时,原对象中的相应部分也会受到影响。
Object.assign(target, ...sources)
用于将一个或多个源对象的所有可枚举属性复制到目标对象中。这个方法返回目标对象。
Array.slice(start, end)
方法用于从一个数组中返回一个浅拷贝的数组片段,包含从起始索引到结束索引(不包括结束索引)之间的所有元素。
Array.prototype.concat()
方法用于合并两个或多个数组。
- 使用拓展运算符 (...) 实现的复制
深拷贝
深拷贝会递归复制对象或数组的所有层级,包括嵌套的对象和数组。这样,深拷贝对象中的任何修改都不会影响原对象。
JSON.parse()和JSON.stringify()
JSON.stringify()
方法将一个 JavaScript 对象或值转换为一个 JSON 字符串。序列化。
JSON.parse()
方法将一个 JSON 字符串解析为一个 JavaScript 对象或值。反序列化。
!延伸:JSON 实现深拷贝有什么缺点
有丢失函数日期 undefined 等问题,无法解决循环引用,原型链丢失。
-
Lodash库的_.cloneDeep方法
-
循环递归
function deepClone(obj, hash = new WeakMap()) { | |
// 处理基本类型和 null | |
if (obj === null || typeof obj !== "object") return obj; | |
// 处理日期对象 | |
//if (obj instanceof Date) return new Date(obj); | |
// 处理正则对象 | |
//if (obj instanceof RegExp) return new RegExp(obj); | |
// 处理循环引用 | |
if (hash.get(obj)) return hash.get(obj); | |
// 创建与原对象相同类型的新对象 | |
let cloneObj = Array.isArray(obj) ? [] : {}; | |
// 将新对象存入 hash 表 | |
hash.set(obj, cloneObj); | |
// 递归拷贝对象的所有自身属性 | |
for (let key in obj) { | |
if (obj.hasOwnProperty(key)) { | |
cloneObj[key] = deepClone(obj[key], hash); | |
} | |
} | |
return cloneObj; | |
} |
# 7.typeof 和 instanceof 区别
typeof
与 instanceof
都是判断数据类型的方法,区别如下:
typeof
会返回一个变量的基本类型的字符串,instanceof
返回的是一个布尔值typeof
可以判断基础数据类型,但是判断 null 会返回 "object",但是引用数据类型中,除了function
类型以外,其他的也无法判断instanceof
依赖于原型链判断,可以判断复杂引用数据类型,但是不能正确判断基础数据类型。
延伸
判断数据类型的方法
typeof
instanceof
Object.prototype.toString.call()
constructor
(Tips:有被修改的可能)
# 8. 闭包
闭包是一个函数以及其捆绑的周边环境状态的引用的组合。闭包让你可以在一个内层函数中访问到其外层函数的作用域。
用处
创建私有变量:闭包可以创建私有变量和私有方法,实现数据的封装和隐藏。
延长变量生命周期:闭包使得函数内部的变量在函数执行完后仍然存在,可以在函数外部继续使用。
柯里化函数:将接受多个参数的函数转换为一系列每次只接收一个参数的函数。
模拟私有方法:闭包可以用来实现私有方法的模拟,隐藏一些不希望外部访问的函数或变量。
缺点
内存占用:闭包会导致外部函数的变量无法被垃圾回收,从而增加内存占用。如果滥用闭包,会导致内存泄漏问题。
# 9.this 对象
this
关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象。
在绝大多数情况下,函数的调用方式决定了 this
的值(运行时绑定)。
- 默认绑定
在全局作用域中(不在任何函数内), this
指向全局对象。在浏览器中,这个全局对象是 window
。严格模式下,不能将全局对象用于默认绑定,this 会绑定到 undefined
。
- 隐式绑定
函数还可以作为某个对象的方法调用,这时 this
指向这个上级对象。
- new 绑定
通过构建函数 new
关键字生成一个实例对象,此时 this
指向这个实例对象。
- 显示绑定
apply()、call()、bind()
是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时 this
指的就是这第一个参数。
new 绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级
注意事项
创建对象不用 new 的时候,如下所示,this 会指向 window。
function Person(name) { | |
this.name = name; //this 指向 window | |
} | |
const person = Person('John'); // 非严格模式下,this 指向 window | |
console.log(person); // 输出 undefined | |
console.log(window.name); // 输出 'John' |
# 10. 事件
# 事件和事件流
javascript
中的事件,可以理解就是在 HTML
文档或者浏览器中发生的一种交互操作,使得网页具备互动性, 常见的有加载事件、鼠标事件、自定义事件等。
事件流都会经历三个阶段:
- 捕获阶段 (capture phase)
在事件捕获阶段,事件从文档的根(通常是 window
或 document
)开始向下传播,直到到达事件目标元素。在这个过程中,浏览器会检查每个祖先元素是否有注册捕获事件处理器。如果有,事件处理器会被调用。
- 目标阶段 (target phase)
事件目标阶段是事件到达目标元素的阶段。在这个阶段,事件直接在目标元素上发生。目标元素上的事件处理器会在这个阶段被调用。
- 冒泡阶段 (bubbling phase)
在事件冒泡阶段,事件从目标元素开始向上传播,直到到达文档的根。在这个过程中,浏览器会检查每个祖先元素是否有注册冒泡事件处理器。如果有,事件处理器会被调用。
事件冒泡是由具体的节点到不太具体的节点。事件捕获与事件冒泡相反,事件最开始由不太具体的节点最早接受事件,而最具体的节点(触发节点)最后接受事件。
# 事件模型
- 原始事件模型(DOM0 级)
添加:直接在 HTML 元素上设置事件属性,或者通过 JavaScript 设置。
移除:将事件处理程序设置为 null。
特点:绑定速度快,只支持冒泡,不支持捕获,同一个类型的事件只能绑定一次。
- 标准事件模型(DOM2 级)
添加:使用 addEventListener
方法。
移除:使用 removeEventListener
方法。
特点:可以在一个 DOM
元素上绑定多个事件处理器,各自并不会冲突。当第三个参数 ( useCapture
) 设置为 true
就在捕获过程中执行,反之在冒泡过程中执行处理函数。
- IE 事件模型(基本不用)
添加:使用 attachEvent
方法。
移除:使用 detachEvent
方法。
# 11. 事件代理
利用事件冒泡机制,将事件处理器添加到父元素上,而不是每个子元素上。这样可以提高性能,减少内存占用,特别是在处理大量动态生成的元素时。事件冒泡机制是指当一个元素上的事件被触发时,该事件会沿着 DOM 树从事件目标(触发事件的元素)向上冒泡,直到文档的根节点。利用这一特性,事件代理可以将事件处理器添加到一个共同的祖先元素上,由这个祖先元素来处理其所有子元素的事件。
适合事件委托的事件有: click
, mousedown
, mouseup
, keydown
, keyup
, keypress
从上面应用场景中,我们就可以看到使用事件委托存在两大优点:
- 减少整个页面所需的内存,提升整体性能
- 动态绑定,减少重复工作
但是使用事件委托也是存在局限性:
focus
、blur
这些事件没有事件冒泡机制,所以无法进行委托绑定事件mousemove
、mouseout
这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的
如果把所有事件都用事件代理,可能会出现事件误判,即本不该被触发的事件被绑定上了事件。
# 12.new 操作符具体干了什么
流程:我们可以看到 new
关键字主要做了以下的工作:
- 创建一个新对象,并将它的隐式原型 (_proto_) 指向构造函数的原型
- 将构建函数中的
this
绑定到新建的对象上。 - 根据构建函数返回类型作判断,如果是返回对象则正常处理,否则返回创建的新对象 obj。
模拟实现:
function myNew(constructor, ...args) { | |
// 1. 创建一个新对象,并它的原型设置为构造函数的原型 | |
const obj = Object.create(constructor.prototype); | |
// 2. 通过 apply 调用构造函数,并将 this 绑定到新对象上 | |
const result = constructor.apply(obj, args); | |
// 3. 如果构造函数返回一个对象类型,返回该对象,否则返回创建的新对象 | |
return result instanceof Object ? result : obj; | |
} |
# 13.ajax 部分
实现过程
- 创建
Ajax
的核心对象XMLHttpRequest
对象 - 通过
XMLHttpRequest
对象的open()
方法与服务端建立连接 - 构建请求所需的数据内容,并通过
XMLHttpRequest
对象的send()
方法发送给服务器端 - 通过
XMLHttpRequest
对象提供的onreadystatechange
事件监听服务器端的通信状态 - 接受并处理服务端向客户端响应的数据结果
- 将处理结果更新到
HTML
页面中
使用举例
const request = new XMLHttpRequest() | |
request.onreadystatechange = function(e){ | |
if(request.readyState === 4){ // 整个请求过程完毕 | |
if(request.status >= 200 && request.status <= 300){ | |
console.log(request.responseText) // 服务端返回的结果 | |
}else if(request.status >=400){ | |
console.log("错误信息:" + request.status) | |
} | |
} | |
} | |
request.open('POST','http://xxxx') | |
request.send() |
# 14.bind,call,apply 的作用和区别
作用:用于改变函数内部 this
指向的方法。
apply
apply
接受两个参数,第一个参数是 this
的指向,第二个参数是函数接受的参数,以数组的形式传入。
改变 this
指向后原函数会立即执行,且此 法只是临时改变 this
指向一次。
call
call
方法的第一个参数也是 this
的指向,后面传入的是一个参数列表。
跟 apply
一样,改变 this
指向后原函数会立即执行,且此方法只是临时改变 this
指向一次。
bind
bind 方法和 call 很相似,第一参数也是 this
的指向,后面传入的也是一个参数列表 (但是这个参数列表可以分多次传入,同样在实现的时候传入参数要采用... 展开操作符)
改变 this
指向后不会立即执行,而是返回一个永久改变 this 指向的函数。
区别:
- 三者都可以改变函数的
this
对象指向。 - 三者第一个参数都是
this
要指向的对象,如果如果没有这个参数或参数为undefined
或null
,则默认指向全局window
。 - 三者都可以传参,但是 apply 传递是数组,而 call 传递的是参数列表,且
apply
和call
是一次性传入参数,而 bind 可以分为多次传入。 bind
是返回绑定 this 之后的函数,apply
、call
则是立即执行。
实现一个 bind
// 实现一个 bind | |
Function.prototype.myBind = function(context, ...args) { | |
if(typeof this !== 'function'){ | |
throw new TypeError('Error'); | |
} | |
const fn = this; // 原函数 | |
function boundFunction(...newArgs) { | |
// 判断是否通过 new 调用 | |
if (this instanceof boundFunction) { | |
// 通过 new 调用,忽略绑定的 context,使用新创建的对象作为 this | |
return new fn(...args, ...newArgs); | |
} | |
// 作为普通函数调用,使用绑定的 context 作为 this | |
return fn.apply(context, [...args, ...newArgs]); | |
} | |
// 保留原函数的原型,以确保通过 new 调用时的原型链正确 | |
boundFunction.prototype = Object.create(fn.prototype); | |
return boundFunction; | |
}; |
# 15. 事件循环
JavaScript 的事件循环(Event Loop)是 JavaScript 运行时处理异步代码的机制。它使得 JavaScript 能够在单线程环境中执行异步操作而不会阻塞主线程。
主要涉及到调用栈、任务队列。
调用栈
调用栈是一个栈结构,负责管理函数的调用顺序。当一个函数被调用时,它会被压入栈顶;当函数执行完毕后,它会从栈顶弹出。JavaScript 运行时会从调用栈顶依次执行函数。
任务队列
宏任务队列
存放宏任务,宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合。常见的有:
- script (可以理解为外层同步代码)
- setTimeout/setInterval
- UI rendering/UI 事件
- postMessage、MessageChannel
- setImmediate、I/O(Node.js)
微任务队列
** 微任务则是一个需要异步执行的函数。** 常见的有:
- Promise.then
- MutaionObserver
事件循环的工作机制
- 执行调用栈中同步代码:事件循环会从调用栈顶开始依次执行任务,如遇到异步任务,则会注册回调函数放到对应的任务队列,直到调用栈为空。
- 检查微任务队列:当调用栈为空时,事件循环会检查微任务队列并依次执行所有的微任务,直到微任务队列为空。
- 执行宏任务:当调用栈和微任务队列都为空时,事件循环会从任务队列中取出一个宏任务并执行。
- 重复上述过程:事件循环会不断重复上述过程,确保 JavaScript 程序能够持续运行和响应异步操作。
# 16.DOM 操作
- 创建节点
createElement
创建新元素,接受一个参数,即要创建元素的标签名。
createTextNode
创建一个文本节点。
createAttribute
创建属性节点,可以是自定义属性。
- 获取节点
querySelector
传入任何有效的 css 选择器,即可选中单个 DOM 元素(首个)。
querySelectorAll
返回一个包含节点子树内所有与之相匹配的 Element 节点列表,如果没有相匹配的,则返回一个空节点列表。返回的是一个 NodeList
的静态实例。
- 更新节点
innerHTML
不但可以修改一个 DOM 节点的文本内容,还可以直接通过 HTML 片段修改 DOM 节点内部的子树。
innerText、textContent
自动对字符串进行 HTML 编码,保证无法设置任何 HTML 标签。
style
element.style.property = 'value'
DOM 节点的 style 属性对应所有的 CSS,可以直接获取或设置。遇到 - 需要转化为驼峰命名
- 添加节点
innerHTML
如果这个 DOM 节点是空的,例如,<div></div>,那么,直接使用 innerHTML = '<span>child</span>' 就可以修改 DOM 节点的内容,相当于添加了新的 DOM 节点。如果这个 DOM 节点不是空的,innerHTML 会直接替换掉原来的所有子节点。
appendChild
把一个子节点添加到父节点的最后一个子节点。
insertBefore
把子节点插入到指定的位置。
- 删除节点
removeChild
删除一个节点,首先要获得该节点本身以及它的父节点,然后,调用父节点的 removeChild
把自己删掉。
# 17.Promise
Promise 是 JavaScript 中用于处理异步操作的一种机制。它代表了一个异步操作的最终结果,可能是成功,也可能是失败。
Promise 有三种状态:
- Pending(待定): 初始状态,操作尚未完成。
- Fulfilled(已兑现): 操作成功完成,并返回一个结果值。
- Rejected(已拒绝): 操作失败,并返回一个失败原因。
一个 resolved 或 rejected 的 promise 都会被称为 (已敲定)“settled”。
then
.then
的第一个参数是一个函数,该函数将在 promise resolved 且接收到结果后执行。
.then
的第二个参数也是一个函数,该函数将在 promise rejected 且接收到 error 信息后执行。
catch
.catch
方法是专门用来处理 Promise 失败的情况的。它等同于 then
的第二个参数, .catch(f)
是 .then(null, f)
的模拟,通常用于捕获整个 Promise 链中的错误。
finally
用于在 Promise 结束时(无论是 fulfilled
状态还是 rejected
状态)执行一个指定的回调函数。与 .then()
和 .catch()
不同, .finally()
不关心 Promise 的结果是成功还是失败,它只会在 Promise 完成后执行。
Promise API
promise.all
Promise.all
接受一个可迭代对象(通常是一个数组项为 promise 的数组),并返回一个新的 promise。
当所有给定的 promise 都 resolve 时,新的 promise 才会 resolve,并且其结果数组将成为新 promise 的结果。
如果任意一个 promise 被 reject,由 Promise.all
返回的 promise 就会立即 reject,并且带有的就是这个 error。
如果出现 error,其他 promise 将被忽略。虽然还会执行,但是 Promise.all
不会再关心它们。
promise.all 实现
function myPromiseAll(promiseArray) { | |
return new Promise((resolve, reject) => { | |
if (!Array.isArray(promiseArray)) { | |
return reject(new Error('传入参数不是一个数组')); | |
} | |
// 如果长度为 0 直接返回空 | |
if (promiseArray.length === 0) { | |
return resolve([]) | |
} | |
const result = []; | |
let completedPromises = 0; | |
promiseArray.forEach((item, index) => { | |
// 将非 Promise 的项转化为一个已经 resolved 的 Promise | |
Promise.resolve(item).then( | |
(value) => { | |
result[index] = value; | |
completedPromises++; | |
// 当所有 Promise 都完成时,resolve 整个数组 | |
if (completedPromises === promiseArray.length) { | |
resolve(result); | |
} | |
}).catch((error) => { | |
// 如果有任一 Promise 被 reject,整个 Promise.all 就被 reject | |
reject(error); | |
}) | |
}); | |
}) | |
} |
Promise.allSettled
接受一个 Promise 的可迭代对象,并返回一个新的 Promise,当所有传入的 Promise 都已完成(无论成功或失败)时处理。返回的数组中包含每个 Promise 的结果对象 {status: "fulfilled", value: ...}
或 {status: "rejected", reason: ...}
。
Promise.race
与 Promise.all
类似,但只等待第一个 settled 的 promise 并获取其结果(或 error)。
Promise.any
与 Promise.race
类似,区别在于 Promise.any
只等待第一个 fulfilled 的 promise,并将这个 fulfilled 的 promise 返回。如果给出的 promise 都 rejected,那么返回的 promise 会带有 [ AggregateError
] —— 一个特殊的 error 对象,在其 errors
属性中存储着所有 promise error。
Promise.resolve
使用给定 value 创建一个 resolved 的 promise。
Promise.reject
使用给定 error 创建一个 rejected 的 promise。
# 18. 原型和原型链
# 原型
原型是 js 继承机制的核心概念。每个对象都有一个原型对象。这个原型对象也是一个普通的对象,它可以包含其他属性和方法。
- 当使用构造函数创建一个对象时,该对象会继承构造函数的
prototype
属性。 - 可以通过实例对象的
__proto__
属性或Object.getPrototypeOf
方法来访问对象的原型。(当然不推荐直接使用_proto_
) - 原型对象有一个自有属性
constructor
,这个属性指向该构造函数。
# 原型链
原型链是由对象及其原型对象组成的链条。当访问一个对象的属性时,它不仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到匹配的属性或到达原型链的末尾。
- 大多数对象的原型链最终追溯到
Object.prototype
,而Object.prototype
的原型是null
。 - 所有函数对象包括构造函数是
Function
的实例,它们的__proto__
指向Function.prototype
。
# 19. 柯里化
柯里化是指封装一个函数,接收原始函数作为参数传入,并返回一个能够接收并处理剩余参数的函数。
优点:
代码复用:柯里化允许你创建更加灵活的函数。通过 ** 将函数分解为多个部分,你可以创建通用的函数,这些函数可以在不同的上下文中复用。** 例如,如果你有一个加法函数,你可以创建一个固定加数的函数,而不必重复编写相同的代码。
延迟计算:柯里化允许你 ** 延迟对函数的求值,直到你获得了所有需要的参数。** 这在处理一些异步操作或需要动态获取参数的情况下特别有用。
提前确认:避免每次重复判断。比如我们使用 addEventListener 来给元素绑定事件,然而在低版本的 ie 浏览器中只能使用 attachEvent ,这样我们在不确定何种浏览器运行情况下可以提前写一个函数在 js 一开始就执行,判断是否可以使用 addEventListener ,否则使用 attachEvent 。
缺点:
柯里化用到了 arguments 对象、递归、闭包等,频繁使用会给性能带来影响。
代码实现:参数传入形式与个数不确认,返回参数相加的值。
// 简单版 实现 add (1)(2)(3)() | |
function add(a) { | |
const sum = (b) => { | |
if (b === undefined) { | |
return a; | |
} | |
return add(a + b); | |
}; | |
return sum; | |
} | |
// 复杂版 实现 add (1)(2, 3)(5) | |
function add(...args) { | |
// 创建一个新的函数,用于接收更多参数 | |
const sumFn = (...nextArgs) => add(...args, ...nextArgs); | |
// 重写 toString 方法,返回当前参数的累加和 | |
sumFn.toString = () => { | |
return args.reduce((sum, num) => sum + num, 0); | |
}; | |
return sumFn; | |
} |
# 20.Proxy
Proxy
用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
用法
var proxy = new Proxy(target, handler) |
target
:表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler
:通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p
的行为。
使用场景
- 数据绑定和观察:在 MVVM 框架中,
Proxy
可以用于实现数据的双向绑定和变化的追踪。 - 对象的验证:你可以使用
Proxy
来验证对象的属性值是否符合某些规则。 - 增强函数行为:使用
Proxy
可以在调用函数时增加一些自定义的逻辑,比如日志记录、参数校验等。 - 防止对象的某些操作:通过拦截和修改操作,可以限制对对象的某些属性进行修改或访问。
详细见 https://zh.javascript.info/proxy
# 21. 防抖节流
# 防抖
防抖函数则是在指定时间后执行该事件,如果再次触发,则重置计时器,重新计时。
使用场景
搜索联想:搜索联想功能。
窗口调整事件:窗口调整完成后,计算窗口大小。防止重复渲染。
实现
// 防抖函数 将多次触发的操作合并成一次 | |
function debounce(func, wait) { | |
let timeout; | |
return function (...args) { | |
const context = this; // 显式保存 this 其实保存不保存问题不大 | |
clearTimeout(timeout); | |
timeout = setTimeout(() => func.apply(context, args), wait); | |
}; | |
} |
# 节流
节流,确保在一定时间内函数最多执行一次。即使在这个时间段内事件多次触发,函数也不会重复执行,直到时间段结束。
使用场景
滚动加载:加载更多或滚到底部监听。
按钮点击:按钮点击次数限制
实现
// 定时器写法 | |
function throttle(func, delay) { | |
let timer = null; | |
return function (...args) { | |
const context = this; | |
if (!timer) { | |
timer = setTimeout(() => { | |
func.apply(context, args); | |
timer = null; | |
}, delay); | |
} | |
}; | |
} | |
// 时间戳写法 | |
function throttle(fn, delay) { | |
let oldtime = Date.now() | |
return function (...args) { | |
let newtime = Date.now() | |
if (newtime - oldtime >= delay) { | |
fn.apply(null, args) | |
oldtime = Date.now() | |
} | |
} | |
} |
# 22.XHR 和 Fetch 区别
XMLHttpRequest (XHR)
定义:XHR 是一个较旧的 API,用于在浏览器中发起网络请求。它是 Fetch API 出现之前的标准。
特点:
- 基于事件的模型:
XHR
使用onreadystatechange
事件处理请求状态变化。 - 请求和响应处理较为复杂:
XHR
需要手动设置请求头和解析响应。 - 兼容性好:
XHR
支持较旧的浏览器。
Fetch
定义:Fetch API 是一个现代的、基于 Promise 的 API。
特点:
- 语法简洁,
Fetch
基于Promise
,代码更简洁明了,尤其是在结合async/await
使用时。 - 更灵活:
Fetch
的设计更灵活,可以处理更多种类的请求和响应。 - 流式处理:
Fetch
允许通过ReadableStream
流式处理响应体,适用于处理大文件或渐进式的加载。
# 23. 数组扁平化
const flatten = (arr) => { | |
let result = []; | |
arr.forEach(item => { | |
if (Array.isArray(item)) { | |
result = result.concat(flatten(item)); // 如果元素是数组,则递归 | |
} else { | |
result.push(item); // 如果不是数组,直接放入结果数组 | |
} | |
}); | |
return result; | |
}; | |
// 使用 reduce | |
const flattenArray = (arr) => { | |
return arr.reduce((acc, item) => { | |
return acc.concat(Array.isArray(item) ? flattenArray(item) : item); | |
}, []); | |
}; |
# 24. 箭头函数
ES6 引入的一种新的函数定义方式。
- 没有
this
箭头函数不会创建自己的 this
,它的 this
值是从外层上下文中继承来的。
- 没有
arguments
箭头函数没有自己的 arguments
对象,如果需要访问参数,可以使用剩余参数语法 ...args
。
- 不能作为构造函数
箭头函数不能使用 new
关键字来创建对象实例,因为它们没有 [[Construct]]
方法。
场景
箭头函数主要用于需要保持 this
上下文的场景,或者定义简洁的函数。
但对于需要 this
指向变化或需要 arguments
对象的函数来说,传统函数更适合。
# 25.async 和 await
async/await
就像是 Promise
的一种语法糖,能让异步处理代码更加清晰。
async
函数会自动返回一个 Promise
。
await
是 Promise.then()
的简化语法,它使代码看起来像是同步的,但实际是异步操作。
async
和 await
的结合使得异步代码的编写和读取更容易,更接近于同步代码的写法。
// 一个简单的转换例子 | |
async function fetchData() { | |
try { | |
const data1 = await asyncOperation1(); | |
const data2 = await asyncOperation2(data1); | |
return data2; | |
} catch (error) { | |
console.error(error); | |
} | |
} | |
// 等效转换 | |
function fetchData() { | |
return asyncOperation1() | |
.then(data1 => { | |
return asyncOperation2(data1); | |
}) | |
.then(data2 => { | |
return data2; | |
}) | |
.catch(error => { | |
console.error(error); | |
}); | |
} | |
总结: |
# 26.ES6 特性
let 和 const
箭头函数:=>
模版字符串:``
解构赋值
展开运算符和剩余参数:...
类:class
模块化:ES
Promise
# 27. 函数传参
原始类型:传值(函数内部的修改不会影响外部变量)。
对象类型:传引用(函数内部的修改会影响外部变量,但重新赋值操作仅影响函数内部的引用)。
注意!
即使传递的是对象的引用副本,如果在函数内部重新赋值给参数(例如 obj = {}),外部对象本身并不会受到影响,因为你只是改变了函数内部的引用,而不是外部对象的引用。
# 28. 垃圾回收
常见的垃圾回收算法
标记 - 清除算法
这是最常见的垃圾回收算法。标记 - 清除算法分为两个阶段:
- 标记阶段:从根对象(如全局对象 window、局部变量、函数调用栈等)开始,遍历所有引用的对象,并标记它们为 “可达”。
- 清除阶段: 检查堆中的所有对象,如果一个对象没有被标记为 “可达”,则视为垃圾,进行回收。
引用计数算法
引用计数算法会为每个对象维护一个引用计数器,计数器记录对象被引用的次数。每当有新的引用指向这个对象时,计数器加一;当引用被销毁时,计数器减一。当计数器变为零时,表示没有引用指向这个对象,GC 就会回收它。
然而,这种算法有一个致命的缺点:不能解决循环引用。
分代式垃圾回收(V8 引擎)
分代式垃圾回收将内存划分为两个区域:新生代(Young Generation)和老生代(Old Generation)。这一策略基于以下两个假设:
- 大多数对象是短命的:即大多数对象在创建后不久就会被回收。
- 幸存的对象可能会长期存在:即那些未被回收的对象可能会长期存活。
新生代
新生代主要用于存储新创建的对象。由于大多数对象生命周期很短,GC 会频繁地扫描和回收这个区域。新生代内存区域又可以进一步细分为两个部分:
- 使用区:新生对象最初被分配到这里。
- 空闲区:当对象在使用区空间存活下来后(即经历了一次垃圾回收),它们会被移到空闲区空间。清空原使用区随后空闲区变为新的使用区,原使用区变为空闲区。
老生代
那些在新生代中存活足够长时间的对象最终会被移到老生代。老生代区域的垃圾回收频率较低,但每次回收的时间较长,因为这个区域通常包含更多的对象,并且这些对象的生命周期更长
# 29. 堆与栈
栈(Stack)
- 存储内容:栈主要用于存储基本数据类型(如
number
,boolean
,string
,undefined
等)和函数调用上下文(包括函数参数、局部变量、返回地址等)。 - 内存分配:栈的内存分配是自动的,由系统来完成。当函数被调用时,栈中的空间自动为函数中的局部变量分配内存;函数执行完毕后,这些变量会自动释放内存。
- 访问速度快:栈内存是一块连续的内存区域,系统通过先进后出的方式管理它,分配和释放速度非常快。
- 空间有限:栈的大小通常比较小(相对于堆来说),其分配的内存是有限的。如果递归过深或局部变量占用过多内存,可能会导致栈溢出(stack overflow)。
堆(Heap)
- 存储内容:堆空间用于存储引用类型的数据(如对象、数组、函数等)。这些数据通常更复杂,占用的内存较多,因此存储在堆中。
- 内存分配:堆内存的分配是动态的。开发者可以根据需求在运行时动态地为对象分配内存。堆内存的管理相对复杂,需要依赖于垃圾回收机制(如 JavaScript 的垃圾回收器)来清理不再使用的对象。
- 访问速度较慢:由于堆是非连续的内存块,需要通过指针引用找到数据的位置,访问速度比栈慢一些。
- 空间较大:堆内存相较于栈内存而言,空间更大,也可以存储更复杂的数据结构。
拓展
为什么分栈和堆空间
内存管理效率:栈是自动管理的,分配和释放内存的效率很高。堆则是动态管理的,适合处理复杂数据类型,比如对象和数组,这些数据通常大小不固定,并且生命周期可能较长。
程序需求差异:栈适合局部性强的数据,比如函数的局部变量,参数,返回值等,调用结束即可释放。堆适合需要长期存储的数据,引用类型的数据往往需要在函数调用结束后继续存在或被其他函数共享。
内存利用率:栈空间有限,但高效。栈空间虽然小,栈的内存利用率很高,分配和释放都非常迅速。堆空间大,但灵活。堆的管理更加复杂且效率相对较低(因为涉及垃圾回收和动态分配),但它提供了更大的存储空间,适合存储需要长时间保留的数据。
方便垃圾回收和内存优化:栈的内存分配和回收是自动的、按顺序进行的,所以无需额外垃圾回收机制。堆则是动态分配的,程序员可以决定对象的生命周期 **,JS 的垃圾回收机制 ** 会自动检测不再使用的对象并释放堆内存。
# 30. 事件绑定
HTML 内联事件处理程序:直接在 HTML 中绑定,不推荐。
DOM 元素的事件属性:通过元素属性直接绑定,简单但有限制。
addEventListener
方法:推荐的现代方法,支持绑定多个处理程序和事件阶段控制。
前端框架特定的事件绑定方式:如 Vue 的 @event
,简化了事件绑定并更符合框架的开发模式。
# 31.Map 和 Set
区别
- 存储方式不同:
- Map:用于存储键值对(
key-value
)。其中每一个元素都是一个键值对,键可以是任何类型的数据(包括对象)。 - Set:只存储值,没有键。每个值都是唯一的,重复的值不会被存储。
- Map:用于存储键值对(
- 键的唯一性:
- Map:允许任何类型的键,且键可以重复(只存最后一次添加的键值对)。
- Set:所有的值都是唯一的,不能有重复的值。
- 访问方式:
- Map:可以使用键来获取对应的值(
map.get(key)
)。 - Set:只能判断值是否存在(
set.has(value)
),没有键值对的概念。
- Map:可以使用键来获取对应的值(
- 迭代方式:
- Map:迭代时返回键值对,可以用
map.entries()
来迭代出[key, value]
的形式。 - Set:迭代时只返回值,可以用
set.values()
来迭代出每个值。
- Map:迭代时返回键值对,可以用
共性
- 数据结构类型:它们都属于 ES6 引入的新的集合类数据结构,用于更高效的存储和操作数据。
- 元素顺序:两者都维护插入元素的顺序。迭代时,顺序按照元素插入的顺序进行。
- 支持迭代:
Map
和Set
都支持for...of
循环以及使用forEach()
方法来遍历元素。 - 大小属性:两者都有
size
属性,返回集合中的元素数量。
# 32.string 和 String 的区别
类型不同: string
是原始类型,而 String
是对象类型。
内存占用:原始的 string
占用的内存较小,而 String
对象由于是引用类型,会占用更多的内存。
属性与方法: String
对象可以拥有属性和方法,而原始的 string
类型没有属性,但是 JavaScript 会在需要的时候临时将 string
转换为 String
对象,以便访问方法(例如 length
属性或 toUpperCase()
方法)。
比较:使用 ==
比较时, String
对象和原始的 string
值相等,但使用 ===
时,它们不相等,因为类型不同。
# 33.Map 和 WeakMap 的区别
键的类型: Map
的键可以是任何类型,包括原始类型(如字符串、数字、布尔值)和对象。 WeakMap
的键只能是对象,不能是原始类型(如字符串、数字等)。键必须是对象或 null
。
垃圾回收:Map 中的键值对是强引用,键不会自动被回收。WeakMap 中的键是弱引用,键可以被垃圾回收。
迭代性:Map 可迭代( keys()
、 values()
等)。WeakMap 不可迭代。
# 34.Proxy 与 Object.definePrototype 区别
Object.definePrototype()
只能拦截某个特定属性的访问(读取、写入),无法拦截对象整体的操作。
不能拦截诸如属性删除、 in
操作符、 for...in
遍历等操作。
对象原本不存在的属性无法拦截,必须先定义该属性。
Object.defineProperty
只影响单个属性。所以在性能上会比 Proxy () 好。
Proxy()
可以拦截对象的几乎所有操作,包括但不限于属性读取、写入、删除、枚举、 in
检查、函数调用等。
可以拦截不存在的属性,也可以对整个对象进行监视和修改。
Proxy
是面向对象级别的。
# 35.null 和 undefined 的区别
undefined
表示 “未定义”,常用于表示变量还没有被赋值,或者一个属性不存在。
null
表示 “空值” 或 “没有对象”,通常用于有意地表明某个变量或对象为空。
类型:
typeof undefined
返回'undefined'
。typeof null
返回'object'
(对象类型的二进制表示是 000,而 null 也是全 0,实际上null
是原始类型)。
赋值:
undefined
一般由 JavaScript 引擎自动赋值,程序员很少显式地将变量设置为undefined
。null
通常是由程序员手动赋值,用于表示 “没有对象” 或 “空”。
# 36.js 继承
- 原型链继承
// 关键代码 | |
Child.prototype = new Parent(); // 子类的原型指向父类的实例 |
缺点:父类实例属性会被所有子类实例共享,修改一个子类实例的引用类型属性,其他子类实例的同属性也会受影响。
- 构造函数继承
// 关键代码 | |
function Child() { | |
Parent.call(this); // 调用父类构造函数 | |
} |
优点:可以解决原型链继承中引用类型被共享的问题。
缺点:父类的实例方法不能被子类共享(因为方法写在父类的原型上,而不是实例上)。
- 组合继承
function Parent(name) { | |
this.name = name; | |
} | |
function Child(name, age) { | |
Parent.call(this, name); // 第二次调用父类构造函数,继承父类的属性 | |
this.age = age; | |
} | |
Child.prototype = new Parent(); // 第一次调用父类构造函数,继承父类的方法 | |
Child.prototype.constructor = Child; // 修正子类的构造函数指向 |
缺点:组合继承的一个潜在问题是父类的构造函数会被调用两次。
- 寄生继承
// 寄生继承函数 | |
function createChild(parentObj) { | |
const clone = Object.create(parentObj); // 创建父类实例的一个浅拷贝 | |
clone.sayHi = function() { // 增强对象 | |
console.log('Hi'); | |
}; | |
return clone; | |
} |
缺点:与原型链继承类似,父类的引用类型属性会被多个实例共享,容易出现篡改属性的问题。方法不会被共享,增加内存开销。
- 组合寄生继承
// 子类构造函数 | |
function Student(name, age) { | |
Person.call(this, name); // 继承父类属性 | |
this.age = age; // 子类自己的属性 | |
} | |
// 设置子类原型,继承父类的方法 | |
Student.prototype = Object.create(Person.prototype); | |
// 确保子类的构造函数指向子类自身 | |
Student.prototype.constructor = Student; |
优点:避免多次调用父类构造函数,避免原型链上的副作用,通过 Object.create
,子类的原型是父类原型的一个浅拷贝,不会直接修改父类原型,从而避免了多个子类之间的相互影响。灵活性高。
# 37. 判断变量是否是数组
Array.prototype.isArray
Object.prototype.toString.call()
instanceof
# 38. 不同刷新网页缓存获取方式
- 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求;
- 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用 (如果匹配的话)。其次才是 disk cache;
- 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache (为了兼容,还带了 Pragma: no-cache), 服务器直接返回 200 和最新内容。
# css 部分
# 1.flex 1 和 flex auto 的区别
- flex:1 相当于 flex: 1 1 0%,表示项目的基准大小为 0%,不考虑项目本身的大小,只根据剩余空间进行伸缩。
- flex:auto 相当于 flex: 1 1 auto,表示项目的基准大小为 auto,即项目本身的大小,同时也会根据剩余空间进行伸缩。
flex:1
和flex:auto
的区别,可以归结于 flex-basis:0 和 flex-basis:auto 的区别- flex-grow 定义项目的放大比例,默认为 0,即如果存在剩余空间,也不放大。
- flex-shrink 定义了项目的缩小比例,默认为 1,即如果空间不足,该项目将缩小。
- flex-basis 定义在分配多余空间之前,项目占据的主轴空间(main size),浏览器根据此属性计算主轴是否有多余空间,默认值为 auto ,即项目本身的大小。
区别
- 如果容器有足够的空间,flex:1 和 flex:auto 都会平分剩余空间,但是 flex:auto 会保持项目本身的最小宽度,而 flex:1 不会。
- 如果容器没有足够的空间,flex:1 会优先压缩内容,使得所有项目都能等分空间,而 flex:auto 会优先保持内容的完整性,挤压其他项目的空间。
# 2. 样式优先级
- 使用
!important
声明的样式具有最高优先级。它会覆盖其他所有声明,包括内联样式。
选择器的优先级可以通过以下公式计算: 优先级 = (a, b, c, d)
,其中:
a
是内联样式的数量(如果有,则为1
,否则为0
)
直接在 HTML 元素的 style
属性中写的样式。例如 <div style="color: red;">
。
b
是 ID 选择器的数量
使用 #
符号选择元素,例如 #myId
。
c
是类选择器、属性选择器和伪类选择器的数量
使用 .
选择类,例如 .myClass
。属性选择器,例如 a[target="_blank"]
。伪类选择器,例如 :hover
。
d
是元素选择器和伪元素选择器的数量
直接选择 HTML 标签,例如 div
、 p
。使用伪元素,例如 ::before
、 ::after
。
大致可以理解为内联样式 > ID 选择器 > 类选择器 > 元素选择器
当优先级相同时,最后定义的样式规则会覆盖之前的规则。
# 3. 盒子模型
一个盒子由四个部分组成: content
、 padding
、 border
、 margin
。
content
,即实际内容,显示文本和图像
padding
,即内边距,清除内容周围的区域,内边距是透明的,取值不能为负,受盒子的 background
属性影响
boreder
,即边框,围绕元素内容的内边距的一条或多条线,由粗细、样式、颜色三部分组成
margin
,即外边距,在元素外创建额外的空白,空白通常指不能放其他元素的区域
在标准盒模型中,元素的宽度和高度只包括内容(content)的大小,不包括内边距(padding)、边框(border)和外边距(margin)。这是 CSS 的默认盒模型。
在 IE 盒模型中,元素的宽度和高度包括内容(content)、内边距(padding)和边框(border)的大小。不包括外边距(margin)。这种模型在布局时更容易控制元素的实际尺寸。
可以使用 box-sizing
属性来设置元素使用哪种盒模型:
box-sizing: content-box;
(默认)box-sizing: border-box;
# 4.css 选择器
基础选择器
- 元素选择器(
p
),选择所有p
标签的元素。 - 类选择器(
.classname
),选择类名为classname
的所有元素。 - ID 选择器(
#unique-id
),选择 ID 为unique-id
的元素。 - 属性选择器(
input[type="text"]
),选择所有type
属性为text
的input
标签。
分组选择器
- 群组选择器(
div, p
),选择所有div
和p
的元素。
组合选择器
- 后代选择器(
div p
),选择所有div
内的p
标签的元素。 - 子选择器(
ul > li
),选择ul
的所有直接子元素li
。 - 相邻兄弟选择器(
h1 + p
),选择紧跟在h1
后面的第一个p
元素。 - 通用兄弟选择器(
h1 ~ p
),选择所有紧跟在h1
后面的p
元素。
伪选择器
- 伪类(
a:hover
),选择鼠标悬停时的链接元素。 - 伪元素(
p::first-line
),选择p
标签的第一行元素。
# 5.px/em/rem/vh/vw 区别
px 就是像素,绝对单位, px
的大小和元素的其他属性无关。
em
- 相对单位:相对于当前元素的字体大小。
- 继承关系:嵌套元素的大小会继承其父元素的大小。
- 应用示例:如果一个元素的字体大小是
16px
,而你设置其子元素的字体大小为2em
,那么这个子元素的字体大小将是32px
。 - 注意事项:嵌套使用时,
em
的计算是累积的,可能导致意想不到的结果。
rem
-
相对单位:相对于 ** 根元素(
<html>
)** 的字体大小。 -
继承关系:所有使用
rem
的元素都只相对于根元素,不受嵌套影响。 -
应用示例:如果根元素的字体大小是
16px
,而你设置一个元素的字体大小为2rem
,那么这个元素的字体大小将是32px
。
vh
- 定义:
vh
表示视口高度的 1%。视口高度是指浏览器可视窗口的高度。 - 使用场景:通常用于设置元素的高度,使其相对于浏览器窗口的高度自适应。
vw
- 定义:
vw
表示视口宽度的 1%。视口宽度是指浏览器可视窗口的宽度。 - 使用场景:通常用于设置元素的宽度,使其相对于浏览器窗口的宽度自适应。
# 6.css 隐藏页面元素
display:none
特点:元素不可见,不占据空间,无法响应点击事件
visibility:hidden
特点:元素不可见,占据页面空间,无法响应点击事件
opacity:0
特点:设置透明度属性为 0,占据页面空间,可以响应点击事件
# 7. 元素水平 / 垂直 / 水平垂直居中
# 水平居中
1. 行内元素:首先看它的父元素是不是块级元素,如果是,则直接给父元素设置 text-align: center; 不是,则把父元素设置 display: block; 再设置 text-align: center;
2. 块级元素:
I. 定宽度:需要谁居中,给其设置 margin: 0 auto; (顺序是上右下左)
II. 不定宽度:默认子元素的宽度和父元素一样,这时需要设置子元素为 display: inline-block; 或 display: inline; 即将其转换成行内块级 / 行内元素,给父元素设置 text-align: center;
III. 使用定位:首先设置父元素为相对定位,再设置子元素为绝对定位。设置子元素的 left:50%,即让子元素的左上角水平居中;
定宽度:设置绝对子元素的 margin-left: - 元素宽度的一半 px; 不定宽度:设置 transform: translateX (-50%);
IV.flex 布局:给待处理的块状元素的父元素添加属性 display: flex; justify-content: center;
# 垂直居中
1. 行内元素:
单行文本父元素确认高度:height === line-height;
多行文本父元素确认高度:display: table-cell; vertical-align: middle
2. 块级元素:
I. 使用定位:首先设置父元素为相对定位,再设置子元素为绝对定位,设置子元素的 top: 50%,即让子元素的左上角垂直居中;
定高度:设置绝对子元素的 margin-top: - 元素高度的一半 px; 不定宽度:transform: translateY(-50%);
II.flex 布局:为父元素添加属性 display: flex; align-items: center;
# 水平垂直居中
已知宽高
I. 使用定位:设置父元素为相对定位,给子元素设置绝对定位,top: 0; right: 0; bottom: 0; left: 0; margin: auto;
II. 使用定位:设置父元素为相对定位,给子元素设置绝对定位,left: 50%; top: 50%; margin-left: - 子元素宽度的一半 px; margin-top: - 子元素高度的一半 px;
未知宽高
I. 使用定位:设置父元素为相对定位,给子元素设置绝对定位,left: 50%; top: 50%; transform: translate(-50%,-50%);
II.flex 布局:display: flex,表示该容器内部的元素将按照 flex 进行布局;align-items: center 设置垂直居中;justify-content: center 设置水平居中。
III.grid 布局:display: grid,表示该容器内部的元素将按照 flex 进行布局;align-items: center 设置垂直居中;justify-content: center 设置水平居中。
# 8.flex 布局
一种布局方式。
# 容器属性
flex-direction | |
flex-wrap | |
flex-flow | |
justify-content | |
align-items | |
align-content |
- flex-direction
决定主轴的方向 (即项目的排列方向)
属性对应如下:
row(默认值):主轴为水平方向,起点在左端
row-reverse:主轴为水平方向,起点在右端
column:主轴为垂直方向,起点在上沿。
column-reverse:主轴为垂直方向,起点在下沿
- flex-wrap
默认情况下,项目都排在一条线(又称” 轴线”)上。flex-wrap 属性定义,如果一条轴线排不下,如何换行。
属性对应如下:
nowrap(默认值):不换行
wrap:换行,第一行在下方
wrap-reverse:换行,第一行在上方
- flex-flow
是 flex-direction
属性和 flex-wrap
属性的简写形式,默认值为 row nowrap
- justify-content
定义了项目在主轴上的对齐方式
属性对应如下:
flex-start(默认值):左对齐
flex-end:右对齐
center:居中
space-between:两端对齐,项目之间的间隔都相等
space-around:两段间隔半个项目之间间隙,项目之间的间隔都相等
- align-items
定义项目在交叉轴上的对齐方式
属性对应如下:
flex-start:交叉轴的起点对齐
flex-end:交叉轴的终点对齐
center:交叉轴的中点对齐
baseline: 项目的第一行文字的基线对齐
stretch(默认值):如果项目未设置高度或设为 auto,将占满整个容器的高度
- align-content
定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。
属性对应如吓:
flex-start:与交叉轴的起点对齐
flex-end:与交叉轴的终点对齐
center:与交叉轴的中点对齐
space-between:与交叉轴两端对齐,轴线之间的间隔平均分布
space-around:每根轴线两侧的间隔都相等。所以,轴线之间的间隔比轴线与边框的间隔大一倍
stretch(默认值):轴线占满整个交叉轴
# 项目属性
order
flex-grow
flex-shrink
flex-basis
flex
align-self
order
order 属性定义项目的排列顺序。数值越小,排列越靠前,默认为 0。
flex-grow
flex-grow 属性定义项目的放大比例,默认为 0,即如果存在剩余空间,也不放大。
flex-shrink
flex-shrink 属性定义了项目的缩小比例,默认为 1,即如果空间不足,该项目将缩小。
flex-basis
flex-basis 属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为 auto,即项目的本来大小。
flex
flex-grow, flex-shrink 和 flex-basis 的简写,默认值为 0 1 auto。
align-self
align-self 属性允许单个项目有与其他项目不一样的对齐方式,可覆盖 align-items 属性。
# 9.BFC 相关
BFC 可以使元素独立地形成一个块级格式化上下文。
当开启元素的 BFC 以后,元素会变成一个独立的布局区域,不会在布局上影响到外面的元素。
清除浮动:当包含浮动的元素时,父元素可能会由于高度塌陷而无法包裹浮动元素。
避免外边距重叠:在普通的文档流中,垂直方向相邻的块级元素之间的外边距会发生重叠。而 BFC 中的元素之间的外边距不会重叠。
创建多栏布局:通过将容器元素设置为 BFC,可以实现多栏布局,使得多个子元素按照一定的规则进行排列。
如何设置 BFC?
overflow
属性设置为 hidden
、 auto
。
display
属性设置为 flex
、 grid
、 inline-block
等。
position
属性为 absolute
或 fixed
。
# 10. 两栏 / 三栏布局
# 两栏布局
- 采用浮动
- 采用定位
- flex 布局
采用浮动
使用 float 左浮左边栏。
左边设置 float: left。右边模块设置 margin-left 即可。
为父级元素添加 BFC,防止下方元素飞到上方内容。
<div class="box"> | |
<div class="left">左边</div> | |
<div class="right">右边</div> | |
</div> | |
<style> | |
.box{ | |
overflow: hidden; //添加BFC | |
} | |
.left { | |
float: left; | |
width: 200px; | |
background-color: gray; | |
height: 400px; | |
} | |
.right { | |
margin-left: 210px; | |
background-color: lightgray; | |
height: 200px; | |
} | |
</style> |
采用定位
父元素相对定位。
左边元素绝对定位,并且设置宽度。右边元素的 margin-left 设置合适即可。
<div class="box"> | |
<div class="left">左边</div> | |
<div class="right">右边</div> | |
</div> | |
<style> | |
.box { | |
position: relative; | |
} | |
.left { | |
position: absolute; | |
width: 200px; | |
height: 100px; | |
background: #66CCFF; | |
} | |
.right { | |
margin-left: 200px; | |
height: 100px; | |
background: gold; | |
} | |
</style> |
flex 布局
设置父元素 display:flex;
左边正常设置宽度。右边使用 flex:1; 即可。
<div class="box"> | |
<div class="left">左边</div> | |
<div class="right">右边</div> | |
</div> | |
<style> | |
.box{ | |
display: flex; | |
} | |
.left { | |
width: 100px; | |
} | |
.right { | |
flex: 1; | |
} | |
</style> |
# 三栏布局
- 两边使用 float,中间使用 margin
- 两边使用 absolute,中间使用 margin
- flex 实现
两边使用 float,中间使用 margin
! 需要将中间的内容放在 html 结构最后,否则右侧会呈现在中间内容的下方。
缺点:
- 主体内容是最后加载的,在加载东西较多时可能会影响用户体验。
- 右边在主体内容之前,如果是响应式设计,不能简单的换行展示。
<div class="container"> | |
<div class="left">left</div> | |
<div class="right">right</div> | |
<div class="main">center</div> | |
</div> | |
<style> | |
.container { | |
background: #eee; | |
overflow: hidden; /* 生成 BFC*/ | |
padding: 20px; | |
height: 200px; | |
} | |
.left { | |
width: 200px; | |
height: 200px; | |
float: left; | |
background: coral; | |
} | |
.right { | |
width: 120px; | |
height: 200px; | |
float: right; | |
background: lightblue; | |
} | |
.main { | |
margin-left: 220px; | |
height: 200px; | |
background: lightpink; | |
margin-right: 140px; | |
} | |
</style> |
两边使用 absolute,中间使用 margin
基于绝对定位的三栏布局:注意绝对定位的元素脱离文档流,相对于最近的已经定位的祖先元素进行定位。无需考虑 HTML 中结构的顺序。
<div class="container"> | |
<div class="left">left</div> | |
<div class="main">center</div> | |
<div class="right">right</div> | |
</div> | |
<style> | |
.container { | |
position: relative; | |
} | |
.left { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100px; | |
background: green; | |
} | |
.right { | |
position: absolute; | |
top: 0; | |
right: 0; | |
width: 100px; | |
background: green; | |
} | |
.main { | |
margin: 0 110px; | |
background: black; | |
color: white; | |
} | |
</style> |
flex 实现
仅需将容器设置为 display:flex;
,
盒内元素两端对齐,将中间元素设置为 100%
宽度,或者设为 flex:1
,即可填充空白
盒内元素的高度撑开容器的高度
优点:
- 结构简单直观
- 可以结合 flex 的其他功能实现更多效果,例如使用 order 属性调整显示顺序,让主体内容优先加载,但展示在中间
<div class="container"> | |
<div class="left">left</div> | |
<div class="main">center</div> | |
<div class="right">right</div> | |
</div> | |
<style> | |
.container { | |
display: flex; | |
} | |
.left { | |
width: 200px; | |
background: red; | |
} | |
.main { | |
flex: 1; | |
background: blue; | |
} | |
.right { | |
width: 200px; | |
background: red; | |
} | |
</style> |
# 11. 块级元素 / 行内元素 / 行内块元素
块级元素
- 独自占一行:块级元素会自动占据一整行,并且后续元素会被推到下一行。
- 可设置宽高:高度、宽度、行高、外边距(
margin
)以及内边距(padding
)都可以通过 CSS 进行控制。 - 填满父元素:如果没有明确设置宽度,块级元素的宽度会默认填满父元素的宽度(即宽度为 100%)。
比如 <div>、<ul>、<li>、<table>、<p>、<h1 > 等。
行内元素
- 不独占一行:行内元素不会独占一行,相邻的行内元素会排列在同一行,直到空间不足时才会换行。
- 不可设置宽高:行内元素的宽度和高度无法直接设置,其宽度由内容决定。
- 边距和内边距的限制:对行内元素,外边距和内边距仅在水平方向(左右)有效,而在垂直方向(上下)不生效。
- 默认宽度由内容决定:行内元素的宽度等于其内容(如文本或图片)的宽度。
- 嵌套限制:行内元素中通常不能直接包含块级元素。
比如 <span>,<a>,<img>,<em > 等。
行内块元素
- 混合特性:行内块元素结合了块级元素和行内元素的特点。它既可以在一行内显示(像行内元素),也可以设置宽度、高度、边距和内边距(像块级元素)。
转换方式
display: block;
将元素转换为块级元素。display: inline;
将元素转换为行内元素。display: inline-block;
将元素转换为行内块元素。
# 12.Scss 和 Less
相同
LESS 和 SCSS 都是 css 的预处理器,可以拥有变量,运算,继承,嵌套的功能,使用两者可以使代码更加的便于阅读和维护
都可以通过自带的插件,转成相对应的 css 文件
都可以参数混入,可以传递参数的 class,就像函数一样
嵌套的规则相同,都是 class 嵌套 class
区别
编写变量:编写变量 Sass 使用 $,而 Less 使用 @
条件语句支持:Sass 支持条件语句,可以使用 if {} else {},for {} 循环等等,而 Less 不行
处理方式:Sass 是在服务端处理的,以前是 Ruby,现在是 Dart-Sass 或 Node-Sass,而 Less 是在客户端处理的
# 13. 绘制同心圆
可以采用 flex 布局绘制
<div class="circle-container"> | |
<div class="circle-outer"></div> | |
<div class="circle-middle"></div> | |
<div class="circle-inner"></div> | |
</div> | |
<style> | |
/* 外层容器,包含所有同心圆 */ | |
.circle-container { | |
position: relative; | |
width: 200px; | |
height: 200px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
/* 外层大圆 */ | |
.circle-outer { | |
width: 100%; | |
height: 100%; | |
border-radius: 50%; | |
background-color: lightblue; | |
position: absolute; | |
} | |
/* 中间圆 */ | |
.circle-middle { | |
width: 70%; | |
height: 70%; | |
border-radius: 50%; | |
background-color: lightcoral; | |
position: absolute; | |
} | |
/* 内层小圆 */ | |
.circle-inner { | |
width: 40%; | |
height: 40%; | |
border-radius: 50%; | |
background-color: lightgreen; | |
position: absolute; | |
} | |
</style> |
# 14.CSS position
static(默认值):
- 元素按照文档流进行排列,不受
top
、right
、bottom
和left
属性影响。
relative:
- 元素相对于其正常位置进行偏移,仍然保留在文档流中,其他元素会占据它原本的位置。
absolute:
- 元素相对于最近的定位父元素(即
position
不为static
的父元素)进行定位。脱离文档流,其他元素不会占据其空间。
fixed:
- 元素相对于浏览器窗口进行定位,即使滚动页面,元素也会保持在同一位置。也脱离文档流。
sticky:
- 元素在特定的滚动位置下表现为相对定位,一旦达到指定的滚动位置,就变为固定定位。它结合了
relative
和fixed
的特性。
# 15.margin 负值相关
设置负值的现象 | 结果 |
---|---|
margin-left | 自身向左移动 |
margin-top | 自身向上移动 |
margin-right | 自身不动,其右边元素向左移动 |
margin-bottom | 自身不动,其下方元素向上移动 |
# 16.Sass 与 CSS 对比
变量
普通 CSS:没有变量概念,重复使用相同的值时需要手动输入。
Sass:支持使用变量,通过 $ 定义变量,可以存储颜色、字体大小、间距等值,方便复用。
嵌套规则
普通 CSS:不支持嵌套,必须一层层书写样式规则。
Sass:支持嵌套规则,允许父子选择器嵌套书写,提升代码可读性。
混合(Mixins)
普通 CSS:没有 mixin 功能,通常需要手动重复相同的样式代码。
Sass:允许创建 mixins(混合),可以像函数一样复用样式代码,并且可以传递参数。
继承(Inheritance)
普通 CSS:不支持样式继承。
Sass:通过 @extend 可以让选择器继承其他选择器的样式,避免重复代码。
运算
普通 CSS:不支持数学运算。
Sass:支持基本的数学运算,可以直接在样式中进行加、减、乘、除等操作。
部分(Partials)和导入
普通 CSS:@import 导入文件,但每次导入都会产生一个新的 HTTP 请求,性能较差。
Sass:允许使用 @import 将不同的 Sass 文件整合为一个文件,且不会产生额外的 HTTP 请求。
# 17. 伪类和伪元素选择器
特性 | 伪类(Pseudo-classes) | 伪元素(Pseudo-elements) |
---|---|---|
作用对象 | 伪类用于定义元素在特定状态下的样式。 | 伪元素用于选取元素的某个特定部分,并对其应用样式。 |
语法表示 | 单冒号 : + 伪类名称 |
双冒号 :: + 伪元素名称(CSS3 推荐)或单冒号 : |
常见用途 | 用户交互状态、结构选择 | 插入内容、选取元素的一部分 |
示例 | :hover , :focus |
::before , ::after |
# 18.link 和 @import 方式引入 css 的区别
link 是 HTML 元素,加载优先级更高。@import 加载优先级低于 link。
link 是同步加载,@import 加载会延后,可能会影响渲染速度。
# 19. 实现一个九宫格
<div class="grid-container"> | |
<div class="grid-item">1</div> | |
<div class="grid-item">2</div> | |
<div class="grid-item">3</div> | |
<div class="grid-item">4</div> | |
<div class="grid-item">5</div> | |
<div class="grid-item">6</div> | |
<div class="grid-item">7</div> | |
<div class="grid-item">8</div> | |
<div class="grid-item">9</div> | |
</div> | |
<style> | |
.grid-container { | |
display: grid; /* 使用 Grid 布局 */ | |
grid-template-columns: repeat(3, 1fr); /* 创建三列 */ | |
grid-template-rows: repeat(3, 1fr); /* 创建三行 */ | |
gap: 5px; /* 网格间距 */ | |
width: 300px; /* 设置容器宽度 */ | |
height: 300px; /* 设置容器高度 */ | |
} | |
.grid-item { | |
background-color: #2ecc71; /* 背景颜色 */ | |
} | |
</style> |
# Vue 部分
# 1.v-show 与 v-if 理解
v-show
与 v-if
的作用效果是相同的 (不含 v-else),都能控制元素在页面是否显示。
控制手段不同: v-show
隐藏则是为该元素添加 css--display:none
, dom
元素依旧还在。 v-if
显示隐藏是将 dom
元素整个添加或删除
编译过程不同: v-if
切换有一个局部编译 / 卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件; v-show
只是简单的基于 css 切换
编译条件不同: v-if
是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染
v-show
由false
变为true
的时候不会触发组件的生命周期v-if
由false
变为true
的时候,触发组件的beforeCreate
、create
、beforeMount
、mounted
钩子,由true
变为false
的时候触发组件的beforeDestory
、destoryed
方法。
性能消耗: v-if
有更高的切换消耗; v-show
有更高的初始渲染消耗。
# 2.v-if 和 v-for 的优先级
在 Vue 2 中, v-for
的优先级高于 v-if
。这意味着 v-for
会先被解析,然后才会处理 v-if
。
在 Vue 3 中, v-if
的优先级高于 v-for
。这意味着 v-if
会先被解析,然后才会处理 v-for
。
- 永远不要把
v-if
和v-for
同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)。 - 如果避免出现这种情况,则在外层嵌套
template
(页面渲染不生成dom
节点),在这一层进行 v-if 判断,然后在内部进行 v-for 循环。 - 如果条件出现在循环内部,可通过计算属性
computed
提前过滤掉那些不需要显示的项。比如下面:
<template> | |
<div> | |
<div v-for="item in visibleItems" :key="item.name"> | |
</div> | |
</div> | |
</template> | |
computed: { | |
visibleItems() { | |
return this.items.filter(item => item.isVisible); | |
} | |
} |
# 3. 组件通信方式
-
父子组件之间通信
props
-
父组件传递数据给子组件
-
子组件设置
props
属性,定义接收父组件传递过来的参数 -
父组件在使用子组件标签中通过字面量来传递值
$emit
- 子组件传递数据给父组件
- 子组件通过
$emit触发
自定义事件,$emit
第二个参数为传递的数值 - 父组件绑定监听器获取到子组件传递过来的参数
ref
- 父组件在使用子组件的时候设置
ref
- 父组件通过设置子组件
ref
来获取数据
兄弟组件之间通信
EventBus
- 创建一个中央事件总线
EventBus
- 兄弟组件通过
$emit
触发自定义事件,$emit
第二个参数为传递的数值 - 另一个兄弟组件通过
$on
监听自定义事件
祖先与后代组件通信
provide 与 inject
- 在祖先组件定义
provide
属性,返回传递的值 - 在后代组件通过
inject
接收组件传递过来的值
复杂的组件通信
可以选用
Vuex
或者Pinia
-
# 4. 生命周期
创建阶段
beforeCreate
: 实例初始化之后调用。** 在这个阶段,数据观察和事件配置尚未完成,** 不能访问data
、computed
、watch
和methods
等。created
: 实例创建完成后调用。在这个阶段,可以访问data
、computed
、watch
和methods
等,但 DOM 尚未挂载。(可以做的事:初始化数据、请求数据)
挂载阶段
beforeMount
: 在挂载开始之前调用。此时虚拟 DOM 已经创建完成,但还没有渲染到页面上。mounted
: 实例挂载完成后调用。此时,DOM 已经被渲染,通常在这个钩子中进行 DOM 操作。(可以做的事:操作 DOM、初始化插件)
更新阶段
beforeUpdate
: 当响应式数据更新时调用,更新过程开始前执行。在这个阶段,DOM 还没有更新,可以在更新之前访问现有的 DOM。updated
: 当组件的数据变化导致 DOM 更新后调用。此时可以执行依赖于更新 DOM 的操作。
销毁阶段
beforeDestory(vue3则是onBeforeUnmounted)
: 组件销毁之前调用。你可以在这个钩子中执行一些清理工作,比如清除定时器等。destoryed(vue3则是onUnmounted)
: 组件销毁之后调用。在这个阶段,组件实例的所有内容都会被销毁,相关的 DOM 元素也会被移除。
# 5.ref 和 reactive 区别
ref
- 用途:
ref
用于定义一个响应式的基本数据类型(如数字、字符串、布尔值等)或者是一个 DOM 元素的引用。 - 用法:
ref
会返回一个包含value
属性的对象,你可以通过该属性来访问和更新实际的值。
reactive
- 用途:
reactive
用于将一个对象或数组转换为响应式对象。它能够让对象中的所有属性都具备响应式能力。 - 用法:
reactive
返回的是一个代理对象,直接操作这个对象的属性即可触发响应式更新。
区别
- ref 主要用于创建单个的响应式数据。reactive 用于创建包含多个响应式属性的对象。所以对于基本类型的变量定义,推荐使用 ref,如果需要响应式包装对象或数组,推荐使用 reactive。
- ref 在 script 中应该使用.value 访问,在模板中使用响应式数据时,无需使用 .value 访问 ref 类型的数据,而是直接使用变量名。reactive 类型的数据,则直接使用对象属性名。
- ref 对基本数据类型直接进行响应式封装,对于对象则只封装顶层对象。reactive 会递归地将对象的所有属性转换为响应式数据。
- ref 返回一个由
RefImpl
类构造出来的对象,而 reactive 返回一个原始对象的响应式代理 Proxy。
# 6. 响应式原理
Vue 通过 v-model
指令实现双向绑定,使得视图(View)和数据(Model)之间保持同步。具体来说,当数据发生变化时,视图会自动更新;同样地,当用户在视图中进行操作(如输入数据),数据也会自动更新。
响应式原理(双向绑定的底层)
Vue2 核心是通过 Object.defineProperty()
来劫持数据的 getter
和 setter
,并在数据变动时通知视图更新。
主要分为三步:数据劫持、依赖收集、派发更新。
数据劫持
Vue2 使用 Object.defineProperty
方法来拦截对象属性的访问和修改。每个响应式对象在创建时,Vue 会递归地遍历对象的每一个属性,为每个属性都设置 getter
和 setter
,从而实现对属性的劫持。如果对象嵌套了其他对象,Vue 也会递归地将子对象转化为响应式的。
依赖收集
Vue 通过一个称为 Dep
的类来管理依赖。每个响应式属性都有一个对应的 Dep
实例,用来存储所有依赖于这个属性的 Watcher
。 Watcher
是一个观察者对象,它会订阅一个或多个响应式属性。当这些属性的值发生变化时, Watcher
就会被通知并触发更新视图的操作。当组件渲染时,每个被访问的响应式属性都会将当前的 Watcher
添加到它的 Dep
中。
派发更新
当数据属性的值发生变化时, setter
会被触发,此时 Vue 会通过 Dep.notify
通知所有依赖于这个数据属性的 Watcher
,并调用每个 Watcher
的 update
方法,从而触发这些组件的重新渲染或执行特定的回调函数。
# 7.nexttick 的理解
官方定义:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
nextTick
可以让你在 Vue 完成 DOM 更新后执行一段代码。
使用场景:在数据变化后等待 DOM 更新,在创建或销毁组件后等待 DOM 更新,处理依赖于 DOM 更新的动画,与第三方库集成。
原理: callbacks
也就是异步操作队列
callbacks
新增回调函数后又执行了 timerFunc
函数, pending
是用来标识同一个时间只能执行一次。
timerFunc
函数定义,这里是根据当前环境支持什么方法则确定调用哪个,分别有:
Promise.then
、 MutationObserver
、 setImmediate
、 setTimeout
大致为
1. 把回调函数放入 callbacks 等待执行。
2. 将执行函数放到微任务或者宏任务中。
3. 事件循环到了微任务或者宏任务,执行函数依次执行 callbacks 中的回调。
# 8.diff 算法
# Vue2 原理
diff 算法主要用于高效地更新 DOM,当数据发生变化时,Vue 通过对新旧虚拟 DOM 的比较(diff 算法)来找出需要更新的部分,从而只对必要的 DOM 节点进行修改,提高了性能。整体策略为:深度优先,同层比较。
当数据发生改变时, set
方法会调用 Dep.notify
通知所有订阅者 Watcher
, Watcher
会通知更新并执行更新函数,它会执行 render
函数获取新的虚拟 DOM 比较,调用 patch
给真实的 DOM
打补丁,更新相应的视图。
patch
函数前两个参数位为 oldVnode
和 Vnode
,分别代表新的节点和之前的旧节点,主要做了四个判断:
- 没有新节点,直接触发旧节点的
destory
钩子 - 没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用
createElm
- 旧节点和新节点自身一样,通过
sameVnode
判断节点是否一样,一样时,直接调用patchVnode
去处理这两个节点 - 旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点
patch 负责对比新旧虚拟 DOM,决定整体更新策略。它首先判断节点是否相同,若相同则调用 patchVnode
进行深层更新,不同则直接替换或者增删整个节点。
patchVnode
则是做了以下操作:
- 找到对应的真实
dom
,称为el
- 如果都有都有文本节点且不相等,将
el
文本节点设置为Vnode
的文本节点 - 如果
oldVnode
有子节点而VNode
没有,则删除el
子节点 - 如果
oldVnode
没有子节点而VNode
有,则将VNode
的子节点真实化后添加到el
- 如果两者都有子节点,则执行
updateChildren
函数比较子节点
patchVnode 专门处理相同节点的更新。它比较新旧节点的文本和子节点等,并决定是否更新文本、删除或添加子节点。
updateChildren
主要做了以下操作:
- 设置新旧
VNode
的头尾指针 - 处理四种场景,新的头和老的头对比,新的尾和老的尾对比,新的头和老的尾对比,新的尾和老的头对比。
- 如果都不满足,分情况操作调用
createElem
创建一个新节点,或从哈希表寻找key
一致的VNode
节点。
updateChildren 用于递归比较和更新子节点。它通过双指针法比较新旧虚拟 DOM 的子节点,并处理四种对比场景,使用 key 提升效率,以最小代价更新子节点。
# Vue3 原理
# 9.slot
slot
是 Vue 中实现内容分发的关键机制,(内容分发是指将父组件的内容传递给子组件的机制)增强了组件的灵活性、可重用性和解耦性。
- 默认插槽
子组件用 <slot>
标签来确定渲染的位置,标签里面可以放 DOM
结构,当父组件使用的时候没有往插槽传入内容,标签内 DOM
结构就会显示在页面。父组件在使用的时候,直接在子组件的标签内写入内容即可。
- 具名插槽
子组件用 ** name
属性 ** 来表示插槽的名字,不传 name 的 <slot>
为默认插槽。父组件中在使用时在默认插槽的基础上加上 slot
属性,值为子组件插槽 name
属性值。
- 作用域插槽
子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件 v-slot
接受的对象上。父组件中在使用时通过 v-slot:
(简写:#)获取子组件的信息,在内容中使用。
# 10.key 的作用
key
属性的作用是帮助 Vue 高效地更新 DOM 元素,尤其是在渲染列表时。 key
用于标识元素或组件,当 Vue 在进行虚拟 DOM diff 运算时,通过 key
来判断哪些元素需要更新、重新排序或删除。
在使用 v-for
时,
- 如果你在渲染列表时没有使用
key
,Vue 会尽可能复用已有的 DOM 元素,而不关心元素在数据中的实际位置变化。这种处理方式在元素的顺序发生变化时可能会导致意外的渲染结果。 - 当你使用了
key
属性,Vue 会通过key
来明确区分每个元素,从而确保元素的状态和 DOM 结构的一致性。
# 11.keep alive
keep-alive
是 vue
中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染 DOM
。
keep-alive
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
keep-alive
可以设置以下 props
属性:
include
- 字符串或正则表达式。只有名称匹配的组件会被缓存exclude
- 字符串或正则表达式。任何名称匹配的组件都不会被缓存max
- 数字。最多可以缓存多少组件实例
设置了 keep-alive 缓存的组件,会多出两个生命周期钩子( activated
与 deactivated
):
- 首次进入组件时:
beforeCreate
>created
>mounted
>activated
> ... ... >deactivated
- 组件被缓存时:
activated
> ... ... >deactivated
- 组件被重新激活时:
deactivated
> ... ... >activated
# 12.Vuex
Vuex 的核心部分包括 State
(状态)、 Getters
(计算属性)、 Mutations
(突变)、 Actions
(动作) 和 Modules
(模块)。
State(状态)
State
是 Vuex 的核心,用于集中管理应用的全局状态。所有需要共享的数据都存储在 state
中,确保在多个组件之间能够一致且实时地访问和更新这些状态。
Getters(计算属性)
Getters
类似于 Vue 组件中的计算属性,用于从 state
中派生出新的数据或对状态进行处理。 Getters
提供了一种方便的方式来计算和复用状态派生数据,同时会对计算结果进行缓存。
Mutations(突变)
Mutations
是修改 Vuex state
的唯一方式。每个 mutation
都有一个事件类型和一个回调函数,接收 state
作为参数,直接更新状态。为了保持状态变更的可追踪性, mutations
通常是同步操作。通过 store.commit('mutationName', payload)
调用。
Actions(动作)
Actions
用于处理异步操作,提交 mutations
以间接修改 state
。 Actions
让你可以在状态更新之前执行异步任务,如数据获取或定时操作,并确保状态更新的流程更加灵活和可控。通过 store.dispatch('actionName', payload)
调用。
Modules(模块)
Modules
允许将庞大的 state
细分为更小的模块,每个模块有自己的 state
、 mutations
、 actions
和 getters
。这使得 Vuex 能够轻松地管理复杂应用的状态,保持代码的可维护性和模块化组织。
# 13.Computed 和 Watch
Computed
计算属性是基于它们的依赖进行缓存的属性。当依赖的数据发生变化时,计算属性才会重新计算。
Watch
侦听器是用于观察 Vue 实例上的数据变动,并在数据变化时执行特定的操作。
- 功能上:
computed
是计算属性,也就是依赖其它的属性计算所得出最后的值。watch
是去监听一个值的变化,然后执行相对应的函数 - 使用上:
computed
中的函数必须要用return
返回;watch
的回调里面会传入监听属性的新旧值,通过这两个值可以做一些特定的操作,不是必须要用return
- 性能上:
computed
中的函数所依赖的属性没有发生变化,那么调用当前的函数的时候会从缓存中读取,而watch
在每次监听的值发生变化的时候都会执行回调 - 场景上:
computed
:当多个属性影响一个属性时的时候,例子:购物车商品结算;watch
:当一条数据影响多条数据的时候,例子:搜索框
# 14.Vue2 与 Vue3 的区别(Vue3 的优点)
- 生命周期钩子
Vue 3 对生命周期钩子的命名做了改进。例如, beforeDestroy
和 destroyed
分别改名为 beforeUnmount
和 unmounted
,使其语义更加清晰。
- 多根支持
Vue 3 支持 Fragments,允许在一个组件中返回多个根元素,解决了 Vue 2 中必须有单一根元素的限制。
- Composition API
Composition API 通过 setup 函数,将逻辑集中在一起,使得逻辑复用和组合更加容易。Vue 2 主要使用 Options API(如 data、methods、computed 等),代码组织上相对固定。
- 响应式原理
Vue2 响应式原理基础是 Object.defineProperty(),Vue3 响应式原理基础是 Proxy()。Vue2 中响应式的缺点,无法监听对象或数组新增、删除的元素。Vue2 的解决方式则是针对常用数组原型方法 push、pop、shift、unshift、splice、sort、reverse 进行了 hack 处理;提供 Vue.set 监听对象 / 数组新增属性。对象的新增 / 删除响应,还可以 new 个新对象,新增则合并新属性和旧对象;删除则将删除属性后的对象深拷贝给新对象。
- 异步组件
Vue3 提供 Suspense 组件,允许程序在等待异步组件加载完成前渲染兜底的内容,如 loading ,使用户的体验更平滑。使用它,需在模板中声明,并包括两个命名插槽:default 和 fallback。Suspense 确保加载完异步内容时显示默认插槽,并将 fallback 插槽用作加载状态。
- Teleport 功能
Vue 3 引入了 Teleport,允许组件的某些部分渲染到 DOM 树的其他地方,方便进行像模态框、工具提示等全局组件的开发。
- TypeScript 支持
Vue 3 提供了更完善的 TypeScript 支持,TypeScript 是 Vue 3 核心代码库的一部分,开发者可以更顺畅地在 TypeScript 项目中使用 Vue 3。
- 虚拟 DOM 优化
Vue 3 引入了静态提升、缓存事件处理程序等机制,使得渲染更高效。
静态内容只会在初次渲染时创建,避免不必要的重新渲染。Vue 3 的 diff 算法在处理大规模动态内容时更加高效,通过 Block Tree 对动态节点做标记,从而在更新时仅比较动态内容,减少了对整个树的比较范围。
缓存事件则是传入的事件的储存位置变成了缓存的形式。当你的页面在不断的更新的时候,你的事件侦听器并不会重复地销毁再创建,而是以缓存的形式存在避免了重复渲染,
- 打包优化
Tree-shaking 支持,Vue 3 重构了全局 API,允许在项目构建时自动移除未使用的部分。这大大减少了打包后的文件体积。Vue 3 提供了更灵活的 API 引入方式,使得开发者可以按需引用,避免将不必要的代码打包到最终文件中。
# 15. 组合式 api 优点
-
更好的逻辑复用: 在 Vue 2 中,使用选项式 API(Options API)时,逻辑复用通常依赖于混入 (mixins)、HOC (高阶组件) 或者 Scoped Slot 等方式。这些方式虽然能一定程度上解决问题,但存在代码不够直观、命名冲突等问题,尤其是混入会让代码难以理解和维护。组合式 API 通过
setup
函数将逻辑分离成独立的函数,可以轻松地复用、组织和管理代码片段。 -
更灵活的代码组织: 在 Options API 中,组件的不同逻辑(如生命周期钩子、计算属性、方法等)通常被拆分到各个选项中。随着组件复杂度的增加,相关的代码往往散落在多个选项中,使得逻辑变得分散,维护起来困难。组合式 API 提供了将逻辑聚合在一起的能力,将相关逻辑组织在一个函数或模块内,使代码更具可读性和维护性。
-
更好的类型推导: Vue 3 更好地支持 TypeScript,而组合式 API 的函数式设计相比于选项式 API 更容易与 TypeScript 结合,为开发者提供了更精确的类型推断和错误检测。
-
更好的性能优化: 组合式 API 支持更好的 tree-shaking,未使用的代码可以在打包时被移除,减少了打包后的代码体积。同时,它还简化了内部的响应式追踪机制。
# 16. 事件总线缺点
- 全局性导致维护困难:事件总线的事件监听和触发是全局的,容易导致代码结构变得松散,事件关系复杂,尤其是在项目规模扩大时,维护和调试会变得困难。某个组件可能会在意外情况下触发或监听某个事件,导致意想不到的行为。
- 事件来源不明确:当多个组件通过事件总线进行通信时,事件的来源不容易追踪。随着事件的数量和复杂度增加,定位问题会变得越来越困难,特别是在调试时难以找到哪个组件触发了特定事件。
- 隐式依赖:组件之间的依赖关系是通过事件传递的,这种依赖关系是隐式的,不像通过 props 或 Vuex 那样清晰明确。这种隐式依赖可能导致组件的复用性降低,也不利于代码的可读性。
- 可能导致内存泄漏:如果事件监听没有在组件销毁时正确移除,事件总线可能导致内存泄漏。Vue 2.x 的事件总线通常使用
$on
和$off
,如果忘记在组件销毁时调用$off
,监听器就会一直存在,无法释放内存。 - 可扩展性差:对于大型项目或复杂应用,事件总线并不是最佳的解决方案。随着事件数量和复杂度增加,维护事件和事件监听器的管理变得更加困难,不如 Vuex 或 Pinia 这种状态管理工具适合大型项目的架构需求。
# 17.Vue-Router 的 hash 和 history 模式
Hash 模式
- URL 结构:
http://example.com/#/home
- 实现方式:Hash 模式使用
URL
中的#
(哈希符号)来模拟不同的路径。哈希部分不会被包含在请求中,因此页面跳转不会重新加载页面,而是由前端路由来处理。 - 优点:配置简单,不需要服务器支持。
- 缺点:URL 中有
#
符号,不够美观。对 SEO 不友好,因为哈希部分不会被服务器解析。
history 模式
- URL 结构:
http://example.com/home
- 实现方式:History 模式使用了 HTML5 的
history.pushState()
和history.replaceState()
来实现路由跳转,不再依赖哈希符号。它可以创建一个没有#
的干净 URL。 - 优点:URL 更加美观、直观,对 SEO 更友好。用户体验更好,因为路径和普通的 URL 一样,用户可以直接访问和分享链接。
- 缺点:需要服务器进行配置来支持所有路由指向同一个入口文件。在不支持 h5 history 的浏览器要降级处理。
# 18.Vuex 原理
# 19.Vue-Router 原理
# 20.v-model 原理
# 浏览器部分
# 1. 在浏览器输入 url 后发生了什么
- 分析 URL 所需要使用的传输协议、域名、请求的资源路径等。
- 检查缓存是否有该域名的 IP 地址,若没有,向 DNS 服务器请求解析该域名,获取对应的 IP 地址。
- 建立 TCP 连接,若 URL 的协议是 https,建立 TLS 握手。
- 发送 HTTP 请求。
- 服务器处理并返回 HTTP 报文。
- 浏览器解析并渲染页面。
- 断开连接。
拓展
DNS 具体流程
先检查本地缓存:浏览器首先检查本地 DNS 缓存,看看是否已经缓存了该域名的 IP 地址。如果有,直接使用这个地址。
向本地 DNS 服务器查询:如果本地缓存中没有,浏览器会向配置的本地 DNS 服务器(通常由互联网服务提供商提供)发送查询请求。本地 DNS 服务器会检查自己的缓存。如果找到该域名的 IP 地址,则将其返回给浏览器。如果没有找到,继续进行下一步。
递归查询:本地 DNS 服务器开始进行递归查询。它会向根 DNS 服务器发送请求。 通过根服务器, 顶级服务器,权威域名服务器进行递归查找。 找到域名服务器以后, 本地服务器向这个域名服务器发送请求, 并将结果缓存。最后反馈给浏览器。(使用 UDP 协议,因为快)
# 2. 浏览器渲染页面的过程
- 通过 HTML 构建文档对象模型 (DOM)。
- 通过 CSS 构建 CSS 对象模型 (CSSOM)。
- 应用任何会更改 DOM 或 CSSOM 的 JavaScript。
- 通过 DOM 和 CSSOM 构建 render 树。
- 在页面上执行样式和布局操作,看看哪些元素适合显示。
- 在内存中绘制元素的像素。如果有像素重叠,则合成像素。
- 以物理方式将所有生成的像素绘制到屏幕上。
# 3. 浏览器重排重绘
首先涉及到浏览器的关键渲染路径。包括生成 DOM 树和 CSSOM 树,并将两者结合形成渲染树,然后浏览器根据渲染树进行布局(layout),确定页面上所有元素的大小和位置。确定布局后,浏览器会将这些元素绘制(paint)到屏幕上。
重排(Reflow)是指在 DOM 树中的元素位置、尺寸或结构发生变化时,浏览器需要重新计算布局。这个过程需要重新确定所有相关元素的位置和大小,因此重排一定会导致重绘。
重绘(Repaint)是指元素的位置没有变化,仅样式发生改变(如颜色或背景),此时浏览器跳过布局步骤,直接进入绘制阶段。因此,重绘不一定会导致重排。
导致原因
???
解决方式
两者都会对性能造成影响,重排由于要重新计算元素布局信息影响很大。重绘只需要重新绘制元素的样式,相当于重排影响较小。
- 避免频繁的 DOM 操作:DOM 操作会导致重排和重绘,因此应尽可能减少 DOM 操作的次数。例如,可以一次性添加多个元素,或使用
documentFragment
(文档碎片)进行批量操作。 - 合并样式修改:将多次样式修改合并为一次操作,而不是逐个修改样式属性。
- 离线修改 DOM:在修改之前先将 DOM 元素设置为不可见,完成所有操作后再将其设置为可见,以减少重排和重绘的次数。
- 优先使用 CSS 动画:CSS3 动画可以使用 GPU 加速,可以使用
transform
和opacity
进行动画,以减少重排和重绘。
# 4. 跨域
浏览器出于安全考虑,在执行跨站请求时,限制网页向不同域名的服务器发送请求的现象,叫做同源策略。
同源(即指在同一个域)具有以下三个相同点
- 协议相同(protocol):如
http
、https
。 - 主机相同(host):如
example.com
。 - 端口相同(port):如
80
、443
。
如果其中任意一项不同,就会被认为是跨域,浏览器会阻止跨域请求,以防止潜在的安全风险(如 CSRF 攻击)。
同源策略限制了以下几种操作:
- DOM 访问:一个网站的 JavaScript 无法访问另一个网站的 DOM。
- Cookies:一个源的 cookies 不能被另一个源读取。
- AJAX 请求:XHR 请求只能向同源的 URL 发起,跨源请求会被阻止。
解决方式
JSONP
它通过 <script>
标签 src 属性,发送带有 callback 参数的 GET 请求,服务端将接口返回数据拼凑到 callback 函数中,返回给浏览器,浏览器解析执行,从而实现跨域数据的请求。
<script> | |
var script = document.createElement('script'); | |
script.type = 'text/javascript'; | |
// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数 | |
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback'; | |
document.head.appendChild(script); | |
// 回调执行函数 | |
function handleCallback(res) { | |
alert(JSON.stringify(res)); | |
} | |
</script> |
当然作用是有限的,主要用于实现跨域的 GET
请求。
CORS
简单请求
简单请求包括 GET
、 POST
、 HEAD
三种请求方式,且请求的 HTTP 头部信息限制较少(如 Content-Type 只能是 text/plain
、 application/x-www-form-urlencoded
、 multipart/form-data
)。
请求流程:
- 浏览器直接发出请求,并在请求头中包含
Origin
头,指明请求的来源(协议、域名、端口)。 - 服务器根据
Origin
头的值决定是否允许请求。 - 如果服务器允许,则返回的响应头中包含 Access-Control-Allow-Origin,并设置允许的来源。
- 浏览器接收到响应后,根据 Access-Control-Allow-Origin 的值决定是否允许前端 JavaScript 访问响应数据。
预检请求
对于非简单请求(例如使用了 PUT
、 DELETE
方法,或 Content-Type 是 application/json
等),浏览器会在正式请求前自动发起一个预检请求。预检请求的目的在于向服务器确认实际请求是否被允许。
非简单请求方法:如 PUT
, DELETE
, PATCH
, 或者自定义方法(非 GET
, POST
, HEAD
)。
非简单的请求头:如果使用了自定义的请求头,或 Content-Type
非 application/x-www-form-urlencoded
, multipart/form-data
, 或 text/plain
。
预检请求流程:
- 浏览器发起
OPTIONS
请求,包括Origin
、Access-Control-Request-Method
和Access-Control-Request-Headers
等头部,用来询问服务器是否允许跨域请求。(OPTIONS 请求:获取目的资源所支持的通信选项) - 服务器响应 预检请求时需要返回
Access-Control-Allow-Origin
、Access-Control-Allow-Methods
、Access-Control-Allow-Headers
等头部,表明允许的跨域规则。 - 浏览器检查服务器响应,如果允许,才会发送实际的跨域请求。
Proxy
代理是一种特殊的网络服务,允许客户端通过这个服务与另一个网络终端进行非直接的连接,因为服务器之间不存在跨域问题。
通过配置 nginx
实现代理
//举例:对www.xxx.com/api的请求代理到api.example.com/api
server {
listen 8080;
server_name www.xxx.com; #服务器名称
location /api {
proxy_pass http://api.example.com; # 将所有 /api 的请求代理到 http://api.example.com
...
}
}
# 5.cookie 和 session
Cookie 和 Session 都是普遍用来跟踪浏览用户身份的会话方式。
安全性:Cookie 存储在客户端,信息容易被窃取;Session 存储在服务端,相对安全一些。
存储大小:单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie。由服务器的存储能力决定。
有效期:Cookie 可以设置过期时间,过期后会被自动删除。Session 通常在用户关闭浏览器后失效,也可以通过设置过期时间来控制。
使用场景:Cookie 更适合存储一些不敏感的、持久化的用户数据,例如记住用户登录状态,个性化设置,追踪用户行为。Session 适用于敏感数据的存储,例如登录状态,购物车内容等。
# 6.localstorage 和 sessionstorage 和 cookie
localStorage
、 sessionStorage
和 cookie
都是用于在客户端存储数据的技术。
Cookie 是由服务器端写入的,而 SessionStorage、 LocalStorage 都是由前端写入。
cookie
- 持久性:可以通过
expires
或max-age
属性设置过期时间,默认情况下是会话级别的(浏览器关闭时失效)。不过,cookie
也可以设定(expires
或max-age
)为长时间存在。 - 存储大小:通常可以存储约 4KB 的数据。
- 作用范围:在同一个域名下的所有页面共享数据。
cookie
还可以被服务器端读取并用于服务器端会话管理。 - 使用场景:适用于需要与服务器交互的数据存储,比如存储登录验证信息 SessionID 或者 token。
localStorage
- 持久性: 数据会一直存在,除非主动删除或用户清除浏览器缓存。即使关闭浏览器,数据也不会丢失。
- 存储大小:通常可以存储约 5MB 的数据(各个浏览器略有不同)。
- 作用范围:在同一个域名下的所有页面共享数据。
- 使用场景:适用于需要长时间存储数据且数据量较大的场景,比如用户偏好设置、购物车内容等。
sessionStorage
- 持久性: 会话级别,数据仅在页面会话期间有效。只要浏览器或标签页关闭,数据就会被清除。
- 存储大小:通常可以存储约 5MB 的数据(各个浏览器略有不同)。
- 作用范围:仅在同一个浏览器标签页内共享数据,其他标签页或窗口无法访问。
- 使用场景:适用于临时数据存储,比如单次会话状态、页面会话状态等。
# 7.cookie 的字段
Name:名称,用于标识该 Cookie。
Value:存储的数据,可以是任何字符串。
Domain:指定哪些域名可以访问该 Cookie。默认为设置 Cookie 的服务器域。设置 Domain
属性可以让子域名也能访问该 Cookie。
Path:指定 Cookie 适用的 URL 路径。只有在请求的路径与 Cookie 的 Path
属性匹配时,Cookie 才会被发送。
Expires(过期时间):指定 Cookie 的过期日期和时间。到期后,浏览器会自动删除该 Cookie。如果不设置,Cookie 会在浏览器会话结束时删除(称为会话 Cookie)。
Max-Age(最大生存时间):指定 Cookie 从创建时开始到过期的秒数。与 Expires
类似,但优先级更高。如果同时设置了 Expires
和 Max-Age
,浏览器会优先使用 Max-Age
。
Secure(安全):指定 Cookie 只能通过 HTTPS 协议传输,增强安全性。设置此属性后,Cookie 不会通过不安全的 HTTP 连接发送。
HttpOnly:指定 Cookie 不能被 JavaScript 通过 document.cookie 访问,防止跨站脚本攻击(XSS)窃取 Cookie。
SameSite(同站策略):控制 Cookie 在跨站请求中的发送行为,以防范跨站请求伪造(CSRF)攻击。取值有 Strict、Lax 和 None。
# HTTP 部分
# 1.HTTP 和 HTTPS
HTTP
HTTP(超文本传输协议)用于在 Web 浏览器和网站服务器之间传递信息,以明文方式发送内容,不提供任何方式的数据加密。
HTTPS
保证隐私数据能加密传输,让 HTTP
运行安全的 SSL/TLS
协议上,即 HTTPS = HTTP + SSL/TLS,通过 SSL
证书来验证服务器的身份。
# 2.HTTP 和 HTTPS 的区别
- HTTPS 是 HTTP 协议的安全版本,HTTP 协议的数据传输是明文的,是不安全的,HTTPS 使用了 SSL/TLS 协议进行了加密处理,相对更安全。
- HTTP 和 HTTPS 使用连接方式不同,默认端口也不一样,HTTP 是 80,HTTPS 是 443。
- HTTPS 由于需要设计加密以及多次握手,性能方面不如 HTTP。
# 3.HTTP1.0/1.1/2.0/3.0
HTTP1.0
- 无状态:服务器不跟踪不记录请求过的状态
- 无连接:浏览器每次请求都需要建立 tcp 连接,无法复用连接
- 队头阻塞:下一个请求必须在前一个请求响应到达之前才能发送,如果前一个请求没有到达,后面则会阻塞。
HTTP1.1
- 默认支持长连接(Connection: keep-alive),允许在同一 TCP 连接上发送多个请求和响应,减少连接建立和关闭的开销。
- 支持请求的管道化,允许客户端在等待响应时发送多个请求,提高效率。解决了发送时候的对头阻塞问题,但是没有解决服务器响应时的队头阻塞。
HTTP2.0
- 二进制分帧,采用二进制格式而非文本格式。
- 完全多路复用,支持在单一连接上并发发送多个请求和响应,而不会相互阻塞。
- 服务器推送。服务器可以主动推送资源到客户端,而不必等待客户端请求。
- 使用报头压缩,减小了数据传输的开销。
HTTP3.0
- QUIC 协议:基于谷歌开发的 QUIC 协议,运行在 UDP 之上。
- 更快连接建立:零往返时间连接建立,将握手和加密结合,只需一次握手。
- 内建加密:默认通过 QUIC 使用 TLS 进行加密。加密功能作为协议的一部分,而不是依赖外部 TLS。
- 解决队头阻塞:每个请求都有独立的数据流,数据丢包只会影响单个流,不会影响其他请求的传输。
# 4.HTTPS 建立连接
- 首先客户端通过 URL 访问服务器建立 SSL 连接。
- 服务端收到客户端请求后,会将网站支持的证书信息(证书中包含公钥)传送一份给客户端。
- 客户端验证证书,如果信任则进入下一步,否之弹出警告。
- 客户端生成一个随机数将用作会话密钥用于加密后续通信中的数据。
- 客户端利用服务器的公钥将会话密钥加密,并传送给网站。
- 服务器利用自己的私钥解密出会话密钥。
- 服务器利用会话密钥对称加密与客户端之间的通信。
# 5.GET 和 POST 的区别
GET
GET
方法请求一个指定资源的表示形式,使用 GET 的请求应该只被用于获取数据。
POST
POST
方法用于将实体提交到指定的资源,通常导致在服务器上的状态变化或副作用。
区别
GET 请求的参数通常通过 URL 传递。POST 请求的参数通常放在请求体(body)中,URL 中一般不会带有查询参数。
GET 在浏览器回退时是无害的,而 POST 会再次提交请求。
GET 比 POST 更不安全,因为参数直接暴露在 URL 上,所以不能用来传递敏感信息。
GET 请求一般会被缓存,而 POST 请求默认不进行缓存。
PUT
PUT
用于更新或创建资源,数据在请求体中。
PUT
请求是幂等的,多次相同请求的效果是一样的。
PUT
请求通常用于完整替换资源。
OPTIONS
OPSTIONS
获取目的资源所支持的通信选项
# 6. 常见的状态码
- 1 表示消息
- 2 表示成功
- 3 表示重定向
- 4 表示请求错误
- 5 表示服务器错误
2XX
代表请求已成功被服务器接收、理解、并接受
常见的有:
- 200(成功):请求已成功,请求所希望的响应头或数据体将随此响应返回
- 201(已创建):请求成功并且服务器创建了新的资源
- 202(已创建):服务器已经接收请求,但尚未处理
- 203(非授权信息):服务器已成功处理请求,但返回的信息可能来自另一来源
- 204(无内容):服务器成功处理请求,但没有返回任何内容
- 205(重置内容):服务器成功处理请求,但没有返回任何内容
- 206(部分内容):服务器成功处理了部分请求
3XX
表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向
常见的有:
- 300(多种选择):针对请求,服务器可执行多种操作。 服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择
- 301(永久移动):请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置
- 302(临时移动): 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求
- 303(查看其他位置):请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码
- 304(未修改):客户端请求的文件没被修改,可以直接使用缓存。
- 305 (使用代理): 请求者只能使用代理访问请求的网页。 如果服务器返回此响应,还表示请求者应使用代理
- 307 (临时重定向): 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求
4XX
代表了客户端看起来可能发生了错误,妨碍了服务器的处理
常见的有:
- 400(错误请求): 服务器不理解请求的语法
- 401(未授权): 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
- 403(禁止): 服务器拒绝请求
- 404(未找到): 服务器找不到请求的网页
- 405(方法禁用): 禁用请求中指定的方法
- 406(不接受): 无法使用请求的内容特性响应请求的网页
- 407(需要代理授权): 此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理
- 408(请求超时): 服务器等候请求时发生超时
5XX
表示服务器无法完成明显有效的请求。这类状态码代表了服务器在处理请求的过程中有错误或者异常状态发生
常见的有:
- 500(服务器内部错误):服务器遇到错误,无法完成请求
- 501(尚未实施):服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码
- 502(错误网关): 服务器作为网关或代理,从上游服务器收到无效响应
- 503(服务不可用): 服务器目前无法使用(由于超载或停机维护)
- 504(网关超时): 服务器作为网关或代理,但是没有及时从上游服务器收到请求
- 505(HTTP 版本不受支持): 服务器不支持请求中所用的 HTTP 协议版本
# 7.HTTP 缓存
HTTP 缓存分为两种:强缓存和协商缓存。
强缓存:在一定时间内,浏览器直接使用缓存而不向服务器发送请求。强缓存的时间由 HTTP 头部字段 Expires
或 Cache-Control
指定。
-
Expires
: 使用绝对时间来指定缓存过期时间。缺点是受客户端时间影响,如果客户端时间不准确,可能导致缓存命中误差。这个是 HTTP1.0 产物。 -
Cache-Control
: 更常用,使用相对时间指定缓存时长,如Cache-Control: max-age=3600
表示缓存可以在 3600 秒内使用。
协商缓存:浏览器每次请求都会向服务器验证缓存是否过期,只有在服务器确认缓存未过期时才使用缓存数据。
-
Last-Modified
和If-Modified-Since
: 服务器在响应中返回资源的最后修改时间(Last-Modified
),下一次请求时,浏览器通过If-Modified-Since
字段向服务器询问资源是否有更新。如果没有更新,服务器返回 304 状态码,浏览器使用缓存;如果有更新,服务器返回 200 状态码并发送新的资源。 -
ETag
和If-None-Match
(优先级高于Last-Modified
和If-Modified-Since
):ETag
是服务器为资源生成的唯一标识符,浏览器在下次请求时通过If-None-Match
发送这个标识符,服务器验证资源是否变化。如果没有变化,返回 304 状态码;如果有变化,返回 200 状态码并发送新的资源。
缓存控制字段
Cache-Control
: 最常用的缓存控制字段。常见指令有:
max-age
: 设置缓存的最大存储时间,单位为秒。no-cache
: 每次请求都要向服务器验证缓存是否有效,当然还是会被缓存的。no-store
: 不缓存响应数据,每次都从服务器获取数据。public
: 表示响应可以被任何缓存存储(如 CDN)。private
: 表示响应仅限于用户私有缓存,不允许共享缓存(如代理服务器)。
Expires
: 指定资源的过期时间点(绝对时间),多用于 HTTP/1.0。
ETag
: 资源的唯一标识符,用于协商缓存。
Last-Modified
: 资源的最后修改时间,用于协商缓存。
工作流程
- 浏览器第一次请求资源时,服务器返回资源以及相关的缓存控制信息。
- 浏览器根据
Cache-Control
或Expires
决定是否将资源缓存。 - 下一次请求时,浏览器首先查看强缓存是否有效,如果有效,直接使用缓存。
- 如果强缓存失效,浏览器通过
If-Modified-Since
或If-None-Match
向服务器发起协商缓存请求。 - 服务器根据资源的状态决定返回 304 状态码(缓存未过期)或 200 状态码(缓存已过期并发送新资源)。
使用场景
强缓存:用于静态资源,比如文章图片等。
协商缓存:用于动态资源,比如实时数据,动态内容等。
# 8.TCP 和 UDP
TCP(传输控制协议)
- 面向连接:TCP 是一种面向连接的协议,这意味着在发送数据之前,通信双方必须建立一个连接。这个连接通过三次握手过程来建立。
- 可靠传输:TCP 提供可靠的数据传输,保证数据包按序到达且不丢失。如果某个数据包丢失或损坏,TCP 会自动重传该数据包,直到确认其正确接收。
- 流量控制和拥塞控制:TCP 有流量控制和拥塞控制机制,确保不会因发送速度过快导致接收方缓冲区溢出,并且可以根据网络状况动态调整发送速率,以避免网络拥塞。
- 有序传输:TCP 确保数据包按照发送顺序到达接收方。如果数据包乱序到达,TCP 会重新排序。
- 应用场景:TCP 适用于对数据传输可靠性要求较高的场景,比如 HTTP(网页浏览)、FTP(文件传输)、SMTP(邮件传输)等。
UDP(用户数据报协议)
- 无连接:UDP 是无连接的协议,数据传输前不需要建立连接,直接将数据包发送给接收方。
- 不可靠传输:UDP 不保证数据包的可靠性。数据包可能会丢失、重复或者乱序到达,且 UDP 不会进行重传或排序。
- 无流量控制和拥塞控制:UDP 不提供流量控制和拥塞控制机制,因此发送方可以任意速率发送数据,可能会导致网络拥塞。
- 传输速度快:由于没有连接建立和可靠性机制,UDP 的传输速度通常比 TCP 快,更适合实时性要求高的应用。
- 应用场景:UDP 适用于对传输速度和实时性要求较高,但对数据可靠性要求较低的场景,比如视频流传输、在线游戏、DNS 查询等。
# 9.TCP
# 三次握手
第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN,此时客户端处于 SYN_SENT 状态
第二次握手:服务器收到 SYN 报文后,回复一个 SYN-ACK 报文,表示同意建立连接,并向客户端确认。此时,服务器处于 SYN_RECV 状态。
第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,值为服务器的 ISN+1。此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。
# 四次挥手
第一次挥手(FIN):
主动关闭连接的一方(通常是客户端)发送一个 FIN 报文,表示它已经完成数据的发送,想要关闭连接。此时,客户端进入 FIN-WAIT-1 状态。
第二次挥手(ACK):
被动关闭连接的一方(通常是服务器)接收到 FIN 报文后,发送一个 ACK 报文进行确认,表示它已经接收到 FIN 报文,但还没有准备好关闭连接。此时,服务器进入 CLOSE-WAIT 状态,客户端则进入 FIN-WAIT-2 状态。
第三次挥手(FIN):
服务器准备好关闭连接时,也发送一个 FIN 报文,表示它也完成了数据的发送,准备关闭连接。此时,服务器进入 LAST-ACK 状态。
第四次挥手(ACK):
客户端接收到服务器的 FIN 报文后,发送一个 ACK 报文进行确认,表示它已经接收到服务器的 FIN 报文。此时,客户端进入 TIME-WAIT 状态,等待一段时间,以确保服务器收到了 ACK 报文后再正式关闭连接。最终,客户端进入 CLOSED 状态,连接完全关闭。
# 10.TLS 四次握手
1. 客户端请求建立 SSL 链接,并向服务端发送一个随机数–Client random 和客户端支持的加密方法,比如 RSA 公钥加密,此时是明文传输。
2. 服务端回复一种客户端支持的加密方法、一个随机数–Server random、授信的服务器证书和非对称加密的公钥。
3. 客户端收到服务端的回复后利用服务端的公钥,加上新的随机数–Premaster secret 通过服务端下发的公钥及加密方法进行加密,发送给服务器。
4. 服务端收到客户端的回复,利用已知的加解密方式进行解密,同时利用 Client random、Server random 和 Premaster secret 通过一定的算法生成 HTTP 链接数据传输的对称加密 key – session key。
# 11.content-type 的含义与常见的类型
Content-Type
是 HTTP 协议中用来表示请求或响应主体的媒体类型(MIME 类型)的头字段。它告诉客户端或服务器如何解析和展示消息主体的数据。
- text/plain
用于表示纯文本内容,不含任何格式信息。浏览器会按照纯文本形式渲染。 - text/html
用于表示 HTML 文档,浏览器会将其解析并渲染为网页。 - application/json
用于表示 JSON 格式的数据。常用于 API 接口的请求和响应中。 - application/xml
用于表示 XML 格式的数据,类似 JSON,也常用于数据交换。 - application/x-www-form-urlencoded
用于表示表单数据,这是 HTML 表单提交的默认编码类型,数据格式为键值对的形式。 - multipart/form-data
用于提交包含文件的表单,支持文件上传,是文件上传表单的常用类型。 - image/jpeg、image/png、image/gif
用于表示图片文件,根据图片的格式有不同的子类型,例如 JPEG、PNG 和 GIF。 - application/javascript
用于表示 JavaScript 脚本,浏览器会按脚本进行解析和执行。 - application/octet-stream
通用的二进制数据类型,用于下载任意文件。当服务器无法确定文件的具体类型时,也可以使用这种类型。
# 12.OSI 七层模型
便携记忆:自下而上是物、数、网、传、会、表、应
- 应用层
应用层位于 OSI 参考模型的第七层,其作用是通过应用程序间的交互来完成特定的网络应用。
该层协议定义了应用进程之间的交互规则,通过不同的应用层协议为不同的网络应用提供服务。例如域名系统 DNS
,支持万维网应用的 HTTP
协议,FTP,SMTP 等。
- 表示层
负责数据的格式转换、加密和解密,使不同系统间的数据可以理解和处理。
- 会话层
管理会话的建立、维护和终止,负责同步和对话控制。
- 传输层
提供端到端的通信,确保数据完整性和顺序。
传输层向高层屏蔽了下层数据通信的细节。因此,它是计算机通信体系结构中关键的一层
其中,主要的传输层协议是 TCP
和 UDP
。
- 网络层
负责数据包的转发和路由,确定数据从源到目的地的路径。
在网络层使用的协议是 IP 协议和许多路由协议,因此我们通常把该层简单地称为 IP 层
- 数据链路层
确保数据在物理层上可靠传输,负责帧的生成、传输和错误检测。
- 物理层
作为 OSI
参考模型中最低的一层,物理层的作用是实现计算机节点之间比特流的透明传送
# 13.HTTP 请求行的字段
User-Agent:客户端软件的信息。
Accept:客户端可接受的内容类型。
Content-Type:请求体的数据类型(主要用于 POST 请求)。
Authorization:用于身份验证的信息。
Cookie:发送给服务器的 cookie 数据。
# 其他
# 1.CommonJs 和 Es Module 的区别
ESM
// 导出模块 | |
export function myFunction() { | |
console.log("Hello from ES Module"); | |
} | |
// 导入模块 | |
import { myFunction } from './myModule.js'; | |
myFunction(); |
CMJ
// 导出模块 | |
module.exports = function() { | |
console.log("Hello from CommonJS"); | |
}; | |
// 导入模块 | |
const myModule = require('./myModule'); | |
myModule(); |
CJS 使用 require/module.exports,ESM 使用 import/export。
CJS 是动态导入,可以在代码运行时调用。ESM 是静态导入。
CJS 模块是同步加载的,ESM 的模块是异步加载的。
CommonJs 导出值是拷贝,可以修改导出的值,这在代码出错时,不好排查引起变量污染。
Es Module 导出是引用值之前都存在映射关系,值是可读的,不能修改。
# 2. 框架相对于原生解决了什么问题
1. 组件化开发:
- 解决的问题:在原生开发中,代码复用性较低,且难以维护和扩展。
- 框架的解决方案:框架(如 React、Vue、Angular)提供了组件化的开发方式,允许开发者将 UI 分解为可重用的独立组件,促进代码复用和可维护性。
2. 状态管理:
- 解决的问题:在复杂应用中,管理和同步状态变得困难。
- 框架的解决方案:框架提供了高效的状态管理工具(如 Redux、Vuex),简化了应用状态的管理和数据流的控制。
3. 路由管理:
- 解决的问题:原生 JavaScript 在处理单页应用的路由时较为繁琐。
- 框架的解决方案:框架自带的路由系统(如 React Router、Vue Router)提供了简洁的路由配置和导航控制,方便创建单页应用。
4. 双向数据绑定:
- 解决的问题:手动更新 DOM 以反映数据变化既繁琐又容易出错。
- 框架的解决方案:像 Angular 和 Vue 这样支持双向数据绑定的框架,可以自动将数据变化反映到 UI 上,并将用户输入的数据同步到模型中。
5. 模板引擎:
- 解决的问题:原生的 HTML 模板缺乏动态数据处理能力。
- 框架的解决方案:大多数框架都提供了强大的模板引擎,允许开发者在 HTML 中嵌入动态数据和逻辑(如 Vue 的模板语法、React 的 JSX)。
6. 开发工具和生态系统:
- 解决的问题:原生开发缺乏统一的工具和最佳实践。
- 框架的解决方案:框架通常附带开发工具、调试工具和丰富的插件生态系统,简化了开发、调试和部署过程。例如,Vue DevTools、React Developer Tools 等。
# 3. 前端常见的攻击方式
- XSS (Cross Site Scripting) 跨站脚本攻击
攻击者在目标网站注入恶意的客户端脚本(如 JavaScript)。当用户访问被感染的页面时,这些脚本会在用户浏览器中执行,窃取用户数据、劫持用户会话或重定向到恶意网站。
防护手段:在用户输入的过程中,进行合法输入验证,过滤掉用户输入的恶劣代码。
- CSRF(Cross-site request forgery)跨站请求伪造
攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。
防护手段:CSRF Token,双重 Cookie 验证,通过服务器生成的 token 作为请求头或者 cookie 表单中的隐藏字段发送。
同源检测,通过 Referer,它记录了该 Http 请求的来源地址进行检测。
- SQL 注入攻击
攻击者通过在输入字段中插入恶意的 SQL 代码,操纵数据库查询。这可能导致数据库泄露、数据篡改甚至系统权限提升。
防护手段:严格检查输入变量的类型和格式,过滤和转义特殊字符,对访问数据库的 Web 应用程序采用 Web 应用防火墙。
# 4.cdn 知识
CDN (全称 Content Delivery Network),即内容分发网络。依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。
# 5. 为什么 0.1+0.2!=0.3
因为 JavaScript 使用 IEEE 754 标准来表示浮点数,这种表示法在存储某些十进制数时会产生精度问题。进制转换和对阶过程会出现精度损失。二进制可以精确表示的十进制小数是那些可以写成 m/(2^n) 的形式。
解决方式
- 可以将浮点数转换为整数进行计算,然后再将结果转换回浮点数。
- 使用
toFixed
方法将结果四舍五入到指定的小数位数。注意,toFixed
返回的是字符串,需要将其转换回数字。 - 使用第三方库比如
decimal.js
或big.js
这样的库来处理精度问题。
# 6.SPA
SPA 是单页面应用,相对于 MPA 切换功能不需要跳页面。
优点
- 具有桌面应用的即时性、网站的可移植性和可访问性
- 用户体验好、快,内容的改变不需要重新加载整个页面
- 良好的前后端分离,分工更明确
缺点
- 不利于搜索引擎的抓取
- 首次渲染速度相对较慢
优化
SSR 服务端渲染
将组件或页面通过服务器生成 html,再返回给浏览器
# 7. 前端缓存
HTTP 缓存:详细见 HTTP 部分的 HTTP 缓存。有强缓存和协商缓存。
内存缓存(Memory Cache):内存缓存是指浏览器将资源暂存在内存中。由于内存的读写速度极快,内存缓存能够显著提高资源的访问速度。
磁盘缓存(Disk Cache):磁盘缓存是指浏览器将资源存储在硬盘(SSD 或 HDD)上的缓存机制。相较于内存缓存,磁盘缓存的容量更大,但访问速度稍慢。
Service Worker:Service Worker 是一种独立于网页运行的脚本,可以精细控制资源的缓存和更新策略。
Web Storage 缓存:包括 localStorage 和 sessionStorage。localStorage 用于存储用户在网站上的永久性数据,而 sessionStorage 则用于存储用户会话过程中的临时数据。
# 8. 跳出 forEach 循环
forEach()
是不能通过 break
或 continue
来中断循环的。可以通过 return 跳出本次循环。
如果需要跳出整个循环,可以抛出异常来跳出循环。
# 9.defer 和 async
defer
加载行为:脚本文件会在后台异步加载,浏览器继续解析 HTML 文档。
执行时机: ** 脚本会在 HTML 文档解析完毕后、DOMContentLoaded 事件触发之前按顺序执行。** 如果有多个带 defer
属性的脚本,它们会按在页面中出现的顺序执行。
async
加载行为:脚本文件同样会在后台异步加载,浏览器继续解析 HTML 文档。
执行时机: 一旦脚本加载完毕,就会立即执行,即使此时 HTML 文档还没有解析完成。如果有多个带 async
属性的脚本,它们的执行顺序是不确定的,取决于加载完成的先后顺序。
区别
defer
: 按顺序执行,并在文档解析完成后执行。
async
: 脚本一旦加载完毕,就立即执行,执行顺序不确定。
延伸:放于 head 或 body 的优缺点
head
优点:
- 脚本在页面解析前加载,保证脚本的依赖被尽早执行。
缺点:
-
阻塞页面的解析:浏览器会在加载并执行脚本时暂停 HTML 文档的解析,可能导致页面的渲染延迟,尤其是当脚本较大时。
使用
defer
或async
属性来避免阻塞页面的解析。
body
优点:
- 避免了阻塞页面解析。浏览器会优先解析和渲染页面的 HTML 内容,之后再加载并执行脚本,这样可以提高页面的初始渲染速度。
缺点:
- 如果脚本依赖于页面的某些内容而执行较早,可能导致一些功能的延迟或错误,尽管这种情况较为罕见。
# 10.URL 的结构
举例
https://www.example.com:8080/docs/index.html?search=javascript&sort=asc#section1
1. 协议(Scheme)
- 这部分指定了使用的协议,表示如何访问资源。常见的协议包括:
http
:超文本传输协议,不加密。https
:加密的超文本传输协议,更安全。ftp
:文件传输协议,用于文件传输。
- 示例:
https://
3. 主机名(Host)
- 这是资源所在的服务器的域名或 IP 地址。它用于标识目标服务器。
- 示例:
www.example.com
或192.168.0.1
4. 端口号(Port) (可选)
- 指定服务器使用的端口号。如果不指定端口号,默认使用协议的标准端口号(HTTP 使用 80,HTTPS 使用 443)。
- 示例:
:8080
5. 路径(Path)
- 路径指向服务器上的特定资源。它类似于文件系统中的路径结构。
- 示例:
/docs/index.html
6. 查询参数(Query)(可选)
- 查询参数用于传递额外的信息给服务器,通常是键值对的形式。多个参数使用
&
符号分隔。 - 示例:
?search=javascript&sort=asc
7. 片段标识符(Fragment Identifier) (可选)(也就是 hash)
- 指向网页中的某个部分,比如一个锚点。它通常用于在加载页面后跳转到特定位置。
- 示例:
#section1
# 12. 白屏排查
1. 检查浏览器控制台
- 打开浏览器的开发者工具(通常按
F12
或Ctrl+Shift+I
),切换到 Console 选项卡。 - 检查是否有 JavaScript 错误,错误信息通常会显示出是哪一行代码出现了问题,或者是哪个依赖没有正确加载。
2. 查看网络请求
- 切换到 Network 选项卡,刷新页面,查看是否有资源请求失败。
- 检查是否有 404、500 等错误状态码,特别是对于重要的资源(如
index.html
、app.js
、chunk.js
等)。 - 如果有接口请求,确认接口是否正常返回,是否有跨域问题(
CORS
)。
3. 检查打包配置
如果你正在使用像 Webpack
、 Vite
这样的打包工具,以下几项是常见的排查方向:
- 是否引入了错误的路径:某些文件或模块可能路径写错,导致打包时找不到文件。
- 代码分割(code splitting)问题:某些动态加载的资源未能正确加载,导致页面渲染失败。
- 环境变量问题:某些打包配置可能依赖特定环境变量,比如 API 地址等,检查是否在生产环境下正确配置了环境变量。
4. 检查前端路由配置
如果你使用了前端路由(如 Vue Router、React Router),可以检查路由配置是否有问题:
- 404 路由处理:是否有未捕获的路由导致白屏。
- 重定向配置错误:某些重定向可能配置错误,导致页面在空白页面停留。
5. 检查 CSS 和布局问题
- CSS 隐藏问题:某些错误的 CSS 样式可能隐藏了整个页面元素,导致看上去像白屏。检查是否有
display: none
或visibility: hidden
等样式。 - 布局错误:有时错误的布局计算(比如 flex 或 grid 布局错误)也会导致内容无法显示。
# 13.pnpm 与 npm 的区别
pnpm:采用硬链接和符号链接,它可以显著减少磁盘空间的占用。相同的依赖项只会存储一次。更快的安装速度、节省磁盘空间、高效的依赖管理方式以及更好的 monorepo 支持,适合对性能要求高、项目依赖复杂或者多个项目共享依赖的情况。
npm:npm 在每个项目的 node_modules
文件夹中下载所有依赖项,会有冗余的依赖副本。** 更广泛的使用,默认集成在 Node.js 环境中,更适合一些简单项目或对包管理工具没有太高性能要求的用户。
# 14. 判断 NaN
isNaN()
函数: 该函数用于判断一个值是否是 NaN
,但它的行为是先将参数转换为数字,然后再进行判断,这可能会导致意外的结果。
Number.isNaN()
方法: 这是 ES6 引入的更严格的判断方式,它只会在参数严格等于 NaN
时返回 true
,并且不会进行类型转换。
使用 Object.is()
方法: Object.is()
可以精确判断两个值是否相同,尤其适用于 NaN
的判断。
NaN !== NaN
: 由于 NaN
是 JavaScript 中唯一一个不等于自身的值,所以可以利用这一特性来判断。
# 15. 虚拟 DOM
Virtual DOM 本质上是 JavaScript 对象,是真实 DOM 的描述,用一个 JS 对象来描述一个 DOM 节点。
Virtual DOM 可以看做一棵模拟 DOM 树的 JavaScript 树,其主要是通过 VNode 实现一个无状态的组件,当组件状态发生更新时,然后触发 Virtual DOM 数据的变化,然后通过 Virtual DOM 和真实 DOM 的比对,再对真实 DOM 更新。可以简单认为 Virtual DOM 是真实 DOM 的缓存。
# 16. 为什么操作虚拟 DOM 比真实 DOM 好
- 减少重排和重绘,提升性能:真实 DOM 的每一次操作都会触发浏览器的重排和重绘,特别是在频繁更新时,性能消耗巨大。而虚拟 DOM 是 JavaScript 对象的轻量表示,所有更新都在内存中进行。在应用变更之前,通过对比新旧虚拟 DOM,仅将最小的差异应用到真实 DOM,避免频繁的页面更新操作,从而有效减少重排和重绘带来的性能开销。
- 保证性能下限:虚拟 DOM 虽然并不是最优的 DOM 操作方式,但其通用性设计可以适应各种场景下的操作。相比直接操作真实 DOM 时可能带来的性能问题,虚拟 DOM 在不需要手动优化的情况下,依然能保证良好的性能表现,为开发者提供了性能的安全底线。
- 便于状态管理和调试:虚拟 DOM 是基于 JavaScript 对象的抽象,这使得它能够轻松保存和管理组件的状态。通过新旧虚拟 DOM 的对比,框架可以自动判断哪些部分需要更新,帮助开发者快速定位渲染问题,降低了复杂性,尤其是在大型应用中,调试变得更加直观。
- 跨平台能力:虚拟 DOM 不仅可以渲染到真实 DOM,还可以渲染到其他平台(如 React Native 生成原生组件,Weex 渲染成小程序组件等)。直接操作真实 DOM 只局限于浏览器,而虚拟 DOM 的抽象使得其具有更强的跨平台能力。
# 17.Etag 解决了 Last-modified 什么问题
精确度问题: Last-Modified
依赖文件的修改时间,但有时候文件内容改变了,修改时间却没有更新(如某些程序手动调整了时间戳);反之,文件内容未变,但时间戳因为轻微的编辑或移动操作被修改了。 ETag
是基于文件内容生成的哈希值,因此能够更加准确地反映文件内容的变化。
修改粒度问题: Last-Modified
只能精确到秒级别,这意味着如果文件在一秒内发生多次修改, Last-Modified
无法识别这些变化。而 ETag
能够检测到每一次的内容改变,避免这种限制。
文件未变化但时间变化的问题:使用 Last-Modified
时,即便文件内容没有变化,只要文件的时间戳发生改变,客户端就会重新下载文件。而 ETag
只关注文件内容,因此在内容未变的情况下,即使时间戳改变,客户端也能正确缓存,不必重新下载。
# 18.package.json 中的 script 部分是什么
package.json
中的 scripts
部分用于定义项目中的可执行命令。你可以在这里指定各种任务,如构建、测试、启动开发服务器等。
{ | |
"name": "my-project", | |
"version": "1.0.0", | |
"scripts": { | |
"start": "node server.js", | |
"build": "webpack --mode production", | |
"test": "jest" | |
} | |
} |
在上述例子中可以通过以下命令来执行不同的任务:
npm run start
会启动server.js
。npm run build
会使用 Webpack 构建项目。npm run test
会运行 Jest 测试。
# 19.devdependencies 和 dependencies 的区别
用于管理项目的依赖。
dependencies:
- 包含项目在生产环境中运行所需的依赖。
- 这些库和框架是应用的核心部分,必须在应用上线时可用。
- 安装时使用命令
npm install <package>
。
devDependencies:
- 包含开发环境中使用的依赖,如测试框架、构建工具和开发服务器。
- 这些依赖在生产环境中并不需要,主要用于开发、测试和构建过程。
- 安装时使用命令
npm install <package> --save-dev
。
# 20. 进程和线程
- 本质区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。
- 在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
- 所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过 CPU 调度,在每个时间片中只有一个线程执行)
- 内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了 CPU 外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。
- 包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
# 21.webpack 的 loader 和 plugin 的区别
loader:** 用来转换或处理模块的内容。** 比如,你可以使用 loader 将 TypeScript 转换为 JavaScript,或者将 SCSS 转换为 CSS。
在在模块被加载时应用的。
plugin:用来执行更广泛的任务,除了处理模块外,还可以影响构建过程的各个方面。比如打包优化、资源管理、环境变量注入等
在整个构建过程中起作用,通过生命周期钩子运行。
# 22. 响应式布局
媒体查询
百分比布局
rem 布局
视口单位
# 23.Vuex 和 Pinia 区别
开发体验
- Vuex:Vuex 的写法相对来说比较复杂,尤其是要分开写 state、getters、mutations、actions、module 等,代码结构会比较繁琐。
- Pinia:Pinia 的 API 设计更加简洁,减少了样板代码。拥有 state、getters、actions。Pinia 的 store 就像一个普通的 JavaScript 模块,可以直接操作 state 而不需要通过 mutations。
模块化
- Vuex:Vuex 通过模块化系统来处理大型项目,模块本身也需要配置 namespaced 来避免命名冲突。
- Pinia:Pinia 使用每个 store 都是独立的模块,模块间的依赖关系更加清晰,模块名自动避免冲突,不需要额外配置 namespaced。
支持 Composition API
- Vuex:Vuex 支持 Options API,但在支持 Composition API 时,需要一些额外的工作和配置,比如 useStore。
- Pinia:Pinia 原生支持 Vue 3 的 Composition API,结合 setup () 语法,写法更加简洁、直观。
类型支持(TypeScript)
- Vuex:Vuex 的类型推导比较复杂,特别是当项目模块化且使用 TypeScript 时,可能需要手动设置和调整类型。
- Pinia:Pinia 对 TypeScript 有更好的支持,提供了完善的类型推导,开发者无需手动处理复杂的类型配置。
插件系统
- Vuex:Vuex 提供了插件系统来扩展其功能。生态相对更好。
- Pinia:Pinia 同样支持插件,并且更加轻量和灵活。
# 24.vite 为什么快
-
在开发环境中,
Webpack
是先打包再启动开发服务器,而Vite
则是直接启动,然后再按需编译依赖文件。 -
Webpack 是基于
Node.js
构建的,而 Vite 则是基于esbuild
进行预构建依赖。esbuild 是采用Go
语言编写的。 -
在 Webpack 中,当一个模块或其依赖的模块内容改变时,需要
重新编译
这些模块。而在 Vite 中,当某个模块内容改变时,只需要让浏览器
重新请求
该模块即可,这大大减少了热更新的时间。