vue 面试题
# vue 面试题
# vue 响应式原理
# vue2 响应式原理
- Object.defineProperty()来进行数据劫持,针对对象特定属性通过设置 getter 和 setter 方法来劫持数据,当读取属性时会触发 getter 函数;当 view 视图层发生变化时,会触发 Object.defineProperty()中的 setter 方法。
# vue3 响应式原理
- Proxy 是针对 object 类型数据的所有属性都添加 get 和 set 方法进行监听,在实现嵌套数据时,更简单,原生支持数组类的监听。
# vue 生命周期
- onErrorCapture 捕获后代组件传递的错误时调用。
- onActivated 激活时(适用于 keep-alive 缓存的组件)
- onDeactivated 解散时调用(适用于 keep-alive 缓存的组件)
# 父子组件生命周期执行顺序
- 父组件 beforeCreated
- 父组件 created
- 父组件 beforeMounted
- 子组件 beforeCreated
- 子组件 created
- 子组件 beforeMounted
- 子组件 mounted
- 父组件 mounted
# 当父组件引入了 mixin 之后
- mixin beforeCreated
- 父组件 beforeCreated
- mixin created
- 父组件 created
- mixin beforeMounted
- 父组件 beforeMounted
- 子组件 beforeCreated
- 子组件 created
- 子组件 beforeMounted
- 子组件 mounted
- mixin mounted
- 父组件 mounted
# 导航守卫
全局守卫
- beforeEach
- beforeResolve
- afterEach
路由守卫
- beforeEnter
组件守卫
- beforeRouteEnter
- beforeRouteUpdate
- beforeRouteLeave
router.beforeEach(to,from,next) 全局前置守卫,路由跳转前触发
router.beforeResolve() 全局解析守卫,在所有组件内守卫和异步组件被解析后触发
router.afterEach() 路由跳转完成后触发
beforeEnter 路由独享守卫
参数
- to 即将要进入的目标路由对象
- from 即将要离开的路由对象
- next 是否可以进入某个具体路由,或者是某个具体路由的路径
# 从 A 页面跳转 B 页面时的生命周期
- A: beforeRouteLeave (组件守卫)
- beforeEach (全局守卫)
- beforeEnter (路由独享守卫)
- beforeRouteEnter (组件守卫)
- beforeResolve (全局解析守卫)
- afterEach (全局守卫)
- B: beforeCreate
- B: created
- B: beforeMount
- A: beforeDestroy
- A: destroyed
- B: mounted
# vue 的传值方式
- $emit
- 路由传参
- query 传参(地址栏显示参数)
- name 和 path 都能用
- 地址栏会显示参数
- params 传参(地址栏不显示参数)
- 跳转只能用 name ,不能用 path
- 地址栏不显示参数
- query 传参(地址栏显示参数)
- vuex
- localStore
- provide/inject
- $ref
- this.$refs.children 、 this.$refs.parents
# 路由的两种模式
- hash 模式
- history 模式
- abstract 模式(用来在不支持浏览器 API 的环境中,充当 fallback)
# vue 的 history 和 hash 的实现原理
- hash 是通过
window.onHashChange()
事件去监听的 - history 是通过
window.history.pushState(null,null,path)
(新增历史记录)、window.history.replaceState(null,null,path)
(替换历史记录)在不进行刷新的情况下,操作浏览器的历史记录
# vue 优化方式
- 减少 v-if 的活用 v-show
- v-for 遍历添加 key 属性
- 将组件进行切割(vue 的更新是组件粒度的,将耗时任务单独拆分成一组件,父组件数据变化时只会重新渲染父组件,耗时组件并不会渲染,这样性能会更好)
- 使用 keep-alive 缓存组件
- 区分 computed 和 watch
- 图片资源懒加载
- 动态加载组件,异步加载组件
- 使用防抖和节流
# keep-alive 原理
- 通过缓存 vnode 实现,在渲染的时候对比组件的 name(通过 include 和 exclude 控制),命中缓存时就从缓存中读取。
- 问题: 使用 keep-alive 的时候数据传递过去不会更新
- 会触发 activated deactivated
- 内存超限时通过 LRU 算法来管理
# nextTick 原理
- 在修改数据时,会开启一个异步队列,将 watcher 的 update 更新操作缓存在同一事件循环中,同一个 watcher 只会被推入队列一次(通过 watcherId 去重),然后再下一事件循环中刷新队列,执行代码,这样做能减少频繁不必要的更新,提升更新渲染效率。
- 原理: nextTick 内部通过一系列异步 api 尝试将回调函数放在异步队列里, 包括 promise、mutationObserver、setImmediate、setTimeout。
- 所有的回调方法都会放在 callbacks 数组里,最后通过 flushCallbacks 方法遍历 callbacks 数组来依次执行回调。
- vue 的 dom 更新是异步的,本身也是使用的 nextTick。
# nextTick 使用场景
- 获取数据更新后的 dom
- 在 created 中进行 dom 操作
- 获取元素的宽高
# vue 模板编译过程
- 将模板编译为抽象语法树 AST(多叉树),将 AST 转化为渲染函数,执行渲染函数生成 VDOM,将 VDOM 映射为真实 DOM。
# Vue.extend()
- 用于拓展组件生成一个构造器,通过 new 构造函数生成一个新的组件实例,类似组件继承。(在 vue3.0 里废弃了)。
# Vue.use()
- 实际调用了该插件的 install 方法,当引入的插件有 install 时需要调用。
# Suspense 组件
- 作用: 可以在组件树上层等待下层的多个嵌套异步依赖项时,渲染一个加载状态 通过 #fallback 的形式。
# teleport
- 作用: 将组件内部的一部分模板“传送”到该组件 dom 结构的最外层。
# vue 中的插槽
- 具名插槽
// 组件
<slot name="default" />
<template v-slot:default> </template>
// 或者
<template #default> </template>
2
3
4
5
6
- 作用域插槽
<!-- <MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
2
3
4
5
6
7
8
# $attrs 和 $listeners
- 透传 Attributes 是指由父组件传入,且没有被子组件声明为 props 或是组件自定义事件的 attributes 和事件处理函数。
- 你可以通过 inheritAttrs: false 来禁用这个默认行为。
# watch 和 watchEffect 的区别
- watch 需要传入侦听的数据源。而 watchEffect 是自动收集数据源作为依赖
- watchEffect 在初始化时就会执行一次。而 watch 的话,只有设置了
immediate: true
时,才会在初始化时监听。
# watchEffect
- 立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行
- 设置 flush: post 将会使侦听器延迟到组件渲染后执行。
- 返回值是一个用来停止该副作用的函数。
// 副作用清除
watchEffect(async (onCleanup) => {
const { response, cancel } = doAsyncWork(id.value);
// `cancel` 会在 `id` 更改时调用
// 以便取消之前
// 未完成的请求
onCleanup(cancel);
data.value = await response;
});
// 停止侦听器
const stop = watchEffect(() => {});
//当不需要此侦听器时:
stop();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 怎么去做按钮权限的处理?
- 菜单栏权限怎么做?
- 按钮权限怎么做?
# vue2 跟 vue3 的区别
# 小程序面试题
# 打开一个小程序到页面展示,经历了什么
- 启动小程序
- 运行环境准备
- 下载代码包
- 缓存代码包
- 代码注入
- 逻辑层(jsCore)与渲染层(webview)双线程并行
- 渲染页面
# 微信小程序原理
- 数据驱动的架构模式,UI 和数据是分离的,所有页面的更新都需要通过数据的更改来实现。
# 小程序的 v-model 和 vue 的有什么不一样
- 小程序设置 this.data 不会被同步到视图,必须要调用
this.setData({});
# 小程序的优化
- 控制图片大小以及比例
- 避免 wxml 节点数过大
- 滚动区域设置惯性滚动。
-webkit-overflow-scrolling: touch
- 避免短时间内发起太多请求
- 合理的使用分包
- 减少 data 的大小,非必要不 setData
- 组件化
- 按需注入
# bindtap 与 bindcatch 区别
- bindtap 不会阻止事件冒泡,bindcatch
会阻止
事件冒泡
# webview 中的页面怎么跳回小程序中?
- wx.miniProgram.navigateTo({url:'/pages/index/index'})
# webview 的页面怎么跳转到小程序导航的页面?
- 通过 switchTab 跳转,但是默认情况下不会重新加载数据,若需要重新加载数据,则需要在 success 属性中手动调用
// 在 webview中的success属性中添加以下代码
success:function (e) {
var page = getCurrentPages().pop()
if(page === undefined || page === null) return
page.onLoad()
}
2
3
4
5
6
7
8
9
# webview 怎么跟小程序通信?
# 微信小程序和 h5 的区别
运行环境不同
- 小程序运行在 webview 内核中
- h5 运行环境是浏览器
开发成本不同
- 只在微信中运行,不用顾虑浏览器兼容问题
# 小程序设置 less
easy less
插件,配置一下settings.json
就可以了
# 小程序的生命周期
App
- onLanch
- onShow
Page
- onLoad
- onShow
- onReady
- onHide
- onUnload
- onShareMessage
- onPageScroll
- onPullDownRefresh
Component
- created
- attached
- ready
- moved
- detached
- error
# 关于无痛刷新 token 的问题
# 封装的 地理位置授权这个问题
# 封装的 request 请求
- 通过事件队列的形式去封装的。
- 判断是否是登录接口
- 登录接口放行。非登录接口,判断是否有 token
- 有 token,放行
- 没有 token,调取刷新 token 的接口,并将请求的接口缓存在一个任务队列里。
- 等 token 请求完成之后,在遍历队列,执行里面的代码
# 如果在页面还没加载出来,就跳到其他页面,将任务队列清空。
# 多个品牌你是写的一套代码吗?还是说写的 7 套代码?
回答:h5 活动写的一套代码,小程序维护的是七套代码。
我是通过配置文件的形式,根据小程序的 appid 来进行区分,一套代码,将配置文件传进去。
# h5 页面你是怎么去做授权的?
- 答:h5 页面获取的用户信息是通过,小程序往 h5 页面传 token,然后 h5 调取后台接口,拿到用户信息。
# 要是在 h5 中浏览之后,token 过期了,怎么处理的?
- 回答:从 h5 页面直接跳到小程序页面的登录页,然后去做登录。
# 小程序绘制海报会出现的问题
- 图片显示不全,或者图片模糊
- 原因: 你设置的图片宽高例如为 750 x 1200 ,但实际在 canvas 画布上画出的尺寸为 375 x 600,图片会显示不全。
# wx.canvasToTempFilePath() 将画布指定区域导出生成指定大小的图片
- 解决
- 图片居中问题
- 生成图模糊问题
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: 50,
height: 50,
destWidth: 100,
destHeight: 100,
canvasId: "myCanvas",
success(res) {
console.log(res.tempFilePath);
},
});
2
3
4
5
6
7
8
9
10
11
12
# wx.onAppRoute 获取路由回调设置全局分享
// wx.onAppRoute 可以获取到当前页面的信息
wx.onAppRoute(() => {
const page = getCurrentPages();
let view = pages[pages.length - 1];
let data;
view.onShareAppMessage = () => {
return {
imageUrl:
"https://img.carlsberg.asia/container-reference/img/somersby/2021-10-28/efe0c7ff61a5462babb1b28fc5dd856c.jpg",
title: "和我一起,在SOMERSBY夏日纷阳光乐园玩趣吧!",
path: `/pages/index/index`,
};
};
});
2
3
4
5
6
7
8
9
10
11
12
13
14
# React
# 组件通信
- 父传子: props;
- 子传父: 子调用父组件中的函数并传参;
- 兄弟: 利用 redux 实现和利用父组件。
# diff 算法
- diff 算法的本质: 就是找出两个对象之间的差异,目的是尽可能做到节点复用。
- 上述中的对象:指的其实就是 vue 中的 virtual dom(虚拟 dom 树),即使用 js 对象来表示页面中的 dom 结构。
三个层级策略: 1、tree 层级:dom 节点跨层级的移动操作特别少,可以将其忽略不计。 2、component 层级:拥有相同类的两个组件会生成类似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。 3、element 层级:对于同一层级的一组子节点,可以通过唯一 key 进行区分。
# 高阶组件
- 高阶组件就是一个函数,且该函数接收一个组件作为参数,并返回一个新的组件。
# 常用的 hooks
- seState: 设置和改变 state,代替原来的 state 和 setState
- useEffect: 代替原来的生命周期,componentDidMount,componentDidUpdate 和 componentWillUnmount 的合并版
- useLayoutEffect: 与 useEffect 作用相同,但它会同步调用 effect
- useMemo: 控制组件更新条件,可根据状态变化控制方法执行,优化传值
- useCallback: useMemo 优化传值,usecallback 优化传的方法,是否更新
- useRef: 跟以前的 ref,一样,只是更简洁了
# useCallback 和 useMemo 的区别
- useMemo 计算结果是 return 出来的值。使用场景: 需要计算的状态。
- useCallback 计算结果是函数,主要用于缓存函数。使用场景:需要缓存的函数,因为函数式组件每次任何一个 state 的变化 整个组件都会被重新刷新,一些函数是没有必要重新刷新的,此时就应该缓存起来,提高性能和减少资源浪费。(利用缓存函数,使子组件不会重新渲染)
# hooks 优势
- 函数组件无 this 问题
- 自定义 Hooks 方便状态复用
- 副作用关注点分离。
# redux
核心概念: store、action、reducer
关键函数: getState() :用于获取当前最新的状态 subscribe() :用于订阅监听当前状态的变化,然后促使页面重新渲染 dispatch() :用于发布最新的状态
执行流程: (1)用户通过事件触发 ActionCreator 制造 action (2)同时,用户触发的事件内调用 dispatch 来派发 action (3)reducer 接收 action,并处理 state 返回 newState (4)View 层通过 getState() 来接收 newState 并重新渲染视图层
# JS 基础
# var, let , const 的区别
- 变量提升、块级作用域、重复声明同名变量、重新赋值
# 面向对象
运用类、继承、封装等进行程序设计的一种思想。
- 能够将复杂问题和复杂逻辑简单化
- 方便扩展
- 方便维护
# 防抖和节流
防抖(debounce):触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间 节流(throttle):高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率
区别:防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。
# Object.is() 对比两个参数是否相等
Object.is(NaN, NaN); // true
Object.is(+0, -0); // false
// 自己实现一个Object.is
if (!Object.is) {
Object.is = function (x, y) {
if (x === y) {
// Steps 1-5, 7-10
// Steps 6.b-6.e: +0 != -0
return x !== 0 || 1 / x === 1 / y;
} else {
// Step 6.a: NaN == NaN
return x !== x && y !== y;
}
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 什么是纯函数
- 没有副作用的函数
# 什么是函数副作用
- 函数在正常工作任务之外对外部环境所施加的影响。
# 怎么在不使用变量的情况下 交换两个变量的值
let a = 1;
let b = 2;
[b, a] = [a, b];
2
3
# [b, a] = [a, b] 的原理是什么,它是怎么交换两个变量的值的?
[a, b] = [b=a, a=3]
解构的过程为:
- 以从左到右的顺序计算出右侧数组的值,得到数组
- 以从左到右的顺序,将右侧数组的值赋值给左侧
可以看到解构赋值的过程中会有一个包含两个元素的的临时数组,并没有比传统方法节省空间,甚至空间会比传统方法多一个 int 值大小的空间
# 使用 DocumentFragment 创建并组成一个 dom 子树
- 目的: 减少重绘重排的次数
<ul id="list"></ul>;
const list = document.querySelector("#list");
const fruits = ["Apple", "Orange", "Banana", "Melon"];
const fragment = new DocumentFragment();
fruits.forEach((fruit) => {
const li = document.createElement("li");
li.textContent = fruit;
fragment.appendChild(li);
});
list.appendChild(fragment);
2
3
4
5
6
7
8
9
10
11
12
13
14
# 自己实现一个 promise.all
Promise.prototype.all = function (promises) {
let results = [];
let promiseCount = 0;
let promisesLength = promises.length;
return new Promise((resolve, reject) => {
for (let val of promises) {
Promise.resolve(val).then(
function (res) {
promiseCount++;
results[i] = res;
if (promiseCount === promisesLength) {
return resolve(results); // 可以不用 return 出去
}
},
function (err) {
return reject(err); // 可以不用 return 出去
}
);
}
});
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 数组对象去重
- 使用 filter 和 map
function uniqueFunc(arr, key) {
const res = new Map();
return arr.filter((item) => !res.has(item[key]) && res.set(item[key], 1));
}
2
3
4
- 使用 reduce
function uniqueFunc(arr, key) {
let hash = {};
return arr.reduce((prevValue, currentValue) => {
hash[currentValue[key]]
? ""
: (hash[currentValue[key]] = true && prevValue.push(currentValue));
return prevValue;
}, []);
}
2
3
4
5
6
7
8
9
# nginx 怎么配置负载均衡?
# meta 标签有多少属性(重要)
- name
- name 属性的几个值
- keywords 关键字
- description 描述
- viewport 视口
- robots 搜索引擎的索引方式
- renderfer 搜索引擎的渲染方式
- revisit-after 搜索引擎的重访时间
- name 属性的几个值
- http-equiv
- http-equiv 属性的几个值
- content-Type 设定网页字符集
- X-UA-Compatible 告知浏览器以何种方式渲染页面
- cache-control 请求和响应的缓存机制
- expires 网站过期时间
- refresh 自动刷新并重新跳转指定页面
- set-Cookie 设置 cookie
- http-equiv 属性的几个值
<meta name="xxx" content="xxxx" />
<!-- 相当于设置http -->
<meta http-equiv="参数" content="具体的描述" />
2
3
4
# 跨终端解决方案
- flutter
- Taro
- React Native
- H5 Hybrid
- 桌面端对应的方案就是 Electron
# jsBridge
Web 端和 Native 可以类比于 Client/Server 模式,Web 端调用原生接口时就如同 Client 向 Server 端发送一个请求类似,JSB 在此充当类似于 HTTP 协议的角色,实现 JSBridge 主要是两点:
- 将 native 端的原生接口封装为 JavaScript 接口
- 将 JavaScript 接口 封装为 原生接口
# 开源的 jsBridge
- DSBridge,主要通过注入 API 的形式。
DSBridge for Android
、DSBridge for IOS
- JSBridge, 主要通过拦截 URL Schema 的形式,
JsBridge
# 跨终端 web 适配方式
- 可以写多套代码,输入网址时判断 pc 或 mobile,返回对应的内容。
- 响应式设计 bootstrap
# less 是怎么编译成 css 的
- 通过 less 和 less-loader
- less 使用 less 语言的特性,嵌套语法,变量,函数,常量,map
- less-loader 将 less 编译成 css 文件
- css-loader 解析 css 文件里面的 css 代码(默认 webpack 只解析 js 代码的)
- style-loader 将 css-loader 解析的内容挂载到 html 页面中
# require 和 import 的区别
- require 是
运行时加载
;import 是编译时输出接口。 - require 是同步加载模块; import 是异步加载模块(有一个独立模块依赖的解析阶段)。
- require 输出的是一个值的(浅)拷贝;import 输出的是值的引用。
# js 基础数据类型和引用数据类型存在哪
基础数据类型
存在栈(stack)
(特点:先进后出)中。引用数据类型
存在堆(heap)
(特点:数据任意存放)中。
# new 一个构造函数会发生什么
- 创建了一个空对象
- 让 this 指向了该对象
- 执行构造函数并给对象的属性赋值,继承构造函数的原型
- 判断构造函数是否有返回值,且根据返回值的类型进行返回,引用类型,返回引用地址,值类型返回值。
# 频繁的请求后台,不会有什么问题吗?用户量不大吗?
# dispatchEvent 点击和真实点击的区别
- 没啥大区别,最终调用的都是 fireEventListener
- 其中的 microtask 执行条件是必须在 host 包装嵌套深度为 0 时才跑
- 用户点击时候,microtask 执行时,是回调完成后,此时包装深度已经是 0 了
- 而 dispatchEvent 时,由于 dispatchEvent 本身就是个层包装,需要它执行完成后包装深度才能是 0,才能 runMicrotask。
<!-- 可以通过 event.isTrusted 判断是否由用户触发 true 用户触发;false 代码触发-->
<button class="btn">测试dispatchEvent()方法</button>
<script>
let btn = document.querySelector(".btn");
btn.addEventListener("click", function () {
alert("点击了");
});
let clickEvent = new Event("click");
btn.dispatchEvent(clickEvent);
</script>
2
3
4
5
6
7
8
9
10
11
12
13
# 深拷贝
function clone(target, map = new Map()) {
if (typeof target === "object") {
let cloneTarget = Array.isArray(target) ? [] : {};
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
for (const key in target) {
cloneTarget[key] = clone(target[key], map);
}
return cloneTarget;
} else {
return target;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 自动化测试有没有了解过? 自动化测试有什么好处?
# 自动化测试的流程
输入 - 断言 - 验证
不管是单测还是 E2E 都可以遵循这个原则,如果不知道如何开始,可以考虑我们的预期结果是什么,以终为始,再去慢慢实现函数或组件的具体功能,这就是 TDD 的一种模式
自动化这种方案基本上能做到不漏测一个功能,而且不需要我们手动去输入数据和点击页面去验证了,提升了代码的质量
人工测试:通过开发人员或测试人员与程序的交互来完成,即手动操作验证。
自动化测试:通过自动化脚本与程序的交互来完成,除了刚开始编写的自动化脚本时间,基本上无需手动操作
# 自动化测试解决了什么问题?
- 解决了耗时时间和人力成本
# 在使用新技术的时候,是怎么去做 新旧技术的融合的?
# fetch 和 pull 的区别
对远端跟踪分支操作的权限不同
- fetch: fetch 能够直接更改远端跟踪分支。
- pull: pull 无法直接对远程跟踪分支操作,我们必须先切回本地分支然后创建一个新的 commit 提交。
拉取后的操作不同
- fetch:fetch 会将数据拉取到本地仓库 - 它并不会自动合并或修改当前的工作。
- pull: pull 是从远程获取最新版本并 merge 到本地,会自动合并或修改当前的工作
使用后 commitID 不同
- fetch:使用 fetch 更新代码,本地的库中 master 的 commitID 不变,还是等于 1
- pull:使用 pull 更新代码,本地的库中 master 的 commitID 发生改变,变成了 2。
# 数据结构与算法
# 你知道哪些算法
- 递归
- 冒泡排序
- 插入排序
- 选择排序
- 快速排序
- 堆排序
# 链表
- 线性递归结构,通过指针相连
- 可以用 git 仓库的储存理解,不同的分支和节点连接在一起
# 什么是单链表
- 线性递归结构,通过指针相连,单链表只有 next 指针
# 判断一个单链表中是否有环?
// 判断一个链表中是否有环
// 例如:A->B->C->D->B->C->D
// D指向B形成环
// 要求:在空间复杂度O(1)的情况下,时间复杂度最小
// 思路: 做一个快指针,做一个慢指针,快指针每次走两步,慢指针每次走一步;如果有环的话,就慢指针就会追上快指针。
function ListNode(x) {
this.val = x;
this.next = null;
}
function EntryNodeOfLoop(pHead) {
if (pHead === null) return null;
var fast = pHead;
var slow = pHead;
while (fast.next !== null && fast.next.next !== null) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) break;
}
if (fast === null || fast.next === null) return null;
// 有环,slow重新回到链表头
slow = pHead;
// slow和fast重新相遇时,相遇节点就是入环节点
while (slow !== fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
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
# 有一个 LinkedList 类,实现一个单链表中的 insert、find、append 方法。
class LinkedList {
constructor() {
this.size = 0; // 单链表的长度
this.head = new Node("head"); // 表头节点
this.currNode = ""; // 当前节点的指向
}
insert(item, element) {
let itemNode = this.find(item);
if (!itemNode) {
// 如果item元素不存在
return;
}
let newNode = new Node(element);
newNode.next = itemNode.next; // 若currNode为最后一个节点,则currNode.next为空
itemNode.next = newNode;
this.size++;
}
isEmpty() {
return this.size === 0;
}
findLast() {
let currNode = this.head;
while (currNode.next) {
currNode = currNode.next;
}
return currNode;
}
find(item) {
let currNode = this.head;
while (currNode && currNode.data !== item) {
currNode = currNode.next;
}
return currNode;
}
delete(item) {
if (!this.find(item)) {
// item元素在单链表中不存在时
return;
}
// 企图删除头结点
if (item === "head") {
if (!this.isEmpty()) {
return;
} else {
this.head.next = null;
return;
}
}
let currNode = this.head;
while (currNode.next.data !== item) {
// 企图删除不存在的节点
if (!currNode.next) {
return;
}
currNode = currNode.next;
}
currNode.next = currNode.next.next;
this.size--;
}
}
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 斐波拉契数列数组
const map = new Map();
const list = [];
function fn(num) {
if (map.has(num)) {
return map.get(num);
}
if (num === 1 || num === 2) {
if (!map.has(num)) {
list.push(1);
map.set(num, 1);
}
return 1;
}
const result = fn(num - 1) + fn(num - 2);
map.set(num, result);
list.push(result);
return result;
}
console.log("list", list);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 斐波那契数列 二叉树分布,寻找最左边节点值组成的集合
// [1,1,2,3,5,8,13,21,34,....,n-2,n-1]
let index = 1;
const newList = [list[0]];
list.forEach((item, i) => {
if (index > i) {
return;
}
if (i === index) {
newList.push(item);
index = 2 * index + 1;
}
});
2
3
4
5
6
7
8
9
10
11
12
# 安全方面
# 黑客上传了一个文件到我们服务器之后,是怎么黑进我们系统的?
# 页面常见攻击方式
- xss 攻击
- crsf 攻击
- sql 注入
# liunx 修改文件读取权限
chmod -R 777
- 表示的意思:
- 第一个 7 表示:文件所有者访问权限
- 第二个 7 表示:群组访问权限
- 第三个 7 表示:其他人访问权限
- 表示的意思:
r(read)w(write)x(eXecute) 读、写、执行
- 权限值:4
- 权限值:2
- 权限值:1
# 你还知道哪些加密方法?
- aes/des 加密
- base64 加密(不可逆)
- MD5 加密
- sha512 加密(不可逆)
# 为什么要使用 aes(对称加密)进行加密?
- 优点: 算法公开、计算量小、加密速度快、加密效率高
- 缺点:
- 交易双方都使用同样钥匙,安全性得不到保证
- 密钥管理困难,使用成本较高
# 前后端交互如何保证数据的安全性?
- 用 aes(对称加密)和 rsa(非对称加密) 配合使用。
- 客户端启动,发送请求到服务端,服务端用 rsa 算法生成一个公钥(public1)和私钥(prikey1)。将公钥返回给客户端。
- 客户端拿到服务端返回的 公钥(public1)之后,自己在前端生成一个公钥(public2)和私钥(prikey2),并将 public2 通过 public1 进行加密,并传输给服务端。
- 服务端接收后通过私钥(prikey1)进行解密,拿到了客户端的公钥(public2)。
- 服务端生成一个 aes 的 16 位的 key,并使用公钥(public2)加密传输给客户端。
- 客户端获取到 key 后,通过私钥(prikey2)进行解密。拿到 aes 加密的 key。
- 然后客户端就用加密的 key 进行数据传输,整个流程结束。
# 前端怎么去防止 sql 注入
- 防止 SQL 注入唯一可靠的方式是
验证输入
和参数化查询
- 开发者必须
检查所有用户输入
,而不是仅检查网页表单中的输入,比如登录表单。他们必须移除潜在的恶意代码因素,比如单引号
。在你的线上环境中屏蔽数据库错误也是个好主意。因为结合数据库错误,SQL 注入攻击将获得更多数据库相关信息
# sql 注入的危害
- 通过 sql 注入可以添加、编辑、删除数据库中的数据
- 可以拿到其他用户的凭证。伪装成这些用户。
- 甚至可以拿到数据库管理员权限。窃取数据。
- 影响应用的可用性。
- 可以通过 sql 注入作为初始手段,攻击防火墙背后的内网。
# sql 注入攻击的过程及原因
- 为了发起 SQL 注入攻击,攻击者首先需要在网站或应用程序中找到那些易受攻击的用户输入。这些用户输入被有漏洞的网站或应用程序直接用于 SQL 查询语句中。攻击者可创建这些输入内容。这些内容往往被称为恶意载体,它们是攻击过程中的关键部分。随后攻击者将内容发送出去,恶意的 SQL 语句便会在数据库中被执行。
- 攻击者可利用 SQL 注入,从数据库中得到其他用户的用户凭证。之后他们便能伪装成这些用户。这些用户中甚至有可能包括有所有数据库权限的数据库管理员。
- SQL 可用于从数据库中选择并输出数据。SQL 注入漏洞允许攻击者访问数据库服务中的所有数据。
- SQL 也可用于修改数据库中数据,或者添加新数据。例如,在金融产品中,攻击者能利用 SQL 注入修改余额,取消交易记录或给他们的账户转账。
- SQL 可用于从数据库中删除记录,甚至删除数据表。即使管理员做了数据库备份,在数据库中数据恢复之前,被删除的数据仍然会影响应用的可用性。而且,备份很可能没有覆盖最近的数据。
- 在某些数据库服务中,可通过数据库服务访问操作系统。这种设计可能是有意的,也可能是无意的。在这种情况下,攻击者将 SQL 注入作为初始手段,进而攻击防火墙背后的内网
# 预防 sql 注入
- 培养并保持安全意识
- 不要信任任何用户输入
- 使用白名单,而不是黑名单: 不要基于黑名单过滤用户输入。因为聪明的攻击者总是能找到绕过黑名单的方法,所以应尽可能只使用严格的白名单,对用户输入进行验证和过滤。
- 采用最新的技术
- 采用经过验证的机制
- 周期性扫描: SQL 注入漏洞可能被开发者引入,也可能被外部库、模块或软件引入。你应该使用网站漏洞扫描器(比如 Acunetix)周期性扫描你的网站。如果你使用 Jenkins,你可以安装 Acunetix 插件,实现每次构建时进行自动扫描。
# typescript
# type 和 interface 的区别
- type 后面有 = ,interface 没有。
- type 可以描述任何类型组合,interface 只能描述对象结构
- interface 可以继承自(extends)interface 或对象结构的 type。type 也可以通过 & 做对象结构的继承。
- 多次声明的同名 interface 会进行声明合并,type 则不允许多次声明。
- type 可以使用 in 关键字 生成映射类型,interface 不行。
- 用来描述对象或函数类型
- 可以通过交叉实现继承
- interface 可以扩展,type 可以通过交叉实现 interface 的 extends 行为
- interface 可以 extends type,同时 type 也可以与 interface 类型交叉
- type 可以使用 in 关键字 生成映射类型,interface 不行。
type Keys = "name" | "sex"
type DuKey = {
[Key in Keys]: string //类似 for ... in
}
let stu: Dukey = {
name: 'wang',
sex: 'man'
}
2
3
4
5
6
7
8
# type 怎么继承
type Name = {
name: string,
};
type Person = Name & {
age: number,
};
const people: Person = {
name: "ren",
age: 20,
};
// interface 可以通过 extends 进行继承 ; type不能通过 extends进行继承
2
3
4
5
6
7
8
9
10
11
12
13
14
# 计算机基础
# 常见 code 码
- 200 - 请求成功
- 301 - 永久重定向
- 302 - 临时重定向
- 304 - 自从上次请求后,请求网页未修改过,服务器返回响应,不返回内容 ()
- 400 - 服务器不理解请求的语法
- 403 - Forbidden 服务器理解请求客户端的请求,但是拒绝执行此请求
- 404 - 请求的资源(网页等)不存在
- 413 - 请求体过大,超出服务器的处理能力上限
- 414 - 请求 url 太长,服务器无法处理
- 500 - 内部服务器错误
- 502 - Bad Gateway 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应
# 浏览器缓存(重要)
# 浏览器缓存的意义
- 避免了冗余的数据传输,节省流量
- 加快了用户访问速度
- 减小了服务器的压力
# 浏览器缓存策略
- 第一次请求数据时,浏览器没有对应的缓存数据,此时需要请求服务器,服务器返回数据后,会把请求数据缓存。
- 当浏览器有缓存数据后,可以根据是否向服务器发送请求,将缓存类型分为: 协商缓存与强缓存
# 强缓存
- 命中缓存后,不会向服务器发送请求,直接从本地获取。返回的状态码为 200(from disk cache) 或 200(from memory cache)
# 协商缓存
- 缓存过期,浏览器会向服务器发送请求,协商对比服务端和本地的资源,验证本地资源是否失效。若服务器资源与本地资源相同,则命中缓存,返回 304,否则直接请求服务器资源。
- 协商缓存的响应结果,不仅验证资源的有效性,同时还会更新浏览器缓存
- Age:0 表示命中了代理服务器的缓存, age 值为 0 表示代理服务器刚刚刷新了一次缓存.
# 强缓存与协商缓存的关系
- 相同点: 强缓存与协商缓存在命中缓存资源后都是从本地读取资源。
- 不同点:
- 如果强缓存生效,不需要再向服务端发起请求。
- 协商缓存,不管是否使用缓存,必须向服务器发送一个请求来协商。
# 优先级
- 两类缓存规则同时存在的情况下,
强缓存 > 协商缓存
- 当执行强缓存的规则时,如果缓存生效,直接使用缓存,不再执行协商缓存规则。如果强制缓存规则不生效,则需要进行协商缓存判断
# 总结
- 浏览器第一次请求数据时,浏览器缓存中没有对应的缓存数据,此时需要请求服务器,浏览器返回数据后,会把请求数据缓存。
- 浏览器再次访问同一 url,则会根据请求资源是否过期向服务器发送不同请求,此时分为强缓存和协商缓存。
- 若资源未过期(expires 和 cache-control)时,命中缓存,不会向服务器发送请求,直接从缓存中读取。
- 若资源显示过期(if-modified-since/Last-Modified)和(if-none-match/Etag),则会向服务器发送请求,协商对比服务器资源,验证是否失效。若相同,命中协商缓存,返回 304,否则直接请求服务器。
# 清除浏览器缓存
- meta 标签中令 cache-control 为 no-cache 或者 expires 为 0
//不缓存
<meta http-equiv="Cache-Control" content="no-cache, must-revalidate">
<meta http-equiv="expires" content="0">
2
3
# 哪些不能被浏览器缓存
- http 头中 cache-control 为 no-cache 或者为 max-age: 0
- 需要根据 cookie,和输入内容的动态请求
- 经过 https 加密的请求
- post 请求不可以被缓存
- http 响应头中不含 Last-Modified/Etag,也不包含 Cache-Control/Expires 的请求无法被缓存
# 在浏览器中输入一个网址到页面展示,会经历什么?(重要)
- 浏览器输入 url
- 查找缓存: 浏览器先查看浏览器缓存-系统缓存-路由缓存中是否有该地址页面,如果有,则显示该页面,没有则进行下一步.
- DNS 域名解析: 浏览器向 dns 服务器发送请求,解析该 URL 域名中对应的 ip 地址。(dns 服务器是基于 UDP 的,因此会用到 UDP 协议)
- 建立 TCP 连接:解析出 IP 地址后,根据 IP 地址和默认的端口,和服务器建立 TCP 连接。
- 发起 http 请求: 浏览器发起读取文件的 http 请求,改请求作为 TCP 三次握手的第三次数据发送给服务器
- 服务器响应并返回结果: 服务器对浏览器请求做出响应,并把对应的 html 文件发送给浏览器
- 关闭 TCP 连接: 通过四次挥手释放 TCP 连接
- 浏览器渲染: 客户端解析 html 内容并渲染出来
- 构建 dom 树:词法分析然后解析成 dom 树(dom tree),是由 dom 元素及属性节点组成,树的跟是 document 对象
- 构建 css 规则树: 生成 css 规则树(CSS Rule Tree)
- 构建 render 树: 浏览器将 dom 和 CSSOM 结合,并构建出渲染树(render Tree)
- 布局(Layout):计算出每个节点在屏幕中的位置
- 绘制(Painting):遍历 render 树,并使用 UI 后端层绘制每个节点
# TCP 的三次握手和四次挥手
三次握手
- 客户端向服务端发送 syn 请求包,询问服务端能不能收到请求
- 服务端接收后向客户端发送 ack 确认包,告诉客户端能收到
- 客户端向服务端发送 ack 确认包,告诉服务端我也能收到,然后客户端进入准备状态,服务器接收后也进入准备状态。
四次挥手
- 客户端向服务端发送 fin 请求包,告诉服务端要断开连接
- 服务端收到后发送 ack 确认包告诉客户端收到了断开请求
- 服务端有未传完的数据会继续传完,然后向客户端发送 fin 请求包,告诉客户端可以断开连接了
- 客户端收到后向服务端发送 ack 确认包,然后客户单断开连接,服务端收到后也断开连接
# 浏览器的重绘、重排/回流
- 重排/回流(Reflow): 当 dom 变化影响元素的几何信息,浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。表现为:重新生成布局,重新排列元素。
- 重绘(Repaint): 当一个元素外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫重绘。表现为某些元素的外观被改变
# 如何触发重排和重绘?
- 添加、删除、更新 DOM 节点
- 通过 display: none 隐藏一个 DOM 节点-触发重排和重绘
- 通过 visibility: hidden 隐藏一个 DOM 节点-只触发重绘,因为没有几何变化
- 移动或者给页面中的 DOM 节点添加动画
- 添加一个样式表,调整样式属性
- 用户行为,例如调整窗口大小,改变字号,或者滚动。
# 如何避免重排重绘?
- 集中改变样式,不要一条一条地修改 DOM 的样式
- 不要把 DOM 结点的属性值放在循环里当成循环里的变量。
- 为动画的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是不会 reflow 的
- 不使用 table 布局
- 尽量只修改 position:absolute 或 fixed 元素,对其他元素影响不大
- 动画开始 GPU 加速,translate 使用 3D 变化
- 提升为合成层
- 合成层的位图,会交由 GPU 合成,比 CPU 处理要快
- 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
- 对于 transform 和 opacity 效果,不会触发 layout 和 paint
# OSI 七层模型
- (1)应用层 (为计算机用户提供应用接口或网络服务。网络协议: HTTP,HTTPS,FTP,POP3,SMTP)
- (2)表示层 (提供应用层数据的加密例如 base64、ase 加密)
- (3)会话层 (负责建立、管理、终止表示层之间的通信)
- (4)传输层 (为上层协议提供端到端的数据传输服务)
- (5)网络层 (通过 IP 寻址建立两个节点之间的连接)
- (6)数据链路层 (将比特组合成字节,再将字节组合成帧,使用链路层地址访问介质,并进行差错检测。)
- (7)物理层 (最终信号的传输。通过物理介质传输比特流网线、中继器)
# https 和 http 的区别
- 安全方面:http 使用的是
无状态和明文传输
。而 https 实际上是http+ssl协议组成的加密传输协议
- 响应速度: 理论上 http 响应速度更快,只需要三次握手;而 https 则在三次握手的情况下,还需要一次 ssl 握手
- 端口: http 默认是
80
端口,而 https 默认端口是443
- 消耗资源方面:https 是构建在 ssl 之上的协议,所以 https 会消耗更多的服务资源。
- 展示方面: https 会在浏览器上面加上一个绿色的锁。
# https 的加密是在 ISO 七层模型中的哪一层?
- https 的加密在 ISO 七层模型中的
表示层
。
# 网络安全
# 前端秘钥怎么存储
- 前端需要传输时,需要先向服务器获取一个
加密公钥
(加密密钥对由后端生成,以公钥为 key,私钥为 value 存储在 redis 中) - 获取到数据后,将数据进行加密并将加密后的数据和公钥一起传给后端。
- 后端通过公钥从 redis 中得到配对的秘钥,然后对密码解密
# 添加加密后接口访问速度比之前快还是慢,慢多少?
- 没有影响
# 有没有自己写过爬虫脚本?
# 怎么让自己的网站不被爬虫爬?
- 限制 User-Agent 字段:User-Agent 字段能识别用户所使用的操作系统、版本、CPU、浏览器等信息,如果请求来自非浏览器,就能识别其为爬虫,阻止爬虫抓取网站信息
- 限制 IP:限制 IP 是最常见的手段之一,为了效率,恶意爬虫的请求频率往往比正常流量高,找出这些 IP 并限制其访问,可以有效降低恶意爬虫造成的危害。
- 添加验证码:在登录页等页面,添加验证码,以识别是正常流量还是恶意爬虫,也是一种基本的操作。不过如今爬虫技术,早已能解决验证码的问题,例如二值化、中值滤波去噪等等。
- Cookies 限制: 根据业务需求,可对 Cookies 进行限制,要求用户登录后才能使用某些功能或权限,并针对同一账号的访问速度进行限制。
- 使用爬虫管理产品: 可以使用防火墙产品,能够有效防范爬虫,比如阿里云、腾讯云
# 性能优化
# 前端怎么做性能优化?
- cdn 服务加速网络访问
- 浏览器缓存
- gzip 压缩
- 图片优化:压缩、base64、svg、字体图标、懒加载
- 首屏优化:路由懒加载、图片懒加载、dom 懒加载、loading 效果、骨架屏
- dns 预解析(dns-prefetch)、js 和 css 的预解析 preload(当前页资源)和 prefetch(其他页资源)
- script 标签放底部,适当地加 async 和 defer 异步属性
- 减少重排重绘
- 节流和防抖
# 首页白屏怎么优化
- ssr 渲染(后端渲染)
- 预渲染(prerender-spa-plugin)
- 使用骨架屏
# fetch 和 ajax 的区别是什么
- ajax 是利用 XMLHttpRequest 对象来请求数据的,而 fetch 是 window 的一个方法
- ajax 基于原生的 XHR 开发,XHR 本身的架构不清晰,已经有了 fetch 的替代方案
- fetch 比较与 ajax 有着更好更方便的写法
- fetch 只对网络请求报错,对 400,500 都当做成功的请求,需要封装去处理
- fetch 没有办法原生监测请求的进度,而 XHR 可以