面试-手写代码

290 阅读6分钟

1.js防抖与节流

函数防抖:将几次操作合并为一此操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。

function debounce(fn, delay, context){
  let timer = null;
  return function(){
    const args = arguments;
    if(timer){
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(context,args);
    }, delay);
  }
}

函数节流:使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。

function throttle1(fn, delay){
  let now = new Date().getTime();
  return function(){
    if(new Date().getTime() - now >= delay){
      fn();
      now = new Date().getTime();
    } 
  }
}

function throttle2(fn, delay, context){
  let canRun = true;
  return function(){
    if(!canRun){
      return
    }
    canRun = false;
    const args = arguments;
    setTimeout(() => {
      fn.apply(context, args);
      canRun = true;
    }, delay);
  }
}

区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。

2.深拷贝

function deepClone1(obj){
  return  JSON.parse(JSON.stringify(obj));
}

function deepClone2(obj){
  let result = Array.isArray(obj) ? [] : {};
  for(let key in obj){
    if(obj.hasOwnProperty(key)){
      if (typeof obj[key] === 'object' && obj[key] !== null) {
        result[key] = deepClone2(obj[key]);
      } else {
        result[key] = obj[key];
      }
    }
  }
  return result
}

3.数组乱序

function mixArr(arr){
    return arr.sort(() => {
        return Math.random - 0.5
    })
}

4.数组flat

function flat(arr, deep = 1){
  const result = []; 
  for(let item of arr){
    if (Array.isArray(item) && deep >= 1) {
      result.push(...flat(item, deep - 1));
    } else {
      result.push(item);
    }
  }
  return result
}

5.数组filter

Array.prototype.myFilter = function(fn, context){
  if(typeof fn !== 'function'){
    throw new TypeError('first params must be function!')
  }
  const arr = this.concat(),len = arr.length,result = [];
  for(let i = 0;i < len;i++){
    fn.call(context, arr[i], i, this) && result.push(arr[i]);
  }
  return result
}

6.手写call、apply、bind

// 通过对象调用方法的特性,将函数中this对象指向call函数this参数,然后使用eval调用函数使得参数添加到要执行的函数中

Function.prototype.mycall = function(){
  if (typeof this !== 'function') {
    throw new TypeError('mycall must be called by function!')
  }
  let context = arguments[0],
      otherArgs = [],
      result;

  for(let i = 1;i < arguments.length;i++){
    otherArgs.push(`arguments[${i}]`);
  }
  
  context.func = this;
  result = eval('context.func('+ otherArgs + ')');
  delete context.func
  return result
}

// apply 实现原理同上

Function.prototype.myapply = function(){
  if(typeof this !== 'function'){
    throw TypeError('myapply must be called by function.')
  }
  const thisArg = arguments[0] || window,
        arr = arguments[1],
        otherArgs = [];

  for(let i = 0;i < arr.length;i++){
    otherArgs.push('arr['+ i +']');
  }

  thisArg.func = this;
  const result = eval('thisArg.func(' + otherArgs + ')');
  delete thisArg.func;
  return result
}

// bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

Function.prototype.mybind = function(){
  if (typeof this !== 'function') {
    throw TypeError("Bind must be called by function")
  }

  const slice = Array.prototype.slice,
        thisArg = Array.prototype.shift.call(arguments),
        otherArgs = slice.call(arguments),
        self = this,
        function F(){};
        
  F.prototype = self.prototype;
  
  const bindFunc = function(){
    return self.apply(this instanceof F ? this : thisArg, otherArgs.concat(slice.call(arguments)));
  }

  bindFunc.prototype = new F();

  return bindFunc
}

7.继承

// ES5(寄生组合式继承)
function Parent(name,age) {
    this.name = name;
    this.age = age;
}

Parent.prototype.sayName = function(){
    console.log(this.name);
}

function Child(name, age, score) {
    Parent.apply(this,[name, age]);
    this.score = score;
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.sayScore = function(){
    console.log(this.score);
}

// ES6
class Parent {
    construtor(name, age){
        this.name = name;
        this.age = age;
    }

    sayName(){
        console.log(this.name);
    }
}

class Child extends Parent {
    construtor(name, age, score){
        super(name, age);
        this.score = score;
    }

    sayScore(){
        console.log(this.score);
    }
}

8.instanceof

// instanceof 原理就是检查实例(左侧)的原型链上是否含有构造函数(右侧)的原型对象(prototype)
function myinstanceof(left,right){
  let prototype = right.prototype, //获取构造函数的原型对象
      proto = Object.getPrototypeOf(left);//获取实例的原型链上的原型
  while(proto){
    if(proto === prototype){
      return true
    }
    proto = Object.getPrototypeOf(proto);
  }
  return false
}
//其实使用Object.prototype.toString.call(instance)更准确
  1. new操作符
function mynew(){
  const constructor = Array.prototype.shift.call(arguments);
  const context = Object.create(constructor.prototype);
  const result = constructor.apply(context, arguments);
  return typeof result === 'object' ? result : context;
}
  1. 函数的柯里化
/*
  函数柯里化:
  1.参数复用-复用最初函数的参数 
  2.提前返回-返回接受余下参数且返回结果的新函数 
  3.延迟执行-返回新函数,等待执行
*/
function curry(fn) {
  const slice = Array.prototype.slice;
  const args = slice.call(arguments, 1);
  return function() {
    const otherArgs = slice.call(arguments);
    return fn.apply(this,args.concat(otherArgs));
  }
}

柯里化扩展:实现一个函数add,其功能是可以无限的调用添加参数,最终输出所有参数相加结果。例如: a(1)(2)(3) 输出 6, a(1)(5)(1)(5)(8) 输出 20,以此类推。

// 实际上这个函数调用后的返回值不是一个number类型,是function类型。但是 infiniteAdd(1)(2)(3) == 6 是 true,且infiniteAdd(1)(2)(3) + 1会输出7,因为==和+ 会进行隐式转换,调用infiniteAdd(1)(2)(3)返回的函数的toString方法最终得到6,然后再继续计算。
function infiniteAdd(){
    let arr = [...arguments];
    function recursion(){
        arr.push(...arguments);
        return recursion
    }

    recursion.toString = function(){
        return arr.reduce((a, b) => {
            return a + b
        }, 0);
    }

    return recursion
}

11.jsonp

//jsonp 的作用是跨域。原理是通过动态插入script标签来实现跨域,因为script脚本不受同源策略的限制。它由两部分组成:回调函数和数据
function jsonp(config) {
  const {url, data} = config;
  if(!url){
    throw new TypeError('params must includes url property');
  }
  return new Promise((resolve,reject)=>{
    const script = document.createElement('script');
    const header = document.querySelector('head');
    const callback = `jsonp_${Date.now()}`;
    script.src = `${url}?${handleData(Object.assign(data,{callback: callback}))}`;
    header.appendChild(script);

    window[callback] = function(res){
      res ? resolve(res) : reject('fail');
      header.removeChild(script);
      window[callback] = null;
    }
  })
}

function handleData(data){
  return Object.keys(data).map((item)=>{
    return `${item}=${data[item]}`
  }).join('&');
}
  1. lazyMan
实现一个LazyMan,可以按照以下方式调用:
LazyMan("Hank")输出:
Hi! This is Hank!
 
LazyMan("Hank").sleep(10).eat("dinner")输出
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner~
 
LazyMan("Hank").eat("dinner").eat("supper")输出
Hi This is Hank!
Eat dinner~
Eat supper~
 
LazyMan("Hank").sleepFirst(5).eat("supper")输出
//等待5秒
Wake up after 5
Hi This is Hank!
Eat supper
 
以此类推。
class Process{
  constructor(name){
    this.queue = [];
    this.name = name;
    this.queue.push(()=>{
      console.log(`Hi! This is ${this.name}!`);
      this.next();
    })
    setTimeout(() => {
      this.next();
    }, 0);
  }

  next(){
    const fn = this.queue.shift();
    typeof fn === 'function' && fn();
  }

  sleep(num){
    this.queue.push(()=>{
      setTimeout(() => {
        console.log(`Wake up after ${num}`);
        this.next();
      }, num * 1000);
    })
    return this
  }

  eat(food){
    this.queue.push(()=>{
      console.log(`Eat ${food}~`);
      this.next();
    })
    return this
  }

  sleepFirst(num){
    this.queue.unshift(()=>{
      setTimeout(() => {
        console.log(`Wake up after ${num}`);
        this.next();
      }, num * 1000);
    })
    return this
  }
}

function LazyMan(name){
  return new Process(name);
}

13.实现eventEmitter

观察者模式是我们工作中经常能接触到的一种设计模式。用过 jquery的应该对这种设计模式都不陌生。eventEmitter 是 node中的核心,主要方法包括on、emit、off、once。

class EventEmitter {
    constructor(){
        this.events = {}
    }
    on(name,cb){
        if(!this.events[name]){
            this.events[name] = [cb];
        }else{
            this.events[name].push(cb)
        }
    }
    emit(name,...arg){
        if(this.events[name]){
            this.events[name].forEach(fn => {
                fn.call(this,...arg)
            })
        }
    }
    off(name,cb){
        if(this.events[name]){
            this.events[name] = this.events[name].filter(fn => {
                return fn != cb
            })
        }
    }
    once(name,fn){
        var onlyOnce = () => {
            fn.apply(this,arguments);
            this.off(name,onlyOnce)
        }
        this.on(name,onlyOnce);
        return this;
    }
}

14.sleep函数

//sleep 函数的作用就是延迟指定时间后再执行接下来的函数
function sleep(fn, timestamp) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, timestamp);
  }).then(() => {
    fn();
  })
}

15.将一个同步callback包装成promise形式

同步的 callback 用的最多的是在 node 的回调中,例如下面这种,包装完之后就可以愉快的使用 .then 了。

function promisify(fn, context) {
  return (...args) => {
    return new Promise((resolve, reject)=>{
      fn.apply(context, [...args,(err,data)=>{
        err ? reject(err) : resolve(data);
      }])
    })
  }
}

16.写一个函数,可以控制最大并发数

微信小程序最一开始对并发数限制为5个,后来升级到10个,如果超过10个会被舍弃。后来微信小程序升级为不限制并发请求,但超过10个会排队机制。也就是当同时调用的请求超过 10 个时,小程序会先发起 10 个并发请求,超过 10 个的部分按调用顺序进行排队,当前一个请求完成时,再发送队列中的下一个请求。

class Controller {
  constructor(num){
    this.queue = [];
    this.limit = num;
  }

  run(){
    let i = 0; 
    while(this.queue.length > 0 && i < this.limit){
      this.next();
      i++;
    }
  }

  next(){
    let fn = this.queue.shift();
    if(typeof fn === 'function'){
      fn().then((data)=>{
        console.log(data);
      }).catch((err)=>{
        console.error('报错啦', err);
      }).finally(()=>{
        this.next();
      })
    }
  }

  addTask(fn){
    this.queue.push(fn);
  }
}

// 测试用例
// let arr = [1,2,3,4,5,6,7,8,89,9,22,234,100,99],
//     controller = new Controller(5),
//     sum = 0;
// arr.forEach((item)=>{
//   controller.addTask(()=>{
//     return new Promise(resolve => {
//       sum++;
//       setTimeout(() => {
//         console.log("并发量", sum);
//         sum--;
//         resolve(item);
//       }, 3000);
//     })
//   })
// })
// controller.run();

17.手写promise

class MyPromise {
    constructor(executor) {
        this.state = 'pending';
        this.value = undefined;
        this.reason = undefined;
        this.onResolvedCallbacks = [];
        this.onRejectedCallbacks = [];

        let resolve = value => {
            if (this.state === 'pending') {
                this.state = 'fulfilled';
                this.value = value;
                this.onResolvedCallbacks.forEach(fn => fn());
            }
        };

        let reject = reason => {
            if (this.state === 'pending') {
                this.state = 'rejected';
                this.reason = reason;
                this.onRejectedCallbacks.forEach(fn => fn());
            }
        };

        try {
            executor(resolve, reject);
        } catch (err) {
            reject(err);
        }
    }

    then(onFulfilled, onRejected) {
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
        onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };

        let promise2 = new Promise((resolve, reject) => {
            if (this.state === 'fulfilled') {
                setTimeout(() => {
                    try {
                        let x = onFulfilled(this.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                }, 0);
            }

            if (this.state === 'rejected') {
                setTimeout(() => {
                    try {
                        let x = onRejected(this.reason);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                }, 0);
            }
            
            if (this.state === 'pending') {
                this.onResolvedCallbacks.push(() => {
                    setTimeout(() => {
                        try {
                            let x = onFulfilled(this.value);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    }, 0);
                });
                this.onRejectedCallbacks.push(() => {
                    setTimeout(() => {
                        try {
                            let x = onRejected(this.reason);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    }, 0)
                });
            }
        });

        return promise2;
    }

    catch(fn) {
        return this.then(null, fn);
    }
}

function resolvePromise(promise2, x, resolve, reject) {
    if (x === promise2) {
        return reject(new TypeError('Chaining cycle detected for promise'));
    }
    
    let called;
    if (x != null && (typeof x === 'object' || typeof x === 'function')) {
        try {
            let then = x.then;
            if (typeof then === 'function') {
                then.call(x, y => {
                    if (called) return;
                    called = true;
                    resolvePromise(promise2, y, resolve, reject);
                }, err => {
                    if (called) return;
                    called = true;
                    reject(err);
                })
            } else {
                resolve(x);
            }
        } catch (e) {
            if (called) return;
            called = true;
            reject(e);
        }
    } else {
        resolve(x);
    }
}

18.手写数组reduce方法

Array.prototype.myReduce = function(){
    if(Object.prototype.toString.call(this) !== '[object Array]'){
        throw new TypeError('myReduce must be called by Array');
    }

    if(Object.prototype.toString.call(arguments[0]) !== '[object Function]'){
        throw new TypeError('first params must be function');
    }

    let arr = this, accumulator;

    if(arr.length === 0){
        if(arguments.length < 2){
            throw new TypeError('empty Array must provide second params');
        }else{
            return arguments[1]
        }
    }

    if(arr.length === 1 && arguments.length === 1){
        return arr[0]
    }

    let fn = arguments[0],
        len = arr.length,
        i;
    
    if(arguments.length < 2){
        i = 1;
        accumulator = arr[0];
    }else{
        i = 0
        accumulator = arguments[1];
    }

    for(;i < len;i++){
        if(arr.hasOwnProperty(i)){
            accumulator = fn(accumulator,arr[i],i,arr)
        } 
    }

    return accumulator
}

// MDN Polyfill
Array.prototype.myReduce = function (callback /*, initialValue*/) {
    if (this === null) {
        throw new TypeError('Array.prototype.myReduce ' + 'called on null or undefined');
    }
    if (typeof callback !== 'function') {
        throw new TypeError(callback + ' is not a function');
    }

    // 1. Let O be ? ToObject(this value).
    var o = Object(this);

    // 2. Let len be ? ToLength(? Get(O, "length")).
    var len = o.length >>> 0;

    // Steps 3, 4, 5, 6, 7      
    var k = 0;
    var value;

    if (arguments.length >= 2) {
        value = arguments[1];
    } else {
        while (k < len && !(k in o)) {
            k++;
        }

        // 3. If len is 0 and initialValue is not present,
        //    throw a TypeError exception.
        if (k >= len) {
            throw new TypeError('Reduce of empty array ' + 'with no initial value');
        }
        value = o[k++];
    }

    // 8. Repeat, while k < len
    while (k < len) {
        // a. Let Pk be ! ToString(k).
        // b. Let kPresent be ? HasProperty(O, Pk).
        // c. If kPresent is true, then
        //    i.  Let kValue be ? Get(O, Pk).
        //    ii. Let accumulator be ? Call(
        //          callbackfn, undefined,
        //          « accumulator, kValue, k, O »).
        if (k in o) {
            value = callback(value, o[k], k, o);
        }

        // d. Increase k by 1.      
        k++;
    }

    // 9. Return accumulator.
    return value;
}

//测试用例
['1',null,undefined,,3,4].reduce((a,b) => a+b); //"1nullundefined34"
['1',null,undefined,,3,4].myReduce((a,b) => a+b); //"1nullundefined34"
  1. 深度优先搜索(DFS)和广度优先搜索(BFS)

深度优先搜索(depth first search),是从根节点开始,沿树的深度进行搜索,尽可能深的搜索分支。当节点所在的边都已经搜多过,则回溯到上一个节点,再搜索其余的边。深度优先搜索采用栈结构,后进先出。

广度优先搜索(breadth first search),是从根节点开始,沿树的宽度进行搜索,如果所有节点都被访问,则算法中止。广度优先搜索采用队列的形式,先进先出。

//DFS
function maxLevel (node){
    let max = 0;
    function recursion(node, level){
        console.log('===============',node.id);
        let child = node.children;
        if(child && child.length > 0){
            child.forEach(item => {
                recursion(item, level + 1);
            });
        } else {
            max = max > level ? max : level;
        }
    }
    recursion(node, 1);
    return max
}
//BFS
function maxNum (node){
    let max = 0, queue = [node], arr = [];
    while(queue.length > 0){
        let node = queue.shift();
        console.log('===============',node.id);
        if(node.children && node.children.length > 0){
            arr.push(...node.children);
        }
        if(queue.length === 0){
            max = max > arr.length ? max : arr.length;
            queue = arr;
            arr = []
        }
    }
    return max
}