美文网首页
手写代码面试题

手写代码面试题

作者: scrollHeart | 来源:发表于2024-01-30 16:39 被阅读0次

    手写获取数组的重复元素,要求尽可能用多种方法实现

    Let arr = [11, 23, 26, 23, 11, 9]
    Const filterUnique = arr => arr.filter(i => arr.indexOf(i) !== arr.lastIndexOf(i))
    console.log(filterUnique(arr))  // [11, 23, 23, 11]
    

    手写 Array.prototype.reduce()

    array.reduce(): 接收两个参数,一个处理数据的函数,一个是初始值
    接收的处理数据函数,可接收4个参数(total, curValue, curIndex, array)
    total(上一次调用回调返回的值,或者是提供的初始值)
    curValue(数组中当前被处理的元素)
    curIndex(当前元素在数组中的索引)
    array(调用reduce的数组)

    手动实现

    1. 定义一个函数myfunction(callback, initValue)
      当this或者callback不为正确语法,抛出错误;this指向调用reduce的数组
      把调用的值通过object(this)包装成一个类对象。

    Array.prototype.reduce.call(1212, (pre,cut) =>{return ‘’ + pre+cur}, 0)

    把手写的myReduce写入Array的原型链上Array.prototype.MyReduce = MyReduce;

    function MyReduce(fun, initValue){
        if (this === null){
            // this 不存在,抛出错误
            throw new TypeError(‘called on null or undefined’)
        }
        if (typeof fun !== ‘function’) {
            // fun 不是function时,抛出错误
            throw new TypeError(fun + ‘is not a function’);
        }
        const value = Object(this);
        let preValue, curValue, curIndex;
        if(initValue !== undefined){
            preValue = initValue;
            curValue = arr[0];
            curIndex = 0;
        } else {
            preValue = arr[0];
            curValue = arr[1];
            curIndex = 1
        }
        for (let I = curIndex; I < value.length; I++){
            const item = value[I];
            preValue = fun(preValue, item, I, arr);
        }
        return preValue;
    }
    
    Function setReduce(preValue, curValue, index, arr){
        return `${preValue} - ${curValue}`
    }
    
    // 把方法写入原型链上
    Array.prototype.MyReduce = MyReduce;
    
    2.Array.prototype.myReduce = function(callbackFn, initialValue) {
    // 处理回调类型异常
        if(Object.prototype.toString.call(callbackFn) !== ‘[object Function]’) {
            throw new TypeError(callbackFn + ‘is not a function’)
        }
        
        var acc = initialValue || this[0];
        var startIndex = initialValue ? 0 : 1;
    
        for(var I = startIndex, Len = this.length; I < Len; I++){
            acc = callbackFn(acc, this[I], I, this)
        }
        return acc;
    }
    
    Const arr = [1, 2, 3, 4]
    arr.myReduce((total, cur) => total + cur);  // 10
    arr.myReduce((total, cur) => total + cur, 10);  // 20
    Arr.myReduce(’555’);  // 555 is not a function
    

    手写 Promise

    先回顾下最简单的Promise使用方式:

    const p1 = new Promise((resolve, reject) => {
        console.log(‘create a promise’)
        resolve(‘成功了’)
    })
    console.log(‘after new promise’)
    
    const p2 = p1.then(data => {
        console.log(data)
        throw new Error(’失败了’)
    })
    
    const p3 = p2.then(data => {
        console.log(‘success’, data)
    }, err => {
        console.log(‘faild’, err) 
    })
    

    promise有三个状态:pending, fulfilled, or rejected
    New promise时, 需要传递一个executor()执行器,立即执行
    executor接受两个参数,分别是resolve和reject;
    promise 的默认状态:pending
    Promise 有一个value保存成功状态的值,可以是undefined/thenable/promise
    Promise 有一个reason保存失败状态的值,
    promise 只能从pending 到 rejected, 或者从 pending 到 fulfilled, 状态一旦确认,就不会再改变
    promise 必须有一个then方法,then 接收两个参数,分别是promise成功的回调onFulfilled, 和 promise 失败的回调onRejected
    若调用then, promise 已经成功,执行onFulfilled, 参数是promise 的value
    若调用then,promise 已经失败,执行onRejected,参数是promise 的reason
    then 抛出异常,把异常作为参数,传递下一个then的失败回调onRejected

    // 三个状态: pending, fulfilled, rejected
    Const pending = ‘pending’;
    Const fulfilled = ‘fulfilled’;
    Const rejected = ‘rejected’;
    Class Promise {
        constructor(executor){
            // 默认状态为pending
            this.status = pending;
            // 存放成功状态的值,默认为undefined
            this.value = undefined;
            // 存放失败状态的值,默认为undefined
            this.reason = undefined;
    
            // 调用此方法就是成功
            let resolve = (value) => {
                // 状态为pending时可以更新状态,防止executor中调用两次resolve/reject方法
                if (this.status === pending){
                    this.status = fulfilled;
                    this.value = value;
                }
    
            }
            // 调用此方法就失败
            let reject = (reason) => {
                // 状态为pending时可以更新状态,防止executor中调用两次resolve/reject方法
                if (this.status === pending){
                    this.status = rejected;
                    this.reason = reason;
                }
    
            }
    
            try {
                executor(resolve, reject)
            } catch(error) {
                // 发生异常时执行失败逻辑
                reject(error)
            }
            //包含一个then,接收两个参数onFulfilled, onRejected
    
            then(onFulfilled, onRejected){
                if(this.status === fulfilled) {
                    onFulfilled(this.value)
                }
                if(this.status === rejected) {
                    onRejected(reason)
                }
            }
        }
    
    }
    

    手写 apply

    Function.prototype.myApply = function(context){
        if(context === undefined || context === null){
            context = window
        } else {
            context = Object(context)
        }
        // 判断是否为类数组对象
        function isArrayLike(o) {
            if (
                o &&                                      // o 不是null,undefined等
                typeof o === ‘object’ &&    // o是对象
                isFinite(o.length) &&           // o.length是有限数值
                o.length >= 0 &&                // o.length为非负值
                o.length === Math.floor(o.length) &&      // o.length是整数
                o.length < Math.pow(2,32)
            ){
                return true
            }
            return false
        }
        const fn = Symbol(‘uniqueFn’)
        context[fn] = this
        let args = arguments[1]
        let result  
        if (!Array.isArray(args) && isArrayLike(args)){
            throw new Error(‘CreateListFromArrayLike called on non-object’)  // 第二个参数不为数组且不为类对象数组
        } else {
            args = Array.from(args)     // 转为数组
            result = context[fn](…args)
        }
        delete context[fn]
        return result
    }
    

    手写 call 函数

    call 接收多个参数,第一个为函数上下文也就是this,后边参数为函数本身的参数

    Function.prototype.myCall = function(context){
        if(context === undefined || context === null){
            context = window // 指定null 和 undefined 的this值会自动指向全局对象
        } else {
            context = Object(context) // 值为原始值(数字,字符串,布尔值)的this会指向该原始值的实例对象 
        }
        const fn = Symbol(‘uniqueFn’)  // 用Symbol是防止跟上下文的原属性冲突
        context[fn] = this
        let arg = […arguments].slice(1)
        let result = context[fn](…arg)
        delete context[fn]
        return result
    }
    

    手写bind方法

    实现bind要额外考虑一个问题:方法一个绑定函数也能使用new操作符创建对象:这种行为像把原函数当成构造器,提供的this值被忽略,同时调用时的参数被提供给模拟函数

    Function.prototype.myBind = function(context){
        if (typeof this !== ‘function’){
            throw new Error(‘Expected this is a function’)
        }
        let self = this
        let args = […arguments].slice(1)
        let fn = function(…innerArg){
            const isNew = this instanceof fn     // 返回的fn是否通过new调用
            return self.apply(isNew ? This : Object(context), args.concat(innerArg)). // new 调用就绑定到this上,否则绑定到传入的context上
        }
        // 复制原函数的prototype给fn,一些情况下函数没有prototype,比如箭头函数
        if(self.prototype){
            fn.prototype = Object.create(self.prototype)
        }
        return fn    // 返回拷贝的函数
    }
    

    手写 new

    New 到底做了什么?

    • 创建一个新的对象
    • 继承父类原型上的方法
    • 添加父类的属性到新的对象上并初始化,保存方法的执行结果
    • 如果执行结果有返回值并且是一个对象,返回执行的结果,否则,返回新创建的对象
    Function _new(obj, …rest){
        // 基于obj的原型创建一个新的对象
        const newObj = Object.create(obj.prototype);
        // 添加属性到新创建的newObj上,并获取obj函数执行的结果
        const result = obj.apply(newObj, rest);
        // 如果执行结果有返回值并且是一个对象,返回执行的结果,否则,返回新创建的对象
        return typeof result === ‘object’ ? result : newObj;
    }
    

    手写深拷贝

    基础版本:

    function deepClone(target){
        let result;
        if (typeof target === ‘object’){
            if(Array.isArray(target)){
                result = []
                for(let I in target){
                    result.push(deepClone(target[I]))
                }
            }
            else {
                result = {}
                for (let key in target){
                    result[key] = target[key]
                }
            }
        } else {
            result = target;
        }
        return result;
    }
    Let A = [1,2,3, {a:1, b:2}]
    Let B = deepClone(A)
    B[3].a = 99
    console.log(A).   // [1,2,3,[4,5]]
    console.log(B)    // [1,2,3,{a:99, b: 2}]
    

    改用class类写

    class DeepClone{
        constructor(){
            cloneVal: null;
        }
        clone(val, map = new WeakMap()){
            let type = this.getType(val);    // 当是引用类型的时候先拿到其确定的类型
            if(this.isObj(val)){
                switch(type){
                    case ‘date’:              // 日期类型重新new 一次传入之前的值,data实例化本身结果不变
                        return new Date(val);
                        break;
                    case ‘regexp’:       // 正则类型直接new 一个新的正则传入source和flags即可
                        return new RegExp(val.source, val.flags);
                        break;
                    case ‘function’:    // 若是函数类型是直接通过function包裹返回一个新的函数,并且改变this指向
                        return new RegExp(val.source, val.flags);
                        break;
                    default: 
                        this.cloneVal = Array.isArray(val) ? [] : {};
                        if (map.has(val)) return map.get(val)
                        map.set(val, this.cloneVal)
                        for(let key in val){
                            if (val.hasOwnProperty(key)) {. // 判断是不是自身的key
                                this.cloneVal[key] = new DeepClone().clone(val[key], map);
                            }
                        }
                        return this.cloneVal;
                }
            } else {
                return val;    // 当是基本数据类型的时候直接返回
            }
        }
        /** 判断是否是引用类型*/
        isObj(val) {
            return (typeof val == ‘object’ || typeof val == ‘function’) && val != null
        }
        /**获取类型*/
        getType(data){
            var s = Object.prototype.toString.call(data);
            return s.match(/\[object(.*?)\]/)[1].toLowerCase();
        }
    }
    
    /** 测试*/
    var a = {
        a: 1,
        b: true,
        c: undefined,
        d: null,
        e: function(alb){
            return a + b
        },
        f: /\W+/gi,
        time: new Date(),
    }
    Const deepClone = new DeepClone()
    Let b = deepClone.clone(a)
    console.log(b)
    

    手写虚拟 dom 转换成真实 dom

    {
        tag: ‘DIV’,
        attrs: {
            id: ‘app’
        },
        children: [
            {
                tag: ’SPAN’,
                children: [
                    {tag: ‘A’, children: []}
                ]
            },
            {
                tag: ’SPAN’,
                children: [
                    {tag: ‘A’, children: []},
                    {tag: ‘A’, children: []}
                ]
            }
        ]
    }
    

    // 把上述虚拟Dom转化成下方真实Dom

    <div id=“app”>
        <span>
            <a></a>
        </span>
        <span>
            <a></a>
            <a></a>
        </span>
    </div>
    

    // 真正的渲染函数

    function _render(vnode){
        // 如果是数字类型转化为字符串
        if(typeof node === ’number’){
            vnode = String(vnode);
        }
        // 字符串类型直接是文本节点
        if (typeof vnode === ‘string’){
            return document.createTextNode(vnode);
        }
        // 普通DOM
        const dom = document.createElement(vnode.tag);
        if (vnode.attrs){
            // 遍历属性
            Object.keys(vnode.attrs).forEach((key) => {
                const value = vnode.attrs[key];
                dom.setAttribute(key, value);
            })
        }
        // 子数组进行递归操作 这一步是关键
        vnode.children.forEach((child) => dom.appendChild(_render(child)));
        return dom;
    }
    

    手写 assign,要考虑全面,包括 symbol 也要考虑在内

    let obj1 = {
        a: 1,
        b: 2
    }
    obj1[Symbol(‘abc’)] = ‘abc’
    
    let obj2 = {
        a: 3,
        d: 5
    }
    
    obj2[Symbol(‘abc’)] = ‘abc’
    function myAssign(…obj){
        let res = {}
        for(let obj of objs) {
            for(let prop of Reflect.ownKeys(obj)) {
                res[prop] = obj[prop]
            }
        }
        return res
    }
    let obj3 = myAssign(obj1, obj2)
    console.log(obj3)
    

    手写 ES6 的模板字符串

    // 1.创建一个正则表达式
        let args =  //
    // 2.从${}中取值
        let args = /${}/
    // 3.由于$, {, }都是有特殊含义的,需要转义
    let args = /\$\{\}/
    
    // 4.现在要将${}中的变量名取到,需要使用()
    /* 正则圆括号:
    (1)在被修饰匹配次数的时候,括号中的表达式可以作为整体被修饰
    (2)取匹配结果的时候,括号中的表达式匹配到的内容可以被单独得到
    */
    
    let args = /\$\{()\}/
    
    // 5.变量名可能是任意的字符, 匹配任意的是. 变量的名称可能是多个字符,需要用到 + 还需要全局匹配
    let args = /\$\{(.+)\}/g
    
    // 6.步骤5中的正则,是贪婪模式的,如果存在多个${}, 那第一次把所有匹配到的内容都拿到,是这样的
    // 惰性的正则(加?可以使其变为惰性的,取到1次就不再取了)
    let args = /\$\{(.+?)\}/g
    // 实现strParse函数
    function strParse(string){
        let args = /\$\{(.+?)\}/g
        // replace方法有两个参数,第二个参数是要替换的内容,或者是一个返回字符串的函数,这个函数有几个参数
        return string.replace(args, function(){
            return eval(arguments[1])
        })
    }
    

    // 测试一下

    Let esStr = ‘第一个字符串是${str1}, 第二个字符串是${str2}’
    Let str = strParse(esStr)
    console.log(str) // 第一个字符串是abc,第二个字符串是def
    

    手写发布订阅模式,订阅,触发,移除

    发布-订阅模式其实是一种对象间一对多的依赖关系 ,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知,订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel), 当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码

    实现思路:
    1.创建一个EventEmitter类
    2.在该类上创建一个事件中心(Map)
    3.on方法用来把函数fn都加到事件中心中(订阅者注册事件到调度中心)
    4.emit方法取到arguments里第一个当做event,根据event值去执行对应事件中心中的函数(发布者发布事件到调度中心,调度中心处理代码)
    5.off方法可根据event值取消订阅
    6.once方法只监听一次,调用完毕后删除缓存函数(订阅一次)
    7.注册一个newListener用于监听新的事件订阅

    第一步,创建一个类,并初始化一个事件存储中心

    class EventEmitter{
        // 用来存放注册的事件和回调
        constructor(){
            this._events = {};
        }
    }
    

    第二步,实现事件的订阅方法on(将事件回调函数存储到对应的事件上)

    class EventEmitter{
        // 用来存放注册的事件与回调
        constructor(){
            this._events = {}
        }
        on(eventName, callback){
            // 由于一个事件可能注册多个回调函数,所以使用数组来存储事件队列
            const callbacks = this._events[eventName] || [];
            callbacks.push(callback);
            this._events[eventName] = callbacks
        }
    }
    

    第三步,实现事件的发布方法emit(基本思路:获取到事件对应的回调函数依次执行)

    class EventEmitter{
        // 用来存放注册的事件和回调
        constructor(){
            this._events = {}
        }
        // args用于收集发布事件时传递的参数
        emit(eventName, …args){
            const callbacks = this._events[eventName] || [];
            callbacks.forEach(cb => cb(…args))
        }
    }
    

    第四步,实现事件的取消订阅的方法off(找到事件对应的回调函数,删除对应的回调函数)

    class EventEmitter{
        // 用来存放注册的事件和回调
        constructor(){
            this._events = {}
        }
        off(eventName, callback){
            const callbacks = this._events[eventName] || []
            const newCallbacks = callbacks.filter(fn => fn != callback && fn.initialCallback != callback // 用于once的取消订阅)
            this._events[eventName] = newCallbacks
        }
    }
    

    第五步,实现事件的单次订阅方法once(基本思路:1先注册 2.事件执行后取消订阅)

    class EventEmitter{
        // 用来存放注册的事件与回调
        constructor(){
            this._events = {}
        }
        once(eventName, callback){
            // 由于需要在回调函数执行后,取消订阅当前事件,需要对传入的回调函数做一层包装,然后绑定包装后的函数
            const one = (…args) => {
                // 执行回调函数
                callback(…args)
                // 取消订阅当前事件
                this.off(eventName, one)
            }
            // 考虑:如果当前事件在未执行,被用户取消订阅,能否取消
    
            // 由于:订阅事件时,修改了原回调函数的引用,所以,用户触发off时不能找到对应回调函数
            // 所以,需要在当前函数与用户传人的回调函数做一个绑定,通过自定义属性来实现
            one.initialCallback = callback;
            this.on(eventName, one)
        }
    }
    

    第六步,注册一个newListener用于监听新的事件订阅(在用户注册事件时,发布一下newListener事件)

    class EventEmitter{
        // 用来存放注册的事件和回调
        constructor(){
            this._events = {}
        }
        on(eventName, callback){
            // 如果绑定事件不是newListener就触发回调
            if (this._events[eventName]){
                if(this.eventName !== ‘newListener’){
                    this.emit(‘newListener’, eventName)
                }
            }
            // 由于一个事件可能注册多个回调函数,使用数组来存储事件队列
            const callbacks = this._events[eventName] || [];
            callbacks.push(callback);
            this._events[eventName] = callbacks
        }
    }
    

    手写斐波那锲数列

    // count: 指的是第几个
    // 返回值是斐波那锲数列第count项的值
    function fibonacci(count){
        if (count === 1 || count === 2) return 1
        return  fibonacci(count - 2) + fibonacci(count -1)
    }
    // for循环版(比前者空间复杂度更低)
    function fibonacci(n){
        let n1 = 1; n2 = 1;
        for(let I = 2; I < n; I++){
            [n1, n2] = [n2, n1+n2]
        }
        return n2
    }
    for(let I = 1; I <=10, I++){
        console.log(fibonacci(I))
    }
    

    手写防抖和节流

    防抖:当事件触发时,相应函数的不会被立即触发,而是会被推迟执行
    只有等待一段时间后也没有再次触发该事件,才会真正执行响应函数

    输入框频繁输入
    频繁点击按钮,触发某个事件
    监听浏览器滚动事件
    监听用户缩放浏览器resize事件

    简易版防抖函数:

    // 第一个参数是需要进行防抖处理的函数,第二个参数是延迟时间,默认是1s,
    function debounce(fn, delay = 1000){
    // 实现防抖函数的核心是使用setTimeout
        // time变量用于保存setTimeout返回的id
        let time = null
        function _debounce(…args){
            // 如果time不为0,有定时器存在,将该定时器清除
            if(time !== null){
                clearTimeout(time)
            }
            time = setTimeout(() => {
                // 使用apply改变fn的this, 同时将参数传递给fn
                fn.apply(this, args)
            }, delay)
        }
        // 防抖函数会返回另一个函数,该函数才是真正被调用的函数
        return _debounce
    }
    

    什么是节流
    节流函数会按照一定的频率来执行函数
    节流类似于技能cd,不管按了多少次,必须等cd结束后才能释放技能,如果在cd时间段,不管触发了几次事件,只会执行一次,只有当下一次cd转换,才会再次执行

    • 实现节流函数
    // interval 间隔时间,也就是cd的长短
    function throttle(fn, interval){
        // 该变量用于记录上一次函数的执行事件
        let lastTime = 0
        
        const _throttle = function(…args){
            // 获取当前时间
            const nowTime = new Date().getTime()
            // cd剩余时间
            const remainTime = nowTime - lastTime
            // 如果剩余时间大于间隔时间,可以再次执行函数
            if(remainTime - interval >= 0){
                fn.apply(this, args)
                // 将上一次函数执行的时间设置为nowTime, 这样下次才能重新进入cd
                lastTime = nowTime
            }
        }
        // 返回_throttle函数
        return _throttle
    }
    

    相关文章

      网友评论

          本文标题:手写代码面试题

          本文链接:https://www.haomeiwen.com/subject/yzanodtx.html