前端面试手写题有备无患(片尾附内推)

3,973 阅读7分钟

前言

手写其实在前端的面试过程中必不可少,因为手写是考验你了解某一原理的最可观的体现。下面我汇总了一些,我在面试复习过程中遇到的手写题。我将实现思路写出来与大家共享,而实现只是一个参考,有兴趣的可以点击参考答案,有问题请指正。

原理实现

实现一个new操作符

首先我们需要了解new操作符干了哪些事情

  • new操作符返回的是一个对象。
  • 对象的原型,指向的是构造函数的原型
  • 如果构造函数有return的话,需要对return的进行判断,如果是对象,那么用函数return的,如果不是对象,那么直接返回新创建的对象
参考答案
function myNew(fn, ...args) {
    let obj = Object.create(fn.prototype);
    let res = fn.apply(obj, args);
    return res instanceof Object ? res : obj;
}

实现一个instanceof操作符

首先我们需要知道instanceof是通过原型链来进行判断的

参考答案
instanceof操作符是判断原型链来生效的,所以只要你将左边的_proto_和右边的prototype做对比
function myInstance(left, right) {
    // 当left是基础类型的时候直接返回false
    if(typeof left !== 'object' || left === null) return false;
    let proto = Object.getPrototypeOf(left);
    while(true) {
        if(proto === null) return false;
        if(proto === right.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
}

实现一个apply

实现apply主要是要注意以下几个方面:

1、参数不能取第一个,因为第一个是上下文。

2、赋值到对象上的方法需要进行删除,不能影响原对象。

3、对于上下文为undefined的情况需要把上下文指向window对象

参考答案
Function.prototype.myApply = function(context, [...args]) {
    // 先判断context是否为空,如果为空则指向window对象
    context = context || window;
    context.fn = this;
    context.fn(...args);
    delete context.fn;
}

实现一个call

call的实现和apply的实现类似,但是参数处理层面会略有不同。

参考答案
Function.prototype.myCall = function(context) {
    // 先判断context是否为空,如果为空则指向window对象
    context = context || window;
    let args = [...arguments].slice(1);
    context.fn = this;
    context.fn(args);
    delete context.fn;
}

实现一个bind

bind的实现需要注意的是函数柯里化的情况。

参考答案
Function.prototype.myBind = function(context) {
    const self = this;
    let args = [...arguments].slice(1);
    return function() {
        // 考虑函数柯里化的情况
        let newArgs = [...arguments];
        this.apply(context, newArgs.concat(args))
    }
}

实现一个promise

promise的实现有必要着重看一下,我自己划分几个步骤来看:

1、先处理同步的逻辑,比如状态值变化之后,将不在发生改变,以及有哪些属性值等。

2、再处理异步逻辑,用回调的形式,以及存放的列表。

3、再处理链式调用的逻辑,链式调用比较复杂,多注意thenable的对象。

4、在处理promise的其他方法。

最后我在这里给大家推荐一篇文章,写的很不错!!promise文章

参考答案
class Promise(exector) {
    constructor() {
        this.value = undefined;
        this.reason = '';
        this.state = 'pending';
        this.onResolveList = [];
        this.onRejectList = [];
        const resolve = (value) => {
            if(this.state === 'fulfilled') {
                this.value = value
                this.state = 'fulfilled';
                this.onResolveList.forEach((fn) => {
                    fn();
                })
            }
        };
        const reject = (reason) => {
            if(this.state === 'rejected') {
                this.reason = reason
                this.state = 'rejected';
                this.onRejectList.forEach((fn) => {
                    fn();
                })
            }
        }
        
        try {
            exector(resolve, reject);
        } catch(err) {
            reject(err)
        }
    }
    then(onFulfilled, onRejected) {
        const promise2 = new Promise((reslove, reject) => {
            if(this.state === 'fulfilled') {
                let x = onFulfilled(this.value);
                resolvePromise(promise2, x, reslove, reject);
            }
            if(this.state === 'rejected') {
                onRejected(this.reason);
                resolvePromise(promise2, x, reslove, reject);
            }
            if(this.state === 'pending') {
                onResolveList.push(() => {
                    let x = onFulfilled(this.value);
                    resolvePromise(promise2, x, reslove, reject);
                });
                onRejectList.push(() => {
                    let x = onRejected(this.reason);
                    resolvePromise(promise2, x, reslove, reject);
                });
            }
        });
        return  promise2;
    }
    race(promises) {
        return new Promise((resolve, reject) => {
            for(let i = 0; i < promises.length; i++) {
                promises[i].then(resolve, reject);
            }
        })
    }
    all(promises) {
        let arr = [];
        let i = 0;
        function processData(index, data) {
            arr[index] = data;
            i++;
            if(i === promises.length) {
                resolve(data);
            }
        }
        return new Promise((resolve, reject) => {
            for(let i = 0; i < promises.length; i++) {
                promises[i].then((val) => {
                    processData(i, val);
                }, reject)
            }
        })
    }
    resolve(val) {
        return new Promise((resovle, reject) => {
            resovle(val);
        })
    }
    reject(val) {
        return new Promise((resovle, reject) => {
            reject(val);
        })
    }
}
// 这边要处理的情况有以下几种:1、循环引用, 2、thenable对象,3、promise对象
resolvePromise(promise2, x, reslove, reject) {
    if(promise2 === x) {
        reject('循环引用');
        return;
    }
    // 防止多次调用
    let called;
    // 检测x的值的类型,如果不是对象或者函数,直接返回resolve
    if(x !== null && (typeof x === 'object' || typeof x === 'function')) {
        // 规范中then逻辑报错也会进入catch
        try {
            if(called) return;
            let then = x.then; 
            if(typeof then === 'function') {
                then.call(x, (y) => {
                    if(called) return;
                    called = true;
                    resolvePromise(promise2, y ,reslove, reject)
                }, (err) => {
                    if(called) return;
                    reject(err);
                    called = true;
                })
            } else {
                resolve(x);
            }
        } catch(err) {
            if(called) return;
            reject(err);
            called = true;
        }
    } else {
        resolve(x);
    }
}

实现一个寄生组合继承

寄生组合继承其实需要注意的是子构造函数constructor的指向问题。以及继承的弊病:超集会调用两次。

参考答案
function Super() {}
function Sub() {
    Super.call(this)
}
Sub.prototype = new Super();
Sub.constructor = Sub;

业务题实现

如何实现一个防抖函数

对于防抖的理解,最好结合业务场景记忆:防抖一般用于输入框场景。所以在实现层面会有以下两个方面:

1、当一定时间内事件再次触发时,定时器应该重置。

2、执行完毕后定时器重置。

参考答案
function debounce(cb, delay, ...args) {
    let time = null;
    return function() {
        if(time) {
            clearTimeout(time);
        }
        time = setTimeout(() => {
            cb.apply(this, ...args);
            clearTimeout(time);
        }, delay);
    }
}

如何实现一个节流函数

对于节流函数,也需要结合场景来记忆。一般用于滚动事件中,一定时间内只会触发一次。实现层面也会有两个需要注意的点:

1、用一个锁变量来保证一定时间内只会触发一次。

2、执行完毕之后,解开锁即可

参考答案
function tr(fn, time, ...args) {
    let lock = false;
    return  function() {
        if(lock) return;
        lock = true;
        setTimeout(() => {
            fn.apply(this, ...args);
            lock = false;
        }, time)
    }
}

实现一个中划线与驼峰的互相转换

这个其实主要考的是正则和replace方法。

参考答案
function camelize(str) {
    return (str + '').replace(/-\D/g, function(match) {
        return match.charAt(1).toUpperCase()
    })
}
function hyphenate(str) {
    return (str + '').replace(/[A-Z]/g, function(match) {
        return '-' + match.toLowerCase();
    })
}

实现一个sleep函数

sleep函数实现的途径有很多,promise,async/await等等。我在这里就将一些最普通的。

参考答案
function sleep(time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(true);
        }, time)
    })
}

实现一个柯里化函数

实现柯里化其实就是把多个参数长度很分开来调用的意思,好处在于可以观测你参数调用的一个中间的过程,或者中间的变量。面试中常考的add(1, 2, 3)和add(1)(2)(3)就是这个问题

参考答案

function curry(fn) {
    const finalLen = fn.length
    let args = [].slice.call(this,1)
    return function currying () {
    args = args.concat(Array.from(arguments))
    const len = args.length
    return len >= fn.length ? fn.apply(this, args) : currying
    }
}

实现一个ajax

实现一个ajax其实主要是一个XMLHttpRequest对象以及其API方法的一个使用的问题。而在这里我建议尽量封装成promise的形式,方便使用。

参考答案
function ajax({url, methods, body, headers}) {
    return new Promise((resolve, reject) => {
        let request = new XMLHttpRequest();
        request.open(url, methods);
        for(let key in headers) {
            let value = headers[key]
            request.setRequestHeader(key, value);
        }
        request.onreadystatechange = () => {
            if(request.readyState === 4) {
                if(request.status >= '200' && request.status < 300) {
                    resolve(request.responeText);
                } else {
                    reject(request)
                }
            }
        }
        request.send(body)
    })
}

实现一个深拷贝

深拷贝也是面试中的一个高频考点,一般的方法,JSON的序列化和反序列化,但是这种方法的弊病有两个:

1、undefined、null和symbol类型的值会被删除

2、碰见循环引用的时候会报错。

那么我们在实现深拷贝的时候,也要时刻关注循环引用这个问题。

下列方法中,我主要是通过数组的形式去解决循环引用的问题。那为什么要有两个数组呢?

主要是一个数组维护的是原来对象的引用,一个数组维护的是新对象的引用。

参考答案
function deepClone(obj) {
    const parents = [];
    const children = [];
    function helper(obj) {
        if(obj === null) return null;
        if(typeof obj !== 'object') return obj;
        let child, proto;
        if(Array.isArray(obj)) {
            child = [];
        } else {
            proto = Object.getPrototypeOf(obj);
            child = Object.create(proto);
        }
        // 处理循环引用
        let index = parents.indexOf(obj)
        if(index === -1) {
            parents.push(obj);
            children.push(child)
        } else {
            return children[index];
        }
        // for in迭代
        for(let i in obj) {
            child[i] = helper(obj[i]);
        }
    }
}

内推

疫情当下,唯有进入大公司,应届生才能有很好的保障。小公司如果活不下去,裁员或者无法转正,被迫社招会很被动。

笔者就职于阿里零售通。想要内推的同学,可以发邮箱1445509994@qq.com。海量HC!!!也可以加q群:912253914。空闲时间帮改简历哦。