前端面试题总结
从面经、博客和Github里搜刮总结出来的,后面有空再分类吧。。。
算法
两个有序数组合并
1 |
|
反转链表
- 栈
- 双链表
- 双指针
1 |
|
1 |
|
1 |
|
两个字符串的最长公共子串
1 |
|
合并两个有序链表
1 |
|
连续子数组的最大和
1 |
|
求集合的所有子集
1 |
|
JavaScript
闭包
闭包是指有权访问另一个函数作用域中的变量的函数–《JavaScript高级程序设计》
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
1 |
|
闭包的作用:
- 私有变量
- 函数工厂
- 柯里化
柯里化
1 |
|
JavaScript 内存管理,垃圾回收,内存泄漏
JavaScript的基本数据类型是存在栈中,引用数据类型存在堆中同时会在栈中存储一个引用指向堆中对应空间的起始地址。
堆中某块空间在栈中没有引用关系时候,就需要进行垃圾回收,垃圾回收策略:
- 标记清除算法:
- 优点:简单
- 缺点:产生碎片,慢,下一个对象进来要找大于size的位置
- 标记整理算法:与清除相似,会移动清除边界内存
- 引用计数算法:
- 优点:清晰,不会阻塞JavaScript线程
- 缺点:计时器占空间,循环引用问题
- 标记清除算法:
内存泄漏的原因:
闭包使用不当
全局变量
分离的DOM节点
控制台的打印
遗忘的定时器
事件监听未移除:重复监听
内存泄漏排查
- performance打开内存选项
- memory标签 过一会搭一个堆快照查看统计信息
CommonJS和ES Module
早期的JavaScript模块化是通过script标签引入js文件夹实现的,但是项目过于庞大之后引入js文件就会越来越多,会造成很多问题
- 不同js文件之间没有隔离,会造成变量污染
- js文件过多不好维护
- js之间可能有依赖关系,引入顺序出错会报错
为了解决模块化问题,社区出现了CommonJS,后面ES6也正是加入了ES Module模块。
- 解决变量污染问题,每个文件都是独立的作用域,所以不存在变量污染
- 解决代码维护问题,一个文件里代码非常清晰
- 解决文件依赖问题,一个文件里可以清楚的看到依赖了那些其它文件
CommonJS
- 暴露模块:本质上都是修改exports对象的值,可以混用
1
2
3
4
5
6
7
8
9
10
11// 导出一个对象
module.exports = {
name: "Sonce",
age: 22,
sex: "male"
}
// 导出任意值
module.exports.name = "Sonce"
module.exports.sex = null
module.exports.age = undefined- 引入模块:导入的值是深拷贝的,第一次导入后重复导入不会执行,支持动态导入
1
2
3
4
5
6// index.js
module.exports.name = "Sonce"
module.exports.age = 22
let data = require("./index.js")
console.log(data) // { name: "Sonce", age: 22 }ES Modules
- 暴露模块:
1
2
3
4
5
6
7
8
9
10
11
12
13// 导出变量
export const name = "Sonce"
export const age = 22
// 导出函数也可以
export function fn() {}
export const test = () => {}
// export default 和 export 可以同时使用且不相互影响
export default {
fn() {},
msg: "hello Sonce"
}- 引入模块:
import { xxx } from 'xxx'
和import xxx from 'xxx'
1
2
3
4
5// 导入默认模块不用花括号
import defalut from './index.js'
// 导入其他模块要使用花括号且命名相同
import { name, age } from './index.js
事件循环
- 常见的宏任务和微任务
- 宏任务(macrotask):script(整体代码)、setTimeout、setInterval、I/O、事件、postMessage、 MessageChannel、setImmediate (Node.js)
- 微任务(microtask):Promise.then、 MutaionObserver、process.nextTick (Node.js)
一次事件循环过程:
执行一个宏任务(栈中没有就从事件队列中获取)
执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
当前宏任务执行完毕且微任务队列中没有微任务,开始检查渲染,然后GUI线程接管渲染
渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
经典题目:
1 |
|
执行结果:
- script start
- async1 start
- async2
- promise1
- script end
- async1 end
- promise2
- setTimeout
作用域和作用域链
三种作用域
全局作用域
1
2
3
4
5
6var greeting = 'Hello World!'
function greet(){
console.log(greeting)
}
// 打印 'Hello World!'
greet()函数作用域
1
2
3
4
5
6
7
8function greet(){
var greeting = 'Hello World!'
console.log(greeting)
}
// 打印 'Hello World!'
greet()
// 报错: Uncaught ReferenceError: greeting is not defined
console.log(greeting)块级作用域(ES6)
1
2
3
4
5
6
7
8
9{
let greeting = 'Hello World!'
var lang = 'English'
console.log(greeting) // Prints 'Hello World!'
}
// 变量 'English'
console.log(lang)
// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting)
作用域链
当在JavaScript中使用一个变量的时候,首先JavaScript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。
ES6新特性
let和const关键字
- 都会形成暂时性死区,在变量声明之前不可用
- 都不可用重复声明
- const定义的变量不可修改
var声明的变量挂在window上,可以用window.或者this.访问
变量解构赋值
1
2
3
4
5
6
7
8
9
10
11// 数组
let arr = ['foo','bar']
let [foo, bar] = arr
// 对象
let obj = {
name: 'Sonce',
age: 22,
sex: 'male'
}
let {name, age, sex} = obj模板字符串
1
2let myName = 'Sonce'
console.log(`Hello ${myName}`)简化对象写法:命名相同不用再用冒号
1
2
3
4
5
6
7
8
9let name = 'Sonce',
let age = 22,
let sex = 'male'
let info = {
name,
age,
sex
}箭头函数
Symbol
- Symbol 可以创建一个唯一的值
1
let mySymbol = Symbol('just a symbol') // symbol是基本数据,不需要new,传入的参数是描述
- 相同描述的Symbol并不相同,可以用
Symbol.for()
去获取上下文的Symbol
1
2
3
4
5
6
7let Symbol_1 = Symbol('Symbol')
let Symbol_2 = Symbol('Symbol')
console.log(Symbol_1 === Symbol_2) // false
let Symbol_3 = Symbol.for('Symbol.for')
let Symbol_4 = Symbol.for('Symbol.for')
console.log(Symbol_3 === Symbol_4) // true- Symbol可以作为对象的键值,
Symbol.valueOf()
获得的就是当前Symbol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27let mySymbol = Symbol('just a symbol')
let obj = {}
obj[mySymbol] = 'mySymbol'
obj[mySymbol.description] = 'description'
obj[mySymbol.toString()] = 'toString'
console.log(obj)
/*
{
'just a symbol': 'description',
'Symbol(just a symbol)': 'toString',
[Symbol(just a symbol)]: 'mySymbol' 可以看到这里没有引号
}
*/
obj[mySymbol.valueOf()] = 'valueOf'
console.log(obj)
/*
{
'just a symbol': 'description',
'Symbol(just a symbol)': 'toString',
[Symbol(just a symbol)]: 'valueOf' 可以看到这里没有引号
}
*/Symbol 的应用场景
消除与业务无关的字符
作为对象属性(遍历对象时不会遍历到Symbol键值)
模拟类的私有方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21const _age = Symbol()
class Person {
constructor(name, age) {
this.name = name
this[_age] = age
}
getAge() {
console.log(this[_age])
}
}
let person = new Person('Sonce', 21)
console.log(person.name, person._age) // 这里的_age被当成字符串了
// Sonce undefined
console.log(person.name, person[_age]) // 这样可以访问的,但是经过模块化可以让使用者拿不到_age,也就访问不到了
// Sonce 21
person.getAge() // 只能用内部的函数去拿
// 21
Promise
Promise是异步微任务,解决了异步任务多层回调嵌套问题,使代码可读性更高。嵌套 -> 链式
Promise有三种状态:pending,fullfill,reject
Promise.all()
、Promise.any()
、Promise.race()
Promise如何实现链式调用
then 方法中,可以创建并返回新的 Promise 实例,这是串行Promise的基础,是实现真正链式调用的根本
Map和Set
Map
Map
对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者基本类型)都可以作为一个键或一个值。- Map.prototype.set():设置key,value
- Map.prototype.get():根据key获取value
- Map.prototype.size:获取Map大小
- Map.prototype.clear():清空Map
- Map.prototype.has():查询是否有key
- Map.prototype.keys():返回key的iterable
- Map.prototype.values():返回values的iterable
1
2
3
4
5
6
7
8
9
10
11
12let map = new Map()
map.set('1', { name: 'foo', age: 22 })
map.set('2', { name: 'bar', age: 21 })
console.log(map.get('1')) // { name: 'foo', age: 22 }
console.log(map)
/**
Map(2) {
'1' => { name: 'foo', age: 22 },
'2' => { name: 'bar', age: 21 }
}
*/Set
Set
对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。- Set.prototype.add():向一个
Set
对象的末尾添加一个指定的值。 - Set.prototype.clear():清空Set
- Set.prototype.delete():删除指定元素
- Set.prototype.has():has() 方法返回一个布尔值来指示对应的值 value 是否存在 Set 对象中。
- Set.prototype.values():**
values()
** 方法按照元素插入顺序返回一个具有Set
对象每个元素值的全新Iterator
对象。
- Set.prototype.add():向一个
ES Module
如何遍历对象
for...in
:遍历对象的key,包含继承的key
可以用for…of遍历可迭代对象的值,Array,Map,Set等
Object.keys()
:返回一个对象的key数组,不包含继承的keyObject.values()
:返回一个对象的value数组,不包含继承的keyObject.getOwnPropertyNames()
:返回一个对象的key数组,不包含继承的key
判断是不是数组
typeof无法区分
1
console.log(typeof []) // Object
instanceof
1
console.log([] instanceof Array) //true
constructor
1
console.log([].constructor) // [Function: Array]
toString
1
console.log(Object.prototype.toString.call([])) // [object Array]
isArray
1
console.log(Array.isArray([])) // true
ES6中的proxy,Vue3双向绑定
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
1
2
3
4
5
6
7
8
9
10
11
12const handler = {
get: function(obj, prop) {
return prop in obj ? obj[prop] : 37;
}
};
const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b); // 1, undefined
console.log('c' in p, p.c); // false, 37handler:前三个用到比较多
- handler.apply(): 拦截函数调用
- handler.get(): 拦截对象读取属性操作
- handler.set(): 拦截对象修改属性操作
- handler.construct()
- handler.defineProperty()
- handler.deleteProperty()
- handler.getOwnPropertyDescriptor()
- handler.getPrototypeOf()
- handler.has()
- handler.isExtensible()
- handler.ownKeys()
- handler.preventExtensions()
- handler.setPrototypeOf()
原型链介绍一下
- 原型链的尽头是什么
- 函数的原型是什么
- 对象的原型是什么?
JavaScript数据可以无限大吗
- 所有 JavaScript 数字均为 64 位
- 数组长度最大为2^32-1
说说节流和防抖
优化高频率执行js代码的一种手段,js中的一些事件如浏览器的resize、scroll,鼠标的mousemove、mouseover,input输入框的keypress等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。为了优化体验,需要对这类事件进行调用次数的限制。
节流:每隔一段时间,只执行一次函数。
应用场景:
- 滚动加载,加载更多或滚到底部监听
- 谷歌搜索框,搜索联想功能
- 高频点击提交,表单重复提交
1
2
3
4
5
6
7
8
9
10function throttle(fn, wait) {
let pre = new Date()
return function () {
let now = new Date()
if (now - pre >= wait) {
fn.apply(this, arguments)
pre = now
}
}
}防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
应用场景:
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
- 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
1
2
3
4
5
6
7
8
9function debounce(fn, wait) {
let timer = null
return function () {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arguments)
}, wait)
}
}
window对象和document对象有什么区别?
- document对象指的是当前浏览器窗口的HTML文档,是window的一个子对象
- window对象指的是当前浏览器窗口
怎么把字符串转换成json?
1 |
|
0.1 + 0.2 !== 0.3
转化为十进制计算中的进度丢失,包括转化中的丢失和对阶运算时候的丢失
async await 原理
实际上是Promise的语法糖
async
1
2
3
4
5
6
7
8
9let fn = async () => {
console.log('async')
}
console.log(fn()) // Promise { 'async' }
let fn = async () => {
return 'async'
}
fn().then((val) => console.log(val)) // asyncawait
如果await等到的是一个 Promise 对象,会阻塞后面的代码(后面的代码会加入到微任务队列中),等待 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。
1
2
3
4
5
6
7
8
9let fn1 = async () => {
return 'async'
}
let fn2 = async () => {
let msg = await fn1()
return msg
}
fn2().then((val) => console.log(val))await阻塞例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26let fn1 = async () => {
console.log('fn1 start')
}
let fn2 = async () => {
console.log('fn2 start')
let msg = await fn1()
// 后面的代码阻塞,被加入微任务中
console.log('fn2 end1')
console.log('fn2 end2')
}
console.log('start')
fn2()
console.log('end')
/** 结果为
start
fn2 start
fn1 start
end
fn2 end1
fn2 end2
*/
JavaScript多线程通信
JavaScript是单线程语言,所以运行时会造成阻塞,所以有了异步操作,对高负载的任务比如网络请求使用异步处理,放入浏览器的任务队列中去,等线程空闲时再执行。但是异步终究还是单线程,不能从根本上解决问题,所以多线程(Web Worker)就应运而生,它是HTML5标准的一部分,这一规范定义了一套 API,允许一段JavaScript程序运行在主线程之外的另外一个线程中。
- 什么是Web Worker
基本使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15if (window.Worker) {
let myWorker = new Worker('worker.js')
myWorker.postMessage('hello, world') // 发送信息到子线程
myWorker.onmessage = function (event) {
// 接收主线程发送过来的数据
console.log('Received message from main ' + event.data)
// 返回给主线程
postMessage('Posting message back to main script')
}
self.onmessage(function (event) {
// 主线程打印子线程发送过来的数据
console.log('Received message from myWorker ' + event.data)
})
}
严格模式
在JavaScript脚本最前面放一个语句:'use strict'
严格模式的好处
- 消除代码运行的一些不安全之处,保证代码运行的安全;
- 提高编译器效率,增加运行速度;
- 为未来新版本的JavaScript做好铺垫。
严格模式的限制:
- 不允许使用未声明的变量
1
2"use strict";
x = 1; // 报错- 不允许删除变量或者对象
1
2
3"use strict";
var x = 3.14;
delete x; // 报错- 不允许变量重名
1
2"use strict";
function x(p1, p1) {}; // 报错- 不允许使用八进制
- 不允许使用转义字符
- 不允许对只读属性赋值
- 不允许对一个使用getter方法读取的属性进行赋值
- 不允许删除一个不允许删除的属性
- 变量名不能使用 “eval” 字符串
- 变量名不能使用 “arguments” 字符串
- 不允许使用以下这种语句
- 由于一些安全原因,在作用域 eval() 创建的变量不能被调用
- 禁止this关键字指向全局对象
fill和map问题
map,forEach会跳过数组的空项,下面以map为例:
1
2
3
4
5
6
7
8
9let arr = new Array(3)
console.log(arr.map(() => 1)) // [ <3 empty items> ]
let arr = new Array(3)
arr[1] = 1
console.log(arr.map(() => 2)) // [ <1 empty item>, 2, <1 empty item> ]
let arr = new Array(3).fill(null)
console.log(arr.map(() => 2)) // [ 2, 2, 2 ]
深拷贝
- 数组的话可以使用
[].concat
Object和Map的区别
- Map可以解决同名碰撞问题
- Map可以用for of迭代
- Map可以直接拿到长度
- Map保持顺序
- Map可以用省略号展开
- Map的键值可以是任意的数据类型
JavaScript为什么是单线程
因为JavaScript主要是用来开发前端的项目,涉及到很多DOM树和CSS样式的操作,如果是多线程的话可能会出现 UI 操作的冲突。
集合变成数组的方式
Object.values()
Object.keys()
JavaScript实现继承
通过原型链继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/**
* 1、通过原型链继承
* 缺点如下:
* 《1》原型中包含的引用值会在所有实例之间共享,修改一个实例,另一个实例会跟着修改
* 即:通过将属性定义在构造函数中
* 《2》子类实例化时,无法给父类构造函数传参
*/
// 父类
function SuperType() {
this.colors = ['red', 'blue', 'green']
}
// 子类
function SubType() { }
SubType.prototype = new SuperType()
let instance1 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) // "red,blue,green,black"
let instance2 = new SubType()
console.log(instance2.colors) // "red,blue,green,black"借用构造函数继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/**
* 2、借用构造函数继承
* 缺点如下:
* 《1》必须在构造函数中定义方法,因此函数不能复用
* 《2》子类不能访问父类原型上定义的方法
* 为确保 SuperType 构造函数不会覆盖 SubType 定义的属性,
* 可以在调用父类构造函数之后再给子类实例添加额外的属性
*/
// 父类
function SuperType(name) {
this.name = name
}
// 子类
function SubType(name1) {
SuperType.call(this, name1)
this.age = 28
}
let instance1 = new SubType('测试名称')
console.log(instance1.name) // 测试名称
console.log(instance1.age) // 28
ES5是如何实现块级作用域的?
立即执行函数实现:(函数执行完,立即被调用)
1 |
|
catch之后的then会不会执行
会,可以改用try…catch来捕获错误
Promise
Promise.all()
和Promise.race()
区别
模板字符串
浏览器
浏览器进程和线程有哪些
浏览器是多进程的
- Browser进程:浏览器的主进程,负责协调和主控
- 插件进程:一个插件对应一个进程,使用时创建
- GPU进程:最多一个,用于3D绘制
- 浏览器渲染进程(内核):一个Tab一个进程,互不影响,控制页面渲染,脚本执行和事件处理
浏览器内核是多线程的
GUI线程
负责渲染浏览器界面,重绘(Repaint)和回流(reflow)的时候执行该线程。
JavaScript线程
也可以称为JavaScript内核,主要负责解析和运行JavaScript脚本程序,例如V8引擎。JavaScript是单线程的。
GUI 渲染线程 与 JavaScript引擎线程互斥!
事件触发线程
当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。
定时器线程
浏览器定时计数器并不是由JavaScript引擎计数的, 因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。
网络请求线程
浏览器有一个单独的线程用于处理AJAX请求,当请求完成时,若有回调函数,将回调事件放入到事件队列中
渲染页面的过程
处理HTML标记并构造DOM树,字节流 → 字符流 → Tokens → 节点 →DOM树
- HTML 解析到 JavaScript 脚本时会停止对 DOM 的解析,因为JS中可能包含了操作DOM的代码 (在srcipt后加上async defer 可以不阻塞DOM解析)
- async 异步执行,通常是与DOM无关的js脚本
- defer 延迟执行,但仍会下载
- HTML 解析到 style 的内联样式和 link 会将 CSS 交由 CSS 渲染器构建 CSSOM;不会阻塞DOM的解析
- CSS 的解析会阻塞 JS 的执行,所以在某些情况下 CSS 的解析会阻塞 DOM 的解析
- HTML 解析到 JavaScript 脚本时会停止对 DOM 的解析,因为JS中可能包含了操作DOM的代码 (在srcipt后加上async defer 可以不阻塞DOM解析)
合成Render树
将DOM和CSSOM组合成一个Render树,计算样式树或渲染树从 DOM 树的根开始构建,遍历每个可见节点。
不可见,拥有display: none样式的结点不可见布局(layout)
在渲染树上运行布局以计算每个节点的几何体。布局是确定呈现树中所有节点的宽度、高度和位置,以及确定页面上每个对象的大小和位置的过程。回流是对页面的任何部分或整个文档的任何后续大小和位置的确定。
绘制(paint)
最后一步是将各个节点绘制到屏幕上,第一次出现的节点称为 first meaningful paint (en-US)。在绘制或光栅化阶段,浏览器将在布局阶段计算的每个框转换为屏幕上的实际像素。绘画包括将元素的每个可视部分绘制到屏幕上,包括文本、颜色、边框、阴影和替换的元素(如按钮和图像)。浏览器需要非常快地完成这项工作。
V8垃圾回收
Scavenge算法:两块空间,空间角色互换
Mark-Sweep & Mark-Compact:空间换时间
分代回收
- 新生代使用Scavenge算法进行垃圾回收,新生代满足一定条件后晋升到老生代中
- 老生代使用Mark-Sweep & Mark-Compact算法进行垃圾回收
全停顿(Stop-The-World)
垃圾回收也是使用的JS引擎,而JS引擎是单线程的所以会造成阻塞,老生代垃圾回收消耗的时间比较长,为了避免JavaScript应用逻辑和垃圾回收器的内存资源竞争导致的不一致性问题,垃圾回收器会将JavaScript应用暂停,这个过程,被称为全停顿(stop-the-world)。
在新生代中,由于空间小、存活对象较少、Scavenge算法执行效率较快,所以全停顿的影响并不大。而老生代中就不一样,如果老生代中的活动对象较多,垃圾回收器就会暂停主线程较长的时间,使得页面变得卡顿。
Orinoco优化
- 增量标记 - Incremental marking
- 懒性清理 - Lazy sweeping
- 并发 - Concurrent
- 并行 - Parallel
浏览器的事件机制
“DOM2级事件“规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。——《JavaScript高级程序设计》
根据W3C模型,事件首先被目标元素所捕获,然后向上冒泡。——《基于MVC的JavaScript Web富应用开发》
事件冒泡
从内层originTarget节点开始触发事件,由内向外传播,逐级冒泡直到顶层节点结束。
事件捕获
从顶层的父节点开始触发事件,从外到内传播,到触发事件originTarget结束。
事件流模型
事件流模型顺序:
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
addEventListener最后一个参数默认为false,false表示事件在冒泡阶段触发,true则表示事件在捕获阶段触发
1
2btn.addEventListener('click', clickFunction, false); // 冒泡阶段触发
btn.addEventListener('click', clickFunction, true); // 捕获阶段触发
内存泄漏排查
Chrome工具
Vue
讲一下Vue Router
Vue Router 是 Vue.js 的官方路由,在patpat项目上我就是用Vue Router来为安卓端提供不同的页面的
router-link
Vue Router没有用常规的
a
标签,而是使用一个自定义组件router-link
来创建链接。这使得 Vue Router 可以在不重新加载页面的情况下更改 URL。router-view
router-view
将显示与 url 对应的组件。动态路由匹配
1
2
3
4
5
6
7
8
9const User = {
template: '<div>User</div>',
}
// 这些都会传递给 `createRouter`
const routes = [
// 动态字段以冒号开始
{ path: '/users/:id', component: User },
]可以通过
$router.params.id
来访问字段
组件之间传值
Props声明
1
<MyComponent foo="hello" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<script setup>
const props = defineProps({
foo: Array
})
console.log(props.foo)
</script>
export default {
props: {
foo: Array
},
setup(props) {
// setup() 接收 props 作为第一个参数
console.log(props.foo)
}
}动态Prop
1
2<MyComponent :title="post.title" />
<MyComponent v-bind:title="post.title" />一个对象绑定多个prop
1
<BlogPost v-bind="post" />
单向数据流
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
Vue2,Vue3的diff算法
Vue2 双端diff算法
首先进行首尾对比,这样找到的可复用节点一定是性能最优(即原地复用 DOM 节点,不需要移动)。
首尾对比完交叉对比,这一步即寻找移动后可复用的节点。
然后在剩余结点中对比寻找可复用 DOM,为了快速对比,于是创建一个 map 记录 key,然后通过 key 查找旧的 DOM。
最后进行善后工作,即移除多余节点或者新增节点。
Vue3 快速diff算法
首先进行首尾对比,这样找到的一定是性能最优,即原地复用 DOM 节点,不需要移动。
Vue3没有首尾交叉对比的步骤。
然后创建一个新节点在旧的 dom 中的位置的映射表,这个映射表的元素如果不为空,代表可复用。
然后根据这个映射表计算出最长递增子序列,这个序列中的结点代表可以原地复用。之后移动剩下的新结点到正确的位置即递增序列的间隙中。
vue2、vue3 的 diff 算法实现差异主要体现在:处理完首尾节点后,对剩余节点的处理方式。
在 vue2 中是通过对旧节点列表建立一个
{ key, oldVnode }
的映射表,然后遍历新节点列表的剩余节点,根据newVnode.key
在旧映射表中寻找可复用的节点,然后打补丁并且移动到正确的位置。而 vue3 则是建立一个存储新节点数组中的剩余节点在旧节点数组上的索引的映射关系数组,建立完成这个数组后也即找到了可复用的节点,然后通过这个数组计算得到最长递增子序列,这个序列中的节点保持不动,然后将新节点数组中的剩余节点移动到正确的位置。
Vue 组件通信有哪些方法
- prop
- emit
- provide && inject
Vue父子,祖孙等传值方式,provide/inject怎么实现响应式更新
传一个响应式的基本类型或者用对象封装
Vue的$nexttick有啥用?
Vue实现响应式不是数据改变后立马修改DOM,而是有一定的延迟,如果想访问修改后的DOM就需要使用$nexttick,函数会在下一个DOM更新后回调。
v-html的缺点
- 有XSS风险
- 子元素会被覆盖
computed和watch
v-if和v-show
- v-if 修改的是DOM中的存在
- v-show 修改的是display属性
- 频繁修改的元素应该使用v-show
v-model
- 双向绑定
vue的生命周期
Create(创建阶段)
beforeCreate
new Vue()
之后的第一个钩子,此时data
、methods
、computed
以及watch
上的数据和方法还未初始化,都不能被访问。created
在实例创建完成后被立即调用,此时已完成以下的配置:数据观测 (data observer),property 和方法的运算,watch/event 事件回调。
What to do:
data 和 methods 都以及被初始化好了,如果要调用methods 中的方法,或者操作 data 中的数据,最早可以在这个阶段中操作。
无法与Dom进行交互,如果非要想,可以通过
nextTick
来访问Dom。nextTick
接受一个函数,在下一次DOM刷新时调用,也可以用await nextTick()
等待下一次DOM刷新异步数据的请求适合在 created 的钩子中使用,例如数据初始化。
Mount(挂载阶段)
beforeMount
发生在挂载之前,在这之前 template 模板已导入渲染函数编译。此时虚拟Dom已经创建完成,即将开始渲染。
mounted
在挂载完成后发生,此时真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。
Update(更新阶段)
beforeUpdate
发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重新渲染,但会再次触发当前钩子函数。
updated
发生在更新完成之后,此时的DOM已经更新。现在可以执行依赖更新后的DOM的操作。应该避免在此期间更改状态。如果要相应状态改变,最好使用计算属性或 watcher 取而代之。最好不要在此期间更改数据,因为这可能会导致无限循环的更新。
Unmounted(卸载阶段)
beforeDestroy
发生在实例销毁之前,在这期间实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。
destroyed
发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,事件监听器被移除,所有子实例也统统被销毁。
异步组件
Vuex
还没用过
Vue响应式原理
- vue2
- 通过defineProperty对对象的已有属性值的读取和修改进行劫持(监视/拦截),通过重写数组更新数组一系列更新元素的方法来实现元素修改的劫持
- 对象直接新添加的属性或删除已有属性,界面不会自动更新,直接通过下标替换元素或更新length,界面不会自动更新arr[1] = {}
- vue3
- 通过Proxy(代理):拦截对data任意属性的任意(13种)操作,包括属性值的读写,属性的添加,属性的删除等,通过Reflect(反射):动态对被代理对象的相应属性进行特定的操作。
v-for为什么使用key
key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key,更准确, 更快的找到对应的vnode节点,设置key
能够大大减少对页面的DOM
操作,提高了diff
效率。
Vue3比Vue2有什么优势
- Composition API 和 setup组件
- Diff算法的提升
- 更好的TS支持
- 数据绑定改用proxy实现
Option API和Composition API对比
Composition API和React Hook对比
setup中如何获取vue实例
setup中没有this(undefined),需要使用 getCurrentInstance
Vue.use
用来安装插件,我就安装过unocss
Vue 中为什么 data 是一个函数(为什么通过函数返回一个对象的方式来实现),为什么要这么设计?
组件可能会在多个地方使用,使用函数返回一个对象声明了新的变量,不同实例之间不会干扰到
computed和watch的区别
computed是用来缓存计算结果,当相关的prop和data没有改变时不会重复计算,会返回上一次的计算结果,watch是用来监听数据变化的,可以在监听到数据变化后做一些操作。
如果一个值依赖多个属性(多对一),用computed肯定是更加方便的。如果一个值变化后会引起一系列操作,或者一个值变化会引起一系列值的变化(一对多),用watch更加方便一些。
vue实现缓存的方式
Vue Router 的keep-alive
Vue Router 重定向
路由配置文件的component改成redirect
Vue的白屏
原因:首屏需要加载很大的js文件( app.js 和vendor.js ),所以当网速不佳的时候会产生一定程度的白屏。
解决方法:路由懒加载,CDN优化,SSR服务端渲染
Vue中ref和reactive
React
React生命周期
Mounting(挂载阶段)
constructor
- 初始化组件的state和props
- 为事件处理函数绑定实例
render
初始化数据完成后渲染DOM
React更新DOM和refs
componentDidMount
DOM挂载后调用,网络请求一般写在这里
Updating(更新阶段)
render
props和state更改都会造成组件重新渲染,也可以调用
forceUpdate()
强制让组件重新渲染React更新DOM和refs
componentDidUpdate
DOM更新后调用,首次渲染不会执行
Unmounted(卸载阶段)
componentWillUnmounted
组件卸载及销毁之前直接调用,执行必要的清理操作,如:timer,取消网络请求等
useEffect 模拟生命周期
useEffect默认会在每次渲染后执行,包括第一次渲染和后面的每次更新
componentDidMount
1
2
3
4// 数组为空时只在第一次渲染调用
useEffect(()=>{
console.log('componentDidMount')
},[])componentDidUpdate
1
2
3
4
5
6
7useEffect(()=>{
console.log('componentDidUpdate for any data')
})
// 针对特定值变化的useEffect, 在两次重渲染之间数组内的值没有发送变化则会跳过对Effect的调用
useEffect(()=>{
console.log('componentDidUpdate for count')
},[count])componentWillUnmounted
1
2
3
4
5
6// 每个 effect 都可以返回一个清除函数, React 会在组件卸载的时候执行清除操作
useEffect(()=>{
return ()=>{
console.log('componentWillUnmounted')
}
},[])
useContext原理
useContext
的原理类似于观察者模式。Provider
是被观察者, Consumer
和useContext
是观察者。当Provider
上的值发生变化, 观察者是可以观察到的,从而同步信息给到组件。
diff算法
非受控组件
异步组件
setState异步
fiber树
hooks对比class的好处
React和Vue的区别
高阶组件的作用
- 属性代理
- 渲染劫持
函数式组件 VS 类组件
- 函数式组件代码量小
- 函数式组件设计思路更偏向组合而不是继承,易拆
- 函数式组件性能更好
useReducer 和 useContext 实现 redux
创建Reducer和Context,将全局useReducer返回的state和dispatch传递给全局Context.Provider的value中,用全局构建好的带有Context的组件包裹应用根组件,根组件使用useContext获取state和dispatch。
useEffect执行两次怎么办
关闭严格模式
HTML/CSS
垂直水平居中
- 父元素设置 display: flex; 子元素设置 margin: auto;
- 父元素设置 display: flex; justify-content: center; align-items: center;
- 子元素设置 position: absolute; left: 50%; top: 50%;
文字渐变色
1 |
|
盒模型
CSS 中组成一个块级盒子需要:
- Content box: 这个区域是用来显示内容,大小可以通过设置
width
和height
. - Padding box: 包围在内容区域外部的空白区域; 大小通过
padding
相关属性设置。 - Border box: 边框盒包裹内容和内边距。大小通过
border
相关属性设置。 - Margin box: 这是最外面的区域,是盒子和其他元素之间的空白区域。大小通过
margin
相关属性设置。
CSS盒模型:
标准盒子模型
The content edge surrounds the rectangle given by the width and height of the box —— W3C文档
根据W3C的说法,width和height指的是content的宽高,不包括padding和border。
IE盒子模型
IE盒子模型的width和height包括了border和padding
BFC
BFC(Block Formatting Context):块级格式化上下文,BFC是一个独立的布局环境,可以理解为一个容器,在这个容器中按照一定规则进行物品摆放,并且不会影响其它环境中的物品。如果一个元素符合触发BFC的条件,则BFC中的元素布局不受外部影响。
BFC触发条件:
- 根元素或包含根元素
- 浮动元素float = left | right 或 inherit(≠none)
- 绝对定位元素position=absolute或fixed
- display=inline-block|flex|inline-flex|table-cell或table-caption
- overflow=hidden|auto或scroll(≠visible)
BFC规则:
- BFC就是一个块级元素,块级元素会在垂直方向一个接一个的排列
- BFC就是页面中的一个隔离的独立容器,容器里的标签不会影响到外部标签
- 垂直方向的距离由margin决定, 属于同一个BFC的两个相邻的标签外边距会发生重叠
- 计算BFC的高度时,浮动元素也参与计算
BFC解决了什么问题:
高度塌陷问题:Float脱离文档流,高度塌陷,可以设置BFC将浮动的高度也计算进去。
Margin重叠/溢出问题:相邻兄弟元素的Margin会重叠,为元素创建不同的BFC可以消除重叠。
文档流
文档流也称为常规流,HTML文档中的元素默认都在文档流中按顺序排布,块级元素自上而下,行内元素从左至右。
脱离文档流的方法:
- 浮动float:脱离文档流但不脱离文本流
- 绝对定位absolute/固定定位fixed:脱离文档流和文本流
flex弹性布局
Flexible Box 模型,通常被称为 flexbox,是一种一维的布局模型。flexbox 是一种一维的布局,是因为一个 flexbox 一次只能处理一个维度上的元素布局,一行或者一列。作为对比的是另外一个二维布局 CSS Grid Layout,可以同时处理行和列上的布局。
flexbox的两条轴线
flexbox布局有两条轴线分别是主轴和交叉轴,主轴由
flex-direction
定义,交叉轴垂直于主轴。Flex容器特点:
- 元素排列为一行 (
flex-direction
属性的初始值是row
)。 - 元素从主轴的起始线开始。
- 元素不会在主维度方向拉伸,但是可以缩小。
- 元素被拉伸来填充交叉轴大小。
flex-basis
属性为auto
。flex-wrap
属性为nowrap
。
- 元素排列为一行 (
flex容器可设置属性
flex-direction
- row:水平方向主轴
- row-reverse:水平反方向主轴
- column:垂直主轴
- column-reverse:垂直反方向主轴
flex-wrap
nowrap
:默认不换行wrap
:正常方向换行wrap-reverse
:反方向换行
- 以上两个属性可以用flex-flow简写,第一个指定的值为
flex-direction
,第二个指定的值为flex-wrap
.
flex容器内元素可设置属性
flex-grow
:设置剩余空间的分配权重,只要设置了一个就会占满。flex-shrink
:设置当flex容器溢出时元素的搜索权重。flex-basis
:规定了flex容器中元素在主轴的初始大小,如果已设置了width或者height,flex-basis
具有更高的优先级。- 以上三个属性可以用
flex
简写,三个数值按这个顺序书写 —flex-grow
,flex-shrink
,flex-basis
。 - 预定义
flex
flex: initial
flex: auto
flex: none
flex: <positive-number>
元素间的对齐和空间分配
align-items
:使元素在交叉轴方向对齐stretch
:元素被拉伸以适应容器。(默认值)flex-start
:对齐开始线flex-end
:对齐结束线center
:居中
justify-content
:使元素在主轴方向上对齐stretch
:元素被拉伸以适应容器。(默认值)flex-start
:对齐开始线flex-end
:对齐结束线center
:居中space-around
:元素之间间隔相同space-between
:首个元素在开始线,末尾元素在结束线
CSS3 有哪些新特性
flex弹性布局
grid栅格布局
边框
css3
新增了三个边框属性,分别是:- border-radius:创建圆角边框
- box-shadow:为元素添加阴影
- border-image:使用图片来绘制边框
文字
word-wrap
语法:
word-wrap: normal|break-word
- normal:使用浏览器默认的换行
- break-all:允许在单词内换行
text-overflow
text-overflow
设置或检索当当前行超过指定容器的边界时如何显示,属性有两个值选择:- clip:修剪文本
- ellipsis:显示省略符号来代表被修剪的文本
text-shadow
text-shadow
可向文本应用阴影。能够规定水平阴影、垂直阴影、模糊距离,以及阴影的颜色text-decoration
CSS3里面开始支持对文字的更深层次的渲染,具体有三个属性可供设置:
- text-fill-color: 设置文字内部填充颜色
- text-stroke-color: 设置文字边界填充颜色
- text-stroke-width: 设置文字边界宽度
渐变
颜色渐变是指在两个颜色之间平稳的过渡,
css3
渐变包括- linear-gradient:线性渐变
background-image: linear-gradient(direction, color-stop1, color-stop2, …);
- radial-gradient:径向渐变
linear-gradient(0deg, red, green);
CSS 选择器优先级
ID选择器 > 类选择器,属性选择器,伪类 > 类型选择器,伪元素
尽量不要使用!import,继承的优先级是最低的,即使加了!import
CSS 组合器
后代组合器
语法:
A B
例子:
div span
匹配所有位于任意div
元素之内的span
元素。直接子代组合器
语法:
A > B
例子:
ul > li
匹配直接嵌套在div
元素内所有的div
元素。一般兄弟组合器
语法:
A ~ B
例子:
p ~ span
匹配同一父元素下,<p>
元素后的所有<span>
元素。紧邻兄弟组合器
语法:
A + B
例子:
h2 + p
会匹配紧邻在h2
元素后的第一个<p>
元素。
1px问题怎么解决?
1px 问题指的是在一些 Retina屏幕 的机型上,移动端页面的 1px 会变得很粗,呈现出不止 1px 的效果。
原因很简单——CSS 中的 1px 并不能和移动设备上的 1px 划等号。它们之间的比例关系有一个专门的属性来描述:
1 |
|
1px 问题的解决方案是其实非常多的。
方案 | 优点 | 缺点 |
---|---|---|
直接写 0.5px | 代码简单 | IOS及Android老设备不支持 |
用图片代替边框 | 全机型兼容 | 修改颜色及不支持圆角 |
background渐变 | 全机型兼容 | 代码多及不支持圆角 |
box-shadow模拟边框实现 | 全机型兼容 | 有边框和虚影无法实现 |
伪元素先放大后缩小 | 简单实用 | 缺点不明显 |
设置viewport解决问题 | 一套代码适用所有页面 | 缺点不明显 |
移动端的动画(CSS3)
还没了解过
SEO了解吗?SEO优化相关
搜索引擎优化,HTML语义化标签
margin负值
负值就会表现出嵌入进去的效果
用CSS画个三角形
CSS预处理器
Sass,Less,主要就是感觉可以嵌套写元素,减少了代码量,其他功能用的比较少。
CSS样式隔离
Vue Scope
React CSS modules
伪类、伪元素
单冒号表示CSS3伪类,双冒号代表伪类。
伪类一般匹配的是元素的一些特殊状态,如hover、link等,而伪元素一般匹配的特殊的位置,比如after、before等。
清除浮动
1 |
|
响应式
媒体cha’x
rem 移动端布局原理
减少回流的方式
react和vue的diff算法
在底层级的DOM上修改
先将DOM离线
分离读写操作
样式集中改变
样式的覆盖
根据引入方式确定优先级
优先级由高到低依次为:“内联属性”——>“写在 style标签里”——>“外部链接”后写的覆盖先写的(同一级别)
即就是在文件上代码行号更靠下的优先级更高加有“!important”的样式,优先级最高
即无论哪一种情况,只要在样式上加了important,那么该样式的优先级最高。加了important的代码如下:选择器优先级
小于12px字体
Zoom 非标属性,有兼容问题,缩放会改变了元素占据的空间大小,触发重排
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<style type="text/css">
.span1{
font-size: 12px;
display: inline-block;
zoom: 0.8;
}
.span2{
display: inline-block;
font-size: 12px;
}
</style>
<body>
<span class="span1">测试10px</span>
<span class="span2">测试12px</span>
</body>-webkit-transform:scale() 大部分现代浏览器支持,并且对英文、数字、中文也能够生效,缩放不会改变了元素占据的空间大小,页面布局不会发生变化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<style type="text/css">
.span1{
font-size: 12px;
display: inline-block;
-webkit-transform:scale(0.8);
}
.span2{
display: inline-block;
font-size: 12px;
}
</style>
<body>
<span class="span1">测试10px</span>
<span class="span2">测试12px</span>
</body>
左边宽度固定,右边宽度随浏览器自适应的3种方案
- flex布局,左元素flex-basis固定,右元素flex-grow:1
- flex布局,左元素min-width固定,右元素flex-grow:1
- 左元素浮动右元素外边距设置为左元素宽度
box-size属性
- content-box:仅内容
- border-box:包含了内外边距,内容和边框
键盘事件
Button上有多个点击事件不想触发怎么办
将Button的disabled设置为true
CSS画一个三角形
利用border
1 |
|
计网
计算机网络模型
名称 | 作用 | 常用协议和标准 | 传输单位 |
---|---|---|---|
应用层 | 特定应用对接收数据的处理 | HTTP、FTP、SMTP | |
表示层 | 设备数据格式与网络标准数据格式转换 | LPP、NBSSN、XDP | |
会话层 | 通信管理,建立和断开通信连接 | RPC、(SSL、TLS) | |
传输层 | 管理两个网络终端之间的数据传输 | TCP、UDP | 段 |
网络层 | 网络地址管理和路由选择 | IP/IPv6、ICMP/ICMPv6 | 分组、包 |
数据链路层 | 互联设备之间传送和识别数据 | ARP、PARP | 帧 |
物理层 | 比特流与电子信号之间的转换 | IEEE 802.3/802.2 | 比特位 |
浏览器输入url之后的处理过程
- 浏览器的前置处理
- 浏览器查找历史记录,收藏等,进行自动补全和排序
- 手动输入完整的url或者使用浏览器补全的url
- 按下回车键后浏览器会得到一个url,并且对url进行解析(如果url不属于任何协议,比如http或者file,则浏览器会使用地址栏使用的默认搜索引擎进行搜索,实际上也就是转换成一个带参数的https链接打开,比如google搜索1:https://www.google.com/search?q=1)
- 正常来说我们都是打开一个http链接,浏览器随后会进行url解析,并且判断网址是否再HSTS (HTTP Strict Transport Security) 列表中,如果是则强制使用https打开网址
- 在发起网络请求之前,浏览器会先在浏览器缓存中查询是否有要请求的文件。当浏览器发现请求的资源已经在浏览器缓存中存有副本,它会拦截请求,返回该资源的副本,并直接结束请求,而不会再去源服务器重新下载。如果缓存查找失败,就会进入网络请求过程了。
- DNS查询
- 浏览器会先去查询域名是否已经缓存了DNS,如果有,则可以直接使用缓存的ip地址而不用去查询,如果未缓存,浏览器则会调用系统方法去查询DNS
- 系统首先会再hosts文件中找有没有对于域名的ip,如果有,则可以直接使用hosts文件中的ip地址而不用去查询,如果找不到系统则会向设置的DNS或者默认的DNS去发送DNS查询请求
- 递归迭代查询的一个过程,最终会获得一个ip地址返回给浏览器
- 默认端口为80,也可以填写其他端口
- TCP握手+TLS
- HTTP请求
- 前端工程师眼中的HTTP,它主要发生在客户端。发送HTTP请求的过程就是构建HTTP请求报文并通过TCP协议中发送到服务器指定端口(HTTP协议80/8080, HTTPS协议443)。HTTP请求报文是由三部分组成: 请求行, 请求报头和请求正文。
- HTTP响应报文也是由三部分组成: 状态码, 响应报头和响应报文。
- 服务器返回给浏览器的文本信息,通常HTML, CSS, JS, 图片等文件就放在响应报文中
- 浏览器处理文件和渲染界面
HTTP和TCP的区别
HTTPS加密原理,相对于HTTP提升了什么?
为什么要TCP要三次握手四次挥手?
- 三次握手
为什么要三次握手
确认双方的收发能力
TCP 建立连接之前,需要确认客户端与服务器双方的收包和发包的能力。
第一次握手:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。
序列号可靠同步
如果是两次握手,服务端无法确定客户端是否已经接收到了自己发送的初始序列号,如果第二次握手报文丢失,那么客户端就无法知道服务端的初始序列号,那 TCP 的可靠性就无从谈起。
阻止重复历史连接的初始化
客户端由于某种原因发送了两个不同序号的
SYN
包,我们知道网络环境是复杂的,旧的数据包有可能先到达服务器。如果是两次握手,服务器收到旧的SYN
就会立刻建立连接,那么会造成网络异常。如果是三次握手,服务器需要回复
SYN+ACK
包,客户端会对比应答的序号,如果发现是旧的报文,就会给服务器发RST
报文,直到正常的SYN
到达服务器后才正常建立连接。所以三次握手才有足够的上下文信息来判断当前连接是否是历史连接。
安全问题
我们知道 TCP 新建连接时,内核会为连接分配一系列的内存资源,如果采用两次握手,就建立连接,那会放大 DDOS 攻击的。
四次挥手
为什么要四次挥手
因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,”你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。
状态码
- 200 成功
- 301 永久重定向
- 302 临时重定向
- 304 资源未被修改
- 400 Bad request
- 401 Unauthorized
- 404 资源未找到
- 403 没有权限
- 500 服务器错误
- 504 网关超时
HTTP的请求方法
- get 获取
- post 新建数据
- patch / put 更新数据
- patch 用来局部更新
- put虽然也是更新,但会提供一个完整的资源对象
- delete 删除
HTTP缓存
- cache-control
- expires
HTTP1.1和HTTP2
HTTP2相对于HTTP1.X的新特性:
- 新的二进制格式(Binary Format),HTTP1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮。
- 多路复用(MultiPlexing),即连接共享,即每一个request都是是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面。
- header压缩,如上文中所言,对前面提到过HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。
- 服务端推送(server push),同SPDY一样,HTTP2.0也具有server push功能。
TCP和UDP
- 都是传输层的协议
- TCP是面向连接协议,建立之前需要先三次握手,UDP是无连接协议
- TCP传输要求可靠,收到会回复,超时、数组错误或顺序错误会要求重传,UDP只有基本的错误检查以及靠高层协议来确保可靠
- UDP传输效率比TCP高
- TCP应用
- HTTP/HTTPS
- SMTP
- FTP
- UDP应用
- 视频流
- 网络电话
- DNS
DNS查询过程
DNS 的查询过程一般为,我们首先将 DNS 请求发送到本地 DNS 服务器,由本地 DNS 服务器来代为请求。
- 从”根域名服务器”查到”顶级域名服务器”的 NS 记录和 A 记录( IP 地址)。
- 从”顶级域名服务器”查到”次级域名服务器”的 NS 记录和 A 记录( IP 地址)。
- 从”次级域名服务器”查出”主机名”的 IP 地址。
比如我们如果想要查询 www.baidu.com 的 IP 地址,我们首先会将请求发送到本地的 DNS 服务器中,本地 DNS 服务器会判断是否存在该域名的缓存,如果不存在,则向根域名服务器发送一个请求,根域名服务器返回负责 .com 的顶级域名 服务器的 IP 地址的列表。然后本地 DNS 服务器再向其中一个负责 .com 的顶级域名服务器发送一个请求,负责 .com 的顶级域名服务器返回负责 .baidu 的权威域名服务器的 IP 地址列表。然后本地 DNS 服务器再向其中一个权威域名服务器发送一个请求,最后权威域名服务器返回一个对应的主机名的 IP 地址列表。
DNS查询用的是tcp还是udp
区域传输用的tcp,因为数据量比较大
域名解析用的udp,数据量小,保证速度,减小负载
DNS劫持的几种方式
- DNS缓存感染:攻击者使用DNS请求将数据放入一个具有漏洞的DNS服务器的缓存中,在用户请求的时候就会返回这些缓存。
- DNS重定向:将DNS名称查询重定到恶意DNS服务器
- 本机DNS劫持:修改本机hosts文件
手撕题
手写一个轮播图
1 |
|
实现自定义表单组件,实现 v-model
1 |
|
对象数组扁平化
数组扁平化
递归
1
2
3
4
5
6
7function flatten(arr) {
let ans = []
for (let i of arr) {
ans = ans.concat(i instanceof Array ? flatten(i) : i)
}
return ans
}循环
1
2
3
4
5
6function flatten(arr) {
while (arr.some((item) => Array.isArray(item))) {
arr = [].concat(...arr)
}
return arr
}字符串
1
2
3function flatten(arr) {
return arr.join(',').split(',').map(Number)
}ES6的flat
1
2
3function flatten(arr) {
return arr.flat(Infinity)
}
对象扁平化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44// 阿里面试题
// 实现一个 flatten 函数,实现如下的转换功能
const obj = {
a: 1,
b: [1, 2, { c: true }],
c: { e: 2, f: 3 },
g: null,
}
// 转换为
let objRes = {
a: 1,
'b[0]': 1,
'b[1]': 2,
'b[2].c': true,
'c.e': 2,
'c.f': 3,
g: null,
}
function obj_flatten(obj) {
let result = {}
let process = (preKey, val) => {
if (val instanceof Object === false) {
// 当val为基本数据类型时,直接赋值
result[preKey] = val
} else if (val instanceof Array) {
// 当val为数组时,遍历数组
for (let idx = 0; idx < val.length; idx++) {
// 这里的preKey肯定不为空
process(preKey + `[${idx}]`, val[idx])
}
} else if (val instanceof Object) {
// 当val为对象时,遍历对象
for (let key in val) {
// preKey不为空才需要加.
process((preKey !== '' ? `${preKey}.` : '') + key, val[key])
}
}
}
process('', obj)
return result
}
实现布局,header,menu,content
手写快排
1 |
|
手写深拷贝
1 |
|
url组成+手写获得url中参数对象
现代方法
1
2
3let url_str = 'https://www.baidu.com/t.html?name=Sonce&age=21'
let url = new URL(url_str)
console.log(url.searchParams.get('name'))分割循环遍历
- 先将参数和url前缀分割
- 再将参数分割存在数组中
- 遍历数组查询(可以用对象存起来)
正则匹配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function getQueryString(name) {
var query_string = '?name=Sonce&age=21' // window.location.search
if (!query_string) return null // 如果无参,返回null
var re = /[?&]?([^=]+)=([^&]*)/g
var tokens
while ((tokens = re.exec(query_string))) {
if (decodeURIComponent(tokens[1]) === name) {
return decodeURIComponent(tokens[2])
}
}
return null
}
console.log(getQueryString('name')) // Sonce
console.log(getQueryString('age')) // 21
手写XMLHttpRequest
手写Promise.all
设计模式
控制反转(Inversion of Control)
控制反转
概念:面向对象编程的一种解耦的思想,基本思想是借助于“第三方”(IoC容器)实现具有依赖关系的对象之间的解耦。
原则:上层模块不应该依赖于下层模块,他们共同依赖于一个抽象,抽象不能够依赖于具体 ,具体必须依赖于抽象。
生动的例子:
假设我们是一家造车的企业,我们有自己的汽车零部件供应商,早期我们和我们的供应商深度合作共创,建立了汽车引擎的依赖关系,汽车依赖于引擎,形成紧耦合。可当造车企业想要更换引擎时候,又需要重新去更汽车中对应的设计(具体接口),当频繁更换引擎时候就很不方便。所以说需要有一个统一的标准(抽象化接口),汽车的设计和引擎的设计都依赖于这个标准,更换引擎的时候就可以直接更换而不需要去改汽车的设计。
常用方法
- 依赖注入(Dependence Injection):将依赖注入给使用该依赖的对象,React和Vue的props,构造函数参数。
- 依赖查找(Dependence Lookup):对象自己去查找所需要的依赖,依赖拖拽(DP)或者上下文化依赖查找(CDL)。
观察者模式和发布订阅模式
- 观察者模式中观察者和被观察者存在一定的依赖关系,属于松耦合,发布订阅模式中发布者和订阅者互不相关,属于完全解耦。
- 发布订阅模式中还有一个消息中心的存在
- 观察者模式,多用于单个应用内部,发布订阅模式,则更多的是一种跨应用的模式,比如我们常用的消息中间件
其他
解释语言和编译语言的区别
- 解释型语言:使用解释器逐句解释为机器代码,JavaScript使用V8引擎执行。
- 编译型语言:使用编译器编译成可执行文件
为什么学习前端,如何学习
后端传过来一个1000个数据的列表,怎么解决卡顿
- 后端分段
- 虚拟列表
前端工程化
Webpack
- Webpack打包流程:
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
- 确定入口:根据配置中的 entry 找出所有的入口文件。
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
- 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
Vite
Vite为什么快
浏览器开始原生支持 ES 模块
现代浏览器大部分已经原生支持 ES 模块,与其他打包工具不同,Vite以原生 ES Module 方式提供源码,让浏览器接管了打包程序的部分工作,Vite 只需要在浏览器请求源码时进行转换并按需提供源码。
使用 esbuild 预构建依赖
一些依赖可能并不是 ESM 的格式,所以Vite会使用 esbuild 预构建依赖,所以第一次启动可能会比较慢。
动态模块热替换(HMR)
在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失活,使得无论应用大小如何,HMR 始终能保持快速更新。
Gulp
Rollup
操作系统
线程、进程、协程
- 进程、线程和协程
- 进程:进程是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
- 线程:线程是进程的一个执行单元,是进程内科调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。
- 协程:协程是一种比线程更加轻量级的存在。一个线程也可以拥有多个协程。其执行过程更类似于子例程,或者说不带返回值的函数调用。
- 进程和线程的区别
- 地址空间:线程共享本进程的地址空间,而进程之间是独立的地址空间。
- 资源:线程共享本进程的资源如内存、I/O、cpu等,不利于资源的管理和保护,而进程之间的资源是独立的,能很好的进行资源管理和保护。
- 健壮性:多进程要比多线程健壮,一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。
- 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口,执行开销大。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,执行开销小。
- 可并发性:两者均可并发执行。
- 切换时:进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
- 其他:线程是处理器调度的基本单位,但是进程不是。
- 协程和线程的区别
- 线程:相对独立,有自己的上下文,切换受系统控制。
- 协程:相对独立,有自己的上下文,切换由自己控制,由当前协程切换到其他协程由当前协程来控制。
安全
XSS和CSRF
XSS
XSS
(Cross-Site Scripting
,跨站脚本攻击)是一种代码注入攻击。攻击者在目标网站上注入恶意代码,当被攻击者登陆网站时就会执行这些恶意代码,这些脚本可以读取cookie,session tokens
,或者其它敏感的网站信息,对用户进行钓鱼欺诈,甚至发起蠕虫攻击等。XSS避免方式:
url
参数使用encodeURIComponent
方法转义- 尽量不是有
InnerHtml
插入HTML
内容 - 使用特殊符号、标签转义符
CSRF
CSRF
(Cross-site request forgery
)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。CSRF
避免方式:- 禁止外域请求
- 使用Token认证
DDoS攻击
DDoS
又叫分布式拒绝服务,全称 Distributed Denial of Service
,其原理就是利用大量的请求造成资源过载,导致服务不可用。
跨域
当前页面中的某个接口请求的地址和当前页面的地址如果协议、域名、端口其中有一项不同,那么就说接口跨域了。
为什么要限制跨域:浏览器为了保证网页安全而采用的同源策略。
跨域解决方案:
CORS(Cross-Origin Resource Sharing):浏览器直接发出 CORS 请求会在头信息之中,增加一个 Origin 字段。Origin 字段用来说明本次请求来自哪个源。服务器根据这个值,决定是否同意这次请求。对于如果 Origin 指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。浏览器发现,这个回应的头信息没有包含 Access-Control-Allow-Origin 字段,就知道出错了,从而抛出一个错误,ajax 不会收到响应信息。如果成功的话会包含一些以 Access-Control- 开头的字段。
node中间件、nginx反向代理:先把请求发给本地的代理服务器,再由本地的代理服务器向后端服务器发送请求,服务器之间没有同源限制。
JSONP:利用的原理是script标签可以跨域请求资源,将回调函数作为参数拼接在url中(callback=foo)。后端收到请求,会先生成JSON数据,组建foo函数,把JSON作为参数传进去,然后将组建后的函数作为结果返回,客户端会将收到的数据解析为script标签,解析后运行foo函数且带有后端传来的JSON数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 前端代码
function showJsonp(obj){
console.log(obj.message);
}
var url = 'http://127.0.0.1:1234/?func=showJsonp'
var script = document.createElement('script');
script.setAttribute('src',url);
script.setAttribute('type','text/javascript');
document.getElementsByTagName('head')[0].appendChild(script);
// 后端代码
app.get('*', function(req, res) {
let callback = req.query.func;
let json = getJson() //获取json数据
let content = callback+"(" + json + ")"; //组建返回函数,传入json
res.send(content);
});