web前端经典面试手写实现各种功能

前言

在js中有许多常用方法,但是往往大部分人只知道用法但不知道其原理,这样会造成很容易记混或者忘记。而自己去实现这些方法可以巩固我们的记忆,同时也能加深我们对javascript这门编程语言的认识。

new

面试中经常会问到在我们使用new关键字的时候,具体发生了什么。自己实现一个其实就一目了然。

    function myNew(func,...args) { //传入构造函数及参数
        const instance = {}; //生成一个空对象
        if(func.prototype) { //为指定空对象的原型为构造函数的prototype
            Object.setPrototypeOf(instance,func.prototype); //为指定空对象的原型为构造函数的prototype
        }
        const  res = func.apply(instance,args); //指定构造函数的this为新生成的对象调用构造函数
        //返回新的对象
        if(typeof res == "function" || (typeof res == "object" && res !== null)) {
            return res;
        }
        return  instance;
    }
    //测试代码
    function  Person(name) {
        this.name = name;
    }
    Person.prototype.sayName = function (){
        console.log(`My name is ${this.name}`);
    }
    const me = myNew(Person,"Lance");
    me.sayName();

完成上面的代码,我们再来回答new发生了什么,是不是就so easy了!
①生成一个新的对象
②设置新对象的原型为构造函数的prototype
③指定构造函数的上下文(this)为新对象并执行构造函数
④返回新对象

call,bind,apply

这三个我想一起讲,因为它们常在面试中被一起问到。

问:怎么修改函数的上下文(this)
答:使用call,apply或者bind都可以修改函数的上下文。
再问:它们三者有什么区别呢?
答:emmm…

接下来我们就自己实现一下这三个方法,看一看他们到底有什么区别!

	Function.prototype.myCall = function (context = globalThis) {
        let args = [...arguments].slice(1); //保存参数
        //利用Symbol不会重复的特效生成一个key,防止覆盖传入对象的属性
        let key = Symbol("key");
        context[key] = this; //将函数作为要指定this的一个属性
        let res = context[key](...args);//执行上一步保存的方法
        delete  context[key];//删除该属性
        return res;//返回函数执行结果
    }
    Function.prototype.myApply = function (context = globalThis) {
        let args = [...arguments][1];//保存参数
        //利用Symbol不会重复的特效生成一个key,防止覆盖传入对象的属性
        let key = Symbol("key");
        context[key] = this; //将函数作为要指定this的一个属性
        let res;
        if(args){ //如果没有传参数,可能会取到undefined
            res = context[key](...args);
        }
        else {
            res = context[key]();
        }
        delete  context[key]; //删除该属性
        return res; //返回函数执行结果
    }
    Function.prototype.myBind = function (){
        var _this = this; //保存函数
        var context = [].shift.call(arguments); //获取要绑定的上下文
        var args = [].slice.call(arguments);//获取参数
        const newFunc = function (){
            //兼容处理new的方式调用
            if(this instanceof newFunc){ //如果使用new的方式调用,需要继承原函数
                newFunc.prototype = Object.create(_this.prototype);
                return _this.apply(this,[].concat.call(args,[].slice.call(arguments)));
            }
            return _this.apply(context,[].concat.call(args,[].slice.call(arguments)));
        }
        return newFunc;
    }

下面我们来总结一下他们的异同
①三个方法的第一个参数均为要指定的上下文对象,call和bind后续可以接收一系列参数,而bind第二个参数为一个数组。
②call和apply是指定上下文执行函数,返回的是一个执行结果,而bind返回的是一个待执行函数。

节流

什么是函数节流?
限制一个函数在一定时间内只能执行一次。
为什么需要节流?
开发过程中,有一些事件或者函数,会被频繁地触发(onresize,scroll,mousemove ,mousehover),不做限制的话,会造成不必要的性能浪费。

节流的整体实现思路为添加一个锁,然后延迟执行函数,一次执行完以后才会解锁重新起定时器执行下一次。
代码实现:

function throttle(func,ms=1000)  {
        let canRun = true;
        return function (...args){
            if(!canRun){
                return;
            }
            canRun = false
            setTimeout(()=>{
                func.apply(this,args);
                canRun = true;
            })
        }

    }

防抖

什么是函数防抖?
触发事件后,在延迟时间内函数只能执行一次,如果触发事件后在延迟时间内又触发了事件,则会重新计算函数延执行时间。等延迟时间计时完毕,则执行目标代码。
为什么需要防抖?
一个查询输入框,每次输入都会进行查询,如果输入速度很快,前一次输入查询还没结果有需要进行新的查询,而实际需要的是最后一次输入结束后的结果,前面的查询就造成浪费。
代码实现:

function debounce(func,ms=1000) {
        let timer;
        return function (...args) {
            if(timer){
                clearTimeout(timer)
            }
            timer = setTimeout(()=>{
                func.apply(this,args);
            },ms)
        }
    }

instanceof

instance用于引用类型的类型判断,原理是判断左边是否在右边的原型链上。

function myInstanceof(left,right) {
        if(left === null){return false;}

        if(left.__proto__ === null) {
            return false;
        }
        if(left.__proto__ === right.prototype) {
            return true;
        }
        return  myInstanceof(left.__proto__,right);

    }

deepCopy

function deepCopy(obj,cache = new WeakMap()) {
        if(! obj instanceof Object) {
            return obj;
        }
        if(cache.get(obj)){
            return cache.get(obj);
        }
        if(obj instanceof  Function) { //兼容function
            return  function () {
                return obj.call(this,...arguments);
            }
        }

        if(obj instanceof Date) { //兼容date
            return new  Date(obj);
        }

        if(obj instanceof RegExp) { //兼容正则
            return new RegExp(obj);
        }
        //创建一个空对象,使用原型上的构造函数直接new,会自动创建空数组或者对象
        const res = new obj.constructor;
        cache.set(obj,res); //处理对象循环引用的情况
        Object.keys(obj).forEach((key)=>{
            if(obj[key] instanceof Object) {
                res[key] = deepCopy(obj[key],cache);
            }else {
                res[key] = obj[key];
            }
        })
        return res;
    }
    // 测试
    const source = {
        name: 'Lance',
        meta: {
            age: 12,
            birth: new Date('1995-09-18'),
            ary: [1, 2, { a: 1 }],
            say() {
                console.log('Hello');
            }
        }
    }
    source.source = source
    const newObj = deepCopy(source)
    console.log(newObj.meta.ary[2] === source.meta.ary[2]); // false
    console.log(newObj.meta.birth === source.meta.birth); // false

柯理化

什么叫函数柯理化?
只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

	function curry(func) {
        let argArr = [];
        return function curried(...args){
            argArr = argArr.concat(args)
            if(argArr.length == func.length){ //如果参数等于func的形参数量,则执行函数
                return func.apply(this,argArr);
            }
            return curried;
        }
    }
    function sum(a,b,c) {
        return a+b+c;
    }
    const currySum = curry(sum);
    console.log(currySum(1)(2)(3));

基于柯理化还会有一道很经典的算法题。
实现一个add方法,具有以下效果

	console.log(add(1));         // 1
    console.log(add(1)(2));      // 3
    console.log(add(1)(2)(3));   // 6
    console.log(add(1)(2, 3));   // 6
    console.log(add(1, 2)(3));   // 6
    console.log(add(1, 2, 3));   // 6
	function add() {
        var arg = [...arguments];
        var func = function(){
            arg = arg.concat([...arguments]);//闭包保存参数
            return func;
        }
        func.toString = function () { //重写toString方法 实现打印。
            return arg.reduce(function (pre,cur) {
                return pre+cur;
            },0)
        }

        return func;
    }

    console.log(add(1));         // 1
    console.log(add(1)(2));      // 3
    console.log(add(1)(2)(3));   // 6
    console.log(add(1)(2, 3));   // 6
    console.log(add(1, 2)(3));   // 6
    console.log(add(1, 2, 3));   // 6

事件总线:发布订阅模式

	class EventEmitter {
        constructor() {
            this.cache = {};//管理所有订阅者
        }

        //订阅方法
        on(name,fn) {
            if(this.cache[name]) {
                this.cache[name].push(fn);
            }else {
                this.cache[name] = [fn];
            }
        }

        //取消订阅方法
        off(name,fn){
            const tasks = this.cache[name];
            if(tasks) {
                const index = tasks.findIndex(item => {
                    return item===fn || item.callback === fn;
                })
                if(index>=0) {
                    tasks.splice(index,1);
                }
            }
        }
        //发布
        emit(name, once = false){
            if(this.cache[name]){
                //拷贝一份任务列表,不然任务里注册相同name的任务会造成死循环
                const tasks = this.cache[name].slice();
                for(let fn of tasks) {
                    fn();
                }
                if(once) {//如果是一次性任务
                    delete this.cache[name];
                }
            }
        }
    }
    // 测试
    const eventBus = new EventEmitter()
    const task1 = () => { console.log('task1'); }
    const task2 = () => { console.log('task2'); }
    eventBus.on('task', task1)
    eventBus.on('task', task2)

    setTimeout(() => {
        eventBus.emit('task')
    }, 1000)

es5实现继承

方式有很多,这里就不一一实现了,可以参考我的另一篇文章。
深入了解原型链,es5应该如何实现继承

异步任务并发数限制

	/**
     * 关键点
     * 1. new promise 一经创建,立即执行
     * 2. 使用 Promise.resolve().then 可以把任务加到微任务队列,防止立即执行迭代方法
     * 3. 微任务处理过程中,产生的新的微任务,会在同一事件循环内,追加到微任务队列里
     * 4. 使用 race 在某个任务完成时,继续添加任务,保持任务按照最大并发数进行执行
     * 5. 任务完成后,需要从 doingTasks 中移出
     */

    function limit(count, array, iterateFunc) { 
        const tasks = [];//任务队列
        const doingTasks = [];//执行队列
        let i = 0
        const enqueue = () => {
            if (i === array.length) { //如果长度满了,执行全部任务
                return Promise.resolve()
            }
            const task = Promise.resolve().then(() => iterateFunc(array[i++]));//创建一个待执行任务
            tasks.push(task);
            const doing = task.then(() => doingTasks.splice(doingTasks.indexOf(doing), 1));//执行异步任务的任务
            doingTasks.push(doing)
            const res = doingTasks.length >= count ? Promise.race(doingTasks) : Promise.resolve()
            return res.then(enqueue)
        };
        return enqueue().then(() => Promise.all(tasks))
    }

    // test
    const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i));//异步任务

    limit(2, [1000, 1000, 1000, 1000], timeout).then((res) => {
        console.log(res);
    })

异步串行||异步并行

	function asyncAdd(a, b, callback) {
        setTimeout(function () {
            callback(null, a + b);
        }, 500);
    }

    // 解决方案
    // 1. promisify
    const promiseAdd = (a, b) => new Promise((resolve, reject) => {
        asyncAdd(a, b, (err, res) => {
            if (err) {
                reject(err)
            } else {
                resolve(res)
            }
        })
    })

    // 2. 串行处理
    async function serialSum(...args) {
        return args.reduce((task, now) => task.then(res => promiseAdd(res, now)), Promise.resolve(0))
    }

    // 3. 并行处理
    async function parallelSum(...args) {
        if (args.length === 1) return args[0]
        const tasks = []
        for (let i = 0; i < args.length; i += 2) {
            tasks.push(promiseAdd(args[i], args[i + 1] || 0))
        }
        const results = await Promise.all(tasks)
        return parallelSum(...results)
    }

    // 测试
    (async () => {
        console.log('Running...');
        const res1 = await serialSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
        console.log(res1)
        const res2 = await parallelSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
        console.log(res2)
        console.log('Done');
    })()

promise

	class MyPromise {
        constructor(func) {
            this.status="pending"; //状态,初始未pending
            this.value = null; //用于管理传递的参数
            this.resolvedTasks = []; //then链注册的resolved任务
            this.rejectedTasks = []; //then链注册的rejected任务
            this._resolve = this._resolve.bind(this); //保证调用的时候上下文是对应的promise实例
            this._reject = this._reject.bind(this); //保证调用的时候上下文是对应的promise实例
            try{
                func(this._resolve,this._reject);
            }catch (err) {
                this._reject(err);
            }
        }

        _resolve(value){ //resolve方法
            setTimeout(()=>{
                this.status = "fulfilled"; //修改状态为fulfilled
                this.value = value; //保存传递的参数
                this.resolvedTasks.forEach((task)=>{ //执行then注册的onFulfilled任务
                    task(value);
                })
            })
        }

        _reject(reason){
            setTimeout(()=>{
                this.status = "rejected"; //修改状态为fulfilled
                this.value = reason; //保存传递的参数
                this.rejectedTasks.forEach((task)=>{ //执行then注册的onRejected任务
                    task(reason);
                })
            })
        }
        then(onFulfilled,onRejected){
            return new MyPromise((resolve,reject)=>{
                this.resolvedTasks.push((value)=>{
                    try{
                        const res = onFulfilled(value);//执行then注册的onFulfilled任务
                        if(res instanceof MyPromise){  //如果返回的是一个promise
                            //用then注册任务,传入本该执行resolve和reject 链回原来的路径
                            res.then(resolve,reject);
                        }else {
                            resolve(value);//执行下一个promise的resolve
                        }
                    }
                    catch (err) {
                        reject(err);
                    }
                });
                this.rejectedTasks.push((value)=>{
                    try{
                        const res = onRejected(value);//执行then注册的onRejected任务
                        if(res instanceof MyPromise){  //如果返回的是一个promise
                            //用then注册任务,传入本该执行resolve和reject 链回原来的路径
                            res.then(resolve,reject);
                        }else {
                            resolve(value);//执行下一个promise的resolve
                        }
                    }
                    catch(err){
                        reject(err);
                    }
                })
            })
        }
        catch(onRejected) {
            return this.then(null, onRejected);
        }
    }

    // 测试
    new MyPromise((resolve) => {
        setTimeout(() => {
            resolve(1);
        }, 500);
    }).then((res) => {
        return new MyPromise((resolve) => {
            setTimeout(() => {
                resolve(2);
            }, 500);
        });
    }).then((res) => {
        console.log(res);
    }).then(res=>{
        console.log(res);
    }).catch(res=>console.log(res))
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值