export const rotate = function (matrix: number[][]) {
let n = matrix.length
// matrix[x][y] => matrix[y][n - 1 - x]
const changeItem = (num: number, x: number, y: number, rodateTime: number, isOnce?: boolean) => {
// 终止条件
rodateTime++
if (num === matrix[x][y] && !isOnce && rodateTime > 4) return
let space = matrix[y][n - 1 - x]
matrix[y][n - 1 - x] = num
changeItem(space, y, n - 1 - x, rodateTime)
}
for (let j = 0; j < Math.floor(n / 2); j++) {
let rodateTime = 0;
for (let i = j; i < n - 1 - j; i++) {
changeItem(matrix[j][i], j, i, rodateTime, true)
rodateTime = 0
}
}
return matrix
};
export const findkhByQuery = (num: number = 3) => {
/**
* arr.push()
* 怎样组合呢? (())()
* ( + )()()
* 思路:一个夸号去移动
*
* ()()() 取出第1个 从 0 移动到 最后一位
* let str = '('
* )()() 发现 不管第一个如何移动 只有第一次才行 移动到右边后发现有首位是 右夸号结束移动
*
* (()() 取出第2个 ')' 从 0 移动到 最后一位
* let str = ')'
*
* (()() 取出第3个 ')' 从 0 移动到 最后一位
* let str = '('
*
*
* value
*/
let initStr = '';
let arr = []
for (let i = 0; i < num; i++) {
initStr += "()"
}
let n = initStr.length
let map: any = {}
const isValid = (str: string): boolean => {
if (!str || str[0] === ')') {
return false
}
let stack = []
for (let i = 0; i < str.length; i++) {
if (str[i] === '(') {
stack.push('(')
}
if (str[i] === ')') {
let x = stack.pop()
if (!x) {
return false
}
}
}
if (stack.length == 0) {
return true
}
return false
}
// 可以再套一个循环 用于决定要 去几个字符串
for (let k = 0; k < num * 2; k++) {
for (let i = 0; i < num * 2; i++) {
// 从 0 移动到最后一位
// let val = initStr[i]
// 取出需要拿出来的字符串
let val = initStr.slice(0, k + 1)
console.log(val, 'val');
// 取到切割剩下的
// 'abcdef' abc cde
let newStr = initStr.slice(k + 1, n)
for (let j = 0; j < num * 2; j++) {
let moveRes = newStr.slice(0, j) + `${val}` + newStr.slice(j, n)
// debugger
// debugger
if (!map[moveRes]
&& isValid(moveRes)
) {
map[moveRes] = moveRes
arr.push(moveRes)
}
// arr.push(moveRes)
}
}
}
console.log(arr, 'arr');
}
// console.log(findkhByQuery(3));
var partition = function (s: string) {
let arr: string[][] = []
arr.push(s.split(''))
for (let i = 0; i < s.length; i++) {
let subitem: string[] = []
for (let j = i + 1; j < s.length; j++) {
let val = s.substring(i, j + 1)
if (val.split('').reverse().join('') === val) {
/*
如何做到自动补全的功能? abcde bcd => a,bcd,e
* abacd_aba => ,aba,c,d,_,aba,
* abacd_abaDDC => ,aba,c,d,_,aba,D,D,C
* 切掉空的
*/
let ns = s.replace(val, `,${val},`)
// 去掉首尾的,号
if (ns.startsWith(',')) {
ns = ns.substring(1, ns.length)
} else if (ns.endsWith(',')) {
ns = ns.substring(0, ns.length - 1)
}
let resItem: any[] = []
// aba,cd_,aba => aba,c,d,_,aba
ns.split(',').forEach(item => {
if (item !== val) {
resItem = [...resItem, ...item.split('')]
} else {
resItem.push(item)
}
})
subitem = resItem
}
}
if (subitem.length) {
arr.push(subitem)
}
}
return arr
};
console.log(partition("fff"));
const template = `{{ name }}{{ age }}`;
let obj = { name: 'glack', age: '23' }
const func = (template: string, obj: any) => {
Object.keys(obj).forEach(item => {
let key = '{{ ' + item + ' }}'
template = template.replace(key, ' ' + obj[item] + ' ')
})
return template
}
console.log(func(template, obj), 'xxx');
实现全排列
/*
[1]
[12, 21]
[312, 132, 123, 321, 231, 213]
[0,-1,1]
[1]
f( [1, 2, 3] ) = f([1, 2]) 和 3 的排列
f( [1, 2]) = f([1]) 和 2 的排列
f( [1] ) = [1]
思路:f(12345) = f(1234) 和 5 组合起来的数组
*/
const qpl = (nums: number[]): any[] => {
let time = 0
const dfs = (a: number[]): any[] => {
let n = a.length
if (n == 1) {
return [a]
}
let res: any[] = []
let fontArr = a.slice(0, n - 1) // [1, -1]
let lastStr = a.slice(n - 1, n)[0] // 0
dfs(fontArr)?.forEach(it1 => {
// [1, -1] [-1, 1] 中 使用 0 来移动位置
for (let i = 0; i < it1.length + 1; i++) {
res.push([...it1.slice(0, i), lastStr, ...it1.slice(i, it1.length)])
}
})
return res
}
return dfs(nums)
}
console.log(qpl([-1, 2, 3]));
有序数组中找大于等于 n 最左边的数
给一个数组:[1,2,3,4,4,4,5,6,7,7,8,11,345,567] 找到大于 4最左边的数字;
- 使用二分法
export const erfen = (arr: number[], n: number) => {
let len = arr.length
if (!arr || len < 2 || typeof n !== 'number') return arr
let curValue = 0, midIndex, midValue
let leftNums = [1]
let rightNums = []
let handleArr = JSON.parse(JSON.stringify(arr))
while (leftNums.length + rightNums.length > 0) {
midIndex = Math.floor(handleArr.length / 2)
midValue = handleArr[midIndex]
leftNums = handleArr.slice(0, midIndex - 1)
rightNums = handleArr.slice(midIndex + 1, handleArr.length)
if (midValue >= n) {
curValue = midValue
handleArr = leftNums
} else {
handleArr =
}
}
return curValue
}
优化简洁版
思路:如果传递的 n 大于等于 midValue 那就将右边的下标往中间挪动。这样就可以开始从 0 到 midIndex 之前找最小的值。如果小于,midIndex 到 len - 1 找小的最左边的值。
// 有序数组中找大于等于 n 最左边的数
export const erfen = (arr: number[], n: number) => {
let len = arr.length
if (!arr || len < 2 || typeof n !== 'number') return -1
let leftIndex = 0
let rightIndex = len - 1
let ans = -1
while(leftIndex <= rightIndex){
let midIndex = Math.floor((leftIndex + rightIndex)/ 2)
if (arr[midIndex] >= n) {
ans = midIndex
rightIndex = midIndex - 1
}else{
leftIndex = midIndex + 1
}
}
return arr[ans]
}
console.log(erfen([1, 2, 3, 3, 3, 3, 4, 5, 5, 7, 8, 10, 11, 13], 3));
反转链表
- 保存gnext数据
- gPrev赋值给当前 head 节点的 next 节点
- 移动 gPrev 到下一步 (head 赋值给 gPrev)
- gPrev 接收搭配报错的gNext数据
interface INode {
value: number,
next?: INode,
prev?: INode
}
export const linkReverseFunc = (head: INode) => {
let prev: INode | undefined = undefined
let next: INode | undefined = undefined
while (head.next) {
// 保存next数据
next = head.next
// 将当前节点的下一个指向prev 如果是第一次 则为空 如果第二次 在第一步prev已经赋值给了当前head节点
head.next = prev
// 当前节点给下一次的prev使用
prev = head
// 下一个节点变成开始全局记录的next
head = next
if (!prev.next) {
delete prev.next
}
}
if(!head.next){
console.log(head,'head');
head.next = prev
prev = head
}
return prev
}
let nodeinit: INode = {
value: 100,
next: {
value: 200,
next: {
value: 300,
next: {
value: 400,
next: {
value: 500,
}
}
}
}
}
console.log(linkReverseFunc(nodeinit), 'test');
并查集(待定)
如果 多个用户 a 或者 b 或者 c 的字段值一样 就合并
返回合并之后的用户加上count字段
思路:声明多个集合,如果,没出现过,就set到map里面,如果出现过,就count+1
下次再来。。。太难了,有思路再来。
// users合并
/* [
{ a: 1, b: 2, c: 3},
{ a: 1, b: 3, c: 4},
{ a: 8, b: 9, c: 10},
{ a: 10, b: 9, c: 8}
]
思路:三个集合 aMap bMap cMap
默认为空 最后会变成 aMap: { a:
{ a: 1, b: 2, c: 3, count: 2}
{ a: 8, b: 9, c: 10} }
{ a: 10, b: 9, c: 8}
},
bMap: { a: { a: 2, b: 3, c: 9, count: 3}}
cMap: { a: { a: 3, b: 4, c: 10, count: 4}}
*/
export const mergeUser = (users: IUserInfo[]) => {
let aMap = new Map()
let bMap = new Map()
let cMap = new Map()
users.forEach(item => {
// 如果三个集合都没有这个item 那就放在
if (!aMap.has(item.a) && !bMap.has(item.b) && !cMap.has(item.c)) {
aMap.set(item.a, { ...item, count: 1 })
bMap.set(item.b, { ...item, count: 1 })
cMap.set(item.c, { ...item, count: 1 })
// return
}
if (aMap.has(item.a)) {
let aItem = aMap.get(item.a)
aItem.count++
// return
}
if (bMap.has(item.b)) {
let bItem = bMap.get(item.b)
bItem.count++
// return
}
if (cMap.has(item.c)) {
let cItem = cMap.get(item.c)
cItem.count++
// return
}
})
console.log(aMap, bMap, cMap, 'abc');
}
mergeUser([{ a: 1, b: 2, c: 3 }, { a: 1, b: 3, c: 4 }, { a: 8, b: 9, c: 10 }, { a: 10, b: 9, c: 8 }])
覆盖绳子最多的点数
// 盖住最大值的绳子上的点
export const ropeCoveringPoint = (arr: number[], n: number) => {
let left = 0, right = 0, max = 0, len = arr.length
while (left < len) {
while (left < len && arr[right] - arr[left] <= n) {
right++
}
// debugger
max = Math.max(max, right - (left++))
}
return max
}
console.log(ropeCoveringPoint([1, 3, 6, 8, 10, 12, 13, 17, 19, 24, 28, 31], 16));
字符串只有 ”G“和”B“ 如果要把G放最左边,B放最右边,求需要移动几部才能让所有g在左边,b在右边。
export const moveChar = (str: string) => {
let len = str.length
let count = 0
let gCount = 0
for (let i = 0; i < len; i++) {
if (str[i] === 'G') {
gCount ++
let res = (i - gCount) < 0 ? 0 : (i - gCount)
// debugger
count = count + res + 1
}
}
return count
}
console.log(moveChar('GGBBGGGBBBGGGGBBBB'));
生成括号
思路:f(n) 的值永远都是 f(n-1) 的结果 里面每一项push ()
export function generateParenthesis(initn: number): string[] {
const func = (n: number): string[] => {
if (n == 1) {
return ['()']
}
let res = func(n - 1)
let pjRes = []
for (let i = 0; i < res.length; i++) {
let str = res[i]
let sL = str.length
for (let j = 0; j < sL; j++) {
pjRes.push(`${str.slice(0, j)}()${str.slice(j, sL)}`)
}
}
return pjRes
}
return Array.from(new Set(func(initn)))
}
console.log(generateParenthesis(3), 'na');
实现深拷贝包括 map set 类型
- map
- set
- 函数
- 循环引用
- 深层数据拷贝
export function cloneDeep(obj: any, map = new WeakMap()): any {
if (typeof obj !== 'object' || obj == null) return obj
// 避免循环引用
const objFromMap = map.get(obj)
if (objFromMap) return objFromMap
let target: any = {}
map.set(obj, target)
// Map
if (obj instanceof Map) {
target = new Map()
obj.forEach((v, k) => {
const v1 = cloneDeep(v, map)
const k1 = cloneDeep(k, map)
target.set(k1, v1)
})
}
// Set
if (obj instanceof Set) {
target = new Set()
obj.forEach(v => {
const v1 = cloneDeep(v, map)
target.add(v1)
})
}
// Array
if (obj instanceof Array) {
target = obj.map(item => cloneDeep(item, map))
}
// Object
for (const key in obj) {
const val = obj[key]
const val1 = cloneDeep(val, map)
target[key] = val1
}
return target
}
const a: any = {
set: new Set([1, 2, 3, 4]),
map: new Map([['x', 10], ['y', 20]]),
info: {
city: '北京'
},
fn: () => { },
}
a.self = a
console.log(cloneDeep(a), 'cloneDeep');
[‘1’, ‘2’, ‘3’].map(parseInt)
上面代码相当于
['1', '2', '3'].map((item, index) => {
parseInt(item, index)
// parseInt
// 转换的数字
// raIndex 第二个参数是 进制 单位 (2-32)
/**
* parInt('1', 0) // 0 的时候 说明没传 是2进制
* parInt('2', 1) // 1 的时候 没有 1 进制 所以返回 NAN
* parInt('3', 2) // 2 的时候 3 不属于二进制的内容(011101010101010这种只有 0 和 1 才属于二进制)
*/
})
所以最终返回 [1, NaN, NaN]
函数的参数传递就是赋值传递
const fn = (x, y) => {
// 这里就相当于 x = num; y = obj
}
let num = 'aaa'
let obj = { name: 'glack' }
fn(num, obj)
数组转为树节点(针对有序的二维数组排列树节点)
const arr1 = [
{ id: 1, name: '部门A', parent_id: 0 },
{ id: 2, name: '部门B', parent_id: 1 },
{ id: 3, name: '部门C', parent_id: 1 },
{ id: 4, name: '部门D', parent_id: 2 },
{ id: 5, name: '部门E', parent_id: 4 },
{ id: 6, name: '部门F', parent_id: 4 },
];
/**
* 针对有序的二维数组排列树节点
* @param arr arr
*/
interface ITreeNode {
id: number,
name: string,
children?: ITreeNode[]
}
interface ITreeItem {
id: number,
name: string,
parent_id: number
}
export const treeTransform = (arr: ITreeItem[]): ITreeNode | null => {
// 用于id 和 treeNode 的映射
let treeMap: Map<number, ITreeNode> = new Map()
let root: ITreeNode | null = null
// 定义tree node 并加入 map
arr.forEach(({ id, name }) => {
const treeNode: ITreeNode = { id, name }
treeMap.set(id, treeNode)
})
arr.forEach(item => {
const { id, parent_id } = item
const treeNode = treeMap.get(id)
// 找到 parentNode 并加入到它的 children
const parentNode = treeMap.get(parent_id)
if (parentNode && treeNode) {
if (parentNode.children == null) parentNode.children = []
parentNode.children.push(treeNode)
}
// console.log(parentNode, 'parentNode');
// console.log(treeNode, 'treeNode');
// 找到根节点
if (parent_id === 0) {
// @ts-ignore
root = treeNode
}
})
return root
}
let result = treeTransform(arr1)
console.log(result);
怎么证明 root 的子元素一直在变?
- 在 map 外部修改map里面的children 元素 里面的元素会跟着修改
let map = new Map();
map.set('arr', []);
let arr = map.get('arr');
arr.push('a');
map.get('arr') // ['a']
树转为数组
let res: any[] = []
let treeTransformArray = (root: any[], parent_id: number = 0) => {
let arr = Array.isArray(root) ? root : [root]
// 遍历所有元素
arr.forEach(item => {
if (item.children) {
treeTransformArray(item.children, item.id)
}
res.push({ id: item.id, name: item.name, parent_id })
})
}
// @ts-ignore 这里的result 是上面的结果
treeTransformArray(result)
console.log(res, 'zz');
原型链面试题
function Foo(){
Foo.a = function name() {
console.log(1);
}
this.a = function () {
console.log(2);
}
}
Foo.prototype.a = function(){ console.log(3); }
Foo.a = function(){ console.log(4) }
Foo.a()
let obj = new Foo()
obj.a()
Foo.a()
// 4 2 1
promise和事件循环的算法
Promise.resolve().then(() => {
console.log(1)
}).then(() => {
console.log(2)
}).then(() => {
console.log(3)
}).then(() => {
console.log(4)
}).then(() => {
console.log(5)
}).then(() => {
console.log(6)
})
Promise.resolve().then(() => {
console.log(10)
}).then(() => {
console.log(20)
}).then(() => {
console.log(30)
}).then(() => {
console.log(40)
}).then(() => {
console.log(50)
}).then(() => {
console.log(60)
})
// 1 10 2 20 3 30 4 40 5 50 6 60
多个promise 已完成的状态同时执行 .then 的链式调用
所以 .then会交替执行
Promise.resolve().then(() => {
console.log(1)
return Promise.resolve(100)
}).then((res) => {
console.log(res)
}).then(() => {
console.log(3)
}).then(() => {
console.log(4)
}).then(() => {
console.log(5)
}).then(() => {
console.log(6)
})
Promise.resolve().then(() => {
console.log(10)
}).then(() => {
console.log(20)
}).then(() => {
console.log(30)
}).then(() => {
console.log(40)
}).then(() => {
console.log(50)
}).then(() => {
console.log(60)
})
// 1 10 20 30 100 40 3 50 4 60 5 6
.then 中返回promise 实例会慢两拍(三步) 因为需要时间把自己 penging 变成 fulfilled状态
setState 同步异步问题
didMount(){
// 假设默认值是 0
this.setState({
val: this.state.val + 1
})
console.log(this.state.val) // 异步更新 0
this.setState({
val: this.state.val + 1
})
console.log(this.state.val) // 异步更新 0
setTimeout(() => {
this.setState({
val: this.state.val + 1
})
console.log(this.state.val) // 同步更新 2
this.setState({
val: this.state.val + 1
})
console.log(this.state.val) // 同步更新 3
})
}
setState 会合并
如果同时多个setState 都要修改val 那么会优先使用 下面的那个setState语句
不合并的情况
传入函数: this.setState((prev, props) => { return …})
同步更新
setState 同步更新:在setTimeout setInterval promise.then
自定义的 dom 事件
ajax 回调
例子:
document.getElementById('id').addEventlistener('click', () => {
this.setState({
val: this.state.val + 1
})
console.log(this.state.val) // 同步更新
})
setState是同步 不过只不过是当做异步处理
React 18 中不一样了
可以在setTimeout 中 异步更新了
Auto Batch
需要将 ReactDOM.render 替换为 ReactDOM.createRoot
a.x 比赋值的优先级高
let a = { n: 1 }
let b = a
a.x = a = { n: 2 }
console.log(a.x) // undefined
console.log(b.x) // n: 2
字符串对象的key 只能是字符串或者 symbol 类型
let a = {}, b = '123', c = 123
a[b] = 'b'
a[c] = 'c'
// log => a[b] c
let a = {}, b = Symbol('123'), c = Symbol('123')
a[b] = 'b'
a[c] = 'c'
// log => a[b] b
设计一个前端统计 SDK
- 统计的范围:性能 错误 pv 自定义事件
- 发送数据使用 img 的 src 来发送get请求
- 报错统计结合 Vue React 报错
sourcemap 的作用
- 线上代码压缩报错,sourcemap 可解决这个问题
- 可以给压缩后的文件识别报错的行数,找到错误的地方
如何设置?
- webpack 通过 devtool 配置sourcemap
- eval - js 在 eval(…) 中 不生产sourcemap
- source-map 生成单独的 map 文件, 并在js 最后指定
- eval-source-map,js在eval(…) 中,sourcemap内嵌
- inline-source-map - sourcemap 内嵌到 js 中
- cheap-source-map - sourcemap 中只有行信息,没有列
- eval-cheap-source-map 同上,只有行没有列
非开源项目:不要泄漏sourcemap ! 否则容易被反解
SPA 和 MPA 怎么选择
-
SPA 特点
- 功能较多,一个页面展示不完
- 以操作为主,非展示为主
- 适合一个综合Web应用
-
MPA
- 功能较少,展示为主,例如:分享页,新闻详情页,微信公众号发出的页面
多种排序算法
快速排序
// slice
export const quickSort1 = (arr: number[]): number[] => {
let length = arr.length
if (length === 0) return []
let left = [], right = []
let midIndex = Math.floor(arr.length / 2)
let midValue = arr.slice(midIndex, midIndex + 1)[0]
for (let i = 0; i < arr.length; i++) {
if (arr[i] < midValue) {
left.push(arr[i])
} else if (arr[i] > midValue) {
right.push(arr[i])
}
}
return quickSort1(left).concat([midValue], quickSort1(right))
}
// splice
export const quickSort2 = (arr: number[]): number[] => {
let length = arr.length
if (length === 0) return []
let left = [], right = []
let midIndex = Math.floor(arr.length / 2)
let midValue = arr.splice(midIndex, 1)[0]
for (let i = 0; i < arr.length; i++) {
if (arr[i] < midValue) {
left.push(arr[i])
}
if (arr[i] > midValue) {
right.push(arr[i])
}
}
return quickSort2(left).concat([midValue], quickSort2(right))
}
const arr = [4,5,7,1,2,3,6,3,15,5,123,8,9,1,9,8]
console.log(quickSort2(arr), 'arr')
选择排序
// 选择排序一
export function sort1(arr: number[]): number[] {
let len = arr.length
if (arr == null || arr.length < 2) return []
let ti = 0
while (ti < len) {
// 找出最小值
let num: number | null = null
let idx: number = 0
for (let i = ti; i < len; i++) {
if (num == null || arr[i] < num) {
num = arr[i]
idx = i
}
}
// 最小下标和 第 ti 项换位置
let n = arr[ti]
arr[ti] = arr[idx]
arr[idx] = n
ti++
}
return arr
}
// 选择排序二
export function sort2(arr: number[]): number[] {
let length = arr.length
for (let i = 0; i < length; i++) {
let minValueIndex = i
for (let j = i + 1; j < length; j++) {
minValueIndex = arr[j] < arr[minValueIndex] ? j : minValueIndex
}
// 交换位置
let n = arr[i]
arr[i] = arr[minValueIndex]
arr[minValueIndex] = n
}
return arr
}
冒泡排序
思路:
[ 3, 2, 1, 6, 5, 3, 7]
0 1 2 3 4 5 6
0 1 对比 大的往右
1 2 对比 大的往右
2 3
…
以此类推
最大的在最右边
export function sort3(arr: number[]): number[] {
let length = arr.length
if (arr.length < 2) return []
let time = 0
for (let i = 0; i < length; i++) {
for (let j = 0; j < length - i; j++) {
time++
console.log(time);
// 如果 i 大于 j 交换
if (arr[j] > arr[j + 1]) {
let n = arr[j + 1]
arr[j + 1] = arr[j]
arr[j] = n
}
}
}
return arr
}
插入排序
/**
*
* [2, 4, 1, 3, 8, 6, 7]
* 思路:
* 0 - 1 arr: [2, 4, 1, 3, 8, 6, 7] 排序
* 0 - 2 arr: [1, 2, 4, 3, 8, 6, 7] 排序
* 0 - 3 arr: [1, 2, 3, 4, 8, 6, 7] 排序
* 0 - len - 1 排序 ...
*/
// 真·插入排序
function sort6(arr: number[]): number[] {
let length = arr.length
if (!arr || length < 2) return arr
for (let i = 1; i < length; i++) {
let endIndex = i
while (endIndex - 1 >= 0 && arr[endIndex] < arr[endIndex - 1]) {
// 交换位置
let n = arr[endIndex]
arr[endIndex] = arr[endIndex - 1]
arr[endIndex - 1] = n
endIndex --
}
}
return arr
}
// 真·插入排序优化
function sort7(arr: number[]): number[] {
let length = arr.length
if (arr == null || length < 2) return arr
for (let i = 1; i < length; i++) {
for (let endI = i; endI - 1 >= 0 && arr[endI] < arr[endI - 1]; endI--) {
let n = arr[endI]
arr[endI] = arr[endI - 1]
arr[endI - 1] = n
}
}
return arr
}
数组扁平化
- 扁平一层数组
- 效果 [1, 2, [3, 4, [5]]] -> [1, 2, [3, 4, 5]]
export const flatten1 = (arr: any[]): any[] => {
let res: any[] = []
for (let i = 0; i < arr.length; i++) {
let item = arr[i]
if (Array.isArray(item)) {
for (let j = 0; j < item.length; j++) {
res.push(item[j])
}
} else {
res.push(item)
}
}
return res
}
export const flatten2 = (arr: any[]): any[] => {
let res: any[] = []
for (let i = 0; i < arr.length; i++) {
res = res.concat(arr[i])
}
return res
}
const initarr = [1, [2, [3], 4], 5]
console.log(flatten1(initarr), '1');
console.log(flatten2(initarr), '2');
- 扁平多层数组
concat的方式
export const flatten3 = (arr: any[]): any[] => {
let res: any[] = []
for (let i = 0; i < arr.length; i++) {
let item = arr[i]
if (Array.isArray(item)) {
let nArr = flatten3(item)
res = res.concat(nArr)
} else {
res.push(item)
}
}
return res
}
push的方式
export const flatten3 = (arr: any[]): any[] => {
let res: any[] = []
for (let i = 0; i < arr.length; i++) {
let item = arr[i]
if (Array.isArray(item)) {
let nArr = flatten3(item)
// 解释:不管如何走,flatten3 返回的永远是拍平的数组,因为他不是数组的话会再次走进里面去。
// 至于为什么需要在contact 或者 forEach 因为返回的是一层的数组。
res = nArr.forEach(n => res.push(n))
} else {
res.push(item)
}
}
return res
}
toString方式
const arr = [1, [2, [3], 4], 5]
arr.toString() // 1,2,3,4,5
// 这样的方法不健壮
常见的类型判断 引用数据类型和基本数据类型的区别
引用类型 (Object,Array) 基本类型 (number, string, boolean, symbol, null, undefined)
export const getType = (x: any) => {
let type = Object.prototype.toString.call(x)
let spaceIndex = type.indexOf(' ')
let res = type.slice(spaceIndex + 1, -1)
return res.toLowerCase()
}
new 一个对象发生了什么
class Foo {
this.name = 'glack'
this.age = 'age'
getName () {
return 'xxx'
}
}
上面转换成了es5 的语法
function Foo {
this.name = 'glack'
this.age = 'age'
}
Foo.prototype.getName = function() {
return 'xxx'
}
手写一个new 方法
/**
*
* @param consturctor
* @param args
*/
export function customNew<T>(consturctor: Function, ...args: any[]): T {
// 1. 创建一个空对象,集成 constructor 的原型
const obj = Object.create(consturctor.prototype)
// 2. 将 obj 作为this,执行 constructor ,传入参数
consturctor.apply(obj, args)
// 3. 返回 obj
return obj
}
class Foo {
// 属性
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
getName() {
return this.name
}
}
// const f = new Foo('glack', 23)
const f = customNew<Foo>(Foo, 'glack', 23)
console.log(f, 'f');
console.log(f.age);
console.log(f.getName());
遍历一个 dom 树
// 遍历子节点
export function visitNode(n: Node) {
if (n instanceof Comment) {
console.log(n, 'n');
}
if (n instanceof Text) {
console.log('Text node ---', n.textContent?.trim());
}
if (n instanceof HTMLElement) {
console.log('HTML node ---', `<${n.tagName.toLowerCase()}>`);
}
}
// 深度优先遍历
function deepFindNode(root: Node) {
visitNode(root)
const childNodes = root.childNodes
if (childNodes.length) {
childNodes.forEach(item => {
deepFindNode(item)
})
}
}
// 广度优先遍历(队列)
function breadFirstTraverse(root: Node) {
const quene: Node[] = []
// 根节点入队
quene.unshift(root)
while(quene.length > 0){
const curNode = quene.pop()
if(curNode == null) break
visitNode(curNode)
// 子节点入队
const childNodes = curNode.childNodes
if (childNodes.length > 0) {
childNodes.forEach(n => quene.unshift(n))
}
}
}
setTimeout(() => {
const box = document.getElementById('box')
if (!box) throw Error('box is null')
breadFirstTraverse(box)
}, 1000);
递归
问题:计算 1 - 100 的和。
- 一般是先假装该函数是存在的,通过数学中的找规律的方法,找到方法的递归体
func(n) = func(n-1) + n
- 找到临界值,比如,1 - 100 的和,就是 func(n-1) + 100, 临界值是 n = 1 reurn 1
if (n === 1) return 1
- 写入函数体
function comput1(num: number): number {
if (num === 1) return 1
return comput1(num - 1) + num
}
一些基础题:
// 求 2,4,6,8,10... 第n项与前n项之和
function sum1(num: number): number {
if (num === 1) return 2
return sum1(num - 1) + 2 * num
}
// 求1 3 5 7 9 ... 第n项与第n项之和
function sum2(num: number): number {
if (num === 1) return 1
return sum2(num - 1) + (2 * num - 1)
}
console.log(sum2(5));
模板字符串替换
export function render(temp: string, person: { name: string, age: number, sex: boolean }): string {
let reg = /\{\{(\w+)\}\}/
if (reg.test(temp)) {
// 找到模板字符串
// @ts-ignore
const name = reg.exec(temp)[1]
// @ts-ignore
temp = temp.replace(reg, person[name])
return render(temp, person)
}
return temp
}
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let person = {
name: '布兰',
age: 12,
sex: true
}
console.log(render(template, person));
reduce 实现 map 和 filter 方法
export function reduceCustom(arr: any[]): any[] {
return arr.reduce((filterRes, item) => {
if (item > 20) {
filterRes.push(item * 2)
}
return filterRes
}, [])
}
const arr = reduceCustom([10, 20, 30, 40, 50])
console.log(arr, 'arr');
reduce 实现 map 方法
export function zMap(arr: any[], fn: Function) {
return arr.reduce((initArr: any[], item: number) => {
return initArr.concat(fn(item))
}, [])
}
const arr = zMap([10, 20, 30, 40, 50], (x: any) => x * 2)
console.log(arr, 'arr'); // 20 40 60 80 100
二维数组累加
const arr = [
{ id: 1, name: 'glack1', count: 1 },
{ id: 2, name: 'glack2', count: 2 },
{ id: 3, name: 'glack3', count: 3 },
{ id: 4, name: 'glack4', count: 4 },
{ id: 1, name: 'glack5', count: 2 },
{ id: 3, name: 'glack6', count: 0 },
{ id: 2, name: 'glack7', count: 1 },
{ id: 4, name: 'glack8', count: 2 },
]
export function arrayAdd(arr: any[]) {
let map: any = {}
return arr.reduce((initArr, item) => {
let key = item.id
if (map[key]) {
let num = map[key].count + item.count
map[key].count = num
initArr = initArr.map((it1: any) => {
if (it1.id == key) {
return {
...it1,
count: num
}
}
return it1
})
} else {
map[key] = {
count: item.count
}
initArr.push(item)
}
return initArr
}, [])
}
console.log(arrayAdd(arr), 'xxx');
一维数组递归变成树节点。
interface Node {
id: number,
parent_id: number | string,
children?: any[]
}
export function treeTransform(arr1: Node[], id: number | string = ''): any {
let nArr: Node[] = arr1.filter(item => item.parent_id == id)
return nArr.map(item => ({
...item,
children: treeTransform(arr1, item.id)
}))
}
const arr1 = [
{ id: 1, parent_id: '' },
{ id: 2, parent_id: 1 },
{ id: 3, parent_id: 1 },
{ id: 4, parent_id: 2 },
{ id: 5, parent_id: 4 }
];
console.log(treeTransform(arr1));
手写 LazyMan
- 支持sleep
- 支持链式调用
思路:
- 首先初始化一个任务队列
- 每碰到eat 或者 sleep 都先注册一个函数,把函数放进任务队列中
- 返回this,让它支持链式调用
- 最后等到 eat 和 sleep 创建完成了任务队列之后
- 再调用第一个next方法(shift)
export class LazyMan {
private tasks: Function[] = []
private name: string
constructor (name: string){
this.name = name
setTimeout(() => {
// 等到初始化执行完毕再执行next
console.log(this.tasks.length);
// 也就是等到 eat(a) eat(b) eat(c) sleep(xx) 才会触发next()
this.next()
});
}
private next(){
const task = this.tasks.shift()
if (task) task()
}
eat(food: string){
const task = () => {
console.log(`${this.name} eat ${food}`);
this.next()
}
this.tasks.push(task)
return this
}
sleep(seconds: number){
const task = () => {
setTimeout(() => {
console.log(`${this.name}睡了${seconds}s 开始执行下一个任务`);
this.next()
}, seconds * 1000);
}
this.tasks.push(task)
return this
}
}
const me = new LazyMan('glack')
me.eat('apple').eat('bannan').sleep(3).eat('pear')
手写一个 curry 函数 把其他函数柯里化(函数式编程,不常用,但是会考)
思路:
curry 最终返回一个函数
执行fn 中间状态返回函数,add(1) 或者 add(1)(2)
最后情况执行函数 add(1)(2)(3) 执行
export function curry(fn: Function) {
// 这里的fnArgsLength 就是指 fn 的参数的长度
const fnArgsLength = fn.length
let args: any[] = []
function calc(this: any, ...newArgs: any[]) {
args = [
...args,
...newArgs
]
if (args.length < fnArgsLength) {
// 如果当前拼接的长度小于整个方法应传递的长度 返回当前函数 给继续调用 add(20)
return calc
}else{
// 如果长度是拼接的三个参数 那就直接执行传递的方法
return fn.apply(this, args.slice(0, fnArgsLength))
}
}
return calc
}
function add(a: number, b: number, c:number): number{
return a + b + c
}
const curryAdd = curry(add)
console.log(curryAdd(10)(20)(30));
// 这里如果传递方式有误的话 curryAdd(10)(20)(30) 会导致length 判断出错 没有返回函数 导致fn()() 报错
intanceof 原理是什么 用代码表示
export const myIntanceof = (intance: any, origin: any) => {
if (intance == null) return false
let type = typeof intance
if (type !== 'object' && type !== 'function') {
// 值类型 直接返回false
return false
}
let tempIntance = intance
while(tempIntance){
if (tempIntance.__proto__ == origin.prototype) {
return true
}
// 向上查找 如果为 null 就不进入循环
tempIntance = tempIntance.__proto__
}
return false
}
console.log(myIntanceof({}, Object)); // true
console.log(myIntanceof([], Object)); // true
console.log(myIntanceof([], Array)); // true
console.log(myIntanceof({}, Array)); // false
console.log(myIntanceof('123', String)); // false
console.log(myIntanceof(123, Number)); // false
console.log(myIntanceof(true, Boolean)); // false
console.log(myIntanceof(()=>{}, Function)); // true
手写bind
分析:
- 返回function
- 重命名传递的this
- 定义调用者的this
- 拼接参数
// @ts-ignore
Function.prototype.customBind = function (context: any, ...bindArgs: any[]) {
// context 是 bind 传入的this,bindArgs 是bind 传入的各个参数
const self = this
return function (...args: any[]) {
const newArgs = bindArgs.concat(args)
return self.apply(context, newArgs)
}
}
function fn(this: any, ...args: any[]) {
console.log(this, ...args);
}
// @ts-ignore
const fn1 = fn.customBind({ x: 100 }, 10, 20, 30)
fn1(40, 50)
bind call apply 区别
不同点:
bind 返回一个函数(不执行)
call 和 apply 会立即执行
传递方式不同:
call(this, 10,20,30)
apply(this, [10,20,30])
相同:
都是用于绑定this
都可以传入执行参数
手写call
// @ts-ignore
Function.prototype.customCall = function (context: any, ...args: any[]) {
if (context == null) context = globalThis
// 值类型 需要转换成对象类型
if (typeof context !== 'object') context = new Object(context)
const fnKey = Symbol() // 防止出现属性名称覆盖
context[fnKey] = this // this 就是当前函数
const res = context[fnKey](...args) // 绑定 this
delete context[fnKey] // 清理fn,防止污染
return res
}
function fn(this: any, a: any, b: any, c: any) {
console.log(JSON.stringify(this), a, b, c);
}
// @ts-ignore
fn.customCall({ x: 100 }, 10, 20, 30)
// @ts-ignore
fn.customCall(100, 10, 20, 30)
手写apply
// @ts-ignore
Function.prototype.customApply = function (context: any, args: any[] = []) {
if (context == null) context = globalThis
// 值类型 需要转换成对象类型
if (typeof context !== 'object') context = new Object(context)
const fnKey = Symbol() // 防止出现属性名称覆盖
context[fnKey] = this // this 就是当前函数
const res = context[fnKey](...args) // 绑定 this
delete context[fnKey] // 清理fn,防止污染
return res
}
function fn(this: any, a: any, b: any, c: any) {
console.log(JSON.stringify(this), a, b, c);
}
// @ts-ignore
fn.customApply({ x: 100 }, [10, 20, 30])
// @ts-ignore
fn.customApply(100, [10, 20, 30])
手写EventBus 自定义事件
实现on once emit方法
方法一:
// 绑定成一个类似这种的数组
{
'key1': [
{fn1, isOnce: false},
{fn2, isOnce: false},
{fn3, isOnce: true},
]
'key2': ...
}
具体代码:
export class EventBus {
private events: {
[key: string]: Array<{ fn: Function, isOnce: boolean }>
}
constructor() {
this.events = {}
}
on(type: string, fn: Function, isOnce: boolean = false) {
const events = this.events
if (events[type] == null) {
events[type] = [] // 初始化key 的 fn 数组
}
events[type].push({ fn, isOnce })
}
once(type: string, fn: Function) {
this.on(type, fn, true)
}
off(type: string, fn?: Function) {
if (!fn) {
// 删除所有type的函数
this.events[type] = []
} else {
// 解绑单个事件
const fnList = this.events[type]
if (fnList) {
this.events[type] = fnList.filter(item => item.fn !== fn)
}
}
}
emit(type: string, ...args: any[]) {
const fnList = this.events[type]
if (fnList == null) return
// filter 可以做到return 删除元素,并且还能遍历
this.events[type] = fnList.filter(item => {
const { fn, isOnce } = item
fn(...args)
if(!isOnce) return true
return false
})
}
}
方法二:
export class EventBus {
private events: {
[key: string]: Array<Function>
}
private onceEvents: {
[key: string]: Array<Function>
}
constructor() {
this.events = {}
this.onceEvents = {}
}
on(type: string, fn: Function) {
const events = this.events
if (events[type] == null) events[type] = [] // 初始化key 的 fn 数组
events[type].push(fn)
}
once(type: string, fn: Function) {
const onceEvents = this.onceEvents
if (onceEvents[type] == null) onceEvents[type] = [] // 初始化key 的 fn 数组
onceEvents[type].push(fn)
}
off(type: string, fn?: Function) {
if (!fn) {
// 删除所有type的函数
this.events[type] = []
this.onceEvents[type] = []
} else {
// 解绑单个事件
const fnList = this.events[type]
const onceFnList = this.onceEvents[type]
if (fnList) {
this.events[type] = fnList.filter(curFn => curFn !== fn)
}
if (onceFnList) {
this.onceEvents[type] = onceFnList.filter(curFn => curFn !== fn)
}
}
}
emit(type: string, ...args: any[]) {
const fnList = this.events[type]
const onceFnList = this.onceEvents[type]
if (fnList) {
fnList.forEach(f => f(...args))
}
if(onceFnList){
onceFnList.forEach(f => f(...args))
// once 执行一次就删除
this.onceEvents[type] = []
}
}
}
效果演示代码
const event = new EventBus()
event.on('key1', function (...args: any[]) {
console.log('fn1', ...args);
})
event.on('key1', function () {
console.log('fn2');
})
event.emit('key1', 10, 20, 30)
event.once('key2', () => {
console.log('zzz');
})
event.emit('key2', () => {
console.log('yyy');
})
event.emit('key2', () => {
console.log('vvv');
})
LRUCache
Map 实现一个LRU 缓存类
/**
* LRU cache
*/
export class LRUCache {
private length: number
private data: Map<any, any> = new Map()
constructor(length: number) {
if (length < 1) throw new Error('请输入大于 1 的数字')
this.length = length
}
set(key: string, value: any) {
const data = this.data
if (data.has(key)) {
data.delete(key)
}
data.set(key, value)
// 如果长度大于传递的长度,需要删掉最后一个
if (data.size > this.length) {
let delKey = data.keys().next().value
data.delete(delKey)
}
}
get(key: string) {
const data = this.data
if(!data.has(key)) return null
let value = data.get(key)
data.delete(key)
data.set(key, value)
return value
}
getList(){
return this.data
}
}
const lruCache = new LRUCache(2)
lruCache.set('xx', 'xx')
lruCache.set('yy', 'yy')
lruCache.set('zz', 'zz')
console.log(lruCache.get('xx'));
console.log(lruCache.getList());
console.log('----------------');
console.log(lruCache.get('yy'));
console.log(lruCache.getList());
console.log('----------------');
生成符合条件的括号
思路:f(n) 的值永远都是 f(n-1) 的结果 里面每一项push ()
export function generateParenthesis(initn: number): string[] {
const func = (n: number): string[] => {
if (n == 1) {
return ['()']
}
let res = func(n - 1)
let pjRes = []
for (let i = 0; i < res.length; i++) {
let str = res[i]
let sL = str.length
for (let j = 0; j < sL; j++) {
pjRes.push(`${str.slice(0, j)}()${str.slice(j, sL)}`)
}
}
return pjRes
}
return Array.from(new Set(func(initn)))
}
console.log(generateParenthesis(3), 'na');
H5页面如何做首屏渲染
- 路由懒加载
- 服务端渲染 ssr
- App 预取
- 分页
- 图片懒加载
- hybrid 混合
后端返回 10w 条数据,前端如何优化
- node 中间层里面去改
- 虚拟列表
- 插件
- vue-virtual-scroll-list
- React-virtualiszed
观察者模式和发布订阅者模式的区别
按钮点击事件
发布订阅是一个订阅事件 一个触发事件
emit on
观察者模式 发布者和订阅事件可以直接调用,比如addEventListener click
发布订阅模式 发布和监听者互相隔离,需要中间媒介来触发。
使用 Vue 的时候遇到那些坑
- 内存泄漏
- Vue 新增属性需要用 Vue.Set
- Vue 删除属性需要用 Vue.delete
- 无法直接修改数据 arr[index]= value
- 从列表页切换至详情页,再返回列表页,会自动scroll到顶部。
解决方案1:
- 在列表页数据缓存下来。 在返回的时候,列表数据渲染上去。
- 通过 MPA(多屏渲染) 的方式。eg:多个 WebView
实际工作中,React 做过什么优化
- 修改css 模拟 v-show
- 使用fragment 减少层级
- render函数中减少定义,由于render 会频繁执行,所以尽量较少在dom节点新建逻辑。
- SCU shouldComponentUpdate 来优化渲染
- pureComponent React.memo
react的坑
js 关键字冲突
setState 异步更新的 需要在回调中用
对于深层数据的state遍历不是很友好,得通过浅拷贝的方式来手动赋值。
错误监听
- window.error 监听全局组件报错,可以监听异步报错
- 事件报错 try catch 报错 catch 抛出异常,让上级组件监听到
- js 中 Promise 的 catch 报错,可以使用 onUnHandledrejection 监听。
- 异步错误没法监听,需要结合window.error
- 报错统计(埋点,上报,统计)
- Vue
- errorHandle 捕获下级组件的报错 但是没法捕获异步的报错
- React
-
ErrorBoundary 组件 (核心就是componentDidCatch)
- 统一监听下级组件内部报错
- 降级展示UI(可以有友好提示)
- 监听渲染报错
- 不监听DOM事件,异步错误
- Prod环境生效,dev环境抛出错误
- 代码演示:
import React from 'react';
import ErrorTemplate from './template'
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { error: null, errorInfo: null };
}
componentDidCatch(error, errorInfo) {
this.setState({
error: error,
errorInfo: errorInfo
})
}
render() {
if (this.state.errorInfo) {
return <ErrorTemplate title="error" tip="糟糕,页面出错了!"/>
}
return this.props.children;
}
}
export default ErrorBoundary
- 生产环境验证
yarn build
http-server -p 8881 通过本地路径打开文件
如果一个H5 很慢,你该如何排查性能问题?
是哪里慢,怎么体现?
可以从很多个方面沟通。重点:沟通!沟通!沟通!
性能指标:
-
FP(First Paint)第一次加载的情况
-
FCP(First Contentful Paint)有内容的情况
-
FMP(First Meaningful Paint)第一次有意义的渲染(没有一个标准,已弃用)
-
DCL(DomContentLoaded)原生dom内容下载完成
-
LCP(Largest ContentFul Paint)重要的数据已经渲染完
-
L (Load)加载完毕
-
devtool 中的 【性能监控】 开启。可以监控加载慢的请求;network 看监控时间;
-
Lighthouse 测试报告
- 第三方性能评测工具
- 支持移动端和pc端
- 优化建议:webp avif 格式的图片;有限的压缩方法,图片尺寸,http2;等等
- 安装:npm i lighthouse -g
- 使用:lighthouse https://www.imooc.com/–view --preset=desktop
-
网页加载慢?
- 优化服务器硬件配置,使用CDN
- 路由懒加载,大组件异步加载 - 减少主包的体积
- 优化HTTP 换成策略
-
网页渲染慢?
- 优化服务端接口(ajax返回时间优化)
- 优化组件内部业务逻辑
- 服务端渲染 SSR
-
持续跟进
- 性能优化是持续的过程
- 持续跟进,统计结果,
- 第三方统计服务:阿里云ARMS、百度统计
-
二分法减少问题范围逐步更近
工作经历中,遇到什么项目难点,如何解决?
- redux中深浅拷贝,深浅比较问题。层次比较深的时候,每一层都需要解构出来,代码容器不好看,而且要是碰到层级非常深导致set的代码很长。
this.state = {fourValue: { a: 'a', b: { c: { d: 'd', e: 'ee' } } }}
// 要想修改e = 'ff'
必须层层解构
let b = this.state.threeValue.b
this.setState({
threeValue: {
...this.state.threeValue,
b: {
...b,
c: 'xxx'
}
}
})
数组里面这么做:
import React, { Component } from 'react'
export default class About extends Component {
state = {
isSubmitting: false,
inputs: {
username: {
touched: false,
value: 'some_initial_value'
},
users: [
{ id: 1, name: 'glack1', count: 1 },
{ id: 2, name: 'glack2', count: 2 },
{ id: 3, name: 'glack3', count: 3 },
{ id: 4, name: 'glack4', count: 4 },
{ id: 5, name: 'glack5', count: 5 },
]
}
}
twochange() {
let s = this.state
s.inputs.users[0].count = 100
this.setState({
inputs: {
...s.inputs,
users: [...s.inputs.users]
}
})
}
render() {
return (
<>
<button onClick={() => this.twochange()}>改变array中的count</button>
{this.state.inputs.users.map(item => {
return <>
<div>name:{item.name}</div>
<div>id:{item.id}</div>
<div>count:{item.count}</div>
</>
})}
</>
)
}
}
- 歌词播放器时间播放和歌词滚动配合。
- 改造之前小程序的代码
- 群聊模块轮询的方式改造成wxWebscoket
- 优化首次加载是app.js 还未执行完毕的情况,通过app中的一个回调。
- 模板:描述问题:背景 + 现象 + 造成的影响
- 解决:分析 + 解决
- 学到了什么?如何避免?
可以整个文档,遇到的难点都归纳起来。
时间复杂度
-
O(1) 一次就够(数量级)
ex: 只走了一次处理逻辑
const func = (a) => {
return a
}
-
O(n) 和传输的数据量一样
ex:时间复杂度和当前arr的长度一致
const func = (arr) => {
for(let i = 0; i < arr.length; i++){
console.log(arr[i])
}
}
-
O(n^2) 数据量的平方
ex
const func = (arr) => {
for(let i = 0; i < arr.length; i++){
for(let j = 0; j < arr.length; j++){
console.log(arr[j])
}
}
}
-
O(logn) 数据量的对数(数量级) 100 => 10
ex: 二分算法,每次把数据砍掉二分之一。
[1,2,3,4,5,6,7,8,…] 找到6,可以通过砍掉中间的来比对
-
O(nlogn)
空间复杂度
- O(1) 一次就够(数量级)
ex: 像这样,不管数组的长度是多少,数组的内存大小是相对固定的const func = (arr: []) => {
arr[1] = 1
arr[2] = 2
arr[3] = 3
arr[4] = 4
return a
}
- O(n)
ex:这里定义了一个新的数组,并且对相关的数组进行了赋值const func = (arr) => {
let arr2 = []
for(let i = 0; i < arr.length; i++){
arr2[i] = arr[i]
}
}
单元测试
jest
判断数组是否相等 toEquel
判断bool是否相等 toBe
二分查找
/**
* desc: 给一个有序的数组,[10, 20, 50, 70, 90] 查到50下标
* 算法:二分法查找
*/
// 循环的方式
export const binarySearch1 = (arr: number[], target: number) => {
let startIndex: number = 0
let endIndex: number = arr.length - 1
while (startIndex < endIndex) {
let mdIndex = Math.floor((endIndex + startIndex) / 2)
let value = arr[mdIndex]
// 如果正好是中间的数字
if (value === target) return mdIndex
// 在左边
if (target < value) {
endIndex = mdIndex - 1
}
// 在右边
if (target > value) {
startIndex = mdIndex - 1
}
}
return -1
}
// 递归的方式
const binarySearch2 = (arr: number[], target: number, startIndex?: number, endIndex?: number): number => {
if(arr.length <= 0) return -1
if(startIndex == null) startIndex = 0
if(endIndex == null) endIndex = arr.length - 1
if(startIndex > endIndex) return -1
// 找到中间下标和value
let midIndex = Math.floor((startIndex + endIndex) / 2)
let midValue = arr[midIndex]
if (midValue > target) {
return binarySearch2(arr, target, startIndex, midIndex - 1)
}else if(midValue < target){
return binarySearch2(arr, target, midIndex + 1, endIndex)
}else{
return midIndex
}
}
const arr = [10, 20, 40, 50, 70, 90, 120]
let res = binarySearch2(arr, 90)
console.log(res, 'res');
这里写了两种方式,递归和循环都可以完成问题,时间复杂度为O(logn),但是如果一定得使用最好的方式,循环更好,因为递归调用方法也会耗时。
将一个数组旋转k步
方案一 unshift pop
时间复杂度:O(n^2)
unshift 也是一个O(n) 的结构
方案二 contact
时间复杂度:O(1)
/**
* descripbe:
* 1. 完成两个算法,k位数往数组的前面加
* 2. 测试时间 log time endTime
*/
// 通过unshift的方法 [1,2,3,4,5,6,7] 3 => [5,6,7,1,2,3,4]
export const rodate1 = (arr: number[], len: number): number[] => {
// 去k的绝对值
let k = Math.abs(len % arr.length)
for (let i = 0; i < k; i++) {
let chu = arr.pop()
if (chu) {
arr.unshift(chu)
}
}
return arr
}
// 通过contact的方法
export const rodate2 = (arr: number[], len: number): number[] => {
let k = Math.abs(len % arr.length)
len = k
let l = arr.length
let left = arr.slice(0, l - len)
let right = arr.slice(l - len, l)
return [...right, ...left]
}
let arrHuge = []
for (let i = 0; i < 100000; i++) {
arrHuge.push(i)
}
let step = 8 * 10000
// 这个过程执行了 4000ms 也就是4s 时间复杂度 O(n^2)
console.time()
rodate1(arrHuge, step)
console.timeEnd()
// 这个过程仅仅2.7毫秒 时间复杂度 O(1)
console.time()
rodate2(arrHuge, step)
console.timeEnd()
数据结构
栈的应用
判断是否匹配 夸号类型
/**
* descripbe: 算法 {a[b(c)d]e}
* 判断是否匹配 夸号类型
*/
const isMatch = (top: string, s: string): boolean => {
if (top === "{" && s === '}') return true
if (top === "(" && s === ')') return true
if (top === "[" && s === ']') return true
return false
}
export const matchFunc = (str: string) => {
let length = str.length
if (length <= 0) return false
let leftSyb = '{[('
let rightSyb = '}])'
let stack = []
for (let i = 0; i < length; i++) {
let s = str[i]
if (leftSyb.includes(s)) {
// 碰到左边的字符串先入栈
stack.push(s)
} else if (rightSyb.includes(s)) {
// 碰到右边的字符串先判断是否和顶层的匹配
let top = stack[stack.length - 1]
if (isMatch(top, s)) {
// 匹配的话,就删除顶层
stack.pop()
} else {
return false
}
}
}
return stack.length === 0
}
let str1 = '{1[2(3)4]5}' // 匹配
let str2 = '{1[2(34]5}19)' // 顺序不一样
console.log(matchFunc(str1),matchFunc(str2), // true, false
找数组中两个字符串相加为10的值
O(n^2)的方式两个循环如下:
/**
* desc: 找数组中两个字符串相加为10的一个值
*/
export const twoNumberAdd = (arr: number[], total: number): number[] => {
if(arr.length === 0) return []
if(total === 0) return []
let length = arr.length
const res: number[] = []
let flag = false
for (let i = 0; i < length - 1; i++) {
for (let j = i; j < length - 1; j++) {
if (arr[i] + arr[j] == total) {
res.push(arr[i])
res.push(arr[j])
flag = true
break;
}
}
if (flag) break // for循环中,break可以停止循环。
}
return res
}
const arrlog = [1,2,3,4,5,6,8]
const res = twoNumberAdd(arrlog, 10)
console.log(res, 'res');
二分理念去查,时间复杂度 O(N)
export const twoNumberAdd2 = (arr: number[], total: number): number[] => {
if(arr.length === 0) return []
if(total === 0) return []
const res: number[] = []
let i = 0
let j = arr.length - 1
while (i < j) {
let a = arr[i]
let b = arr[j]
let t = a + b
if (t > total) {
j--
} else if (t < total) {
i++
} else {
res.push(a)
res.push(b)
break
}
}
return res
}
const arrlog = [1, 2, 3, 4, 5, 6, 8]
const res = twoNumberAdd2(arrlog, 10)
console.log(res, 'res');
时间复杂度对比,可以看出,二分法的时间复杂度更低。选第二个方法。
const arrlog = [1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 4, 5, 6, 8]
console.time()
for (let i = 0; i < 100 * 10000; i++) {
twoNumberAdd(arrlog, 10)
}
console.timeEnd()
console.time()
for (let i = 0; i < 100 * 10000; i++) {
twoNumberAdd2(arrlog, 10)
}
console.timeEnd()
// default: 288.071044921875 ms
// default: 14.84619140625 ms
链表
js实现单向链表结构
// 实现一个单项链表功能
// 变成 {value: '100', next: { value: "200", next: { value: '300', next: { value: '400' } } }}
const arrInit = [100, 200, 300, 400, 500]
const func = (arr) => {
let length = arr.length
if (length <= 0) return []
let curNode = {
value: arr[length-1],
}
for (let i = length-2; i >= 0; i--) {
console.log(i, 'i');
curNode = {
value: arr[i],
next: curNode
}
}
return curNode
}
console.log(func(arrInit), 'arr')
实现反转单向链表算法
/**
* {value: 100, next: { value: 200, next: { value: 300, next: { value: 400, next: { value: 500 } } } }}
* 处理成为下面结构
* {value: 500, next: { value: 400, next: { value: 300, next: { value: 200, next: { value: 100 } } } }}
* @param arr 反转单向链表结构结构
*/
interface INode {
value: number,
next?: INode
}
export const reverseLinkNode = (linkNode: INode) => {
// 定义三个指针用于接收
let prevNode: INode | undefined = undefined
let curNode: INode | undefined = undefined
let nextNode: INode | undefined = linkNode
// 循环赋值引用
while(nextNode){
console.log('1');
// 第一种情况
if (curNode && !prevNode) {
delete curNode.next
}
// 第二种和倒数第二种情况
if (curNode && prevNode) {
curNode.next = prevNode
}
prevNode = curNode
curNode = nextNode
// @ts-ignore
nextNode = nextNode.next
}
curNode!.next = prevNode
return curNode
}
const list = {value: 100, next: { value: 200, next: { value: 300, next: { value: 400, next: { value: 500 } } } }}
let list2 = reverseLinkNode(list)
console.log(list2, 'list2');
链表中,查询慢删除快
数组中,查询快删除慢。
队列
用两个栈实现队列(数组方式)
/**
* 两个栈实现队列
*/
export class MyQuene {
constructor(stack1: number[]){
this.stack1 = stack1
}
stack1: number[] = []
stack2: number[] = []
add(n: number) {
this.stack1.push(n)
}
delete(): number | null {
let stack1 = this.stack1
let stack2 = this.stack2
// 1. 压栈处理成stack2
while(stack1.length){
let n = stack1.pop()
if (n) stack2.push(n)
}
// 2. stack2.pop
let deleteItem = stack2.pop()
// 3. stack2 压栈成stack1
while(stack2.length){
let n = stack2.pop()
if(n) stack1.push(n)
}
return deleteItem as number
}
get length() {
return this.stack1.length
}
}
const q = new MyQuene([1,2,3,4,5])
let dItem = q.delete()
console.log(dItem, q.stack1); // 1, [2,3,4,5]
q.add(6)
console.log('add', q.stack1); // add, [2,3,4,5,6]
console.log(q.length); // 5
大致思路如下:
首先有两个栈,一个栈用于存初始值。一个用于转换的时候用,stack1压栈成stack2,顺序改变,A在首部变成尾部,直接pop删除最后一个元素,再压栈成stack1 得到的就是 删掉了A的 BCDE。
一些数据量庞大的场景中,由于数组unshift
和shift
消耗的性能比较大时间复杂度(O(n^2)),使用栈的概念,可以通过 两个栈+队列 的方式完成数组的unshift操作。
用链表实现队列
/**
* 链表实现队列
*/
interface INode {
value: number,
next: INode | null
}
export class MyQuene {
head: INode | null = null
tail: INode | null = null
len: number = 0
add(n: number) {
let newNode: INode = {
value: n,
next: null
}
// 处理head
if (this.head == null) {
this.head = newNode
}
// // 处理tail
let tailNode = this.tail
if (tailNode) {
// 第二次进入已经新add了一个newNode,可以指定next为新节点
tailNode.next = newNode
}
this.tail = newNode
this.len++
}
delete(): number | null {
const headNode = this.head
if (this.len == 0) return null
if (this.head == null) return null
let value = headNode!.value
// 处理head
this.head = headNode!.next
this.len--
return value
}
get length(): number {
return this.len
}
}
数组和链表哪个更快?
- 数组是连续存储,push很快,shift很慢
- 链表是非连续存储,add和delete都很快(查找很慢)
- 所以链表实现队列更快
二叉树结构
题目:给一个有序的二叉树结构,找到第k位的树
/**
* 二叉搜索树中的第k位
*/
interface INode {
value: number,
left: INode | null,
right: INode | null
}
const bst: INode = {
value: 5,
left: {
left: {
value: 2,
left: null,
right: null
},
right: {
value: 4,
left: null,
right: null
},
value: 3
},
right: {
left: {
value: 6,
left: null,
right: null
},
right: {
value: 8,
left: null,
right: null
},
value: 7
},
}
let arr: number[] = []
/**
* 前序遍历
* Preorder traversal
*/
const preorderTraversal = (node: INode | null) => {
if (!node) return
if (!node) return
arr.push(node.value)
preorderTraversal(node.left)
preorderTraversal(node.right)
}
/**
* 中序遍历
* Preorder traversal
*/
const inorderTraversal = (node: INode | null) => {
if (node === null) return
inorderTraversal(node.left)
arr.push(node.value)
inorderTraversal(node.right)
}
/**
* 后序遍历
* Preorder traversal
*/
const postorderTraversal = (node: INode | null) => {
if (!node) return
if (!node) return
postorderTraversal(node.left)
postorderTraversal(node.right)
arr.push(node.value)
}
export const biranyTreeSearch1 = (node: INode, k: number): number | null=> {
inorderTraversal(node)
return arr[k - 1] || null
}
console.log(biranyTreeSearch1(bst, 3));
单元测试
一般react-create-app
有集成jest
包,可以直接通过 yarn test
运行
describe:描述内容
it: 提示内容
expect: 期望的方法
toBe: 期望得到某个数字
toBeNull: 期望得到null
import { INode, biranyTreeSearch1, bst } from './index'
describe('二叉搜索树求key位的值', () => {
it('普通情况', () => {
expect(biranyTreeSearch1(bst, 3)).toBe(4)
});
it('为0的情况', () => {
expect(biranyTreeSearch1(bst, 0)).toBeNull()
});
})
// Test Suites: 1 passed, 1 total
// Tests: 2 passed, 2 total
三种遍历
- 前序遍历
- 中序遍历
- 后序遍历
重点:
- 二叉树和三种遍历
- 二叉搜索树的特点:left <= root;right >= root
- 二分搜索树的价值:可使用二分法进行快速查找
算法三大规则
- 贪心
- 二分
- 动态规划
斐波拉切数列 找第 N 位
递归方式
这种方式 时间复杂度(O(n^2))
export function fiboracheSequence1(n: number): number {
if (n <= 0) return 0
if (n === 1) return 1
return fiboracheSequence1(n - 1) + fiboracheSequence1(n - 2)
}
// 测试
console.log(fiboracheSequence1(8)) // 21
循环的方式
export function fiboracheSequence2(n: number): number {
if (n <= 0) return 0
if (n === 1) return 1
let n1 = 1
let n2 = 0
let res = 0
for (let i = 2; i < n; i++) {
res = n1 + n2
n2 = n1
n1 = res
}
return res
}
移动数组中的0到末尾
O(n^2) 不好用
export const moveNumber1 = (arr: number[]) => {
if (arr.length === 0) return []
let length = arr.length
let blLen = 0
for (let i = 0; i < length - blLen; i++) {
if (arr[i] === 0) {
arr.push(0)
arr.splice(i, 1)
i--
blLen ++
}
}
return arr
}
let initArr = [1, 0, 1, 0, 1, 0, 234, 1, 34521, 0, 1, 90, 0]
moveNumber1(initArr)
console.log(initArr, 'arr')
O(n)时间复杂度
// O(n)的时间复杂度
export const moveNumber2 = (arr: number[]) => {
let i, j = -1, length = arr.length
for (i = 0; i < length; i++){
if (arr[i] === 0) {
if (j < 0) {
j = i
}
}
if(arr[i] !== 0 && arr[j] >= 0){
// 交换
const n = arr[i]
arr[i] = arr[j]
arr[j] = n
j++
}
}
}
let initArr = [1, 0, 1, 0, 1, 0, 234, 1, 34521, 0, 1, 90, 0]
moveNumber2(initArr)
console.log(initArr, 'arr')
字符串中连续最多的字符,以及次数
O(n)
// abcdddddfffg
interface IRes {
char: string,
length: number
}
export const findStr = (str: string) => {
let length = str.length
let res: IRes = {
char: '',
length: 0
}
let temLength
for (let i = 0; i < length; i++) {
console.log('---------------');
temLength = 0
for (let j = i; j < length; j++) {
console.log(`i:${i}\nj:${j}\ntemp:${temLength}\nres.length:${res.length}\nres.char:${res.char}`);
if (str[i] === str[j]) {
temLength++
}
if (str[i] !== str[j] || j === length - 1) {
if (temLength > res.length) {
res.length = temLength
res.char = str[i]
}
if (i < length - 1) {
i = j - 1
}
break;
}
}
}
return res
}
let str = 'abcdddefg'
console.log(findStr(str));
O(n)的时间复杂度
思路: 通过判断当前元素和上一个元素是否相等,来决定要不要累加。
export const findStr2 = (str: string): IRes => {
let length = str.length
let res: IRes = {
char: '',
length: 0
}
if (length === 0) return res
let tempLength = 1
for (let i = 0; i < length - 1; i++){
if (str[i] === str[i+1]) {
tempLength++
}else if(str[i] !== str[i+1]){
if (tempLength > res.length) {
res = {
char: str[i],
length: tempLength
}
tempLength = 1
}
}
}
return res
}
let str = 'abcdddeeeeeeefffg'
console.log(findStr2(str)); // char: e, length: 7
双指针
export const findStr3 = (str: string): IRes => {
let length = str.length
let res: IRes = {
char: '',
length: 0
}
if (length === 0) return res
let tempLength = 0
let i = 0,j = 0
for (; i < length; i++){
if (str[i] === str[j]) {
tempLength++
}else if(str[i] !== str[j] || i === length - 1){
if (tempLength > res.length) {
res = {
char: str[j],
length: tempLength
}
}
tempLength = 0
if (i < length - 1){
j = i
i --
}
}
}
return res
}
单元测试
import { findStr1, findStr2 } from './index'
describe('寻找重复的字串第一种方法:跳步', () => {
it('普通情况', () => {
expect(findStr1('abcdddedff')).toEqual({char: 'd', length: 3})
})
it('都是连续字符', () => {
expect(findStr1('dddeeeeddd')).toEqual({char: 'e', length: 4})
})
it('字符串为空', () => {
expect(findStr1('')).toEqual({char: '', length: 0})
})
it('无连续字符', () => {
expect(findStr1('abcdefghijk')).toEqual({char: 'a', length: 1})
})
})
describe('寻找重复的字串第二种方法:一次循环判断是否为相同字符', () => {
it('普通情况', () => {
expect(findStr2('abcdddedff')).toEqual({char: 'd', length: 3})
})
it('都是连续字符', () => {
expect(findStr2('dddeeeeddd')).toEqual({char: 'e', length: 4})
})
it('字符串为空', () => {
expect(findStr2('')).toEqual({char: '', length: 0})
})
it('无连续字符', () => {
expect(findStr2('abcdefghijk')).toEqual({char: 'a', length: 1})
})
})
求回文数(aba 121 454)
// 求一个范围内的回文数 转字符串 转数字判断是否相等
export const getPalindromeNumberFunc1 = (max: number): number[] => {
let res: number[] = []
if (max <= 0) return []
for (let i = 1; i <= max; i++) {
let n = i.toString()
if (n === n.split('').reverse().join('')) {
res.push(i)
}
}
return res
}
// 求一个范围内的回文数 依次判断首位和末尾是否一直相等
export const getPalindromeNumberFunc2 = (max: number): number[] => {
let res: number[] = []
if (max <= 0) return []
for (let i = 1; i <= max; i++) {
let n = i.toString()
let startIndex = 0
let endIndex = n.length - 1
let flag = true
while (startIndex < endIndex) {
if (n[startIndex] === n[endIndex]) {
startIndex++
endIndex--
} else {
flag = false
break
}
}
if (flag) {
res.push(i)
}
}
return res
}
// 求一个范围内的回文数 反转数字
export const getPalindromeNumberFunc3 = (max: number): number[] => {
let res: number[] = []
if (max <= 0) return []
for (let i = 1; i <= max; i++) {
let n = i
let rev = 0
while (n > 0) {
rev = rev * 10 + n % 10
n = Math.floor(n / 10)
}
if (rev === i) res.push(i)
}
return res
}
console.log(getPalindromeNumberFunc2(500));
测试效果:方法1:400ms;方法二:50ms;方法三:42ms
时间复杂度分析:方法一最慢,因为数组转换需要时间
数字转成千分位的字符串
export const numberToStr = (num: number): string => {
let res = ''
let str = num.toString()
let length = str.length
let times = 0
for (let i = length - 1; i >= 0; i--) {
times ++
if (times == 3 && i !== 0) {
res = ',' + str[i] + res
times = 0
}else{
res = str[i] + res
}
}
return res
}
console.log(numberToStr(13880000000));
大小写转换
// 正则表达式大小写转换
export const toggleCase1 = (str: string): string => {
let res = ''
let length = str.length
for (let i = 0;i < length; i++){
let s = str[i]
let reg1 = /[a-z]/
let reg2 = /[A-Z]/
if (reg1.test(s)) {
res = res + s.toUpperCase()
}else if(reg2.test(s)){
res = res + s.toLowerCase()
}else {
res = res + s
}
}
return res
}
// 通过ASCII编码
export const toggleCase2 = (str: string): string => {
let res = ''
let length = str.length
for (let i = 0;i < length; i++){
let s = str[i]
let code = s.charCodeAt(0)
if (code >= 65 && code <= 90) {
res = res + s.toLowerCase()
}else if(code >= 97 && code <= 122){
res = res + s.toUpperCase()
}else {
res = res + s
}
}
return res
}
console.log(toggleCase2('avheDF!DsadSFEWF'));
0.1 + 0.2 !== 0.3
整数转换二进制没有误差
有些小数可能是无法用二进制精准转化
各个计算机语言的通病
通过math.js 进行准确的计算
跨域问题的解决
- 前端解决
- Vue中可以修改proxy代理
- React
- 通过package.json修改proxy属性;
- 通过下载http-proxy-middleware ,修改setUp.js文件
- 通过npm run eject 暴露配置修改webpack配置,不推荐。
- 普通的html页面使用jsonp也可以实现,需要和后端规定一些标准。
- 后端通过修改跨域头解决
- 小程序可以通过添加白名单解决
This指向问题
- 在function中,this的指向是指调用该方法的指针本身。
- 在箭头函数中,this指向是当前的作用域
- class的class component 指向一般都是指当前组件,有一些情况,class组件的方法没有bind this 指针,需要手动指定。如果不指定,可能会导致方法调用不到的问题。
原型作用域的相关问题
- 学习链接戳我:
- 原型
- 在我们创建函数的时候,会自动生成一个prototype属性,我们函数需要的所有属性和方法都绑定在这个prototype上,这个属性指向一个对象,也就是原型对象。
- 原型对象里面拥有constructor,这个对象指向的是他的构造函数。比如用new 出来的对象下的prototype下面的consturctor会与函数相等,他们都是一个构造函数。他们指向的都是该构造函数本身
- 原型特点:
- 实例中的属性和方法会共享。
- new出来的实例。如果set了同名属性,将会覆盖原本原型链上的属性。如果在实例外获取不到,会在原型链上面查找。
- 如果碰到需求,需要重构原型函数,需要重写该函数时,需要将constructor再次赋值给原型琏上的实例方法。否则会导致方法没法完全相等的问题。
- 举例:
function Person(){}
var p = new Person();
// 这里的重写就是通过修改propotype来重写
Person.prototype = {
name: 'tt',
age: 18
}
Person.prototype.constructor === Person // false
p.name // undefined
优化:Person.prototype = {
constructor: Person,
name: 'tt',
age: 18
}
- 原型链
- 原型链的问题
- 引用问题
- 原型链上面如果是一个引用类型(Object Array),多个实例同时修改原型属性的值,所有的实例都会收到影响二发生改变。
- 例子;这里不管如何修改arr的数据,其他引用arr的地方都会改变。
function Person(){}
Person.prototype.arr = [1, 2, 3, 4];
var person1 = new Person();
var person2 = new Person();
person1.arr.push(5)
person2.arr // [1, 2, 3, 4, 5]
- class和原型链的关系:
宏任务和微任务
- 干货
- http://events.jianshu.io/p/fd15db94a034
总结:
常见的宏任务:script语句 setTimeout setInterval setImmedate promise
微任务:Promise.[ then/catch/finally ] node中的process.nextTick()
- 一个任务开始的时候先执行srcipt 第一个宏任务,其中碰到了其他宏任务(setTimeout),先放入宏任务队列。碰到了微任务(.then),放入微任务队列,以此类推。
- 接着执行完宏任务的结尾,微任务开始按顺序执行,
- 再回来执行第二个宏任务(setTimeout的回调)以此类推。
函数的节流和防抖
- https://gitee.com/HeiGes/net-case-must/blob/master/clientNetCaseMusic/README.md#%E9%98%B2%E6%8A%96%E5%92%8C%E8%8A%82%E6%B5%81
- 为了解决一些场景连续触发一个函数,或者请求,需要通过防抖节流将方法减少运行。
class生命周期
- https://blog.csdn.net/qq_43013884/article/details/125421659
- 初始化
- constructor
- getDeriviedStateFormProps
- render
- componentDidMount
- 更新时
- getDeriviedStateFormProps
- shouComponentUpdate
- render
- getSnapShotBeforeUpdate
- componentDidUpdate
- 卸载时
- componentWillUnMount
一个前端大佬的文章
https://blog.csdn.net/qq_34998786/article/details/118887109?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1-118887109-blog-122952429.pc_relevant_multi_platform_whitelistv3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1-118887109-blog-122952429.pc_relevant_multi_platform_whitelistv3&utm_relevant_index=2
浏览器对于小数单位是怎么计算的?
interface和type的区别是什么?你日常工作中是用interface还是type?
快速理解 TypeScript 的逆变、协变、双向协变、不变
https://zhuanlan.zhihu.com/p/500762226
能不能讲讲小程序的原理?