面试高频JS考查点手写实现

8,619 阅读4分钟

原文链接

考查 this

call、apply

Function.prototype._call = function(ctx = window, ...args) {
  const fnKey = 'tmp_' + Date.now();
  ctx[fnKey] = this;
  const result = ctx[fnKey](...args);
  delete ctx[fnKey];
  return result;
};

// 第二个参数是数组
Function.prototype._apply = function(ctx = window, args = []) {
  const fnKey = 'tmp_' + Date.now();
  ctx[fnKey] = this;
  const result = ctx[fnKey](...args);
  delete ctx[fnKey];
  return result;
};

let obj = { x: 1 };
function fn() {
  console.log(this.x, arguments);
}
fn._call(obj, 1, 2, 3);
fn._apply(obj, [1, 2, 3]);

bind

Function.prototype._bind = function() {
  const slice = [].slice;
  const _fn = this;
  const ctx = arguments[0];
  const args = slice.call(arguments, 1);
  if (typeof _fn !== 'function') {
    throw new TypeError('Function.prototype.bind - ' +
      'what is trying to be bound is not callable');
  }
  return function() {
    const funcArgs = args.concat(slice.call(arguments));
    return _fn.apply(ctx, funcArgs);
  };
};

const obj = { x: 1 };
function A() {
  console.log(this.x);
}
const fn = A._bind(obj);
fn();

new

function newObject(fn) {
  const obj = {};
  const res = fn.apply(obj, [].slice.call(arguments, 1));
+  Object.setPrototypeOf(obj, fn.prototype);
  return res instanceof Object ? res : obj;
}

function Con(a) {
  this.a = a;
}
const obj = newObject(Con, 1);

链式调用

Number.prototype.add = function(num) {
  num = +num;
  if(num !== num) return Number(this);
  return Number(this + num);
};

let n = 1;
n.add(1).add(2).add(3);

考查原型链

instanceof

function instance_of(lhs, rhs) {
  while (lhs) {
    lhs = lhs.__proto__;
    if (lhs === rhs.prototype) return true;
  }
  return false;
}

组合寄生继承

function Super(){}
function Sub() {
    Super.call(this); // 继承自有属性
}

// 继承原型链上的属性和方法
Sub.prototype = Object.create(Super.prototype); // 继承原型链
Sub.prototype.constructor = Sub;

Object.create

if(!Object.create) {
  Object.create = function(proto) {
    function F(){}
    F.prototype = proto;
    return new F;
  }
}

纯对象

  • redux 版
function isPlainObject(obj) {
  if (typeof obj !== 'object' || obj === null) return false;
  var proto = obj;

  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto);
  }

  return Object.getPrototypeOf(obj) === proto;
}

function A() {}
isPlainObject( new A );      // false
isPlainObject( new Object ); // true
isPlainObject( {} );         // true

判断类型

  • jQuery 3.4.1版
const type = 'Boolean Number String Function Array Date RegExp Object Error Symbol'
  .split(' ')
  .reduce((pre, name) => {
    pre[`[object ${name}]`] = name.toLowerCase();
    return pre;
  }, {});

function toType( obj ) {
  if ( obj == null ) {
    return obj + '';
  }

  return typeof obj === 'object' || typeof obj === 'function' ?
    type[ Object.prototype.toString.call( obj ) ] || 'object' :
    typeof obj;
}

toType(/xxx/) // regexp

考查闭包

柯里化

function curry(fn, ...args) {
  if(args.length >= fn.length) {
    return fn.apply(null, args);
  }

  return (...args2) => curry(fn, ...args, ...args2);
}

const add = curry(function(a, b, c) {
  return a + b + c;
});
add(1, 2, 3);
add(1)(2)(3);
add(1, 2)(3);
add(1)(2, 3);

考查性能意识

防抖

function debounce(fn, delay) {
  let timer = null;
  return function() {
    // 中间态一律清除掉
    timer && clearTimeout(timer);
    // 只需要最终的状态,执行
    timer = setTimeout(() => fn.apply(this, arguments), delay);
  };
}

节流

function throttle(fn, delay) {
  let timer, lastTime;
  return function() {
    const now = Date.now();
    const space = now - lastTime; // 间隔时间
    if( lastTime && space < delay ) { // 为了响应用户最后一次操作
      // 非首次,还未到时间,清除掉计时器,重新计时。
      timer && clearTimeout(timer);
      // 重新设置定时器
      timer = setTimeout(() => {
-        lastTime = now; // 不要忘了记录时间
+        lastTime = Date.now(); // 重新计时
        fn.apply(this, arguments);
-      }, delay);
+      }, delay - space); // 保证最后一次准确
      return;
    }
    // 首次或到时间了
    lastTime = now;
    fn.apply(this, arguments);
+    timer && clearTimeout(timer); // 别忘了清
  };
}

节流防抖详解

事件代理

function delegate(ele, selector, type, fn) {
  function callback(e) {
    e = e || window.event;
    const target = e.target || e.srcElement;
    let selectors = ele.querySelectorAll(selector);
    selectors = [].slice.call(selectors);
    if( selectors.includes(target) ) {
      fn.call(target, e);
    }
  }
  ele.addEventListener(type, callback, false);
}

delegate(document.querySelector('body'), 'button', 'click', function () {
  console.log('bingo');
});

限制并发数

requestLimit

function sendRequest(urls, max, callback) {
  const last = performance.now();
  const len = urls.length;
  let limit = max; // 控制并发数
  let cnt = 0;     // 累积执行任务数
  let res = [];    // 有序存储执行结果
  const tasks = urls.map((url, index) => () => fetch(url)
    .then(data => {
      res[index] = data;
    })
    .catch(reason => {
      res[index] = reason;
    })
    .finally(() => {
      if( ++cnt === len ) return callback(res);
      ++limit;
      doTasks();
    }));

  doTasks();

  function doTasks() {
    while( limit && tasks.length ) {
      --limit;
      const task = tasks.shift();
      task();
      console.log(`执行间隔:${performance.now() - last}`);
    }
  }
}

// 模拟 fetch
function fetch(url) {
  return new Promise(function (resolve, reject) {
    let good, bad;
    const time = 3000;

    good = setTimeout(function () {
      clearTimeout(bad);
      const data = `resolve: ${url}`;
      resolve(data);
      console.log(data);
    }, Math.random() * time);

    bad = setTimeout(function () {
      clearTimeout(good);
      const reason = `reject: ${url}`;
      reject(reason);
      console.log(reason);
    }, Math.random() * time);
  });
}

// 测试
sendRequest([1,2,3,4,5,6,7,8,9,10], 5, (res) => console.log('all done:' + res));

考查跨域

JSONP

function fn({ip}) {
  console.log(ip); // 
}
function jsonp(cb, domain) {
  const script = document.createElement('script');
  script.src = `https://api.asilu.com/ip/?callback=${cb}&ip=${domain}`;
  document.querySelector('head').appendChild(script);
}

// 获取百度IP
jsonp('fn', 'www.baidu.com');

考查 ES6

数组去重

  • Map 版
function deleteDuplicate(arr) {
  const map = new Map();
  arr.forEach( value => map.set(value, value) );
  return Array.from( map.values() ); // return [ ...map.values() ];
}

const arr = [NaN, 1, [1], [1], 1, '1', 4, 1, 2, 4, 5, 5, NaN, NaN, null, null, undefined, undefined];
deleteDuplicate( arr );
// [NaN, 1, Array(1), Array(1), "1", 4, 2, 5, null, undefined]
// Map 的遍历顺序就是插入顺序
  • Set 版
function deleteDuplicate(arr) {
  const set = new Set( arr );
  return Array.from( set ); // return [ ...set ];
}
deleteDuplicate( arr );
//[NaN, 1, Array(1), Array(1), "1", 4, 2, 5, null, undefined]

Promise.all

Promise._all = function (promises) {
  return new Promise(function(resolve, reject) {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('arguments must be an array'));
    }
    const len = promises.length;
    let cnt = 0;
    let res = [];
    for(let i = 0; i < len; i++) {
      Promise
        .resolve(promises[i])
        .then(function(value) {
          cnt++;
          res[i] = value;
          if (cnt === len) {
            return resolve(res);
          }
        }, function(reason) {
          return reject(reason);
        });
    }
  });
};

Promise._all([1,2,3,4]);
// Promise {<resolved>: Array(4)}
Promise._all([1,2,3,4]).then(res => console.log(res));
// [1, 2, 3, 4]
Promise._all([1,2,3,Promise.reject('error')]);
// Promise {<rejected>: "error"}
Promise._all([1,2,3,Promise.reject('error')]).catch(reason => console.log(reason));
// error

考查设计模式

订阅发布模式

mitt:极简事件监听

function mitt(all/*: EventHandlerMap*/) {
  all = all || Object.create(null);

  return {
    on(type/*: string*/, handler/*: EventHandler*/) {
      (all[type] || (all[type] = [])).push(handler);
    },
    off(type/*: string*/, handler/*: EventHandler*/) {
      if (all[type]) {
        all[type].splice(all[type].indexOf(handler) >>> 0, 1);
      }
    },
    emit(type/*: string*/, evt/*: any*/) { // * 表示订阅所有事件消息
      (all[type] || []).slice().map((handler) => { handler(evt); });
      (all['*'] || []).slice().map((handler) => { handler(type, evt); });
    }
  };
}

const m = mitt();
m.on('hello', (name) => console.log('hello ' + name));
m.emit('hello', 'world');

考查算法

深拷贝

Vuex版

/**
 * Get the first item that pass the test
 * by second argument function
 *
 * @param {Array} list
 * @param {Function} f
 * @return {*}
 */
export function find (list, f) {
  return list.filter(f)[0]
}

/**
 * Deep copy the given object considering circular structure.
 * This function caches all nested objects and its copies.
 * If it detects circular structure, use cached copy to avoid infinite loop.
 *
 * @param {*} obj
 * @param {Array<Object>} cache
 * @return {*}
 */
export function deepCopy (obj, cache = []) {
  // just return if obj is immutable value
  if (obj === null || typeof obj !== 'object') {
    return obj
  }

  // if obj is hit, it is in circular structure
  const hit = find(cache, c => c.original === obj)
  if (hit) {
    return hit.copy
  }

  const copy = Array.isArray(obj) ? [] : {}
  // put the copy into cache at first
  // because we want to refer it in recursive deepCopy
  cache.push({
    original: obj,
    copy
  })

  Object.keys(obj).forEach(key => {
    copy[key] = deepCopy(obj[key], cache)
  })

  return copy
}

DFS 全排列

输入 `abc`
输出
  abc
  acb
  bac
  bca
  cab
  cba
const str = 'abc';
const len = str.length;
const flag = [];
const res = [];

DFS(0);

// cur 表示第 cur 位取得字符
// 每一位有 len 种取法
function DFS(cur) {
  if(cur === len)
    return console.log(res.join(''));

  for(let i = 0; i < len; i++) {
    if(!flag[i]) {
      res[cur] = str[i];
      flag[i] = true;
      DFS(cur + 1);
      flag[i] = false;
    }
  }
}

快排

function swap(arr, i, j) {
  if(i === j) return;
  let tmp = arr[i];
  arr[i] = arr[j];
  arr[j] = tmp;
}

function quicksort(arr, s, e) {
  if( s >= e ) return;
  const r = s + Math.floor( (e - s) / 2 );

  // 选取中间位r(也可以随机)放在首位
  swap(arr, s, r);

  // 找选取的数应该在数组中的位置
  // 小于则表示位置挪后一位
  let m = s, j = s + 1;
  for(; m < e && j < e; j++) {
    if(arr[j] < arr[s]) {
      swap(arr, ++m, j);
    }
  }
  // 找到 r 的序位
  swap(arr, s, m);

  // 递归排序左左右两边
  quicksort(arr, s, m);
  quicksort(arr, m + 1, e);
}

let arr = [2, 7, 3, 4, 1, 8, 6];
quicksort(arr, 0, arr.length);

考查正则、replace 技巧

数钱格式化

'99999999'.replace(/\d{1,3}(?=(\d{3})+$)/g, '$&,');

// antd 的例子
'99999999'.replace(/\B(?=(\d{3})+(?!\d))/g, ',');

// 非正则版
function money(str) {
  const len = str.length;
  let start = len % 3 || 3;
  let arr = [str.slice(0, start)];
  while(start < len) {
    arr.push( str.slice(start, start + 3) );
    start += 3;
  }
  return arr.join(',')
}

首尾去空格

'   12 34 5 6   '.replace(/^\s+|\s+$/g, '');

相邻字符去重

'aaabbbcdfgghhjjkkk'.replace(/([A-Za-z]{1})(\1)+/g, '$1');

单词首字母大写

' hi man good  luck '.replace(/\w+/g, function(word) { 
    return word.substr(0,1).toUpperCase() + word.substr(1);
});