高频前端笔试题,带你直通面试——手写系列

1,265 阅读5分钟

一、Promise场景题

01. 交通灯

function red() {
  console.log('red')
}

function green() {
  console.log('green')
}

function yellow() {
  console.log('yellow')
}

const task = (timer, light) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (light === 'red') {
        red()
      } else if (light === 'green') {
        green()
      } else if (light === 'yellow') {
        yellow()
      }
      resolve()
    }, timer)
  })
}

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

taskRunner()

02. 封装异步的fetch,使用async await方式来使用

async function myFetchAsync(url, options) {
  try {
    //等 获取到数据
    const response = await fetch(url, options);
    if (!response.ok) {
      throw new Error(`${response.status} ${response.statusText}`);
    }
    return response;
  } catch (error) {
    console.error(error);
    throw error;
  }
}

03. repeat(console.log, 5, 1000)

const repeat = (cb, delay = 1000, times = 5) => {
  /* 高阶函数 */
  return (text) => {
    /* 封装为 promise */
    const asyncFn = () => {
      return new Promise((resolve) => {
        setTimeout(() => {
          cb(text);
          resolve();
        }, delay);
      })
    }

    /* 执行串:Promise.resolve().then(()=>a()).then(()=>b()) */
    new Array(times).fill(asyncFn).reduce((pre, cur) => {
      return pre.then(() => cur());
    }, Promise.resolve())
  }
}

const mockLog = repeat(console.log);

mockLog("Hello world!!")

04. 封装一个工具函数输入一个promiseA返回一个promiseB如果超过1s没返回则抛出异常如果正常则输出正确的值

function timeoutPromise(promise, timeout) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      reject(new Error(`Promise timed out after ${timeout} ms`));
    }, timeout);

    promise
      .then((result) => {
        clearTimeout(timer);
        resolve(result);
      })
      .catch((error) => {
        clearTimeout(timer);
        reject(error);
      });
  });
}

05. 请求5s未完成就终止

// 方式一:
const funWait = (call, timeout = 5000) => {
  let wait = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('请求超时')
    }, timeout)
  })
  return Promise.race([call(), wait])
}
const t = () => new Promise(resolve => setTimeout(resolve, 4000))
funWait(t).then(res => {
  console.log("t1 resolve")
}).catch(err => {
  console.log("t1 timeout")
})

// 方式二:
const abort = new AbortController();
let res = null;
fetch(url, {
  signal: abort.signal
}).then(_res => {
  res = _res;
})
​
setTimeout(() => {
  if (!res) abort.abort();
}, 5000);

06. 每隔一秒打印1,2,3,4,5

// 1. let 块级作用域
for (let i = 1; i <= 5; i++){
  setTimeout(()=>console.log(i),i*1000)
}
// 2. var+闭包 ---- setTimeout传参
for (var i = 1; i <= 5; i++){
  setTimeout((i)=>console.log(i),i*1000,i)
}
// 3. var+闭包 ---- 闭包传参
for (var i = 1; i <= 5; i++) {
  (function (j) {
    setTimeout(() => {
      console.log(j);
    }, j * 1000);
  })(i);
}
// 4. Promise
function delay(i) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(i);
      resolve();
    }, 1000);
  });
}
async function generate() {
  for (let i = 1; i <= 5; i++) {
    await delay(i);
  }
}
generate();

07. 使用Promise封装AJAX请求

function ajaxPromise(url, method, data) {
  return new Promise((resolve, reject) => {
    // 1.创建XMLHttpRequest对象
    const xhr = new XMLHttpRequest();

    // 2.与服务器建立连接
    xhr.open(method, url, true);

    // 3.给服务器发送数据
    xhr.send(method === "POST" && data ? JSON.stringify(data) : null);

    // 4.接收请求
    xhr.addEventListener("readystatechange", function () {
      if (this.readyState === 4) {
        if (this.status >= 200 && this.status < 300) {
          resolve(JSON.parse(this.responseText));
        } else {
          reject(this.status);
        }
      }
    });
  });
}

08. 处理并发请求

/**
 * 并发请求
 * @param {string[]} urls 待请求的url数组
 * @param {number} maxNum 最大并发数
 */
function concurRequest(urls, maxNum) {
  return new Promise(reslove => {
    if (urls.length === 0) {
      reslove([]);
      return;
    }

    let count = 0; // 记录完成的数量
    let index = 0; // 下一个请求的下标
    let res = []; // 存放结果
    // 请求方法,每调用一次,就从urls从取出一个地址发送请求
    async function request() {
      if (index === urls.length) {
        return;
      }
      const i = index; // 记录当前请求的下标,保证输出顺序
      const url = urls[index++];
      try {
        let resp = await fetch(url);
        res[i] = resp;
      } catch (err) {
        res[i] = err;
      } finally {
        if (++count === urls.length) {
          reslove(res);
        }
        request();
      }
    }
    const times = Math.min(maxNum, urls.length);
    for (let i = 0; i < times; i++) {
      request();
    }
  });
}

09. 实现有并行限制的 Promise 调度器

// Scheduler调度器:
class Scheduler {
  constructor(max) {
    // 最大可并发任务数
    this.max = max;
    // 当前并发任务数
    this.count = 0;
    // 阻塞的任务队列
    this.queue = [];
  }
​
  async add(fn) {
    if (this.count >= this.max) {
      // 若当前正在执行的任务,达到最大容量max
      // 阻塞在此处,等待前面的任务执行完毕后将resolve弹出并执行
      await new Promise(resolve => this.queue.push(resolve));
    }
    // 当前并发任务数++
    this.count++;
    // 使用await执行此函数
    const res = await fn();
    // 执行完毕,当前并发任务数--
    this.count--;
    // 若队列中有值,将其resolve弹出,并执行
    // 以便阻塞的任务,可以正常执行
    this.queue.length && this.queue.shift()();
    // 返回函数执行的结果
    return res;
  }
}
​
// 使用
// 延迟函数
const sleep = time => new Promise(resolve => setTimeout(resolve, time));
​
// 同时进行的任务最多2个
const scheduler = new Scheduler(2);
​
// 添加异步任务
// time: 任务执行的时间
// val: 参数
const addTask = (time, val) => {
  scheduler.add(() => {
    return sleep(time).then(() => console.log(val));
  });
};
​
addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
// 2
// 3
// 1
// 4

10. Promise.retry 超时重新请求,并在重试一定次数依然失败时输出缓存内容

/**
 * 超时重新请求,并在重试一定次数依然失败时输出缓存内容
 * @param {*} promiseFactory 一个返回 Promise 的函数,表示要执行的异步操作。
 * @param {*} maxRetries 一个整数,表示最大的重试次数。
 * @param {*} timeout 一个整数,表示每次重试的间隔时间(单位为毫秒)。
 * @param {*} cache 一个可选的参数,表示缓存的内容,如果重试多次依然失败,则会返回该缓存内容。
 * @returns promise
 */
function retry(promiseFactory, maxRetries, timeout, cache=null) {
  return new Promise((resolve, reject) => {
    let retries = 0;
    const executePromise = () => {
      promiseFactory()
        .then(resolve)
        .catch((error) => {
          retries++;
          if (retries >= maxRetries) {
            if (cache) {
              resolve(cache);
            } else {
              reject(error);
            }
          } else {
            setTimeout(executePromise, timeout);
          }
        });
    };
    executePromise();
  });
}

二、实现 Promise 的一系列方法

01. Promise.all

Promise.all() 静态方法接受一个 Promise 可迭代对象作为输入,并返回一个 Promise。当所有输入的 Promise 都被兑现时,返回的 Promise 也将被兑现(即使传入的是一个空的可迭代对象),并返回一个包含所有兑现值的数组;如果输入的任何 Promise 被拒绝,则返回的 Promise 将被拒绝,并带有第一个被拒绝的原因。

Promise._all = (iterObj) => {
    if(!(typeof iterObj === "object" && iterObj !== null && typeof iterObj[Symbol.iterator] === "function")){
        throw new TypeError(`${iterObj} is not iterable`);
    }
    iterObj = [...iterObj];
    return new Promise((resolve, reject) => {
        const len = iterObj.length;
        let count = 0;
        if(len === 0) return resolve([]);
​
        const res = new Array(len);
        iterObj.forEach(async (item, index) => {
            const newItem = Promise.resolve(item);
            try{
                const result = await newItem;
                res[index] = result;
                if(++count === len){
                    resolve(res)
                }
            }catch(err){
                reject(err);
            }
        })
    })
}

02. Promise.prototype.catch

Promise 实例的 catch() 方法用于注册一个在 promise 被拒绝时调用的函数,它会立即返回一个等效的 Promise 对象,这可以允许你链式调用其他 promise 的方法。

此方法是 Promise.prototype.then(undefined, onRejected) 的一种简写形式。

 /**
   * 本质就是then,只是少传了一个onFulfilled
   * 所以仅处理失败的场景
   * @param {*} onRejected
   */
Promise.prototype.catch = function(onRejected) {
    return this.then(null, onRejected);
}

03. Promise.race

Promise.race() 静态方法接受一个 promise 可迭代对象作为输入,并返回一个 Promise,这个返回的 promise 会随着第一个 promise 的敲定而敲定。

Promise._race = function (promises) {
  if(!(typeof promises === "object" && promises !== null && typeof promises[Symbol.iterator] === "function")){
        throw new TypeError(`${promises} is not iterable`);
    }
  promises = [...promises];
  return new Promise((resolve, reject) => {
    for (let p of promises) {
      Promise.resolve(p).then(resolve).catch(reject);
    }
  });
};

04. Promise.allSettled

Promise.allSettled() 静态方法将一个 Promise 可迭代对象作为输入,并返回一个单独的 Promise。当所有输入的 Promise 都已敲定时(包括传入空的可迭代对象时),返回的 Promise 将被兑现,并带有描述每个 Promise 结果的对象数组。

Promise.prototype.mySettled = function (promises) {
  return new Promise((resolve) => {
    const data = [],
      len = promises.length;
    let cnt = 0;
    for (let i = 0; i < len; i++) {
      const promise = promises[i];
      Promise.resolve(promise)
        .then(
          (res) => {
            data[i] = { status: "fulfilled", value: res };
          },
          (error) => {
            data[i] = { status: "rejected", reason: error };
          }
        )
        .finally(() => {
          if (cnt === len) resolve(data);
        });
    }
  });
};

05. Promise.reject

  Promise._reject = function(reason) {
    return new MyPromise((resolve, reject) => {
      reject(reason);
    });
  }

06. Promise.resolve

Promise.resolve() 静态方法将给定的值转换为一个 Promise,如果该值本身就是一个 Promise,那么该 Promise 将被返回;如果该值是一个 thenable 对象,Promise.resolve() 将调用其 then() 方法及其两个回调函数;否则,返回的 Promise 将会以该值兑现。

该函数将嵌套的类 Promise 对象(例如,一个将被兑现为另一个 Promise 对象的 Promise 对象)展平,转化为单个 Promise 对象,其兑现值为一个非 thenable 值。

Promise._resolve = function(value) {
  // 如果 value 已经是 Promise 对象,则直接返回该 Promise 对象
  if (value && value instanceof Promise) {
    return value;
  }
  // 如果 value 是 thenable 对象,则包装成 Promise 对象并返回
  if (value && typeof value.then === 'function') {
    return new Promise(function(resolve, reject) {
      value.then(resolve, reject);
    });
  }
  // 将传入的值作为 Promise 的成功值,并返回 Promise 对象
  return new Promise(function(resolve) {
    resolve(value);
  });
}

07. Promise.prototype.finally

Promise 实例的 finally() 方法用于注册一个在 promise 敲定(兑现或拒绝)时调用的函数。它会立即返回一个等效的 Promise 对象,这可以允许你链式调用其他 promise 方法;这可以让你避免在 promise 的 then()catch() 处理器中重复编写代码。

/**
   * 无论成功还是失败都会执行回调
   * @param {Function} onSettled
   */
 Promise.prototype.finally = function (onSettled) {
    return this.then(
      (data) => {
        onSettled(); // 实现了收不到参数了
        return data;
      },
      (reason) => {
        onSettled();
        throw reason;
      }
    );
    // finally函数 返回结果应该是无效的
  }

三、数组原型上的方法

01. Array.prototype.push

let arr = [];
Array.prototype.push = function() {
    for( let i = 0 ; i < arguments.length ; i++){
        this[this.length] = arguments[i] ;
    }
    return this.length;
}

02. Array.prototype.map

Array.prototype._map = function (cb, thisBinding) {
  // 排除回调非函数情况
  if (typeof cb !== "function") {
    throw new TypeError(`${cb} is not a function`);
  }
  // 排除this为非可迭代对象情况
  if (this == null || typeof this[Symbol.iterator] !== "function") {
    throw new TypeError(`${this} is not a iterable`);
  }
  // 将可迭代对象转换成数组
  const array = [...this];
  const result = [];
  // 执行遍历并回调
  for (let i = 0; i < array.length; i++) {
    result.push(cb.call(thisBinding, array[i], i, this));
  }
  return result;
};

03. Array.prototype.forEach

Array.prototype._forEach = function (cb, thisBinding = globalThis) {
  // 排除回调非函数情况
  if (typeof cb !== "function") {
    throw new TypeError(`${cb} is not a function`);
  }
  // 排除this为非可迭代对象情况
  if (this == null || typeof this[Symbol.iterator] !== "function") {
    throw new TypeError(`${this} is not a iterable`);
  }
  // 将可迭代对象转换成数组
  const array = [...this];
  // 执行遍历并回调
  for (let i = 0; i < array.length; i++) {
    cb.call(thisBinding, array[i], i, this);
  }
};

04. Array.prototype.reduce

Array.prototype.myReduce = function (callback, ...args) {
  let start = 0,
    pre;
  if (args.length) {
    //有参数的话pre等于参数第0项
    pre = args[0];
  } else {
    //没参数的话,默认从数组0项开始
    pre = this[0];
    start = 1;
  }
  for (let i = start; i < this.length; i++) {
    pre = callback(pre, this[i], i, this);
  }
  return pre;
};

05. Array.prototype.unshift

Array.prototype.myUnshift = function (...items) {
    this.reverse().push(...items.reverse())
    this.reverse()
    return this.length
}

06. Array.prototype.pop

Array.prototype.myPop = function() {
    if(this == undefined){
        throw new TypeError('this is null or not defined');
    }
    if(this.length == undefined){
        this.length = 0;
        return undefined;
    }
    const item = this[this.length-1];
    this.length--;
    if(!Array.isArray(this)){
        delete this[this.length];
    }
    return item;
}

07. Array.prototype.shift

Array.prototype.myShift = function() {
    if(this == undefined){
        throw new TypeError('this is null or not defined');
    }
    if(this.length = undefined){
        this.length = 0;
        return undefined;
    }
    const item = this[0],
          len = this.length;
    for(let i = 0; i < len - 1; i++){
        this[i] = this[i+1];
    }
    this.length--;
    if(!Array.isArray(this)){
        delete this[this.length];
    }
    return item;            
}

四、JavaScript原理题

01. async await 如何实现

function asyncToGenerator(generatorFunc) {
  //传入一个生成器函数
  //返回一个新的函数
  return function () {
    //先调用generator函数生成<迭代器>
    const gen = generatorFunc.apply(this, arguments);
    //返回一个promise
    return new Promise((resolve, reject) => {
      //内部定义一个step函数来源 用来一步步跨过yield的阻碍
      //key有next和throw两种取值,分别对应了gen的next和throw方法
      //arg参数则是用来promise resolve得带的值交给下一个yield
      function step(key, arg) {
        let generatorResult;

        try {
          generatorResult = gen[key](arg);
        } catch (err) {
          return reject(err);
        }
        //gen.next()得到的结果是一个{value,done}的结构
        const { value, done } = generatorResult;
        if (done) {
          //已经完成
          return resolve(value);
        } else {
          return Promise.resolve(
            //对value不是promise的情况包裹一层
            value //这个value对应的是yield后面的promise
          ).then(
            function onResolve(val) {
              step("next", val);
            },
            function onReject(err) {
              step("throw", err);
            }
          );
        }
      }
      step("next"); //第一次调用next
    });
  };
}
function fn(nums) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(nums * 2);
    }, 1000);
  });
}
function* gen() {
  const num1 = yield fn(1);
  console.log(num1); // 2
  const num2 = yield fn(num1);
  console.log(num2); // 4
  const num3 = yield fn(num2);
  console.log(num3); // 8
  return num3;
}
const testGAsync = asyncToGenerator(gen);
// 返回的是一个函数,函数调用返回一个promise
testGAsync().then(res => {
    console.log(res);
});
//对应上面的gen()
async function asyncFn() {
  const num1 = await fn(1);
  console.log(num1); // 2
  const num2 = await fn(num1);
  console.log(num2); // 4
  const num3 = await fn(num2);
  console.log(num3); // 8
  return num3;
}
asyncFn()

02. 手写vue2响应式

let data = {
  name: "hdf",
  age: 19,
  friend: {
    name: "zwl"
  }
};

//变成响应式数据
observer(data);

function observer(target) {
  if (!target || typeof target == "object") {
    return target;
  }

  for (let key in target) {
    defineReactive(target, key, target[key]);
  }
}

function defineReactive(target, key, value) {
  //深度观察
  observer(value);

  Object.defineProperty(target, key),
    {
      get() {
        return value;
      },
      set(newValue) {
        observer(newValue);
        if (newValue !== value) {
          value = newValue;
          console.log("更新视图");
        }
      }
    };
}

03. 手写v-model简易版

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div>
        <input type="text" id="myInput">
        <h1 id="myTitle"></h1>
    </div>
</body>
<script>
    let userinfo = {
        username: '小明',
    };
    //开始监控
    watcher();
    function watcher() {
        Object.defineProperty(userinfo, "username", {
            set(value) {
                updateDom(value);
            },
            get(val) {
                return val;
            },
        });
    }
    //更新dom数据
    function updateDom(value) {
        document.querySelector('#myInput').value = value;
        document.querySelector('#myTitle').innerHTML = value;
    }
    //给input绑定input事件,实时修改username的值
    document.querySelector('#myInput').oninput = function (e) {
        let value = e.target.value;
        userinfo.username = value;
    }
</script>

</html>

04. 手写call apply bind

Function.prototype.myCall = function(ctx, ...args) {
  // 使用globalThis的原因,node环境没有window
  ctx = (ctx === undefined || ctx === null) ? globalThis : Object(ctx)
  // 生成唯一的key,防止污染对象中的属性
  let key = Symbol('temp')
  // this表示调用的函数  比如 fun.call(obj, 2)  this就是fun
  // 原理:在obj中添加一个属性(fun),然后通过obj.fun的形式调用,谁调用this就指向谁,使得fun里的this指向obj
  Object.defineProperty(ctx, key, {
      enumerable: fasle,
      value: this
  })
  // 执行函数,拿到结果
  let result = ctx[key](...args)
  // 删除属性
  delete ctx[key]
  return result
}
​
Function.prototype.myApply = function(ctx, args) {
  ctx = (ctx === undefined || ctx === null) ? globalThis : Object(ctx)
  let key = Symbol('temp')
  Object.defineProperty(ctx, key, {
      enumerable: fasle,
      value: this
  })
  let result = ctx[key](...args)
  delete ctx[key]
  return result
}
​
Function.prototype.myBind = function(context, ...args1) {
  ctx = (ctx === undefined || ctx === null) ? globalThis : Object(ctx)
  let _this = this
  return function(...args2) {
    let key = Symbol('temp')
    Object.defineProperty(ctx, key, {
        enumerable: fasle,
        value: this
    })
    let result = ctx[key](...[...args1, ...args2])
    delete ctx[key]
    return result
  }
}

05. 节流防抖

防抖:在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时

// 简易版
function debounce(func, wait) {
    let timeout;
    return function () {
        const context = this;
        const args = arguments;
        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}
​
// 立即执行版
// 有时希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。
function debounce(func, wait, immediate) {
  let timeout;
  return function () {
    const context = this;
    const args = arguments;
    if (timeout) clearTimeout(timeout);
    if (immediate) {
      const callNow = !timeout;
      timeout = setTimeout(function () {
        timeout = null;
      }, wait)
      if (callNow) func.apply(context, args)
    } else {
      timeout = setTimeout(function () {
        func.apply(context, args)
      }, wait);
    }
  }
}
​
// 返回值版实现
function debounce(func, wait, immediate) {
  let timeout, result;
  return function () {
    const context = this;
    const args = arguments;
    if (timeout) clearTimeout(timeout);
    if (immediate) {
      const callNow = !timeout;
      timeout = setTimeout(function () {
        timeout = null;
      }, wait)
      if (callNow) result = func.apply(context, args)
    }
    else {
      timeout = setTimeout(function () {
        func.apply(context, args)
      }, wait);
    }
    return result;
  }
}

节流:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效

// 使用时间戳实现
function throttle(func, wait) {
  let previous = 0;

  return function () {
    const context = this;
    const args = arguments;
    let now = +new Date();
    if (now - previous > wait) {
      func.apply(context, args);
      previous = now;
    }
  }
}

// 使用定时器实现
function throttle(func, wait) {
  let timeout;
  return function () {
    const context = this;
    const args = arguments;
    if (!timeout) {
      timeout = setTimeout(function () {
        timeout = null;
        func.apply(context, args)
      }, wait)
    }
  }
}

06. 函数柯里化

function add(a, b, c) {
    return a + b + c
}

function curry(fn) {
    let judge = (...args1) => {
        if (args1.length == fn.length) return fn(...args1)
        return (...args2) => judge(...args1, ...args2)
    }
    return judge
}
let addCurry = curry(add)
const res1 = addCurry(1, 2)(3)
const res2 = addCurry(1)(2)(3)

07. 实现深拷贝

  • 浅拷贝只复制一层对象的属性,并不包括对象里面的为引用类型的属性值,因此修改拷贝后的属性值是引用类型的,就会影响源对象
  • 深拷贝就是对对象以及对象的所有子对象进行拷贝
// 方式一  MessageChannel
function deepClone(obj) {
  return new Promise(reslove => {
    const { port1, port2 } = new MessageChannel();
    port1.postMessage(obj);
    port2.onmessage = msg => {
      reslove(msg.data);
    };
  });
}

// 方式二  递归
const _completeDeepClone = (target, map = new WeakMap()) => {
  // 基本数据类型,直接返回
  if (typeof target !== 'object' || target === null) return target
  // 函数 正则 日期 ES6新对象,执行构造题,返回新的对象
  const constructor = target.constructor
  if (/^(Function|RegExp|Date|Map|Set)$/i.test(constructor.name)) return new constructor(target)
  // map标记每一个出现过的属性,避免循环引用
  if (map.get(target)) return map.get(target)
  let cloneTarget = Array.isArray(target) ? [] : {}
  map.set(target, cloneTarget)
  for (prop in target) {
    if (target.hasOwnProperty(prop)) {
      cloneTarget[prop] = _completeDeepClone(target[prop], map)
    }
  }
  return cloneTarget
}

08. 模拟一个微任务

function asyncRun(func) {
  if (typeof Promise !== 'undefined') {
    Promise.resolve().then(func);
  } else if (typeof MutationObserver !== 'undefined') {
    const ob = new MutationObserver(func);
    const textNode = document.createTextNode('0');
    ob.observe(textNode, {
      characterData: true,
    });
    textNode.data = '1';
  } else {
    setTimeout(func);
  }
}

09. 实现发布订阅模式

class EventEmitter {
  constructor() {
    // key: 事件名
    // value: callback [] 回调数组
    this.events = {}
  }
  on(name, callback) {
    if (this.events[name]) {
      this.events[name].push(callback)
    } else {
      this.events[name] = [callback]
    }
  }
  off(name, callback) {
    if (!this.events[name]) return;
    if (!callback) {
      // 如果没有callback,就删掉整个事件
      this.events[name] = undefined;
    }
    this.events[name] = this.events[name].filter((item) => item !== callback);
​
  }
  emit(name, ...args) {
    if (!this.events[name]) return
    this.events[name].forEach(cb => cb(...args))
  }
}

10. 实现 instanceof 方法

function myInstanceof(left, right) {
  let proto = Object.getPrototypeOf(left), // 获取对象的原型
      prototype = right.prototype; // 获取构造函数的 prototype 对象
  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
}

11. 实现 new 操作符

function myNew(constructorFn, ...args) {
    let newObj = {}
    newObj.__proto__ = constructorFn.prototype;
    // newObj = Object.create(constructor.prototype);
    let result = constructorFn.apply(newObj, args)
    return result instanceof Object ? result : newObj
}

12. 树形结构转成列表

const data = [
    {
        id: 1,
        text: '节点1',
        parentId: 0,
        children: [
            {
                id: 2,
                text: '节点1_1',
                parentId: 1
            }
        ]
    }
]
function treeToList(data) {
    let res = [];
    const dfs = (tree) => {
        tree.forEach((item) => {
            if (item.children) {
                dfs(item.children);
                delete item.children;
            }
            res.push(item);
        });
    };
    dfs(data);
    return res;
}

13. 列表转成树形结构

let arr = [
    { id: 1, name: '部门1', pid: 0 },
    { id: 2, name: '部门2', pid: 1 },
    { id: 3, name: '部门3', pid: 1 },
    { id: 4, name: '部门4', pid: 3 },
    { id: 5, name: '部门5', pid: 4 },
    { id: 6, name: '部门6', pid: 0 },
]
function getTreeList(rootList, pid, list) {
    for (const item of rootList) {
        if (item.pid === pid) {
            list.push(item);
        }
    }
    for (const i of list) {
        i.children = [];
        getTreeList(rootList, i.id, i.children);
        if (i.children.length === 0) {
            delete i.children;
        }
    }
    return list;
}
const res = getTreeList(arr, 0, []);

14. 扁平化数组 || Array.prototype._flat

(1)递归实现

const arr = [1, [2, [3, [4, [5, [6, [7, [8, [9], 10], 11], 12], 13], 14], 15], 16]]
Array.prototype.flat = function (deep = 1) {
    let res = []
    // 完全展开
    if (deep === 'Infinity') {
        this.forEach((item) => {
            if (Array.isArray(item)) {
                res = res.concat(item.flat());
            } else {
                res.push(item)
            }
        })
    } else {
        // 展开指定层数
        deep--
        this.forEach(item => {
            if (Array.isArray(item) && deep >= 0) {
                res = res.concat(item.flat(deep))
            } else {
                res.push(item)
            }
        })
​
        return res
    }
​
}
console.log('展开一层', arr.flat(1))
console.log('完全展开', arr.flat(Infinity))

(2)reduce 函数

const arr = [1, 2, [3, 4, [5, 6]]] 
Array.prototype._flat = function (depth = Infinity) {
  --depth
  return this.reduce((prev, cur) => {
      if(Array.isArray(cur) && depth >= 0) {
          prev = prev.concat(cur._flat(depth))
      } else {
          prev.push(cur)
      }
      return prev
  }, [])
}
​
console.log(arr._flat())
console.log(arr._flat(1))

(3)扩展运算符实现

let arr = [1, [2, [3, 4]]];
function flatten(arr) {
    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}

(4)split 和 toString

let arr = [1, [2, [3, 4]]];
function flatten(arr) {
    return arr.toString().split(',');
}

16. 实现 lodash 的_.get

在 js 中经常会出现嵌套调用这种情况,如 a.b.c.d.e,但是这么写很容易抛出异常。你需要这么写 a && a.b && a.b.c && a.b.c.d && a.b.c.d.e,但是显得有些啰嗦与冗长了。特别是在 graphql 中,这种嵌套调用更是难以避免。 这时就需要一个 get 函数,使用 get(a, 'b.c.d.e') 简单清晰,并且容错性提高了很多。

function get(source, path, defaultValue = undefined) {
  // a[3].b -> a.3.b -> [a,3,b]
  // path 中也可能是数组的路径,全部转化成 . 运算符并组成数组
  const paths = path.replace(/[(\d+)]/g, ".$1").split(".");
  let result = source;
  for (const p of paths) {
    // 注意 null 与 undefined 取属性会报错,所以使用 Object 包装一下。
    result = Object(result)[p];
    if (result == undefined) {
      return defaultValue;
    }
  }
  return result;
}

五、场景题大全

01. 交换a,b的值,不能用临时变量

a = a + b
b = a - b
a = a - b

02. 实现数组的乱序输出

洗牌算法

  • 取出数组的第一个元素,随机产生一个索引值,将该第一个元素和这个索引对应的元素进行交换。
  • 第二次取出数据数组第二个元素,随机产生一个除了索引为1的之外的索引值,并将第二个元素与该索引值对应的元素进行交换
  • 按照上面的规律执行,直到遍历完成
function shuffleSelf(array, size) {
    let index = -1,
        length = array.length,
        lastIndex = length - 1
 
    size = size === undefined ? length : size
    while (++index < size) {
        let rand = index + Math.floor( Math.random() * (lastIndex - index + 1))  //含最大值,含最小值
        value = array[rand]
        array[rand] = array[index]
        array[index] = value
    }
    array.length = size
    return array
}

03. 实现数组元素求和

(1)for 循环

function sum(arr) {
  let s = 0
  for (let i = 0; i < arr.length; i++) {
    s += arr[i]
  }
  return s
}

(2)forEach 遍历

function sum(arr) {
  let s = 0
  arr.forEach(function(val, idx, arr) {
    s += val
  }, 0)
  return s
}

(3)eval 函数

function sum(arr) {
  return eval(arr.join("+"));
}

(4)reduce 方法

let sum = arr.reduce( (total,i) => total += i,0)

(5)递归实现

function add(arr) {
    if (arr.length == 1) return arr[0] 
    return arr[0] + add(arr.slice(1)) 
}

05. 实现数组去重

(1)Set

const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
Array.from(new Set(array)); // [1, 2, 3, 5, 9, 8]

(2)filter() + indexOf

let arr = ['apple','apps','pear','apple','orange','apps']
let newArr = arr.filter(function(item,index){
    return arr.indexOf(item) === index  // 因为indexOf 只能查找到第一个  
})

(3)indexOf

let arr = [1,9,8,8,7,2,5,3,3,3,2,3,1,4,5,444,55,22]
function noRepeat(arr) {
        let newArr=[]
        for(let i=0;i<arr.length;i++) {
          if(newArr.indexOf(arr[i]) === -1) {
             newArr.push(arr[i])
          }
        }
    return newArr
}

(4)includes

let arr = [1,9,8,8,7,2,5,3,3,3,2,3,1,4,5,444,55,22]
function noRepeat(arr) {
      let newArr = []
      for(let i = 0; i<arr.length; i++){
        if(!newArr.includes(arr[i])){
            newArr.push(arr[i])
        }
      }
    return newArr
}

(5)Map

let arr = ['blue', 'green', 'blue', 'yellow', 'black', 'yellow', 'blue', 'green']
let unique = (arr)=> {
    let seen = new Map()
    return arr.filter((item) => {
        return !seen.has(item) && seen.set(item,1)
    })
}

06. 实现字符串翻转

const reverse = (a) => {
    return a.split("").reverse().join("");
}

07. 用proxy 实现 arr[-1] 的访问

let arr= [1,2,3,4]
let proxy = new Proxy(arr,{
  get(target,key){
    if(key<0){
      return target[target.length+parseInt(key)]
    }
    return target[key] 
  }
})

08. 实现一个 sleep() 函数

// 1. Promise
const sleep = time => {
  return new Promise(resolve => setTimeout(resolve,time))
}
sleep(1000).then(()=>{
  console.log(1)
})

// 2. Generator
function* sleepGenerator(time) {
  yield new Promise(function(resolve,reject){
    setTimeout(resolve,time);
  })
}
sleepGenerator(1000).next().value.then(()=>{console.log(1)})

// 3. async
function sleep(time) {
  return new Promise(resolve => setTimeout(resolve,time))
}
async function output() {
  let out = await sleep(1000);
  console.log(1);
  return out;
}
output();

// 4. ES5
function sleep(callback,time) {
  if(typeof callback === 'function')
    setTimeout(callback,time)
}

function output(){
  console.log(1);
}
sleep(output,1000);

09. 原始 list 转换成树形结构

// 原始 list 如下
let list =[
    {id:1,name:'部门A',parentId:0},
    {id:2,name:'部门B',parentId:0},
    {id:3,name:'部门C',parentId:1},
    {id:4,name:'部门D',parentId:1},
    {id:5,name:'部门E',parentId:2},
    {id:6,name:'部门F',parentId:3},
    {id:7,name:'部门G',parentId:2},
    {id:8,name:'部门H',parentId:4}
];
const result = convert(list, ...);

// 转换后的结果如下
let result = [
    {
      id: 1,
      name: '部门A',
      parentId: 0,
      children: [
        {
          id: 3,
          name: '部门C',
          parentId: 1,
          children: [
            {
              id: 6,
              name: '部门F',
              parentId: 3
            }, {
              id: 16,
              name: '部门L',
              parentId: 3
            }
          ]
        },
        {
          id: 4,
          name: '部门D',
          parentId: 1,
          children: [
            {
              id: 8,
              name: '部门H',
              parentId: 4
            }
          ]
        }
      ]
    },
  ···
];

const convert = (list) => {
    const res = []
    // 将list中的每一项放到一个新的对象当中
    const map = list.reduce((res, v) => (res[v.id] = v, res), {})
    for (const item of list) {
        if (item.parentId === 0) {
            res.push(item)
            continue
        }
        if (item.parentId in map) {
            const parent = map[item.parentId]
            parent.children = parent.children || []
            parent.children.push(item)
        }
    }
    return res
}

10. 千分位转化

function regexHandleNum(num) {
  return String(num).replace(/\B(?=(\d{3})+(?!\d))/g, ','); // 3是千分位,4是万分位
}

function handleNum(num) {
  return String(num)
    .split('')
    .reverse()
    .reduce((prev, next, index) => {
      return (index % 3 ? next : next + ',') + prev; // 3是千分位,4是万分位
    });
}

11. 实现 (5).add(3).minus(2) 功能

Number.prototype.add = function (number) {
    if (typeof number !== 'number') {
        throw new Error('请输入数字~');
    }
    return this + number;
};
Number.prototype.minus = function (number) {
    if (typeof number !== 'number') {
        throw new Error('请输入数字~');
    }
    return this - number;
};
console.log((5).add(3).minus(2));

12. 按字母顺序合并两个数组

请把两个数组 [A1, A2, B1, B2, C1, C2, D1, D2] 和 [A, B, C, D],合并为 [A1, A2, A, B1, B2, B, C1, C2, C, D1, D2, D]。

const arr1 = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2']
const arr2 = ['A', 'B', 'C', 'D']
const ret = []
let tmp = arr2[0]
let j = 0
for (let i=0;i<arr1.length;i++) {
  if (tmp === arr1[i].charAt(0)){
    ret.push(arr1[i])
  }else {
    ret.push(tmp)
    ret.push(arr1[i])
    tmp=arr2[++j]
  }
   if(i===arr1.length-1){
      ret.push(tmp)
    }
}
console.log(ret)

13. 转驼峰命名法

let str = 'get-element-by-id'
const transFrom = (str) => {
    let arr = str.split('-')
    for (let i = 1; i < arr.length; i++) {
        arr[i] = arr[i][0].toUpperCase() + arr[i].substring(1, arr[i].length)
    }
    return arr.join('')
}

14. URL参数解析为对象

function getURLParams(url) {
    let params = {};
    url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(match, key, value) {
        key = decodeURIComponent(key);
        value = decodeURIComponent(value);
       if (obj[key]) {
          if(Array.isArray(params[key])) {
            params[key].push(value);
          } else {
            params[key] = [params[key], value];
          }
      } else {
        params[key] = value;
      }
    });
    return params;
}
​
let url = "http://example.com/?param1=abc&param2=def";
console.log(getURLParams(url));  // 输出: { param1: 'abc', param2: 'def' }

15. 输出页面所有标签名称

获取当前页面中所有 HTML tag 的 名字,以数组形式输出, 重复的标签不重复输出(不考虑 iframe 和 shadowDOM)

1. 借助API
function getAllHTMLTags() {
    let set = new Set()
    const tags = [...window.document.querySelectorAll('*')]
    tags.forEach((dom) => {
        if (!set.has(dom.tagName)) {
            set.add(dom.tagName)
        }
    })
    return [...set]
} 

2. 递归:这里利用了DOM结点的nodeType属性
/*
    如果节点是一个元素节点,nodeType 属性返回 1
    如果节点是属性节点, nodeType 属性返回 2
    如果节点是一个文本节点,nodeType 属性返回 3
    如果节点是一个注释节点,nodeType 属性返回 8
*/
let res = []
function getAllHTMLTags(node) {
    if (node.nodeType === 1) {
        let tagName = node.nodeName
        res.push(tagName)
    }
    let children = node.childNodes;
    for (let i = 0; i < children.length; i++) {
        getAllHTMLTags(children[i])
    }
}
getAllHTMLTags(document)
console.log([...new Set(res)])

16. 实现链式调用

链式调用的核心就在于调用完的方法将自身实例返回

function Class1() {
    console.log('初始化')
}
Class1.prototype.method = function(param) {
    console.log(param)
    return this
}
let cl = new Class1()
cl.method('第一次调用').method('第二次链式调用').method('第三次链式调用')

17. 实现两个超过整数存储范围的大整数相加

// a,b为字符串,返回也为字符串
function sum(a, b) {
    const len = Math.max(a.length, b.length)
    a = a.padStart(len, '0')
    b = b.padStart(len, '0')
    let carry = 0
    let result = ''
    for (let i = len - 1;i >= 0; i--) {
        const sum = +a[i] + +b[i] + carry
        result = (sum % 10) + result
        carry = Math.floor(sum / 10)
    }
    if (carry) {
        result = carry + result
    }
    return result
}

18. 手写 dom 操作,翻转 li 标签,如何处理更优

// (1)拼接html字符串,然后一次性插入ul中
const oUl = document.getElementById('root');
const aLi = Array.from(oUl.getElementsByTagName('li'));
let str = '';
for (let index = aLi.length - 1; index >= 0; index--) {
    str += `<li>${aLi[index].innerHTML}</li>`;
}
oUl.innerHTML = str;

// (2)使用文档片段
// ownerDocument:返回当前节点所在的顶层文档对象
// node.lastChild:返回最后一个子元素
function reverseChildNodes(node = document) {
    const frag = node.ownerDocument.createDocumentFragment();
    while(node.lastChild) {
        // 每次取出最后一个子节点也会将该节点从源节点中移除,并且更新lastChild
        frag.appendChild(node.lastChild);
    }
    // 将文档碎片直接插入到node节点下
    node.appendChild(frag);
}
const oUl = document.getElementById('root');
reverseChildNodes(oUl);

19. 实现输出一个十六进制的随机颜色

function getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

function getColor() {
    let result = new Array(6)
    let i = 0
    let hexMap = ['a', 'b', 'c', 'd', 'e', 'f']
    while (i < 6) {
        let data = getRandomInt(0, 16)
        result[i] = data > 10 ? hexMap[data % 10] : data
        i++
    }
    return `#${result.join('')}`
}

20. 设计一个函数,奇数次执行的时候打印 1,偶数次执行的时候打印 2

function countFn() {
    let count = 0;
    return function (...args) {
        count++;
        if (count & 1 === 1) return console.log(1);
        console.log(2);
    }
}
const testFn = countFn();
testFn(); // 1
testFn(); // 2
testFn(); // 1
testFn(); // 2