【面试宝典】高频前端面试题之手写常用JS方法

1,295 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

作为前端开发,JS是重中之重,代码能力毋庸置疑是非常重要的,本文可以帮你扩展并巩固自己的JS基础,在工作中还可以对常用的需求进行手写实现,比如深拷贝、防抖节流等可以直接用于往后的项目中,提高项目开发效率。

阅读期间我们应该知道它具体在干什么,为什么要这么写,是不是能够写得更好。 阅读之后还可以,通过前端工具进行手写来做到更好的理解以及使用。

1 手写数组去重

实现一个方法使数组去重返回新数组

//利用ES6新语法set
function uniqueArr(arr) {
  return [...new Set(arr)];
}

//利用indexOf
function uniqueArr(arr) {
   const res = [];
   for (let i = 0; i < arr.length; i++) {
      if (res.indexOf(arr[i]) === -1){
          res.push(arr[i]);
      }
   }
   return res;
}


// console.log(uniqueArr([1, 2, 3, 4, 1, 2]));

2 手写数组扁平化

实现一个方法使多维数组变成一维数组

function flatter(arr) {
  if (!arr.length) return;
  return arr.reduce(
    (pre, cur) =>
      Array.isArray(cur) ? [...pre, ...flatter(cur)] : [...pre, cur],
    []
  );
}
// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));

3. 手写数据类型判断:

数据类型判断主要可以使用以下三种方式进行判断,但是都具有一定的局限性,因此在项目实际开发过程中会重写类型判断方法。

typeof:Undefined、Boolean、Number、String、Symbol、Function 等基本数据类型,但是对于其他的都会认为是 object,例如 Null、Date 等,typeof返回的是一个变量的基本类型。

instanceof:可以准确地判断复杂引用数据类型,instanceof返回的是一个布尔值。

Object.prototype.toString.call():调用该方法,统一返回格式“[object Xxx]”的字符串。

function typeOf(obj) {
   let res = Object.prototype.toString.call(obj).split(' ')[1]
   res = res.substring(0, res.length - 1).toLowerCase()
   return res
}

typeOf([])        // 'array'
typeOf({})        // 'object'
typeOf(new Date)  // 'date'

4.手写深浅拷贝

浅拷贝

对于引用类型而言,直接通过原对象生成一个新对象,原对象和新对象共同指向堆中内存地址,这样的话更改新对象或原对象便会互相影响。

function assign2(target, ...source) {
    if (target == null) {
        return target
    }
    let ret = Object(target) 
    source.forEach(function(obj) {
        if (obj != null) {
            for (let key in obj) {
                if (obj.hasOwnProperty(key)) {
                    ret[key] = obj[key]
                }
            }
        }
    })
    return ret
}

深拷贝

对于引用类型而言,我们通过深拷贝实现开辟新对象堆中自己的内存空间,这样的话更改新对象或原对象便不会互相影响。

function copyObj(obj){
        var cloneObj;
        //当输入数据为简单数据类型时直接复制
        if(obj&&typeof obj!=='object'){cloneObj=obj;}
        //当输入数据为对象或数组时
        else if(obj&&typeof obj==='object'){
            //检测输入数据是数组还是对象
            cloneObj=Array.isArray(obj)?[]:{};
            for(let key in obj){
                if(obj.hasOwnProperty(key)){
                    if(obj[key]&&typeof obj[key]==='object') {
                        //若当前元素类型为对象时,递归调用
                        cloneObj[key] = copyObj(obj[key]);
                    }
                    //若当前元素类型为基本数据类型
                    else{cloneObj[key]=obj[key];}
                }

            }
        }
        return cloneObj;

    }

5. 手写防抖节流:

防抖

防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于设置的时间,防抖的情况下只会调用一次,而节流的情况会每隔一定时间调用一次函数。

防抖应用场景:根据搜索内容进行匹配数据,但是如果用户快速的输入了一连串的字符,假设是10个字符,那么就会在瞬间触发了10次的请求,我们想要的是用户停止输入的时候才去触发查询的请求,这时候函数防抖可以帮到我们。 他的作用就是当持续触发的时候,函数是不执行的,等最后一次触发结束的一段时间之后再去执行。比如输入结束之后再去进行接口搜索回显数据。

function debounce(func, wait) {
    var timeout;
    return function () {
        var context = this;
        var args = arguments;
        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}

节流

节流应用场景:滚动浏览器滚动条的时候,更新页面上的某些布局内容或者去调用后台的某接口查询内容,同样的,如果不对函数调用的频率加以限制的话,那么可能我们滚动一次滚动条就会产生N次的调用了。 但是这次的情况跟上面的有所不同,我们不是要在每完成等待某个时间后去执行某函数,而是要每间隔某个时间去执行某函数,避免函数的过多执行。

function throttle(func, wait) {
    var context, args;
    var previous = 0;

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

6. 手写 new 关键字

new 运算符用来创建用户自定义的对象类型的实例或者具有构造函数的内置对象的实例。

function myNew(fn, ...args) {
    //创建一个新的对象
    const obj = {};
    //新对象的__proto__属性指向构造函数的原型对象
    obj.__proto__ = fn.prototype;
    //将构造函数的作用域赋值给新对象(也就是this对象指向新对象)
    let result = fn.apply(obj, args)
    //返回新对象
    return typeof result === "object" ? result : obj
}

7. 手写函数原型三大方法:

call

call使用一个指定的 this 值和一个或多个参数来调用一个函数。

实现要点:this 可能传入 null;传入不固定个数的参数;函数可能有返回值;

Function.prototype.call2 = function (context) {
    var context = context || window;
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}

apply

apply 和 call 一样,唯一的区别就是 call 是传入不固定个数的参数,而 apply 是传入一个数组。

实现要点:this 可能传入 null;传入一个数组;函数可能有返回值;

Function.prototype.apply2 = function (context, arr) {
    var context = context || window;
    context.fn = this;

    var result;
    if (!arr) {
        result = context.fn();
    } else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}

bind

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

实现要点:bind() 除了 this 外,还可传入多个参数;bing 创建的新函数可能传入多个参数;新函数可能被当做构造函数调用;函数可能有返回值;

Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

8. 手写Promise

Promise.all

Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。

Promise.all = function(promiseArr) {
    let index = 0, result = []
    return new Promise((resolve, reject) => {
        promiseArr.forEach((p, i) => {
            Promise.resolve(p).then(val => {
                index++
                result[i] = val
                if (index === promiseArr.length) {
                    resolve(result)
                }
            }, err => {
                reject(err)
            })
        })
    })
}

Promise.race

Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。

Promise.race = function(promiseArr) {
    return new Promise((resolve, reject) => {
        promiseArr.forEach(p => {
            Promise.resolve(p).then(val => {
                resolve(val)
            }, err => {
                rejecte(err)
            })
        })
    })
}

推荐

如果有想要继续学习浏览器篇的读者,可以观看笔者的另一篇文章【面试宝典】高频前端面试题之浏览器篇 - 掘金 (juejin.cn)

如果有想要继续学习CSS原理篇的读者,可以观看笔者的另一篇文章【面试宝典】高频前端面试题之CSS篇 - 掘金 (juejin.cn)

如果有想要继续学习Vue原理篇的读者,可以观看笔者的另一篇文章【面试宝典】高频前端面试题之Vue原理篇 - 掘金 (juejin.cn)

如果有想要继续学习JS原理篇的读者,可以观看笔者的另一篇文章【面试宝典】高频前端面试题之JavaScript原理篇 - 掘金 (juejin.cn)

结语

本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力。