JS手写系列面试题

2,159 阅读8分钟

说在前面

最近这段时间一直在整理学习js的手写部分,有哪写的不对的地方,欢迎指出,对哪些有不同见解的,欢迎补充,也希望这些整理能对你有所帮助!!废话就不多说了,直接进入主题吧!!

es5实现let

  1. 采用自执行函数的形式来定义一个不被污染的变量
(function () {
    for (var i = 0; i < 10; i++) {
        console.log(i);//0 - 9 
    }
})()
console.log(i) //a is not defined

es5实现const

  1. 采用数据劫持defineProperty(只能劫持对象)
  2. 将所有的变量声明挂载到window对象上
  3. 由于es5没有块级作用域的概念,无法完整实现const
function _const(name1,value){
    window.name1 = value;//挂载到window上便于劫持
    Object.defineProperty(window,value,{
        enumerable:false,
        configurable:false,
        get:function(){
            return value
        },
        set: function (newValue) {
            throw TypeError("const声明的变量不能修改");
        },
    })
}

new操作符的实现

  1. 一个新对象被创建
  2. 该对象的__proto__指向该构造函数的原型,即fn.prototype
  3. 将执行上下文(this)绑定到新创建的对象上
  4. 如果构造函数有返回值(对象或函数),那么这个返回值将取代第一步中新创建的对象
function myNew(fn,...args){
    //一个新对象被创建
    let res = {};
    //该对象的__proto__指向该构造函数的原型,即fn.prototype
    if(fn.prototype !== null){
        // res.__proto__ = fn.prototype;
        Object.setPrototypeOf(res,fn.prototype);
    }
    //将函数的执行上下文绑定到fn上,并执行
    const returnRes = fn.apply(res,args);
    //如果构造函数有返回值(对象或函数),那么这个返回值将取代第一步中新创建的对象
    if((typeof returnRes == "object" || typeof returnRes == "Function") && returnRes != null){
        return returnRes;
    }
    return res;
}

instanceof 操作符的实现

  1. 判断隐式原型是否等于显示原型,按原型链一直往上找
function myInstanceof(a,b){
    let a1 = a.__proto__;//隐式原型
    let b1 = b.prototype;//显示原型
    while(a1){
        if(a1 != b1){
            a1 = a1.__proto__;
        }else{
            return true
        }
    }
    return false;
}

call、apply、bind实现

call实现

  1. 获取第一个参数(第一个参数是null或undefined,则this指向window)
  2. 将this所指向的函数赋给对象
  3. 执行函数
  4. 删除传入对象上被赋值的函数
function myCall(obj,...args){
    //this指向问题  为null或undefined则指向window
    obj = obj ? Object(obj) : window;
    //将this所指向的函数赋给对象
    obj.fn = this;
    //执行函数
    let res = obj.fn(...args);
    //删除传入对象上被赋值的函数 消除影响
    delete obj.fn;
    return res;
}

apply实现

  1. apply和call相差无几,一个传入的是数组,一个传入的是参数(大同小异)
function myCall(obj,arr){
    //this指向问题  为null或undefined则指向window
    obj = obj ? Object(obj) : window;
    //将this所指向的函数赋给对象
    obj.fn = this;
    //判断是否传入了参数,然后执行函数
    let res = arr ? obj.fn(...arr) : obj.fn();
    //删除传入对象上被赋值的函数 消除影响
    delete obj.fn;
    return res;
}

bind实现

  1. 能够改变this指向
  2. 返回一个函数
  3. 能接收多个参数
  4. 支持柯里化形式传参 fn(arg1)(arg2)
  5. 获取调用bind()返回值后,若使用new调用(被当作构造函数),bind传入的上下文context失效
  6. 绑定函数也可以使用new运算符构造,提供的this值会被忽略。新的this指向就应该是new运算符构造出来的this指向
function myBind(context,...args){
    if(typeof this != 'Function'){
        throw new TypeError('The bound object needs to be a function')
    }
    // 存下被bind的函数
    const self = this;
    const fNOP = function(){};

    const fBound = function(...fBoundArgs){
        // 利用apply改变this调用
        // 接受多个参数+支持柯里化形式传参
        // 当返回值通过new调用时,this指向当前实例(因为this是当前实例,实例的隐式原型上有fNOP的实例
        return self.apply(this instanceof fNOP ? this : context, [...args,...fBoundArgs]);
    }

    if(this.prototype){
        fNOP.prototype = this.prototype;
    }
    // 通过原型的方式继承调用函数的原型  构造函数继承
    fBound.prototype = new fNOP();

    return fBound;
}

柯里化实现

  1. length 存储函数所需要的参数个数
  2. _args 存储收集到的参数
  3. _args.length < length 则循环调用收集参数 第一种:
//可能这一版本不太好理解
function curry(fn, args) {
    //函数所接收参数的总数
    let length = fn.length;
    args = args || [];

    return function () {
        let _args = args;
        for (let i = 0; i < arguments.length; i++) {
            _args.push(arguments[i]);
        }

        if (_args.length < length) {
            return curry.call(this, fn, _args);//收集参数
        }
        //收集完参数执行
        return fn.apply(this, _args);
    }
}

第二种:

function curry(fn, currArgs) {

    return function () {
        let args = [].slice.call(arguments);

        // 首次调用时,若未提供最后一个参数currArgs,则不用进行args的拼接
        if (currArgs !== undefined) {
            args = args.concat(currArgs);
        }

        // 递归调用
        if (args.length < fn.length) {
            return curry(fn, args);
        }

        // 递归出口
        return fn.apply(null, args);
    }
}

其实,这两种方法大同小异,主要思路都是收集参数然后运算,细心的同学会发现上面我们提到了bind支持柯里化传参,说明bind的实现就是通过柯里化来实现的,但是也有其局限性,如果只用一个bind他只能实现两层,例如res = add.bind(null,1)(2,3),因为bind返回函数,如果执行完就没了,而真正的柯里化是等参数收集完再执行

第三种:

function add(a, b, c){
    return a + b + c
}

let res = add.bind(null,1);
let res1 = res.bind(null,2);
res1(3);//6

扁平化数组

  1. 去掉中间的那些框框 let arr = [1, [2, 3, [4, 5]]];

第一种:es2019的语法

arr.flat(Infinity);//es2019的语法 最直接

第二种:通过JSON.stringify(arr)方法转为字符串

function flatten(arr){
    return JSON.parse(`[${JSON.stringify(arr).replace(/\[|\]/g,'')}]`);
}

第三种:通过concat不断覆盖之前的arr

function flatten(arr){
    //Array.some会遍历整个数组
    //Array.isArray()判断是否为数组
    //每去掉一层[],原数组arr都会发生变化,直到arr中没有数组
    while(arr.some(item => Array.isArray(item))){
        arr = [].concat(...arr)//不断覆盖arr  不断去掉一层[]
    }
    return arr;
}

第四种:递归

function flatten(arr){
    let res = [];
    arr.map(item => {
        if(Array.isArray(item)){
            res = res.concat(flatten(item))
        }else{
            res.push(item)
        }
    })
    return res;
}

第五种: reduce方法

该方法其实和concat大同小异,两者都是数组合并

function flatten(arr){
    return arr.reduce((res,item) =>{
        return res.concat(Array.isArray(item) ? flatten(item) : item)
    },[])
}

实现一个对象的扁平化

深浅拷贝

浅拷贝

  1. 只拷贝第一层,若第一层中有值为引用类型则拷贝其内存地址,也可以这么认为,当你将a的值赋值给b后,修改b中的值a中的值也在变,这就是浅拷贝

第一种:

function copy(item){
    if(typeof item !== 'object' && typeof item !== 'null'){
        return item;
    }
    let obj = {};//这里只考虑对象
    for(let [ket,value] of Object.entries(item)){
        obj[key] = value
    }
    return obj;
}

第二种:Object.assign() 对象合并

深拷贝

  1. 这么认为,将a的值赋值给b,当修改b的值时,a的值不会变

第一种:通过JSON.parse()实现

但是其有一定局限性:

  1. 如果遇到正则表达式的话,其会变为空对象
  2. 如果value为function的话,则会忽略掉该key
  3. 如果value为undefined的话,则会忽略掉该key
  4. 如果value为 NaN、Infinity或-Infinity,则该key会被赋值为null
  5. 存在循环引用的话,也无法正确深拷贝

image.png

function deepCopy(obj){
    return JSON.parse(JSON.stringify(obj))
}

第二种:递归实现

  1. 通过递归判断,如果遇到value为函数则递归
  2. 存在爆栈的风险(递归调用次数太多,堆栈会有溢出风险)
function deepCopy(obj){
    if(typeof obj == 'object' && typeof obj !== null){
        let temp = Array.isArray(obj) ? [] : {};//判断类型并创建相应类型
        for([key,value] of Object.entries(obj)){
            if(typeof value == 'object' && value !== null && Object.prototype.toString.call(value) !== '[object RegExp]'){
                temp[key] = deepCopy(value);//value为对象
            }else{
                temp[key] = value
            }
        }
        return temp
    }else{
        return obj;
    }
}

防抖

  1. 防止误操作,在一定时间内只执行第一次
  2. 用闭包实现
function debounce(fn,delay){
    let timer = null;//返回一个函数,也将这个属性返回出去,从而形成闭包,垃圾回收机制清除不了

    return function(){
        let that = this;
        let arg = arguments;//类数组(不具备数组的属性)
        clearTimeout(timer);//清除定时器
        timer = setTimeout(() => {
            fn.apply(that,[...arg]);//fn在这被执行,this指向被修改
        }, delay);
    }
}

节流

  1. 在规定的时间内只执行一次
  2. 通过闭包保存一个执行标记
function throttle(fn,delay) {
    let canRun = true; // 通过闭包保存一个标记
    return function () {
         // 在函数开头判断标记是否为true,不为true则return
        if (!canRun) return;
         // 立即设置为false
        canRun = false;
        // 将外部传入的函数的执行放在setTimeout中
        setTimeout(() => { 
        // 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。
        // 当定时器没有执行的时候标记永远是false,在开头被return掉
            fn.apply(this, arguments);
            canRun = true;
        }, delay);
    };
}

AJAX实现

  1. 依靠XMLHttpRequest对象与服务端交互--- var ajax = new XMLHttpRequest();
  2. 初始化 HTTP 请求参数,例如 URL 和 HTTP 方法,但是并不发送请求。 --- ajax.open(method,url)
  3. 当为post请求时,JSON.stringify(data)处理下数据
  4. 实现链式调用
function myAjax({ method, url, data }) {
    return new Promise((resolve, reject) => {
        const ajax = new XMLHttpRequest();
        ajax.open(method.toUpperCase(), url, false)
        ajax.setRequestHeader("Content-Type", "application/json");
        ajax.onreadystatechange = function () {
            if (ajax.readyState == 4 && ajax.status == 200) {
                resolve(ajax.responseText);
            } else {
                reject(ajax.responseText)
            }
        }
        ajax.send(method.toUpperCase() === 'POST' ? JSON.stringify(data) : null)
    })
}

Promise及其相关方法的实现

Promise实现

  1. 三个状态且变化之后是不可逆的 pending、rejected、fulfilled

  2. promise.then方法中的两个参数是可选的,且两个参数都需要是函数,否则忽略

  3. then方法的第一个参数函数执行的条件是状态变为fulfilled,第二个参数函数执行条件是状态为rejected,且都最多只能被调用一次

  4. 因为then方法中两个参数函数的执行是需要状态改变的,当状态还未改变时需要将then的参数方法放到对应容器中等状态变化后再全部统一执行

    例如:

    new Promise((resolve, reject) => {
        setTimeout(() => {//挂起
            resolve('你好帅!!');
        }, 1000)
    }).then((res) => {//执行微任务
        console.log(res);
    })
    
  5. Promise能链式调用,所以then方法应该是返回一个Promise对象

  6. 原生Promise中,如果then方法return返回自身的话会报错

// 三个状态
const RESOLVE = 'fulfilled';//已完成
const REJECT = 'rejected';//已失败
const PENDING = 'pending';//进行中


class myPromise {
    status = PENDING;//状态变更
    res = undefined;//保存留给then方法用
    err = undefined;
    onRejectedArr = [];//状态还未变更时,存储回调函数
    onResolvedArr = [];
    constructor(fn) {
        const resolve = (res) => {
            if (this.status === PENDING) {
                this.res = res;
                this.status = RESOLVE;
                // 状态变更后调用
                this.onResolvedArr.map((fn) => fn());
            }
        }
        const reject = (err) => {
            if (this.status === PENDING) {
                this.err = err;
                this.status = REJECT;
                this.onRejectedArr.map((fn) => fn());
            }
        }
        try {
            fn(resolve, reject);//执行fn
        } catch (err) {
            reject(err)
        }
    }
    
    //then方法
    then(onResolved, onRejected) {
        //onResolved, onRejected可选参数
        //onResolved, onRejected必须作为函数调用
        const seft = this;
        onResolved = typeof onResolved == 'function' ? onResolved : (res) => res;
        onRejected = typeof onRejected == 'function' ? onRejected : (err) => { throw new Error(err) };
        // 实现链式调用
        const newPromise = new myPromise((resolve, reject) => {
            if (seft.status === RESOLVE) {
                // .then后的代码需要在自己的执行栈中执行  佯装一下  原生Promise.then是微任务
                setTimeout(() => {
                    try {
                        const result = onResolved(seft.res);
                        handlePromise(result, myPromise, resolve, reject);
                    } catch (error) {
                        reject(error)
                    }
                }, 0)
            }
            if (seft.status === REJECT) {
                setTimeout(() => {
                    try {
                        const result = onRejected(seft.err)
                        handlePromise(result, myPromise, resolve, reject);
                    } catch (error) {
                        reject(error)
                    }
                })
            }
            // promise里面是异步的话,状态还未变更,先存储到各自的数组中,状态变更后再调用 
            // 这一点采用了发布订阅模式
            if (seft.status === PENDING) {
                seft.onResolvedArr.push(() => {//订阅
                    try {
                        const result = onResolved(seft.res);
                        seft.handlePromise(result, myPromise, resolve, reject);
                    } catch (error) {
                        reject(error)
                    }
                });
                seft.onRejectedArr.push(() => {
                    try {
                        const result = onRejected(seft.err);
                        seft.handlePromise(result, myPromise, resolve, reject);
                    } catch (error) {
                        reject(error)
                    }
                });
            }
        })
        // then方法返回的是一个新的Promise实例
        return newPromise;
    }
    
    //catch方法其实就相当于then(undefined, onRejected)的别名
    catch(onRejected) {
        return this.then(undefined, onRejected);
    }
    
    handlePromise(result, myPromise, resolve, reject) {
        // 如果result和myPromise引用的是同一个对象,则抛出错误
        // 原生Promise如果return返回自身的话会报错
        if (result === myPromise) {
            throw new Error('can not return oneseft')
        }

        // 如果返回的是其他promise,promise也可能是一个构造函数
        if ((result && typeof result == 'object') || typeof result == 'function') {
            let lock = 'false'//控制then方法里面回调函数的执行
            try {
                // 是否又then方法
                const then = result.then;
                // then是一个函数的话就认为是一个promise
                if (typeof then === 'function') {
                    then.call(result,
                        res => {
                            if (lock) return;
                            // then里面可能还有then   这里递归
                            handlePromise(res, myPromise, resolve, reject);
                            lock = true;
                        },
                        err => {
                            if (lock) return;
                            reject(err);
                            lock = true;
                        })
                } else {
                    // 不是函数直接返回出去
                    resolve(result);
                }
            } catch (error) {
                reject(error)
            }

        } else {
            resolve(result);
        }
    }
}

Promise.resolve()方法实现

  1. 参数如果是Promise实例,该方法将直接返回该实例
  2. 如果参数是具有then方法的对象,该方法则会先将其转化为Promise对象,然后执行里面的then方法
  3. 参数是不具备then方法的对象,或者不是对象,则返回一个新的Promise对象,执行resolve(),状态变为resolved
class Promise {
    // ...
    //私有方法,只能通过Promise.resolve访问
    static resolve(value){
        // 参数如果是Promise实例,该方法将直接返回该实例
        if(value instanceof Promise) return value;

        // 如果参数是具有then方法的对象,该方法则会先将其转化为Promise对象,然后执行里面的then方法
        // 然后就立即执行里面的then方法
        if(typeof value === 'object' || typeof value === 'function'){
            try{
                let then = value.then;
                // 如果then是函数则立即执行
                if(typeof then === 'function'){
                    return new Promise(then.bind(value));
                }
            }catch(e){
                return new Promise((resolve,reject) => {
                    reject(e)
                })
            }
        }

        // 参数是不具备then方法的对象,或者不是对象,则返回一个新的Promise对象,执行resolve(),状态变为resolved
        return new Promise((resolve,reject) => {
            resolve(value)
        })
    }
}

Promise.all()方法实现

  1. 用于实现多个Promsie运行,返回一个新的Promise实例
  2. 接收一个具有迭代器的变量(例如数组),里面存放着Promise实例对象
  3. 如果存放的不是Promise实例对象,则会通过Promise.resolve将其转化为Promise实例
  4. 当迭代器中所有项的状态全变为fulfilled时,结果的状态才会是fulfilled状态
  5. 输入的所有promise的resolve回调的结果是一个数组,如果有一个是rejected状态,此时第一个被reject的值,会被立即返回出来
class Promise {
    // ...
    // 用于将多个Promise实例,包装成一个新的Promise实例,只有所有状态都变为fulfilled,p的状态才会是fulfilled
    static all(promiseAll){
        const values = [];//存放结果
        let resolvedCount = 0;//计数
        return new Promise((resolve,reject) => {
            promiseAll.forEach((p,index) => {
                Promise.resolve(p).then(value =>{
                    resolvedCount++;
                    values[index] = value;
                    if(resolvedCount == promiseAll.length){//当全变为fulfilled,返回结果数组
                        resolve(values);
                    }
                },err => {
                    reject(err)//有错误则返回
                })
            });
        })
    }
}

Promise.rave()方法实现

  1. 参数必须是一个可迭代器对象
  2. 该方法返回一个Promsie,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。
  3. 结果可以是完成( resolves),也可以是失败(rejects),这要取决于第一个完成的方式是两个中的哪个。
  4. 如果传的迭代是空的,则返回的 promise 将永远等待。
  5. 如果迭代包含一个或多个非承诺值和/或已解决/拒绝的承诺,则 Promise.race 将解析为迭代中找到的第一个值。 顾名思义竞赛
class Promise {
    // ...
    
    static race(promise){
        return new Promise((resolve,reject) => {
            promise.forEach((p,index) => {
                Promise.resolve(p).then(value => {
                    resolve(value);
                },err => {
                    reject(err)
                })
            })
        })
    }
}

写在后面

其实手写类的题还有好多好多(例如:对象的扁平化、对象扁平化恢复),但是篇幅有限,我所总结的也只是冰山一角,也希望大佬们告诉我还有哪些,咱一起卷!!!