笔试题

JS 常用内置函数实现

String.prototype.indexOf

String.prototype.indexOf = function(target, startIndex = 0) {
  if (target === '') return startIndex;
  const str = this;
  strLoop: for (let i = startIndex; i < str.length; i++) {
    if (str[i] === target[0]) {
      for (let j = 1; j < target.length; j++) {
        if (str[i + j] !== target[j]) {
          break strLoop;
        }
      }
      return i;
    }
  }
  return -1;
}

Array.prototype.reduce

Array.prototype.reduce = function(reducer, initial) {
  const arr = this;
  initial = initial === undefined ? arr.shift() : initial;
  let result = initial;
  for (let i = 0; i < arr.length; i++) {
    result = reducer(result, arr[i], i, arr);
  }
  return result;
}

Function.prototype.bind

Function.prototype.bind = function(scope) {
  const fn = this;
  const bindArgs = [].slice.call(arguments, 1);
  return function() {
    const args = [].slice.call(arguments);
    return fn.apply(scope, bindArgs.concat(args));
  };
}

// or

Function.prototype.bind = function(scope, ...bindArgs) {
  return (...args) => this.call(scope, ...bindArgs, ...args);
}

Array.prototype.flat

Array.prototype.flat = function(depth = 1) {
  const arr = this;
  const newArr = [];
  function flat(curArr, curDepth = 0) {
    curArr.forEach(item => {
      if (!(item instanceof Array) || (curDepth >= depth)) {
        newArr.push(item);
        return;
      }
      flat(item, curDepth + 1);
    });
  }
  flat(arr);
  return newArr;
}

数组

数组去重

function uniqueArr(arr) {
  return [...new Set(arr)];
}

function uniqueArr(arr) {
  return arr.filter((num, index) => arr.indexOf(num) === index);
}

function uniqueArr(arr) {
  const map = new Map();
  arr.forEach(data => {
    map.set(data, 1);
  });

  return [...map.keys()];
}
  • 基本 api 运用

  • 如果用 arr.includes,追问复杂度理解、数组的底层存储结构

  • 如果用 obj 的 key 缓存,追问数组元素有不同类型的场景

  • 如果用 map,追问和 obj 的区别

数组打平

const flatten = (arr) => {
  return arr.reduce((result, item) => {
    return Array.isArray(item)
      ? result.concat(...flatten(item))
      : result.concat(item);
  }, []);
};
  • 递归使用

  • 是否数组的判断

对象

对象深拷贝

// 实现对一个复杂对象的深拷贝,可能包含 Date 等数据类型
function deepClone(data) {
  // todo
}
  
// const original = {
//   name: 'Joe',
//   age: 30,
//   info: {
//     hobby: ['coding', 'singing'],
//     birthday: new Date('2000-01-02'),
//   },
// };

// const copied = deepClone(original);
// console.log(copied);
// console.log(copied.info.birthday === original.info.birthday); // false

// 
  • 注意对特殊对象的处理,可能还有 Set、Map 等

  • 注意对 Object 类型的 key 遍历方式,简单 for in 会带上原型上的属性

  • instanceof 和原型相关概念

  • 可追问有循环引用的场景(比如对象引用了自身),可以增加一个 WeakMap 缓存已处理的值,deepClone 优先从缓存取值

function deepClone(data) {
  if (data instanceof Date) {
    return new Date(data);
  }

  if (data instanceof RegExp) {
    return new RegExp(data);
  }

  if (Array.isArray(data)) {
    return data.map(item => deepClone(item));
  }

  if (Object.prototype.toString.call(data) === '[object Object]') {
    const copied = {};
    Object.keys(data).forEach(key => {
      copied[key] = deepClone(data[key]);
    });
    return copied;
  }

  return data;
}

函数

函数防抖

// 实现一个防抖函数,当它在一定时间内被连续触发时,只会执行一次
function debounce(fn, timeout) {
  // todo
}

// const fn = debounce((num) => console.log(num), 1000);
// fn(1);
// fn(2);
// 延迟 1000 毫秒后输出一次 2
  • call/apply 方法的使用

  • this 指向

  • 对闭包的理解

function debounce(fn, timeout) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.call(this, ...args);
    }, timeout);
  }
}

异步编程

异步循环打印

// 要求按顺序每隔 1 秒打印 arr 中的内容,完成打印后调用 callback 函数
function asyncConsole(arr, callback) {
  // todo
}

// asyncConsole([1, 2, 3, 4, 5], () => console.log('done'));
  • setTimout 的使用

  • async/await 结合 Promise 的使用

  • 可追问主线程、事件循环

function asyncConsole(arr, callback) {
  const sleep = (timeout) => new Promise(r => setTimeout(r, timeout));

  return new Promise(async (resolve) => {
    for (const item of arr) {
      await sleep(1000);
      console.log(item);
    }

    resolve(callback());
  });
}

执行顺序判断

console.log(1);

setTimeout(() => {
  Promise.resolve().then(() => {
    console.log(2);
  });
  console.log(3);
}, 0);

new Promise((resolve) => {
  for (let i = 0; i <= 1000; i++) {
    if (i === 1000) {
      resolve();
    }
  }
  console.log(4);
}).then(() => {
  console.log(5);
});

console.log(6);

// 1 4 6 5 3 2

// 第一轮事件循环:[宏任务] 1 4 6 [微任务] 5

// 第二轮事件循环:[宏任务] 3 [微任务] 2

Promise 超时

// 超时实现
const timeLimit = (fn, timeout) => {
  return async (...args) => {
    return Promise.race([
      fn(...args),
      new Promise((_, reject) => {
        setTimeout(reject, timeout, 'Time Limit Exceeded');
      }),
    ]);
  };
};

// 测试用例
(async () => {
  const testFn = async (value, delay = 0) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(value);
      }, delay);
    });
  };

  const limitedFn = timeLimit(testFn, 500);

  const a = await limitedFn('in limit', 100);
  console.log(a);
  const b = await limitedFn('exceed limit', 1000);
  console.log(b);
})();

Promise 重试


// retry 实现
Promise.retry = (fn, times = 0) => {
  return new Promise(async (resolve, reject)=> {
    let remainTimes = times;

    const tryFn = async () => {
      try {
        const result = await fn();
        resolve(result);
      } catch (error) {
        if (remainTimes) {
          remainTimes -= 1;
          return tryFn();
        }
        reject(error);
      }
    };

    return tryFn();
  });
};


// 测试用例
(async () => {
  const fn = async () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        const value = Math.random();
        if (value > 0.5) {
          resolve(value);
        } else {
          reject(value);
        }
      }, Math.random() * 1000);
    })
  };

  const a = await Promise.retry(fn, 5);
  console.log(a);
})();

远程异步调用

// 假设你的本地机器不支持小数的加减乘除(但整数支持),需要借用远程 api 来实现。
// 现需要实现本地任意数字的加法,请补全 add 函数
// 加分项:考虑缓存、并发


// 模拟远程 api 调用
function decimalAdd(a, b) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(a + b);
    }, 100);
  });
}

// 测试代码
async function test() {
  console.log(
    await add(1.1, 2.2),
    await add(1.1, 2.2, 5, 7, 100, 100.1),
  );
}

test();

// @todo: 本地加法实现
// async function add(...nums) {
  
// }

// 未考虑缓存清理
const cache = {};
async function proxyDecimalAdd (a, b) {
  const key = `${a},${b}`;
  if (cache[key] === undefined) {
    cache[key] = await decimalAdd(a, b);
  }

  return cache[key];
}

// 本地加法实现
async function add(...nums) {
  let tempNums = [...nums];
  
  while (tempNums.length > 1) {
    // 两两分组并发
    const pairs = [];
    while (tempNums.length > 1) {
      pairs.push([
        tempNums.shift(),
        tempNums.shift(),
      ]);
    } 

    const nums = await Promise.all(pairs.map(pair => proxyDecimalAdd(pair[0], pair[1])));
    tempNums.push(...nums);
  }

  return tempNums[0];
}

并发控制

/**
 * 给定一个异步的 request 方法,要求将其包装成可并发调用的 batchRequest 方法,并可控制并发数
 */

const https = require('https');

// 提供的 request 方法
const request = async (url) => {
  return new Promise((resolve, reject) => {
    const req = https.get(url, (res) => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => resolve(data));
    
    });

    req.on('error', (err) => reject(err));
  });
};

const batchRequest = async (urls, concurrent = 1) => {
  return new Promise((resolve => {
    let currentConcurrent = 0;
    const results = [];
    const remainTasks = urls.map((url, i) => {
      return {
        url,
        i,
      };
    });

    const doRequest = async (url, callback) => {
      const result = await request(url)
        .then(res => {
          return {
            content: res,
            error: '',
          }
        })
        .catch((error) => {
          return {
            content: '',
            error: String(error),
          }
        });
    
      callback(result);
    };
  
    const checkAndExecTasks = () => {
      // 当结果数量等于输入时,认为整体任务结束,根据执行前的任务序号重新排序后返回
      if (results.length >= urls.length) {
        const resps = results.sort((a, b) => a.i < b.i ? -1 : 1).map(r => r.result);
        resolve(resps);
      }

      // 仅当并发数有空闲,且队列有剩余任务时,取一个任务出来执行
      while (currentConcurrent < concurrent && remainTasks.length) {
        const task = remainTasks.shift();
        currentConcurrent += 1;
        doRequest(task.url, (result) => {
          currentConcurrent -= 1;
          results.push({
            i: task.i,
            result,
          });

          // 每个任务完成后,重新检查并执行新任务
          checkAndExecTasks();
        });
      }
    };

    checkAndExecTasks();
  }));
};

const test = async () => {
  const resps = await batchRequest([
    'https://nossika.com',
    'https://www.baidu.com',
    'https://www.taobao.com',
    'https://www.bytedance.com',
    'https://nossika2.com',
  ], 2);

  console.log(resps);
};

test();