一些JavaScript手写题目实现

panda2022-08-01 18:55:16前端手写 基础 JavaScript

开局

不按分类顺序排放,我是持续加进去的,想到一个写一个属于是~

浅拷贝和深拷贝(来源于某次面试的思考)

首先拷贝需要创建一个新的对象,浅拷贝即指把对象的属性拷贝一份,如果属性是基础类型,那么拷贝的是基础类型的值,如果属性是引用类型(对象),那么拷贝的是引用地址;而深拷贝与浅拷贝的区别是如果属性是引用类型,那么拷贝的是这个引用类型的浅拷贝,深拷贝的作用是创建对象完全的副本,互不影响! 其中浅拷贝需要考虑的东西相对比较简单,只需要判断一下类型即可;但是深度拷贝还需要考虑循环引用等情况

const isObject = (target) => {
  return target !== null && typeof target === "object";
};

// 浅拷贝
const shallowCopy = (original) => {
  /**
   * 判断类型
   *  基础类型 > 直接返回
   *  引用类型 > copy
   *  */
  if (!isObject(original)) return original;
  const result = Array.isArray(original) ? [] : {};
  for (const key in original) {
    if (original.hasOwnProperty(key)) {
      result[key] = original[key];
    }
  }
  return result;
};


// 深拷贝
const deepCopy = (original) => {
  // 用一个缓存来解决循环依赖的问题
  const map = new Map();
  const nestedCopy = () => {
    if (!isObject(original)) return original;
    const result = Array.isArray(original) ? [] : {};
    // 设置缓存
    map.set(original, result);
    for (const key in original) {
      if (original.hasOwnProperty(key)) {
        const val = original[key];
        if (isObject(val)) {
          // TODO 还需要判断原生对象类型,比如Date,你需要new一个Date;这里就不实现了
          if (map.has(val)) {
            const cacheVal = map.get(val);
            result[key] = cacheVal;
          } else {
            const copyVal = nestedCopy(val);
            result[key] = copyVal;
          }
        } else {
          result[key] = original[key];
        }
      }
    }
    return result;
  };
  const result = nestedCopy();
  // 置空缓存
  map.clear();
  return result;
};

apply,call,bind

实现其中之一即可,bind 由于需要返回一个函数,并且会缓存参数,所以稍微复杂一点点

// 我们先准备一个对象以及一个函数
const _t = {
  name: 'panda'
}
function sayName(age, sex) {
  console.log(this.name)
  console.log(age)
  console.log(sex)
}
Function.prototype.myApply = function (context, args) {
  var t_obj = context || window;
  // 将fn作为对象的一个属性
  t_obj.fn = this;
  // 然后从这个对象去访问这个方法并执行
  const result = t_obj.fn(...args)
  delete t_obj.fn;
  // 别忘记返回函数返回的返回值
  return result;
};

Function.prototype.myBind = function(context, ...args) {
  const self = this;
  return function(...innerArgs) {
    return self.apply(context, args.concat(innerArgs));
  }
};

数组去重

// 第一种 利用set的特性
const duplicateRemoval=(arr)=>{
  // 另外一种写法:Array.from(new Set(arr))
 return [...new Set(arr)]
}
// 第二种 双重循环,最笨方法(for+findIndex,for+find,filter+indexOf,for+includes是一样的原理)
const duplicateRemoval=(arr)=>{
  const resultArr=[]
  for(let i=0;i<arr.length;i++){
    const node=arr[i]
    let flag=false
    for(let j=0;j<resultArr.length;j++){
      const node2=resultArr[j]
      if(node===node2){
        flag=true
        break;
      }
    }
    if(!flag)resultArr.push(node)
  }
  return resultArr
}
// for + object(也可以用map)
const duplicateRemoval =(arr)=>{
  // 利用对象属性名不能重复这一特点
  const resultArr = []
  const obj = {}
  for(let i = 0;i<arr.length;i++){
    if (!obj[arr[i]]) {
      resultArr.push(arr[i])
      obj[arr[i]] = 1
    } else {
      obj[arr[i]] ++
    }
  };
  return resultArr
}
// 利用reduce
function duplicateRemoval (arr) {
  let resultArr = []
  return  arr.reduce((prev, next,index, arr) => {
    // 如果包含,就返回原数据,不包含,就把新数据追加进去
    return newArr.includes(next) ? newArr :  newArr.push(next)
  }, 0)
}

数组打平,多维数组转换为一维数组

// 原生已经实现了
const expandArray=(array)=>{
 return array.flat(Infinity);
}
// 递归实现,可设置层级
const expandArray = (array, level = Infinity) => {
  const resultArr = []
  let dep = 0
  const run = (arr) => {
    // 每当需要递归调用说明层级+1了
    dep++
    arr.forEach((node) => {
      if (Array.isArray(node) && dep < level) {
        run(node)
      } else {
        resultArr.push(node)
      }
    })
    // 一轮执行完毕,应该将层级减去1,恢复到上一层级
    dep--
  }
  run(array)
  return resultArr
}

instanceof

这个操作符用来判断该对象是否在某一条原型链上,通常用来判断是不是某个对象的子集

const myInstanceof = (obj, { prototype }) => {
  let p = Object.getPrototypeOf(obj)
  while (true) {
    if (p === null) {
      return false
    } else if (p === prototype) {
      return true
    }
    p = Object.getPrototypeOf(p)
  }
}

解析 url 参数

这块熟悉正则的话应该能玩的 6 一点

const getUrlParams = (url) => {
  const params = {}
  const paramsString = url.split('?')[1]
  const kvArray = paramsString.split('&')
  kvArray.forEach((kv) => {
    const lr = kv.split('=')
    params[lr[0]] = lr[1]
  })
  return params
}

// 使用 URLSearchParams API 实现解析 URL 参数
const getUrlParams = (url) => {
  const searchParams = new URLSearchParams(url.split('?')[1]);
  const params = {};
  for (const [key, value] of searchParams) {
    params[key] = value;
  }
  return params;
}

Object.create

该函数用来创建一个干净的对象

function createObj(obj){
  function Fn(){}
  Fn.prototype = obj
  return new Fn()
}

实现简易 hash 路由系统

class myRoute {
  constructor() {
    // 路由映射关系
    this.routes = {}
    // 当前hash值
    this.currentHash = ''
    window.addEventListener('load', this.updateRoute, false)
    window.addEventListener('hashchange', this.updateRoute, false)
  }
  addRoute(path, cb) {
    this.routes[path] = cb || function () { }
  }
  // 更新
  updateRoute = () => {
    this.currentHash = location.hash.slice(1) || '/'
    this.routes[this.currentHash]()
  }
}

节流

通常用来稀释函数执行的次数

const d = (fn, delay) => {
  let timer = null;
  return (...args) => {
    if (timer) return;
    timer = setTimeout(() => {
      fn(...args);
      timer = null;
    }, delay);
  };
};

防抖

在一段时间内只会执行最后一次调用

const t = (fn, delay) => {
  let timer = null;
  return (...args) => {
    timer && clearTimeout(timer);
    timer = setTimeout(() => {
      fn(...args);
    }, delay);
  };
};

当防抖函数遇到先发起的请求响应巨慢的问题(其实这里防不防抖无所谓,主要是前边响应慢的问题)

来源于朋友某次面试题的思考,不废话,直接模拟场景

const t = (fn, delay) => {
  let timer = null;
  return (...args) => {
    timer && clearTimeout(timer);
    timer = setTimeout(() => {
      fn(...args);
    }, delay);
  };
};

const req = () => {
  // 请求发起时间戳
  const now = Date.now();
  setTimeout(
    () => {
      console.log(`请求响应了,本次预计返回的数据:${now}`);
      // 假设返回的就是时间戳
      data.count = now;
    },
    // 模拟第一次请求慢一点,第二次立即返回
    i === 1 ? 2000 : 0
  );
};

const data = {
  count: 0,
};

// 防抖函数,间隔500
const reqT = t(req, 500);
// 分别发送两次请求,必须满足防抖下执行两次的时间间隔
let i = 0;
const timer = setInterval(() => {
  i++;
  reqT();
  i === 2 && clearInterval(timer);
}, 700);

setTimeout(
  () => {
    console.log("最终结果:" + data.count);
  },
  // 最短需要700*2+2000才能看到最终结果
  700 * 2 + 2000 + 1
);

通过上边的例子可以得出结果总是得到第一次响应慢的结果,而我们希望得到的最新发起请求的结果;怎么办呢?其实这里是一个典型的过期副作用问题,已在一篇文章中阐述过:https://pandavips.github.io/blogs/2024/guoqidefuzuoyong.html

发布订阅

class EventBus {
  constructor() {
    this._map = new Map();
    this.$on = this.on;
    this.$once = this.once;
    this.$off = this.off;
    this.$emit = this.emit;
  }
  on(event, handler) {
    const eventHandlers = this._map.get(event);
    if (!eventHandlers || eventHandlers.length === 0) {
      this._map.set(event, [handler]);
    } else {
      eventHandlers.push(handler);
    }
  }
  once(event, handler) {
    const fn = (...args) => {
      handler(...args);
      this.off(event, fn);
    };
    this.on(event, fn);
  }
  emit(event, ...args) {
    const eventHandlers = this._map.get(event);
    if (!eventHandlers || eventHandlers.length === 0) return;
    eventHandlers.forEach((node) => {
      node(args);
    });
  }
  off(event, handler) {
    const eventHandlers = this._map.get(event);
    if (!eventHandlers || eventHandlers.length === 0) return;
    this._map.set(
      event,
      eventHandlers.filter((node) => {
        return node !== handler;
      })
    );
  }
}

mini 版本的 vuex

import { inject, reactive } from "vue";
// 注入时候的key,可以做当一个命名空间来理解,用户可以自行传入name做区分
const STORE_KEY = "__store__";
// hooks
function useStore(name = STORE_KEY) {
  return inject(STORE_KEY);
}
// 创建实例
function createStore(options) {
  return new Store(options);
}
class Store {
  constructor(options) {
    this.$options = options;
    this._state = reactive({
      data: options.state,
    });
    this._name = options.name || STORE_KEY;
    this._mutations = options.mutations;
    this._actions = options.actions;
  }
  get state() {
    return this._state.data;
  }
  // 触发mutation
  commit = (type, payload) => {
    const mutation = this._mutations[type];
    mutation && mutation(this.state, payload);
  };
  // 触发action
  dispatch = (type, payload) => {
    const action = this._actions[type];
    action && action(this, payload);
  };
  // 注册的时候,vue会自动调用这个方法
  install(app) {
    // 这里的app就是vue的实例,将状态注入
    app.provide(this._name, this);
  }
}
export { createStore, useStore };

封装一个最大请求数的请求函数

来源于成都博智信息的面试问题,他想要得是你封装一个函数,参数是一堆请求列表,然后可以设置最大同时请求数目,等待所有请求完成后返回这些列表的响应请求

// 模拟请求函数,响应时间随机,抛错随机
const req = (url) => {
  const time = Date.now();
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      Math.random() > 0.5
        ? resolve({
            url,
            time,
            done_time: Date.now(),
          })
        : reject({
            url,
            time,
            done_time: Date.now(),
          });
    }, Math.random() * 3000);
  });
};
/**
 * @param {*} urls 请求列表
 * @param {*} max 同时请求数目
 */
const maxReq = (urls = [], max = 3) => {
  const result = [];
  //请求完成数目,当前操作的索引
  let count = 0,
    index = -1;
  const len = urls.length;
  return new Promise((resolve) => {
    const next = () => {
      const index2 = ++index;
      if (index2 >= len) return;
      const url = urls[index2];
      req(url)
        .then((res) => {
          result[index2] = res;
          next();
        })
        .catch((err) => {
          result[index2] = err;
          next();
        })
        .finally(() => {
          if (++count >= len) resolve(result);
        });
    };
    for (let i = 0; i < (max < len ? max : len); i++) {
      next();
    }
  });
};
const urls = ["url1", "url2", "url3", "url4", "url5", "url6", "url7"];
// test
maxReq(urls).then((res) => {
  console.log(res);
  debugger;
});

其实可以封装一个调度的类,那么代码就可以改为下面的方式:

class Scheduler {
  // 用于判断是否是一个promise,灵感来源于vue源码
  static isPromise(val) {
    return val !== undefined && val !== null && typeof val.then === "function" && typeof val.catch === "function";
  }
  constructor(limit = 2, callback = () => {}) {
    // 任务编号索引
    this.index = -1;
    // 任务队列
    this.queue = [];
    // 完成数目
    this.doneCount = 0;
    // 限制并发数
    this.limit = limit;
    // 结果数组
    this.resultArray = [];
    // 所有请求完成之后的回调
    this.callback = callback;
  }
  // 添加任务
  add = (task) => {
    this.index++;
    if (Scheduler.isPromise(task)) {
      console.log("任务是promise类型");
      this.queue.push(this.decoratTask(task));
    } else if (typeof task === "function") {
      console.warn("你似乎传入的不是一个promise,这种情况请查看下使用方式.");
      // 这里需要遵循一定的规范去使用,需要提前告知使用者,实际上还不如强制告诉使用者必须传入promise
      this.queue.push(
        this.decoratTask(
          new Promise((resolve, reject) => {
            task(resolve, reject);
          })
        )
      );
    }
  };
  // 开始任务
  start = () => {
    const count = this.limit > this.queue.length ? this.queue.length : this.limit;
    for (let i = 0; i < count; i++) {
      this.next();
    }
  };
  // 下一个
  next = () => {
    if (this.queue.length) {
      const task = this.queue.shift();
      this.run(task);
    }
  };
  // 装饰任务对象,其实这里的作用是为了保存结果数组的有序性
  decoratTask = (task) => {
    return {
      index: this.index,
      p: task,
    };
  };
  // 执行单次任务
  run = (task) => {
    console.log("run");
    const { index, p } = task;
    p.then((res) => {
      this.resultArray[index] = res;
    })
      .catch((err) => {
        console.log(err);
        this.resultArray[index] = err;
      })
      .finally(() => {
        console.log("will next");
        this.doneCount++;
        if (this.index + 1 === this.doneCount) {
          this.callback(this.resultArray);
        } else {
          this.next();
        }
      });
  };
}
const req = (url) => {
  const time = Date.now();
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        url,
        time,
        done_time: Date.now(),
      });
    }, Math.random() * 3000);
  });
};
const scheduler = new Scheduler(99, (res) => {
  console.log(res);
});
const urls = ["url1", "url2", "url3", "url4", "url5", "url6", "url7"];
urls.forEach((url) => {
  scheduler.add(req(url));
});
scheduler.start();

封装一个管道函数(上海通办服务面试题)

实现一个高阶函数pipe,让传递的函数依次执行,上一个函数的返回值是下一个函数的参数;形如这样调用:pipe(fn,fn,fn,fn,fn)(params)

const pipe = (...fns) => {
  return (params) => {
    let result = params;
    [...fns].map((fn) => {
      result = fn(result);
    });
    return result;
  };
};
const res = pipe(
  (a) => {
    return a + 1;
  },
  (a) => {
    return a + 2;
  }
)(0);
console.log(res); //3

如果函数内有异步行为的话,那么返回的结果也会是一个 promise,稍加改造即可。

手写一个符合 promiseA+规范的 Promise

关于如何手写一个符合 PromiseA+ 规范的 Promise 实现,我已经在另一篇文章中详细讨论过了。你可以在以下链接中找到完整的实现和解释:

手写一个符合 PromiseA+ 规范的 Promiseopen in new window

这篇文章深入探讨了 Promise 的内部工作原理,并提供了一个完整的、符合规范的实现。我建议你查看该文章以获取更详细的信息和代码示例。

Last Updated 2024-10-08 04:04:14