常见的需要手写的前端面试题代码

290 阅读5分钟

1. 防抖

防抖是指定时间内只触发一次函数,如果还没到指定时间,就触发了事件,则计时器会重新设置,保证只有最后一次才会触发函数。一般是为了防止用户连续点击事件触发按钮。

function debounce(fn, ms) {
  let timer = null;
  return function() {
    if(timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, ms);
  }
}

2. 节流

节流是保证间隔时间内只触发一次函数,也就是每隔一段时间,就会触发一次函数,这一点跟防抖是不一样的。一般是在监听页面滚动时用的比较多。

function throttle(fn, ms) {
  let preTime = null;
  return function() {
    let nowTime = new Date().getTime();
    if(!preTime || nowTime - preTime > ms) {
        fn.apply(this, arguments);
        preTime = nowTime;
    }
  }
}

3. 实现call函数

call的常规用法是fn.call(this, param1, param2);

Function.prototype.myCall = function(context) {
  if(typeof this !== 'function') throw new TypeError('type error');
  let result;
  let args = [...arguments].slice(1);
  context = context || window;
  context.fn = this;
  result = context.fn(...args);
  delete context.fn;
  return result;
}

4. 实现apply函数

apply的常规用法是fn.apply(this, [param1, param2]);

Function.prototype.myApply = function(context) {
  if(typeof this !== 'function') throw new TypeError('type error');
  let result = null;
  let args = arguments[1];
  context = context || window;
  context.fn = this;
  if(args) {
    result = context.fn(...args);
  }else{
    result = context.fn();
  }
  delete context.fn;
  return result;
}

5. 实现bind函数

bind的常规用法是fn.bind(this, param1, param2)();

Function.prototype.myBind = function(context) {
  if(typeof this !== 'function') throw new TypeError('type error');
  let fn = this, args = [...arguments].slice(1);
  return function Fn() {
    return fn.apply(
      this instanceof Fn? this: context,
      args.concat(...arguments)
    );
  }
}

6. 实现发布订阅模式

class EventEmitter {
  constructor() {
    this.caches = [];
  }
  
  on(name, fn) {
    if(this.caches[name]) {
      this.caches[name].push(fn);
    }else{
      this.caches[name] = [fn];
    }
  }
  
  off(name, fn) {
    const task = this.caches[name];
    if(task) {
      const idx = task.findIndex(f => {
        return f === fn || f.callback === fn;
      });
      if(idx > -1) {
        task.splice(idx, 1);
      }
    }
  }
  
  emit(name, once = false) {
    const task = this.cache[name];
    if(task) {
      for(let fn of task) {
        fn();
      }
      if(once) {
        delete this.cache[name];
      }
    }
  }
}

7. 实现斐波那契数列

斐波那契数列的公式是: F(0) = 0; F(1) = 1; F(n) = F(n - 1) + F(n - 2);

function fib(n) {
  if(n < 0) throw new Error('数字不能小于0');
  if(n == 0 || n == 1) return n;
  let arr = [];
  arr[0] = 0, arr[1] = 1;
  for(let i = 1; i < n; i++) {
    arr[i + 1] = arr[i] + arr[i - 1];
  }
  return arr[n];
}

8. 实现add(1)(2)(3)

const addFn = (...args) => args.reduce((count, curr) => count + curr, 0);

function curring(fn) {
  let args = [];
  return function temp(...newArgs) { 
    if(newArgs.length > 0) {
      args.push(...newArgs);
      return temp;
    }else{
      let val = fn.apply(this, args);
      args = [];
      return val;
    }
  }
}

const add = curring(addFn);
console.log(add(1)(2)(3)());

9. 实现flat函数

数组的flat函数用来将数组维度打平,比如arr.flat(2)就是打平两个维度,直接arr.flat()等同于arr.flat(1),arr.flat(Infinity)是打平任意深度的数组。

Array.prototype.myFlat(depth = 1) {
  if(Object.prototype.toString.call(this).slice(8, -1) !== 'Array'){
    throw new TypeError('type is not Array');
  }
  return this.reduce((res, curr) => {
    return res.concat(
      Array.isArray(curr) && depth > 1? curr.myFlat(depth - 1): curr
    );
  }, []);
}

10. 实现定时函数,时间间隔是a,a+b,a+2b,a+(n*b),并定义清除定时的方法

function myTimeout(fn, a, b) {
  let count = 0;
  let timer = null;
  const loop = () => {
    timer = setTimeout(() => {
      fn();
      count++;
      loop();
    }, a + count * b);
  }
  loop();
  return () => {
    if(timer) clearTimeout(timer); 
  }
}

11. 实现lodash中的.get()

lodash中的get使用方法如下:

let obj = { a: { b: { val: 10 } } };

let val = _.get(obj, 'a.b.val'); //找到val会返回10,如果找不到会返回undefined而不会报错

function myGet(source, path, defaultValue = undefined) {
  const paths = path.replace(/\[(\d+)\]/g, '.$1').split('.');
  let result = source;
  for(let p of paths) {
    result = Object(result[p]);
    if(result === undefined) return defaultValue;
  }
  return result;
}

12. 实现Promise

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

const resolvePromise = (promise, x, resolve, reject) => {
  if(x === promise) throw new Error('循环引用');
  if(x !== null && (typeof x === 'object' || typeof x === 'function')) {
    let called;
    try{
      let then = x.then;
      if(typeof then === 'function') {
        then.call(x, y => {
          if(called) return;
          called = true;
          resolvePromise(promise, y, resolve, reject);
        }, e => {
          if(called) return;
          called = true;
          reject(e);
        });
      }else{
        resolve(x);
      }
    }catch(err){
      if(called) return;
      called = true;
      reject(err);
    }
  }
}

function myPromise(constructor) {
  let self = this;
  self.status = PENDING;
  self.value = undefined;
  self.error = undefined;
  self.onFulfilledCallbacks = [];
  self.onRejectedCallbacks = [];
  
  function resolve(val) {
    if(val instanceof myPromise) {
      return val.then(resolve, reject);
    }
    setTimeout(() => {
      if(self.status === PENDING) {
        self.status = FULFILLED;
        self.value = val;
        self.onFulfilledCallbacks.forEach(cb => {
          return cb(self.value);
        });
      }
    },0);
  }
  
  function reject(error) {
    setTimeout(() => {
      if(self.status === PENDING) {
        self.status = REJECTED;
        self.error = error;
        self.onRejectedCallbacks.forEach(cb => {
          return cb(self.error);
        });
      }
    },0);
  }
  
  try{
    constructor(resolve, reject);
  }catch(err) {
    reject(err);
  }
}

myPromise.prototype.then = function(onFulfilled, onRejected) {
  const self = this;
  let newPromise;
  onFulfilled = typeof onFulfilled === 'function'? onFulfilled : value => value;
  onRejected = typeof onRejected === 'function'? onRejected : err => { throw err };
  
  if(self.status === FULFILLED) {
    return newPromise = new myPromise((resolve, reject) => {
      setTimeout(() => {
        try{
          let x = onFulfilled(self.value);
          resolvePromise(newPromise, x, resolve, reject);
        }catch(err){
          reject(err);
        }
      }, 0);
    });
  }
  
  if(self.status === REJECTED) {
    return newPromise = new myPromise((resolve, reject) => {
      setTimeout(() => {
        try{
          let x = onRejected(self.error);
          resolvePromise(newPromise, x, resolve, reject);
        }catch(err){
          reject(err);
        }
      }, 0);
    });
  }
  
  if(self.status === PENDING) {
    return newPromise = new myPromise((resolve, reject) => {
      self.onFulfilledCallbacks.push(val => {
        try{
          let x = onFulfilled(val);
          resolvePromise(newPromise, x, resolve, reject);
        }catch(e){
          reject(e);
        }
      });
      
      self.onRejectedCallbacks.push(err => {
        try{
          let x = onRejected(err);
          resolvePromise(newPromise, x, resolve, reject);
        }catch(e){
          reject(e);
        }
      });
    });
  }
}

13. 实现Promise.all

Promise.all用来处理需要等待多个异步请求结果的场景,常规使用如下:

const p1 = new Promise((resolve, reject) => {
  resolve('p1成功');
});

const p2 = new Promise((resolve, reject) => {
  resolve('p2成功');
});

const p3 = new Promise((resolve, reject) => {
  reject('p3失败');
});

Promise.all([p1,p2]).then(resArr => {
  //最终返回是一个包含了多个结果的数组
  console.log(resArr);
}).catch(err => {
  //多个异步请求中,只要有一个promise返回了reject,就会进入失败回调
  //上面的p3如果加入运行数组中,就会导致进入catch回调
  console.error(err);
});

手写实现如下:

Promise.prototype.myAll = function(promises) {
  if(!Array.isArray(promises)) throw new TypeError('必须传入一个数组');
  return new Promise((resolve, reject) => {
    let resArr = [];
    let count = 0;
    const resByKey = (res, index) => {
      resArr[index] = res;
      count++;
      if(count === promises.length) {
        resolve(resArr);
      }
    };
    promises.forEach((promise, index) => {
      promise.then(res => {
        resByKey(res, index);
      }, reject);
    });
  });
}

14. 实现Promise.race

Promise.race用来处理多个并行的promise请求,且只返回最先出结果的请求,使用示例如下:

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p1成功');
  }, 1000);
});

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('p2失败');
  }, 200);
});

//这里最终会进入catch回调打印出p2失败,因为p2比p1先返回了结果
Promise.race([p1, p2]).then(res => {
  console.log(res);
}).catch(err => {
  console.log(err);
});

手写实现如下:

Promise.prototype.myRace = function(promises) {
  if(!Array.isArray(promises)) throw new TypeError('必须传入一个数组');
  return new Promise((resolve, reject) => {
    promises.forEach(p => {
      p.then(res => {
        resolve(res);
      }, err => {
        reject(err);
      });
    });
  });
}

15. 实现new

new具体做了哪些事情,可以看看掘友写的这篇文章,讲的还是比较清楚的:面试官问:能否模拟实现JS的new操作符

function myNew(constructor) {
  if(typeof constructor !== 'function') throw new TypeError('constructor must be a function');
  myNew.target = constructor;
  let obj = Object.create(constructor.prototype);
  let args = [...arguments].slice(1);
  let result = constructor.apply(obj, args);
  const isObject = typeof result === 'object' && result !== null;
  const isFunction = typeof result === 'function';
  if(isObject || isFunction) {
    return result;
  }else{
    return obj;
  }
}

16. 手写数组转树

let arr = [
  {id: 1, name: '部门1', parentId: 0},
  {id: 2, name: '部门2', parentId: 1},
  {id: 3, name: '部门3', parentId: 1},
  {id: 4, name: '部门4', parentId: 2},
  {id: 5, name: '部门5', parentId: 3},
  {id: 6, name: '部门6', parentId: 4},
  {id: 7, name: '部门7', parentId: 2},
  {id: 8, name: '部门8', parentId: 3},
];

function convert(arr) {
  let map = arr.reduce((acc, cur) => {
    acc[cur.id] = cur;
    return acc;
  }, {});
  let result = [];
  for(let key in map) {
    const item = map[key];
    if(item.parentId == 0) {
      result.push(item);
    }else{
      const parent = map[item.parentId];
      if(parent) {
        parent.children = parent.children || [];
        parent.children.push(item);
      }
    }
  }
  return result;
}

console.log(convert(arr));

17. 使用ES6的Proxy实现数组负索引,例如,可以简单地使用arr[-1]替代arr[arr.length-1]访问最后一个元素,[-2]访问倒数第二个元素

function proxyArray(arr) {
  const len = arr.length;
  return new Proxy(arr, {
    get(target, key) {
      key = Number(key);
      while(key < 0) {
        key += len;
      }
      return target[key];
    }
  });
}

var a = proxyArray([1, 2, 3, 4, 5, 6, 7, 8, 9]);
console.log(a[1]);  // 2
console.log(a[-10]);  // 9
console.log(a[-20]);  // 8

18. 实现深拷贝

function deepClone(obj) {
  if(obj === null) return null;
  if(typeof obj !== 'object') return obj;
  if(obj instanceof Date) return new Date(obj);
  if(obj instanceof RegExp) return new RegExp(obj);
  let newObj = Object.create(obj.prototype);
  Object.keys(obj).forEach(key => {
    newObj[key] = deepClone(obj[key]);
  });
  return newObj;
}

19. 实现事件委托

<ul>
  <li class="li">1</li>
  <li class="li">2</li>
  <li class="li">3</li>
</ul>

<script type="text/javascript">
  function myEntrust(to, evtType, from, fn) {
    const dom = document.querySelector(to);
    dom.addEventListener(evtType, function(evt) {
      let target = evt.target || evt.srcElement;
      if(/^[.]/.test(from)) {
        if(target.className === from.slice(1)) {
          fn.call(target);
        }
      }else{
        if(target.nodeName.toLowerCase() === from) {
          fn.call(target);
        }
      }
    });
  }
  
  myEntrust('ul', 'click', '.li', function(){
    console.log('li~~');
  });
</script>