前端基础面试题

158 阅读5分钟

自我整理,若是有回答的不详细或还不错的面试题可评论告诉我

Js基础

0.1+0.2 !== 0.3 ?

因为计算机在计算时是使用二进制计算,0.2转换成二进制为1.10011001...无限循环,从而出现精度问题,很多其他的语言亦是如此

['1','2','3'].map(parseInt)

结果:[1, NaN, NaN]

map函数回调的第一个参数是值,第二个是索引,parseInt的第一个参数是值,第二个是进制,

itemindex进制
10传0默认10进制
21没有1进制,直接NaN
322进制不会出现3,NaN

AJAX、Fetch、Axios 区别

Ajax即Asynchronous Javascript And XML(异步JavaScript和XML),是一个技术统称,最早用XMLHttpRequest实现

  • Fetch是js的原生网络请求API,是为了替代XMLHttpRequest而出现的,使用Promise封装而成
  • Axios是第三方请求库
  • Fetch和Axios都是Ajax技术

防抖和节流

防抖:限制执行次数,密集触发只执行最后一次

用于搜索框触发事件等

function debounce(fn,delay=200){
    let timer = 0
    return function (){
        if(timer) clearTimeout(timer)

        timer = setTimeout(()=>{
            fn.apply(this,arguments)
            timer = 0
        },delay)
    }
}

节流:限制执行频率,有节奏的执行

用于滚动或拖拽等

function throttle(fn){
    let time = null
    return ()=>{
        if(time) return
        setTimeout(()=>{
            fn.apply(this,arguments)
        },100)
    }
}

什么时候不能使用箭头函数

箭头函数无法使用call更改this指向!

  1. 对象中的属性使用箭头函数,this不是对象本身
  2. 原型对象上的函数使用箭头函数,this不是原型对象本身
  3. 构造函数也不能使用箭头函数

总之,箭头函数的this指向是其父级作用域,注意即可

for...in和for...of的区别 | for...await...of

for...in: 用于枚举数据,数组、对象、字符串

for...of: 用于迭代数据,数组、迭代器、Map、Set

for...await...of: 类似Promise.All()方法

HtmlCollection和NodeList的区别

都是不是数组,而是"类数组"

HtmlCollection只包含标签

NodeList会包含标签、文本、注释 image.png

严格模式是什么

JavaScript被设计为新手开发者更容易上手,所以有时候本来错误语法,被认为也是可以正常被解析的;但是这种方式可能给带来留下来安全隐患; 在严格模式下,这种失误就会被当做错误,以便可以快速的发现和修正

严格模式通过在脚本或函数的头部添加 use strict; 表达式来声明。

  • 全局变量必须声明
  • 禁止使用with
  • eval的作用域隔离
  • 禁止this直接指向window
  • 禁止函数参数同名

HTTP的options请求是什么

当发起跨域请求时,浏览器默认会先发送一个options预检请求,来检查请求的服务器是否支持该请求

垃圾回收机制

  • 引用计数:当一个变量被持有的引用为0时则释放内存,但无法解决循环引用的问题,存在内存泄漏
  • 标记清除:当一个变量无法从window作用域中找到引用时则释放其内存

内存泄漏

什么情况下会发生内存泄漏

  • 使用不当的闭包
  • 遗忘的定时器
  • 意外的全局变量

闭包是内存泄漏吗

是也不是,因为确实无法释放闭包内的引用,但这是调用者可控的,也可称为js特性的一种

怎么检测是否存在内存泄漏

开发者工具 Performance

Map和WeakMap的区别

  • Map的key可以是任意类型,WeakMap只能是引用类型
  • 垃圾回收机制不会考虑WeakMap持有的引用

内存泄漏

EventLoop

  • 微任务:promise,dom渲染前执行
  • 宏任务:settimeout/setinterval,dom渲染后执行

for和forEach哪个快

for快,forEach需要创建一个函数来调用,会开辟额外的作用域,有额外的开销,但是比for看起来简洁

requestAnimationFrame和requestIdleCallback

  • requestAnimationFrame:可以自动匹配设备帧率来展示动画,高性能
  • requestIdleCallback:渲染空闲时调用,优先级低

script标签上的defer和async的区别

默认是浏览器解析到script标签会暂停解析剩余html,获取script资源并执行过后在继续解析

defer:解析和获取script资源并行执行,html解析完成后再运行js

async:解析和获取script资源并行执行,获取完毕后运行js再继续解析

repaint和reflow的区别

repaint:重绘,仅元素自身的外观改变时 reflow:重排,元素的定位布局改变时,性能损耗高

避免多次重排

  • 集中修改样式,如切换class
  • 设置display: none脱离文档流,修改样式后再恢复
  • BFC

多标签通讯

localstorage通讯

  • 必须同域
  • storage监听事件

SharedWorker

网页和iframe通讯

postMessage Api

常用设计模式

标题描述
工厂模式提供一个函数来创建对象,省去new,简化创建对象,根据传入参数来对应返回需要的对象
单例模式适用于单一对象,只生成一个对象实例,避免频繁创建和销毁实例,减少内存占用
发布订阅模式发布者,调度中心,订阅者三个角色,发布者和订阅者互不感知,完全由调度中心来处理
观察者模式观察者和被观察者是一对多的关系,其中一个对象的状态发生改变,所有依赖它的对象都会收到通知
装饰器模式给类、参数、方法封装新的功能

实现Array.flat

这里是深度flat,只需一层的话去掉递归即可

function flattenDeep(arr: any[]): any[] {
    let res: any[] = []
    arr.forEach(item => {
        if (Array.isArray(item)) {
            const flatItem = flattenDeep(item) // 递归
            res = res.concat(flatItem)
        } else {
            res = res.concat(item)
        }
    })

    return res
}

实现获取变量类型的函数

function getType(x) {
    const originType = Object.prototype.toString.call(x) // '[object String]'
    const type = originType.slice(8, -1) // 'String'
    return type.toLowerCase()
}

实现new函数

function customNew<T>(constructor: Function, ...args: any[]): T {
    // 1. 创建一个空对象,继承 constructor 的原型
    const obj = Object.create(constructor.prototype)
    // 2. 将 obj 作为 this ,执行 constructor ,传入参数
    constructor.apply(obj, args)
    // 3. 返回 obj
    return obj
}

LazyMan

class LazyMan {
    private name: string
    private tasks: Function[] = [] // 任务列表

    constructor(name: string) {
        this.name = name

        setTimeout(() => {
            this.next()
        })
    }

    private next() {
        if(this.tasks.length){
            this.tasks.shift()!()
        }
    }

    eat(food: string) {
        const task = () => {
            console.info(`${this.name} eat ${food}`)
            this.next() // 立刻执行下一个任务
        }
        this.tasks.push(task)

        return this // 链式调用
    }

    sleep(seconds: number) {
        const task = () => {
            console.info(`${this.name} 开始睡觉`)
            setTimeout(() => {
                console.info(`${this.name} 已经睡完了 ${seconds}s,开始执行下一个任务`)
                this.next() // xx 秒之后再执行下一个任务
            }, seconds * 1000)
        }
        this.tasks.push(task)

        return this // 链式调用
    }
}

函数转换成柯里化函数

function curry(fn: Function) {
    const fnArgsLength = fn.length // 传入函数的参数长度
    let args: any[] = []

    // ts 中,独立的函数,this 需要声明类型
    function calc(this: any, ...newArgs: any[]) {
        // 积累参数
        args = [ ...args, ...newArgs ]
        if (args.length < fnArgsLength) {
            // 参数不够,返回函数
            return calc
        } else {
            // 参数够了,返回执行结果
            return fn.apply(this, args)
        }
    }
    return calc
}

实现instanceof

原理:实例的原型对象和目标的原型对象是否一致

function myInstanceof(instance: any, origin: any): boolean {
    if (instance == null) return false // null undefined

    const type = typeof instance
    if (type !== 'object' && type !== 'function') {
        // 值类型
        return false
    }

    let tempInstance = instance // 为了防止修改 instance
    while (tempInstance) {
        if (tempInstance.__proto__ === origin.prototype) {
            return true // 配上了
        }
        // 未匹配
        tempInstance = tempInstance.__proto__ // 顺着原型链,往上找
    }

    return false
}

实现bind、call、apply

Function.prototype.myBind = function (ctx:any,...args: any) {
    return (...args2:any)=> {
        this.apply(ctx,args.concat(args2))
    }
}
Function.prototype.myCall = function (ctx:any,...args: any) {
    let key = Symbol()
    Object.defineProperty(ctx,key,{
        enumerable: false,
        value: this
    })
    ctx[key](...args)
}
Function.prototype.myApply = function (ctx:any,args: any[] = []) {

    let key = Symbol()
    Object.defineProperty(ctx,key,{
        enumerable: false,
        value: this
    })
    ctx[key](...args)
}

实现EventBus

class EventBus {
    private readonly event: {
        [key in string]: Array<{isOnce: boolean, fn:Function}>
    }
    constructor() {
        this.event = {}
    }
    once(key:string,fn:Function){
        this.on(key,fn,true)
    }
    on(key:string,fn:Function,isOnce=false){
        if (!this.event[key]) {
            this.event[key] = []
        }
        this.event[key].push({isOnce,fn})
    }
    emit(key:string){
        if (!this.event[key]) return
        this.event[key] = this.event[key].filter(item=>{
            let {fn,isOnce}  = item
            fn()
            return !isOnce
        })
    }
    off(key:string,fn ?:Function){
        if(!this.event[key]) return
        if(fn){
            this.event[key] = this.event[key].filter(item=>item.fn!==fn)
        }else{
            this.event[key] = []
        }
    }
}

实现LRU缓存

LRU:Least Recently Used 仅保留最近使用的数据

map实现

class LRUCache {
    private length: number
    private data: Map<any, any> = new Map()

    constructor(length: number) {
        if (length < 1) throw new Error('invalid length')
        this.length = length
    }

    set(key: any, value: any) {
        const data = this.data

        // 若已存在 删除重新添加 刷新到最前
        if (data.has(key)) {
            data.delete(key)
        }
        data.set(key, value)

        if (data.size > this.length) {
            // 如果超出了容量,则删除 Map 最老的元素
            const delKey = data.keys().next().value
            data.delete(delKey)
        }
    }

    get(key: any): any {
        const data = this.data

        if (!data.has(key)) return null

        const value = data.get(key)

        data.delete(key)
        data.set(key, value)

        return value
    }
}

链表实现

class LRUCache {
    private length: number
    private data: { [key: string]: IListNode } = {}
    private dataLength: number = 0
    private listHead: IListNode | null = null
    private listTail: IListNode | null = null

    getData(){
        console.log(this.data);
    }

    constructor(length: number) {
        if (length < 1) throw new Error('invalid length')
        this.length = length
    }

    private moveToTail(curNode: IListNode) {
        if (this.listTail === curNode) return

        // ---- 1. 让 prevNode nextNode 断绝与 curNode 的关系 ----
        const prevNode = curNode.prev
        const nextNode = curNode.next
        if (prevNode) {
            prevNode.next = nextNode
        }
        if (nextNode) {
            nextNode.prev = prevNode

            if (this.listHead === curNode) this.listHead = nextNode
        }

        // ---- 2. 让 curNode 断绝与 prevNode nextNode 的关系 ----
        delete curNode.prev
        delete curNode.next

        // ---- 3. 在 list 末尾重新建立 curNode 的新关系 ----
        if (this.listTail) {
            this.listTail.next = curNode
            curNode.prev = this.listTail
        }
        this.listTail = curNode
    }

    private tryClean() {
        while (this.dataLength > this.length) {
            const head = this.listHead
            if (head == null) throw new Error('head is null')
            const headNext = head.next
            if (headNext == null) throw new Error('headNext is null')

            // 1. 断绝 head 和 next 的关系
            delete headNext.prev
            delete head.next

            // 2. 重新赋值 listHead
            this.listHead = headNext

            // 3. 清理 data ,重新计数
            delete this.data[head.key]
            this.dataLength = this.dataLength - 1
        }
    }

    get(key: string): any {
        const data = this.data
        const curNode = data[key]

        if (curNode == null) return null

        if (this.listTail === curNode) {
            // 本身在末尾(最新鲜的位置),直接返回 value
            return curNode.value
        }

        // curNode 移动到末尾
        this.moveToTail(curNode)

        return curNode.value
    }

    set(key: string, value: any) {
        const curNode = this.data[key]

        if (curNode == null) {
            // 新增数据
            const newNode: IListNode = { key, value }
            // 移动到末尾
            this.moveToTail(newNode)

            this.data[key] = newNode
            this.dataLength++

            if (this.dataLength === 1) this.listHead = newNode
        } else {
            // 修改现有数据
            curNode.value = value
            // 移动到末尾
            this.moveToTail(curNode)
        }

        // 尝试清理长度
        this.tryClean()
    }
}

实现深拷贝

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) => {
            target.set(cloneDeep(v, map), cloneDeep(k, map))
        })
    }

    // Set
    if (obj instanceof Set) {
        target = new Set()
        obj.forEach(v => {
            target.add(cloneDeep(v, map))
        })
    }

    // Array
    if (obj instanceof Array) {
        target = obj.map(item => cloneDeep(item, map))
    }

    // Object
    for (const key in obj) {
        target[key] = cloneDeep(obj[key], map)
    }

    return target
}

Css基础

px、%、em、rem、vw/vh、vmax/vmin 的区别

  • px: 像素
  • %: 百分比
  • em: 当前元素的fontsize
  • rem: root的fontsize
  • vw/vh: 屏幕宽/高
  • vmax/vmin: 屏幕宽高对比哪个大/小取哪个

offset、client、scroll

  • offsetHeight|offsetWidth: border + padding + content
  • clientHeight|clientWidth: padding + content
  • scrollHeight|scrollWidth: padding + 实际内容尺寸

Vue相关

watch和computed的区别

  • computed函数不能有异步;watch可以
  • computed必须有返回
  • computed有缓存,watch每次监听对象发生改变时都会调用回调
  • computed适用于多个属性影响一个属性,watch则是一个属性影响多个属性

组件通信方式

父子

  • props
  • $emit
  • $attrs, 未声明的属性
  • provide/inject
  • refs

兄弟

  • 全局事件总线
  • vuex/pinia

虚拟DOM和真实DOM哪个快

  • 虚拟DOM没有操作原生DOM快
  • 虚拟DOM的优势在于节点进行改动的时候会进行diff算法,尽量减少开销
  • 真正的价值是数据驱动视图,大大节省了开发的效率

生命周期

  • beforeCreate:会在实例初始化完成、props 解析之后、data() 和 computed 等选项处理之前立即调用
  • created:初始化vue实例完成
  • beforeMount:生成vdom
  • mounted:组件被挂载之后调用
  • beforeUpdate:data发生变化,dom更新之前
  • updated:data发生变化,dom更新后(不要再update里修改data,死循环
  • beforeUnmount:组件销毁前调用,还保留vue实例所有功能
  • unmounted:组件实例被卸载之后调用,响应式作用已停止
  • activated:keepalive组件被激活
  • deactivated:keepalive组件被隐藏

Vue2/3 diff算法的区别

Vue2/3 响应式的区别

常用优化

  • v-if和v-show
  • v-for必须有key
  • computed缓存
  • 频繁切换用keep-alive
  • 耗时加载的组件使用异步加载
  • 路由懒加载
  • nuxt.js ssr渲染

Node.js相关

扩展

URL到页面展示的完整过程

  1. DNS查询获取服务器地址,建立TCP连接
  2. 发起HTTP请求
  3. 获取HTML并解析,获取js css等静态资源
  4. 生成dom树并渲染页面

TCP的三次握手和四次挥手

三次握手和四次挥手

cookie和token的区别

  • cookie不能跨域携带,token无限制
  • cookie需要配合session在服务端查询用户信息,token是无状态的
  • token可防止csrf攻击

Session和JWT的区别

  • session是空间换时间,token是cpu计算时间换取存储空间

SSO单点登录

相同顶级域名 多个子域名通过设置domain为顶级域名的cookie可实现信息交换

不同顶级域名 CAS方案

HTTP、TCP、UDP

HTTP

  • 处于应用层
  • 无状态的短连接
  • 是传输数据的内容的规范

TCP

  • 处于传输层
  • 有稳定的连接和断开,传输稳定
  • 有状态的长连接
  • 是数据传输和连接方式的规范

UDP

  • 处于传输层
  • 无连接无断开,传输效率高,丢包率也高

HTTP1.0、1.1、2.0的区别

HTTP1.0

  • 默认短链接,需要手动开启长连接 HTTP1.1
  • 默认长连接
  • 支持断点续传
  • 支持只发送header信息

HTTP2.0

  • header可压缩
  • 多路复用,做到同一个连接并发处理多个请求
  • 服务端主动推送

WebSocket和HTTP

WebSocket请求需要先发送一个HTTP请求建立连接,再把这个请求升级为WS请求,双端都可主动推送消息

WebSocket和长轮询

长轮询是模拟服务端发送消息的一个常见手段,由客户端主动发送一个请求,服务端将这个请求挂起等待,当需要发送消息的时候在返回,客户端发现该请求超时需要继续发送一个请求

常见的攻击手段

XSS CSRF DDos SQL注入

基础算法

队列

栈实现

一个栈用来入列顺序,出列时 出栈到第二个栈颠倒顺序,在将第一个出栈,在全部回到第一个栈

class MyQueue {
    private stack1: number[] = []
    private stack2: number[] = []

    add(n: number) {
        this.stack1.push(n)
    }

    delete(): number | null {
        // 将 stack1 所有元素移动到 stack2 中
        while(this.stack1.length) {
            const n = this.stack1.pop()
            if (n != null) {
                this.stack2.push(n)
            }
        }

        // stack2 pop
        let res = this.stack2.pop()

        // 将 stack2 所有元素“还给”stack1
        while(this.stack2.length) {
            const n = this.stack2.pop()
            if (n != null) {
                this.stack1.push(n)
            }
        }

        return res || null
    }

    get length(): number {
        return this.stack1.length
    }
}

数组实现

class MyQueue {
    data: number[]
    add(n: number) {
        this.data.push(n)
    }
    delete(): number | null {
        if(this.data.length){
            return this.data.shift()
        }
        return null
    }
    get length(): number {
        return this.data.length
    }
}

链表实现

性能最好

interface IListNode {
    value: number
    next: IListNode | null
}
class MyQueue {
    private head: IListNode | null = null
    private tail: IListNode | null = null
    private len = 0

    add(n: number) {
        const newNode: IListNode = {
            value: n,
            next: null,
        }

        // 处理 head
        if (this.head == null) {
            this.head = newNode
        }

        // 处理 tail
        const tailNode = this.tail
        if (tailNode) {
            tailNode.next = newNode
        }
        this.tail = newNode

        // 记录长度
        this.len++
    }

    delete(): number | null {
        if (this.head == null || this.len <= 0) return null

        // 取值
        const value = this.head.value

        // 处理 head
        this.head = this.head.next

        // 记录长度
        this.len--

        return value
    }

    get length(): number {
        return this.len
    }
}

反转单向链表

function reverseLinkList(listNode: ILinkListNode): ILinkListNode {
    // 定义三个指针
    let prevNode: ILinkListNode | undefined = undefined
    let curNode: ILinkListNode | undefined = undefined
    let nextNode: ILinkListNode | undefined = listNode

    // 以 nextNode 为主,遍历链表
    while(nextNode) {
        // 第一个元素,删掉 next ,防止循环引用
        if (curNode && !prevNode) {
            delete curNode.next
        }

        // 反转指针
        if (curNode && prevNode) {
            // 把当前节点的下个节点变成上个节点
            curNode.next = prevNode
        }

        // 整体向后移动指针
        prevNode = curNode
        curNode = nextNode
        // 获取原顺序的下个节点,若无则结束循环
        nextNode = nextNode?.next
    }

    // 循环结束,curNode是原最后一个节点,此时变成第一个
    curNode!.next = prevNode

    return curNode!
}

二分查找法

O(logn)

必须有序

循环

function binarySearch1(arr: number[], target: number): number {
    if (arr.length === 0) return -1

    let startIndex = 0 // 开始位置
    let endIndex = arr.length - 1 // 结束位置

    while (startIndex <= endIndex) {
        const midIndex = Math.floor((startIndex + endIndex) / 2)
        const midValue = arr[midIndex]
        if (target < midValue) {
            // 目标值较小,则继续在左侧查找
            endIndex = midIndex - 1
        } else if (target > midValue) {
            // 目标值较大,则继续在右侧查找
            startIndex = midIndex + 1
        } else {
            // 相等,返回
            return midIndex
        }
    }

    return -1
}

递归

function 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

    // 中间位置
    const midIndex = Math.floor((startIndex + endIndex) / 2)
    const midValue = arr[midIndex]

    if (target < midValue) {
        // 目标值较小,则继续在左侧查找
        return binarySearch2(arr, target, startIndex, midIndex - 1)
    } else if (target > midValue) {
        // 目标值较大,则继续在右侧查找
        return binarySearch2(arr, target, midIndex + 1, endIndex)
    } else {
        // 相等,返回
        return midIndex
    }
}

深度优先和广度优先遍历dom树

// 递归 深度
depthFirstTraverse1(root: Node) {
    console.log(root);

    const childNodes = root.childNodes; // .childNodes 和 .children 不一样
    if (childNodes.length) {
        childNodes.forEach(child => {
            depthFirstTraverse1(child); // 递归
        });
    }
}
// 循环 深度
function depthFirstTraverse2(root: Node) {
    const stack: Node[] = [];

    // 根节点压栈
    stack.push(root);

    while (stack.length > 0) {
        const curNode = stack.pop(); // 出栈
        if (curNode == null) break;

        console.log(curNode);

        // 子节点压栈
        const childNodes = curNode.childNodes;
        if (childNodes.length > 0) {
            // reverse 反顺序压栈
            Array.from(childNodes).reverse().forEach(child => stack.push(child));
        }
    }
}
// 广度优先
function breadthFirstTraverse(root: Node) {
    const queue: Node[] = []; // 模拟单项队列,链表更合适

    // 根节点入队列
    queue.unshift(root);

    while (queue.length > 0) {
        const curNode = queue.pop();
        if (curNode == null) break;

        console.log(curNode);

        // 子节点入队
        const childNodes = curNode.childNodes;
        if (childNodes.length) {
            childNodes.forEach(child => queue.unshift(child));
        }
    }
}

斐波那契数列 | 青蛙跳台阶

循环

function fibonacci2(n: number): number {
    if (n <= 0) return 0
    if (n === 1) return 1

    let n1 = 1 // 记录 n-1 的结果
    let n2 = 0 // 记录 n-2 的结果
    let res = 0

    for (let i = 2; i <= n; i++) {
        res = n1 + n2

        // 记录中间结果
        n2 = n1
        n1 = res
    }

    return res
}

递归

function fibonacci1(n: number): number {
    if (n <= 0) return 0
    if (n === 1) return 1

    return fibonacci1(n - 1) + fibonacci1(n - 2)
}

将数组中所有的0移动到末尾

不使用额外空间

循环

function moveZero1(arr: number[]): void {
    const length = arr.length
    if (length === 0) return

    let zeroLength = 0

    // O(n^2)
    for (let i = 0; i < length - zeroLength; i++) {
        if (arr[i] === 0) {
            arr.push(0)
            arr.splice(i, 1) // 本身就有 O(n)
            i-- // 数组截取了一个元素,i 要递减,否则连续 0 就会有错误
            zeroLength++ // 累加 0 的长度
        }
    }
}

双指针

function moveZero2(arr: number[]): void {
    const length = arr.length
    if (length === 0) return

    let j = -1 // 指向第一个 0

    for (let i = 0; i < length; i++) {
        if (arr[i] === 0) {
            // 第一个 0
            if (j < 0) {
                j = i
            }
        }

        if (arr[i] !== 0 && j >= 0) {
            // 交换
            [arr[j],arr[i]] = [arr[i],arr[j]]
            j++
        }
    }
}

字符串中连续相同最多的字串

也可嵌套循环实现

function findContinuousChar2(str: string): IRes {
    const res: IRes = {
        char: '',
        length: 0
    }

    const length = str.length
    if (length === 0) return res

    let tempLength = 0 // 临时记录当前连续字符的长度
    
    let j = 0

    // O(n)
    for (let i = 0 ; i < length; i++) {
        if (str[i] === str[j]) {
            tempLength++
        }

        if (str[i] !== str[j] || i === length - 1) {
            // 不相等,或者 i 到了字符串的末尾
            if (tempLength > res.length) {
                res.char = str[j]
                res.length = tempLength
            }
            tempLength = 0 // reset

            if (i < length - 1) {
                j = i // 让 j “追上” i
                i-- // 细节
            }
        }
    }

    return res
}

快速排序

function quickSort(arr: number[]): number[] {
    const length = arr.length
    if (length === 0) return arr

    const midIndex = Math.floor(length / 2)
    const midValue = arr[midIndex]

    const left: number[] = []
    const right: number[] = []

    for (let i = 0; i < length; i++) {
        if (i !== midIndex) {
            const n = arr[i]
            if (n < midValue) {
                // 小于 midValue ,则放在 left
                left.push(n)
            } else {
                // 大于 midValue ,则放在 right
                right.push(n)
            }
        }
    }

    // return [...quickSort2(left), midValue, ...quickSort2(right)]
    return quickSort2(left).concat(
        [midValue],
        quickSort2(right)
    )
}

对称数 | 回数

转数组反转

数组操作很慢

function findPalindromeNumbers1(max: number): number[] {
    const res: number[] = []
    if (max <= 0) return res

    for (let i = 1; i <= max; i++) {
        // 转换为字符串,转换为数组,再反转,比较
        const s = Array.from(String(i))
        if (s === s.reverse()) {
            res.push(i)
        }
    }

    return res
}

前后双指针

转字符串相对数组更快

function findPalindromeNumbers2(max: number): number[] {
    const res: number[] = []
    if (max <= 0) return res

    for (let i = 1; i <= max; i++) {
        const s = i.toString()
        const length = s.length

        // 字符串头尾比较
        let flag = true
        let startIndex = 0 // 字符串开始
        let endIndex = length - 1 // 字符串结束
        while (startIndex < endIndex) {
            if (s[startIndex] !== s[endIndex]) {
                flag = false
                break
            } else {
                // 继续比较
                startIndex++
                endIndex--
            }
        }

        if (flag) res.push(i)
    }

    return res
}

数字反转

直接操作数字,不做转换最快

function findPalindromeNumbers3(max: number): number[] {
    const res: number[] = []
    if (max <= 0) return res

    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 (i === rev) res.push(i)
    }

    return res
}

数字千分位分割

循环拼接

function format2(n: number): string {
    n = Math.floor(n) // 只考虑整数

    let res = ''
    const s = n.toString()
    const length = s.length

    for (let i = length - 1; i >= 0; i--) {
        const j = length - i
        if (j % 3 === 0) {
            if (i === 0) {
                // 第一位不加
                res = s[i] + res
            } else {
                res = ',' + s[i] + res
            }
        } else {
            res = s[i] + res
        }
    }

    return res
}

正则

吊炸天的写法,但正则开启有性能消耗,性能测试慢一倍

function format3(n:number): string{
    let reg = /(?=\B(\d{3})+$)/g
    return (String(n)).replace(reg,",")
}