面经-前端常见手写

560 阅读2分钟

手写路由

hash

利用了url的hash值变化但是页面不刷新的原理。我们想要一个路由实例,在实例中注册对应的url可以进行不同的组件的显示。 因此我们首先有个类创建路由实例。 有个注册方法,将不同的hash值与不同的页面操作连接起来,页面操作封装到callback函数里,我们可以用一个map封装。

class HashRouter(){
constructor() {
this.routess = [];
window.addEventListener('hashChange',this.load().bind(this));//监听变化
load(){
let hash = window.location.hash.splice(1);
let  callback = this.routes[hash];
callback&&callback.call(this);
    }
    }
}

history

history是HTML5新增的api,简单来说能够控制用户的会话和携带数据但是不刷新页面。

注意: hash路由通过监听hashchange来改变页面内容

但是浏览器没有提供类似onurlchange这样的事件。您可能想到监听window.location的变化,但在JavaScript中这也没有直接的方法(location对象没有变化事件) 在history中URL变化可能来自三个不同的来源:

  1. 用户点击浏览器的前进/后退按钮(只会改变url,并不会存储页面)
  • 这会触发popstate事件
  • 我们必须监听这个事件来响应这类变化
  1. 通过history.pushState/replaceState编程方式改变URL
  • 这些方法不会触发任何事件!
  • 调用后需要手动更新页面内容
  1. 用户点击应用内的链接
  • 需要拦截点击事件并阻止默认行为
  • 然后手动处理路由变化

我们的history路由刷新可能出现404,这时候服务器重新返回一下HTML即可

class HistoryRouter {
            constructor() {
                // 存储路由映射
                this.routes = {};
                
                // 内容容器
                this.container = document.getElementById('app');
                
                // 绑定方法的this
                this.handlePopState = this.handlePopState.bind(this);
                this.handleLink = this.handleLink.bind(this);
                
                // 初始化
                this.init();
            }
            
            init() {
                // 监听链接点击事件
                document.addEventListener('click', this.handleLink);
                
                // 监听浏览器前进/后退按钮
                window.addEventListener('popstate', this.handlePopState);
                
                // 加载当前页面
                this.loadRoute(location.pathname);
            }
            
            // 注册路由
            register(path, callback) {
                this.routes[path] = callback;
                return this; // 允许链式调用
            }
            
            // 处理链接点击
            handleLink(e) {
                // 只处理带有data-link属性的链接
                if (e.target.matches('[data-link]')) {
                    e.preventDefault();
                    const url = e.target.getAttribute('href');
                    this.navigate(url);
                }
            }
            
            // 导航到指定路径
            navigate(path) {
                // 更新历史记录和URL
                history.pushState({path}, '', path);
                
                // 加载对应的路由内容
                this.loadRoute(path);
            }
            
            // 处理浏览器前进/后退
            handlePopState(e) {
                const path = location.pathname;
                this.loadRoute(path);
            }
            
            // 加载路由对应的内容
            loadRoute(path) {
                const route = this.routes[path] || this.routes['*']; // 尝试获取路由或404路由
                
                if (route && typeof route === 'function') {
                    const content = route();
                    this.renderContent(content);
                } else {
                    this.renderContent(`<h2>404 未找到</h2><p>路径 "${path}" 不存在</p>`);
                }
            }
            
            // 渲染内容到容器
            renderContent(content) {
                this.container.innerHTML = content;
            }
        }

手写响应式

reactive

// reactive.js
import {
   mutableHandlers
} from './baseHandles';


export const reactiveMap = new WeakMap();
export const shallowReactiveMap = new WeakMap();// 浅响应式
// 大型项目 响应式对象很多,但是reactiveMap 只有一个 性能?
// 垃圾回收 弱引用
// router-view 
export const reactive = (target ) => {
   return createReactiveObject(target,reactiveMap,mutableHandlers);
}

export const shallowReactive = (target ) => {
   return createReactiveObject(target,shallowReactiveMap,shallowReactiveHandlers);
}


function createReactiveObject(target, proxyMap, proxyHandlers) {
   if (typeof target !== 'object') {
      console.warn('reactive 必须是一个对象')
      return target;
   }

   const existingProxy = proxyMap.get(target);
   if (existingProxy) {
      return existingProxy;
   }

    const proxy = new Proxy(target,mutableHandlers); // 被代理对象,拦截对象方法
    proxyMap.set(target,proxy);
    return proxy;
}
//baseHandle.js
import { track } from './effect';
import { trigger } from './effect';
import { reactive } from './reactive';
import { isObject } from '../shared';

// 代理对象的拦截操作
const get = createGetter();
const set = createSetter();
const shallowReactiveGet = createGetter(true);

function has(target,key){
const res = Reflect.has(target,key);
track(target,'has',key);
return res;
}

// 代理对象get
function createGetter(shallow = false) {
   return function get(target, key, receiver) {
      // 收集依赖
      track(target,'get',key);
      let res = target[key];
      if(shallow){
        return res;
      }
      if(isObject(res)){
        return reactive(res);
      }

    return res;
   }
}

// 代理对象set
function createSetter() {
    return function set(target, key, value, receiver) {
      target[key] = value;
      trigger(target,'set',key);
      return true;
   }
}

export const mutableHandlers = {
   get,
   set,
   //has,
}

export const shallowReactiveHandlers = {
   get: shallowReactiveGet,
   set
}

//effect.js
let activeEffect = null;
let targetMap = new WeakMap();// 弱引用

export function effect(fn) {
  // 返回一个函数 立即执行一次
  const effectFn = () => {
    try{
      activeEffect = effectFn;
      let res = fn();
      return res;
    }finally{
      activeEffect = null;
    }
  }
  console.log(fn,'fn')
  effectFn();
  return effectFn;
}

// 拦截到get请求进行的操作
export function track(target,type,key) {  //<obj,<obj.key,set>>
  console.log('触发track ->  target type key')
  let depsMap = targetMap.get(target);
  if(!depsMap){
    targetMap.set(target,depsMap = new Map());
  }
  let dep = depsMap.get(key);
  if(!dep){
    depsMap.set(key,dep = new Set());
}
  dep.add(activeEffect);
}

// 拦截到set请求进行的操作
export function trigger(target,type,key) {
    console.log('触发trigger ->  target type key')
  let depsMap = targetMap.get(target);
  if(!depsMap){
    return;
  }
  let dep = depsMap.get(key);
  if(!dep){
    return;
  }
  dep.forEach(effectFn => {
    console.log(effectFn,'effectFn')
      effectFn();
  })
}

简单响应式:

let effectList = []
let activeEffect = null

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      // 依赖收集
      if (activeEffect) {
        effectList.push(activeEffect)
      }
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      // 触发更新
      effectList.forEach(effect => effect())
      return true
    }
  })
}

function effect(callback) {
  activeEffect = callback
  callback() // 执行一次进行依赖收集
  activeEffect = null
}

const data = reactive({count:0});


effect(()=>{
  console.log('count changed:', data.count);
})

总结:我们的复杂对象使用Proxy来进行拦截。 proxy是es6引入的一个新语法。

  • 当我们的数据类型为复杂对象时,我们无需一个个的将每个属性遍历的用defineProperty进行设置getter和setter,所以他的性能在这种复杂对象上会比defineProterty好很多。
  • 并且他支持13种底层操作的拦截(in,delete,函数调用等,所以可以拦截数组索引和length,对象添加删除)。
  • 对于嵌套的对象,可以实现惰性监听(只有被访问才递归监听)(defineproperty必须知道拦截的属性进行设置) 所以vue3选择了proxy进行响应式的拦截。

我们会创建一个WeakMap(当我们的对象在组件销毁,没有引用指向时候,会自动回收)来存储对象的响应式属性以及需要重新执行的响应式方法 例如Effect()。

当我们使用了Reactive,我们首先会查看一下这个对象是否是响应式对象。他有一个专门的map存储我们的原始对象和代理对象。假如已经是响应式对象,就把我们的响应式对象返回。假如不是,会根据选项将对象放入map或者sharrowmap中:防止出现这种情况:第一次浅度,第二次深度,直接返回浅度对象。

假如不是,他会创建我们的一个代理拦截对象。 当我们的用户第一次访问属性的时候,他会去调用track方法去搜集依赖。假如调用的不是响应式方法,就会直接返回,是响应式方法就加入到依赖map之中。

当用户设置属性的时候,他会拦截去调用 我们的trigger方法。接着遍历我们的每一个方法,重新执行一遍。

ref

import { reactive } from './reactive';
import {track, trigger} from './effect'
export function ref(value) {
    if (isRef(value)) {
        return value;
    }
    return new RefImpl(value);
}
// 最轻量的拦截器

class RefImpl {
   constructor(val){
    // 私有
    this.__isRef = true;
    this._val = convert(val);
   }
   get value(){
    track(this,'get','value');
    return this._val;
   }
   set value(val){
    if(val !== this._val){
      this._val = convert(val);
      trigger(this,'set','value');
    }
   }
}

function convert(val){
    return typeof val === 'object' ? reactive(val) : val;
}

function isRef(value) {
    return !!value.__isRef;
}

总结: 简单属性的响应式采用了class关键字的getter和setter来实现,它可以看作是defineProper的语法糖。 我们通过将属性包装成一个对象,同样去使用track和trigger方法去进行进行处理。假如传入的是一个对象,他会用reacctive将他的对象进行响应式处理,接着包装成一个value的二级对象进行返回。

简单diff 算法

首先是模板编译,编译成render函数。render函数中包括我们的js代码。

当我们的响应式数据发生变化时,他不可能说直接追踪更新我们每个依赖发生变化的DOM部分。他会重新执行我们的render函数进行渲染。描述出我们的新虚拟DOM树,他是在内存中的一个DOM副本。接着将新旧虚拟DOM树来进行对比。计算出最优差异变更法,更新差异,这个算法,就叫diff算法。

首先他是是同层的节点之间进行比较。当找到类型相同节点时(key,),则会调用patch方法,patch方法主要干两件事:递归遍历比较子节点,查看新旧节点的不同地方(比如文本)进行更新。

假如在新DOM树中相同节点的位置不同,则会进行节点移动。主要通过一个lastIndex来记录已经处理好的节点在旧节点中的索引值,假如找到的旧DOM节点index j在lastIndex之前,则会将VNode往后调,否则不动。

假如没在旧树中找到新节点,那就找到他应该插入的位置,进行插入。 最后再到旧树中找新树中没有的节点,调用DOM方法进行删除。

但是这种算法有时候很耗费性能的,例如(abcde,edcba)完全逆序,你要移动4次DOM。于是改进了算法,采用双端比较法,头头,尾尾,头尾,尾头依次比较。

在vue3中使用的是一个最长连续字串的动态规划算法。

有没有想过为什么我们的每次响应式数据变化render函数都会重新执行?不会很耗费性能吗?为什么不能像我们操控dom一样对特定的依赖数据的DOM进行原子化的更新呢? 现在已经有类似的前端框架出现(svelet)

const oldChildren = n1.children;
const newChildren = n2.children;

let lastIndex = 0;

// 遍历新的 children
for (let i = 0; i < newChildren.length; i++) {
    const newVNode = newChildren[i];
    let j = 0;
    let find = false;

    // 遍历旧的 children
    for (; j < oldChildren.length; j++) {
        const oldVNode = oldChildren[j];

        // 如果找到了具有相同 key 值的两个节点,则调用 patch 函数更新
        if (newVNode.key === oldVNode.key) {
            find = true;
            patch(oldVNode, newVNode, container);// 更新当前节点标签,子节点递归更新

            if (j < lastIndex) {
                // 需要移动
                const prevVNode = newChildren[i - 1];
                if (prevVNode) {
                    const anchor = prevVNode.el.nextSibling;
                    insert(newVNode.el, container, anchor);
                }
            } else {
                // 更新 lastIndex
                lastIndex = j;
            }
            break;
        }
    }

    if (!find) {
        const prevVNode = newChildren[i - 1];
        let anchor = null;
        if (prevVNode) {
            anchor = prevVNode.el.nextSibling;
        } else {
            anchor = container.firstChild;
        }
        patch(null, newVNode, container, anchor);
    }
}

// 遍历旧的节点
for (let i = 0; i < oldChildren.length; i++) {
    const oldVNode = oldChildren[i];

    // 拿着旧 VNode 去新 children 中寻找相同的节点
    const has = newChildren.find(
        vnode => vnode.key === oldVNode.key
    );

    if (!has) {
        // 如果没有找到相同的节点,则移除
        unmount(oldVNode);
    }
}

手写简单axios

function simpleAxios({baseURL = ''}){
    // 拦截器
    const interceptors = {
        request: [],
        response: []
    }
    // 推入拦截器
    function useRequestInterceptor(interceptor){
        interceptors.request.push(interceptor);
    }
    // 拦截器注册执行
    function executeInterceptors(interceptors, config){
        return interceptors.reduce((promise, interceptor) => {
            return promise.then(interceptor);
        }, Promise.resolve(config));
    }

    function sendRequest(method, url, data) {
        return executeInterceptors(interceptors.request, {method, url, data})
        .then(({method, url, data}) => {
            return new Promise((resolve, reject) => {
                const xhr = new XMLHttpRequest();
                xhr.open(method, url);//异步 || 同步
                if(method === 'POST'){
                xhr.setRequestHeader('Content-Type', 'application/json');
            }
            xhr.onreadystatechange = function(){
                if(xhr.readyState === 4 && xhr.status === 200){
                    resolve(xhr.responseText);
                }
                else{
                    reject(xhr.statusText);
                }
                }
                xhr.send(JSON.stringify(data));
            });
        });
    }

    return {
        get(url){
            return sendRequest('GET', `${baseURL}${url}`);
        },
        post(url, data){
            return sendRequest('POST', `${baseURL}${url}`, data);
        },
        useRequestInterceptor(interceptor){
            interceptors.request.push(interceptor);
        }
    }
}

export default simpleAxios;

总结: axios底层使用了XMLHttpRequest进行发送消息。我的axios主要实现了baseURL配置,请求相应拦截,连续的tehnable调用。 首先我的axios中有一个baseURL设置,我们可以在初始化的时候传入这个baseURL,接着在发送请求的时候进行模板字符串的拼接。baseURL能很好的进行一些切换,比如我们开发环境和上线的baseurl肯定是不一样的,我们可以通过process.env判断是否是dev选择不同的baseurl。

axios中需要一个拦截器,我在函数中设置了一个拦截器对象,里面有请求拦截器数组和响应拦截器数组。当我们调用对应的拦截器注册方法可以进行注册,也就是将回调函数推入数组中。

因为拦截器是依次执行的,我们可以通过让拦截器函数依次执行即可。我们使用promise的thenable调用,这样能够很好的处理链式错误捕获和值传递。

axios中使用的是reduce函数,他可以对我们的数组进行一个连续的操作,我们设置从初始值为我们的一个promise,因为promise.then返回值一定是一个promise,所以可以进行连续的thenable调用。

接着就可以使用XMLHttpRequest对象进行发送。我们可以把他封装到promise里,方便发送之后的thenable调用。

防抖 节流

// 相同间隔内多次取消前一次执行本次

function debounce(callback, wait) {//防抖:一定时间内取消前一次
    let timeout;
    return function (...args) {
        if (timeout) {
            clearTimeout(timeout);
        }
        timeout = setTimeout(() => {
            callback.apply(this, args);
        }, wait);
    }
}

//节流:没到时间不执行这一次
function throttle(callback, wait) {
    let time = 0;
    return function (...args) {
        let now = new Date();
        if (now - time > wait) {
            time = now;
            callback.apply(this, args);
        }
    }
}

这里再给出一个高级的节流,支持leading,trailing,remaining和cancle,通过判断选项配置当前时间来设置前置执行,通过一个定时器来设置后置执行,通过取消定时器取消后置执行。

/**
 * 节流函数 - 限制函数在一定时间内只执行一次
 * @param {Function} func - 需要节流的函数
 * @param {Number} wait - 等待时间(毫秒)
 * @param {Object} options - 配置选项
 * @param {Boolean} options.leading - 是否在延迟开始前执行函数(默认: true)
 * @param {Boolean} options.trailing - 是否在延迟结束后执行函数(默认: true)
 * @returns {Function} - 返回节流后的函数
 */
function throttle(func, wait, options={}) {
    // 声明函数内部变量
    let timeout;        // 定时器引用
    let context;        // 执行上下文
    let args;           // 函数参数
    let result;         // 函数返回结果
    let previous = 0;   // 上次执行时间点
    // options对象已在参数中默认初始化为空对象

    /**
     * 延迟执行函数(在wait时间后执行)
     * 作为setTimeout的回调使用
     */
    const later = function() {
        // 若leading为false,重置为0;否则更新为当前时间戳
        previous = options.leading === false ? 0 : new Date().getTime();
        // 清除定时器标识
        timeout = null;
        // 执行原函数
        func.apply(context, args);
        // 如果没有定时器了,清除上下文和参数引用
        if (!timeout) context = args = null;
    };

    /**
     * 节流化后返回的函数
     * 每次事件触发时会执行此函数
     */
    var throttled = function() {
        // 获取当前时间戳
        var now = new Date().getTime();
        // 第一次执行且不希望立即执行时,将previous设为当前时间
        if (!previous && options.leading === false) previous = now;
        
        // 计算距离下次执行func的剩余时间
        var remaining = wait - (now - previous);
        // 保存调用时的上下文和参数
        context = this;
        args = arguments;
        
        // 如果已经到了执行时间点或者时钟回拨了(remaining > wait)
        if (remaining <= 0 || remaining > wait) {
            // 如果有定时器,清除它
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            // 更新上次执行时间点
            previous = now;
            // 执行函数
            func.apply(context, args);
            // 执行完毕后清除上下文和参数引用
            if (!timeout) context = args = null;
        } 
        // 如果还没到执行时间点,且允许trailing执行
        else if (!timeout && options.trailing !== false) {
            // 设置定时器,在剩余时间后执行later
            timeout = setTimeout(later, remaining);
        }
    };
    
    /**
     * 取消节流
     * 用于停止计时器并重置状态
     */
    throttled.cancel = function() {
        clearTimeout(timeout);
        previous = 0;
        timeout = null;
    }
    
    // 返回节流化后的函数
    return throttled;
}

发布订阅模式

class EventEmitter {
    constructor() {
        this.cache = {};// 发布者
    }
    on(name,fn){
        // 建立订阅关系的
        if (this.cache[name]){
            this.cache[name].push(fn)
        }else{
            this.cache[name] = [fn]
        }
    }
    emit(name,...args){ 
        // 触发事件
        if(this.cache[name]){
            let tasks = this.cache[name].slice()
            tasks.forEach(fn=>{
                fn(...args)
            })

        }
    }
    off(name,fn){
        let tasks = this.cache[name]
        if(tasks) {
            const index = tasks.findIndex(f => f === fn)
            if(index >= 0){
                tasks.splice(index,1)
            }
        }
    }
}

  1. 维护一个事件中心(cache对象),用于存储事件与对应的回调函数
  1. 提供三个核心方法:
  • on:订阅事件,将回调函数存入对应事件名的数组中
  • emit:发布事件,触发特定事件名下所有回调函数的执行
  • off:取消订阅,从事件数组中移除特定的回调函数

数组扁平化

arr.flat() 可以实现扁平化

//1
function flatten(arr) {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    // 判断当前元素是否为数组
    if (Array.isArray(arr[i])) {
      // 递归扁平化子数组,并合并到结果中
      result = result.concat(flatten(arr[i]));
    } else {
      // 非数组元素直接添加
      result.push(arr[i]);
    }
  }
  return result;
}

//2
const arr = [1, [2, [3, [4]], 5], 6];

// 扁平化任意深度
console.log(arr.flat(Infinity)); // [1, 2, 3, 4, 5, 6]

函数柯里化

function currying(fn,...args){ 
    if(fn.length <= args.length){ 
        return fn(...args) 
    } 
    return function(...args1){ 
        return currying(fn,...args,...args1) } 
    } 

大数相加

function addBigIntegers(a, b) { 
// 初始化指针(从末尾开始)和进位 
let i = a.length - 1; 
let j = b.length - 1; 
let carry = 0; 
const result = []; // 遍历两个数字字符串,处理每一位相加 
while (i >= 0 || j >= 0 || carry > 0) { 
// 获取当前位的数字(超出长度则视为0) 
const digitA = i >= 0 ? parseInt(a[i], 10) : 0; 
const digitB = j >= 0 ? parseInt(b[j], 10) : 0; 

// 计算当前位总和(包含进位)
const sum = digitA + digitB + carry; 

// 取当前位结果(sum % 10) 
result.push(sum % 10); 

// 更新进位(Math.floor(sum / 10)) 
carry = Math.floor(sum / 10);
// 移动指针 
i--; j--; } // 结果数组反转后拼接成字符串 
return result.reverse().join(''); 
}

instanceof

function myInstanceof(obj, constructor) { 
// 处理基本类型和null/undefined的情况 
if (obj === null || typeof obj !== 'object') {
 return false;
 } // 获取对象的原型
 let proto = Object.getPrototypeOf(obj); 

// 遍历原型链 
while (proto !== null) { 
// 如果找到匹配的原型,返回true 
if (proto === constructor.prototype) {
 return true; } 
// 继续向上查找原型链
 proto = Object.getPrototypeOf(proto); 
} // 
遍历完原型链都没找到匹配,返回false
 return false; 
} 

数值千分位

function format(num) {
    const numStrArr = String(num).split('.');
    let res = []
    let numStr = numStrArr[0];
    for (let i = numStr.length; i > 0; i -= 3) {
        res.unshift(numStr.slice(Math.max(i - 3, 0), i));
    }
    if (numStrArr.length == 1) {//每隔三位加,
        return res.join(',');
    }
    else {
        return res.join(',') + '.' + numStrArr[1]
    }
}

手写new

function myNew(Consructor,...args){
    let obj = Object.create(Consructor.prototype);
    let res =  Consructor.apply(obj,args);
    return (typeof res === 'object' &&res!==null )? res:obj;
}

手写call apply bind

//call

Function.prototype.myCall = function(obj,...args){
 let context = obj?obj:window;
 const fn = Symbol('fn')
 context[fn] = this;
 const res = context[fn](...args);
 delete context[fn];
 return res;
}

//apply

Function.prototype.myApply = function(obj,args){
 let context = obj?obj:window;
 const fn = Symbol('fn')
 context[fn] = this;
 const res = context[fn](...args);
 delete context[fn];
 return res;
}

//bind
Function.prototype.myBind = function(obj){
    let context = obj?obj:window;
    let fun = this;
    const bindFun = function(...args){
        if(this instanceof bindFun){
            return new fun(...args);
        }
        return fun.apply(context,args);
    }
    return bindFun
}

手写map filter reduce foreach

//map
Array.prototype.sx_map = function (callback) {
    const res = []
    for (let i = 0; i < this.length; i++) {
        res.push(callback(this[i], i, this))
    }
    return res
}

//filter
Array.prototype.sx_filter = function (callback) {
    const res = []
    for (let i = 0; i < this.length; i++) {
        callback(this[i], i, this) && res.push(this[i])
    }
    return res
}

// reduce
Array.prototype.myReduce = function(callback, init){
    let res = init;
    this.forEach((item, index) => {
        res = callback(res, item);
    })
    return res;
}

// 计算所有num相加
const sum = players.sx_reduce((pre, next) => {
    return pre + next.num
}, 0)
console.log(sum) // 85


//forEach
Array.prototype.sx_forEach = function (callback) {
    for (let i = 0; i < this.length; i++) {
        callback(this[i], i, this)
    }
}

players.sx_forEach((item, index, arr) => {
    console.log(item, index)
})

setTimeout实现setTimeInterval

function mySetTimout(fn, delay) {
    let timer = null
    const interval = () => {
        fn()
        timer = setTimeout(interval, delay)
    }
    setTimeout(interval, delay)
    return {
        cancel: () => {
            clearTimeout(timer)
        }
    }
}

实现compose函数

function compose(...fn) {
    if (fn.length === 0) return (num) => num
    if (fn.length === 1) return fn[0]
    return fn.reduce((pre, next) => {
        return (num) => {
            return next(pre(num))
        }
    })
}

function fn1(x) {
    return x + 1;
}
function fn2(x) {
    return x + 2;
}
function fn3(x) {
    return x + 3;
}
function fn4(x) {
    return x + 4;
}
const a = compose(fn1, fn2, fn3, fn4);
console.log(a)
console.log(a(1)); // 1+2+3+4=11


DOM和树

//dom转树

function dom2tree(dom) {
    const obj = {}
    obj.tag = dom.tagName
    obj.children = []
    dom.childNodes.forEach(child => obj.children.push(dom2tree(child)))
    return obj
}


//树转dom render函数
function _render(vnode) {
  // 如果是数字类型转化为字符串
  if (typeof vnode === "number") {
    vnode = String(vnode);
  }
  // 字符串类型直接就是文本节点
  if (typeof vnode === "string") {
    return document.createTextNode(vnode);
  }
  // 普通DOM
  const dom = document.createElement(vnode.tag);
  if (vnode.attrs) {
    // 遍历属性
    Object.keys(vnode.attrs).forEach((key) => {
      const value = vnode.attrs[key];
      dom.setAttribute(key, value);
    });
  }
  // 子数组进行递归操作
  vnode.children.forEach((child) => dom.appendChild(_render(child)));
  return dom;
}

浅拷贝 & 深拷贝

// 浅拷贝
const a = { name: 'sunshine_lin', age: 23, arr: [] }
const b = {}
for (let key in a){
    b[key] = a[key]
}

const b = {...a};

//深拷贝
// 1 
function deepClone(target) { 
return JSON.parse(JSON.stringify(target)) 
}
//缺点:
-   1、对象中有字段值为`undefined`,转换后则会直接字段消失
-   2、对象如果有字段值为`RegExp`对象,转换后则字段值会变成{}
-   3、对象如果有字段值为`NaN、+-Infinity`,转换后则字段值变成null
-   4、对象如果有`环引用`,转换直接报错


//2
function deepClone(target) { // 基本数据类型直接返回 
if (typeof target !== 'object') { 
return target 
}

const temp = Array.isArray(target) ? [] : {} 
    for (const key in target) { // 递归
        temp[key] = deepClone(target[key]) 
     } 
    return temp 
}
//缺点:
没解决循环引用问题,

//3
-   每次遍历到有引用数据类型,
就把他当做`key`放到`Map`中,
对应的`value`是新创建的`对象temp`

-   每次遍历到有引用数据类型,
就去Map中找找有没有对应的`key`,
如果有,就说明这个对象之前已经注册过,
现在又遇到第二次,那肯定就是环引用了,
直接根据`key`获取`value`,并返回`value`

function deepClone(target, map = new Map()) {
    // 基本数据类型直接返回
    if (typeof target !== 'object') {
        return target
    }

    // 引用数据类型特殊处理
    // 判断数组还是对象
    const temp = Array.isArray(target) ? [] : {}

+   if (map.get(target)) {
+        // 已存在则直接返回
+        return map.get(target)
+    }
+    // 不存在则第一次设置
+    map.set(target, temp)

    for (const key in target) {
        // 递归
        temp[key] = deepClone(target[key], map)
    }
    return temp
}

版本号排序

function sortVersions(versions) {
  // 复制原数组避免修改原始数据
  return [...versions].sort((a, b) => {
    // 分割版本号为数组
    const aParts = a.split('.');
    const bParts = b.split('.');
    
    // 取最长的版本号长度进行比较
    const maxLength = Math.max(aParts.length, bParts.length);
    
    for (let i = 0; i < maxLength; i++) {
      // 获取当前位置的版本号部分,不存在则视为0
      const aPart = parseInt(aParts[i] || 0, 10);
      const bPart = parseInt(bParts[i] || 0, 10);
      
      // 比较当前部分
      if (aPart > bPart) return 1;
      if (aPart < bPart) return -1;
    }
    
    // 所有部分都相等
    return 0;
  });
}

对象key转驼峰 驼峰转下滑线

function strTransform(str) {
  return String(str).replace(/_([a-z])/g, (match, letter) => {
    console.log(letter);
    return letter.toUpperCase();
  });
}

function transform(target) {//传入对象,返回key更改后的新对象
    if (target === null || typeof target !== 'object') {
    return target;
  }
  if (Array.isArray(target)) {
    return target.map((item) => {
      return transform(item)
    })
  }
  let result = {};
  for (key in target) {
    let obj = transform(target[key]);
    result[strTransform(key)] = obj;
  }
  return result;
}


//驼峰转下滑
function toSnakeCase(str) {
  // 处理驼峰命名:在大写字母前添加下划线,并转为小写
  return str.replace(/[A-Z]/g, (match) => {
    return '_' + match.toLowerCase();
  });
}

// 递归将对象的所有驼峰key转换为下划线命名
function convertCamelToSnakeCase(obj) {
  // 如果不是对象或为null,直接返回
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 如果是数组,递归处理每个元素
  if (Array.isArray(obj)) {
    return obj.map(item => convertCamelToSnakeCase(item));
  }

  // 处理对象
  const result = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 将驼峰key转换为下划线命名
      const snakeCaseKey = toSnakeCase(key);
      // 递归处理值(可能是嵌套对象)
      result[snakeCaseKey] = convertCamelToSnakeCase(obj[key]);
    }
  }
  return result;
}

数组转树 子树添加父id

function arrayToTree(arrayData) {
    // 创建一个映射,存储所有节点
    const map = {};
    const tree = [];
    
    // 第一次遍历,将所有节点存入映射
    arrayData.forEach(item => {
        map[item.id] = { ...item, children: [] };
    });
    
    // 第二次遍历,构建树结构
    arrayData.forEach(item => {
        const node = map[item.id];
        if (item.parentId === 0) {
            // 根节点
            tree.push(node);
        } else {
            // 子节点,添加到父节点的children数组中
            const parent = map[item.parentId];
            if (parent) {
                parent.children.push(node);
            }
        }
    });
    
    return tree[0] || null;
}

// 2. 树转数组(子树添加父id)
function treeToArray(node, parentId = 0, result = []) {
    // 复制节点并添加父ID,删除children属性
    const item = { ...node, parentId };
    delete item.children;
    result.push(item);
    
    // 递归处理子节点
    if (node.children && node.children.length) {
        node.children.forEach(child => 
            treeToArray(child, node.id, result)
        );
    }
    
    return result;
}


// 测试数据
const arrayData = [
  { id: 1, name: '根节点', parentId: 0 },
  { id: 2, name: '子节点1', parentId: 1 },
  { id: 3, name: '子节点2', parentId: 1 },
  { id: 4, name: '孙节点1', parentId: 2 },
  { id: 5, name: '孙节点2', parentId: 3 },
  { id: 6, name: '曾孙节点', parentId: 4 }
];

// 数组转树测试
const treeData = arrayToTree(arrayData);
console.log('数组转树结果:', JSON.stringify(treeData, null, 2));

// 树转数组测试
const convertedArray = treeToArray(treeData);

解析URL

function parseURL(url) {
    const res = {};
    const paramsRUL = url.split('?')[1];
    const arr = paramsRUL.split('&');
    arr.forEach((item) => {
        const [key, value] = item.split('=');
        if (res[key]) {
            if (!Array.isArray(res[key])) {
                let a = res[key];
                res[key] = [];
                res[key].push(a, value)
            } else {
                res[key].push(value);
            }
        }
        else {
            res[key] = value;
        }
    })
    return res;
}

const testUrl = 'https://example.com?name=张三&age=25&hobby=篮球&hobby=音乐&active';

console.log(parseURL(testUrl));
// 输出: 
// {
//   name: "张三",
//   age: "25",
//   hobby: ["篮球", "音乐"],
//   active: ""
// }

手写promise静态方法

resolve,rejected

Promise.myResolve = function(value) {
    return new Promise((resolve, reject) => {
        resolve(value);
    })
}

//Promise.reject:返回一个带有拒绝原因的Promise对象
Promise.myReject = function(reason) {
    return new Promise((resolve, reject) => {
        reject(reason);
    })
}

all allSettled

//all
Promise.myAll=function(promises){
    let res = [];
    let count = 0;
    return new Promise((resolve,reject)=>{
        promises.forEach((item,index) => {
            item.then((resolveStr)=>{
                res[index] = resolveStr;
                count++;
                if(count==promises.length){
                    resolve(res);
                }
            }).catch((rejectStr)=>reject(rejectStr));
        });
    })
}

// allSettled
Promise.myAllSettled=function(promises){
    let res = [];
    let count = 0;
    return new Promise((resolve,reject)=>{
        promises.forEach((item,index) => {
            item.then((resolveStr)=>{
                res[index] = {status:'fulfilled',value:resolveStr};
                count++;
                if(count==promises.length){
                    resolve(res);
                }
            }).catch((rejectStr)=>{
                res[index] = {status:'rejected',reason:rejectStr};
                count++;
                if(count==promises.length){
                    resolve(res);
                }
            });
        });

race any

//race
Promise.myRace = function (promises) {
    return new Promise((resolve, reject) => {
        promises.forEach((item, index) => {
            item.then((resolveStr) => {
                    resolve(resolveStr);
            }).catch((rejectStr) => reject(rejectStr));
        });
    })
}

//any
Promise.myAny = function (promises) {
    return new Promise((resolve, reject) => {
        let rejectNum = 0;
        let rejectReason = [];
        promises.forEach((item, index) => {
            item.then((resolveStr) => {
                    resolve(resolveStr);
            }).catch((rejectStr) => {
                rejectNum++;
                rejectReason[index] = rejectStr;
                if(rejectNum == promises.length){
                    reject(rejectReason);
                }
            });
        });
    })
}

promise并发控制池

function promisePool(promises, maxConcurrent) {
    let count = 0; // 记录正在执行的 Promise 数量
    const results = []; // 存储所有 Promise 的执行结果
    const queue = [...promises]; // 克隆任务队列

    const enqueue = () => {
        while (count < maxConcurrent && queue.length > 0) {
            const promise = queue.shift();
            count++;
            promise()
            
              .then((res) => {
                    results.push(res);
                })
              .catch((err) => {
                    results.push(err);
                })
              .finally(() => {
                    count--;
                    if (queue.length > 0) {
                        enqueue();
                    }
                });
        }

        if (count === 0 && queue.length === 0) {
            return Promise.resolve(results);
        }
    };

    return enqueue();
}

记录当前正在执行的 Promise 数量(如示例代码中的  count  变量),并与最大并发数进行比较,以此决定是否可以从任务队列中取出新的任务来执行。

当一个 Promise 执行完成( fulfilled  或  rejected )时,通过  finally  回调来减少正在执行的 Promise 数量,并检查是否还有未执行的任务,如果有则继续执行。

使用一个任务队列(如示例代码中的  queue  数组)来存储所有待执行的任务,并在合适的时机从中取出任务执行。

红绿灯

//async await
const task = (light, timeout) => { 
    return new Promise((resolve) => { 
        setTimeout(() => resolve(console.log(light))
        , timeout) 
    }) 
} 

const taskRunner = async () => { 
    await task('red', 1000) 
    await task('green', 2000) 
    await task('yellow', 3000) 
    taskRunner() 
} 
taskRunner()

//promise
const task = (timer, light) =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      if (light === 'red') {
        red()
      } else if (light === 'green') {
        green()
      } else if (light === 'yellow') {
        yellow()
      }
      resolve()
    }, timer)
  })
const step = () => {
  task(1000, 'red')
    .then(() => task(2000, 'green'))
    .then(() => task(3000, 'yellow'))
    .then(step)
}
step()

lazyman

class _LazyMan {
  constructor(name) {
    this.tasks = []
    const task = () => {
      console.log(`Hi! This is ${name}`)
      this.next()
    }
    this.tasks.push(task)
    setTimeout(() => {
      this.next()
    }, 0)
  }
  next() {
    const task = this.tasks.shift()
    task && task()
  }
  sleep(time) {
    this.sleepWrapper(time, false)
    return this
  }
  sleepFirst(time) {
    this.sleepWrapper(time, true)
    return this
  }
  sleepWrapper(time, first) {
    const task = () => {
      setTimeout(() => {
        console.log(`Wake up after ${time}`)
        this.next()
      }, time * 1000)
    }
    if (first) {
      this.tasks.unshift(task)
    } else {
      this.tasks.push(task)
    }
  }
  eat(food) {
    const task = () => {
      console.log(`Eat ${food}`);
      this.next();
    };
    this.tasks.push(task);
    return this;
  }
}

// 测试
const lazyMan = (name) => new _LazyMan(name)

lazyMan('Hank').sleep(1).eat('dinner')

lazyMan('Hank').eat('dinner').eat('supper')

lazyMan('Hank').eat('supper').sleepFirst(5)

手写async await