# 手写常见的JS面试题 1-10

# 1、寄生组合式继承


function Parent(name) {
    this.name = name;
    this.say = () => {
        console.log(111);
    }
}

Parent.prototype.play = () => {
    console.log(222);
}

function Children(name) {
    Parent.call(this);
    this.name = name;
}

Children.prototype = Object.create(Parent.prototype);
Children.prototype.constructor = Children;

# 2、compose

function fn1(x) {
    return x + 1;
}

function fn2(x) {
    return x + 2;
}

function fn3(x) {
    return x + 3;
}

function fn4(x) {
    return x + 4;
}

const a = compose(fn1, fn2, fn3, fn4);
console.log(a(1));

实现代码如下

function compose() {
    const arr = [...arguments];
    return function (x) {
        return arr.reduceRight((pre, cur) => {
            return pre += cur(pre);
        }, x)
    }
}

# 3、setTimeout模拟实现setInterval(带定时清除);

setInterval的缺点:

  • 使用 setInterval 时,某些间隔会被跳过;(函数操作耗时过长导致的不准确)
  • 可能多个定时器会连续执行;

可以这么理解:每个 setTimeout 产生的任务会直接 push 到任务队列中; 而 setInterval 在每次把任务 push 到任务队列前, 都要进行一下判断(看上次的任务是否仍在队列中,如果有则不添加,没有则添加)。


function mySetTimeout(fn, delay) {
    this.timer = null;

    function interval() {
        timer = setTimeout(fn, delay);
        interval();
    }

    interval();
    return {
        calcel: () => clearTimeout(timer)
    }
}

# 4、发布订阅模式

题目描述: 实现一个发布订阅模式拥有, 书写一下vue的bus管线

class EventEmitter {
    constructor() {
        // handlers是一个map,用于存储事件与回调之间的对应关系
        this.handlers = {}
    }

    // on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
    on(eventName, cb) {
        // 先检查一下目标事件名有没有对应的监听函数队列
        if (!this.handlers[eventName]) {
            // 如果没有,那么首先初始化一个监听函数队列
            this.handlers[eventName] = []
        }

        // 把回调函数推入目标事件的监听函数队列里去
        this.handlers[eventName].push(cb);
    }

    // emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
    emit(eventName, ...args) {
        // 检查目标事件是否有监听函数队列
        if (this.handlers[eventName]) {
            // 这里需要对 this.handlers[eventName] 做一次浅拷贝,主要目的是为了避免通过 once 安装的监听器在移除的过程中出现顺序问题
            const handlers = this.handlers[eventName].slice()
            // 如果有,则逐个调用队列里的回调函数
            handlers.forEach((callback) => {
                callback(...args)
            })
        }
    }

    // 移除某个事件回调队列里的指定回调函数
    off(eventName, cb) {
        const callbacks = this.handlers[eventName]
        const index = callbacks.indexOf(cb)
        if (index !== -1) {
            callbacks.splice(index, 1)
        }
    }

    // 为事件注册单次监听器
    once(eventName, cb) {
        // 对回调函数进行包装,使其执行完毕自动被移除
        const wrapper = (...args) => {
            cb(...args)
            this.off(eventName, wrapper)
        }
        this.on(eventName, wrapper)
    }
}

// 使用如下
// const event = new EventEmitter();

// const handle = (...rest) => {
//   console.log(rest);
// };

// event.on("click", handle);

// event.emit("click", 1, 2, 3, 4);

// event.off("click", handle);

// event.emit("click", 1, 2);

// event.once("dbClick", () => {
//   console.log(123456);
// });
// event.emit("dbClick");
// event.emit("dbClick");

# 5、数组去重

function uniqueArr(arr) {
    return [...new Set(arr)]; // 简单类型
}

function uniqueFunc(arr, uniId) {
    const res = new Map(); // 对象去重
    return arr.filter((item) => !res.has(item[uniId]) && res.set(item[uniId], 1));
}

# 6、数组扁平化

题目描述:实现一个方法使多维数组变成一维数组

// 最常见的递归版本如下:
function flatter(arr) {
    if (!arr.length) return;
    return;
    arr.reduce((pre, cur) => {
        return Array.isArray(cur) ? [...pre, ...flatter(cur)] : [...pre, cur];
    }, []);
}

// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));
// 迭代的方法
function flatter(arr) {
    if (!arr.length) return;
    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}

// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));

# 7、实现并行限制的Promise调度器。

题目描述:JS实现一个带并发限制的异步调度器Scheduler,保证同时运行的任务最多有两个。

addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
的输出顺序是:2 3 1 4

整个的完整执行流程:

一开始1、2两个任务开始执行
500ms时,2任务执行完毕,输出2,任务3开始执行
800ms时,3任务执行完毕,输出3,任务4开始执行
1000ms时,1任务执行完毕,输出1,此时只剩下4任务在执行
1200ms时,4任务执行完毕,输出4
class Scheduler {
    constructor(limit) {
        this.queue = [];
        this.limit = limit;
        this.runCounts = 0;
    }

    add(delay, order) {
        const fun = () => {
            return new Promise((resolve) => {
                setTimeout(() => {
                    console.log(order);
                    resolve(order);
                }, delay)
            })
        }
        this.queue.push(fun);
    }

    start() {
        for (let i = 0; i < this.limit; i++) {
            this.request();
        }
    }

    request() {
        if (!this.queue.length || this.runCounts >= this.limit) return;
        this.runCounts++;
        this.queue
            .shift()()
            .then(() => {
                    this.runCounts--;
                    this.request();
                }
            );
    }
}

const scheduler = new Scheduler(2);
const addTask = (delay, order) => {
    scheduler.add(delay, order);
}
addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
scheduler.start();

# 8、new操作符

function _new(fn, ...arg) {
    const obj = Object.create(fn.prototype);
    const result = fn.apply(obj, arg);
    return result instanceof Object ? result : obj;
}

# 9、call、apply、bind

Function.prototype.call = function (context, ...arg) {
    context = context || window;
    // 创造唯一的key值  作为我们构造的context内部方法名
    const fn = Symbol();
    context[fn] = this; // this指向调用call的函数
    // 执行函数并返回结果 相当于把自身作为传入的context的方法进行调用了
    return context[fn](...arg);
}
// apply原理一致  只是第二个参数是传入的数组
Function.prototype.apply = function (context, arg) {
    context = context || window;
    const fn = Symbol();
    context[fn] = this;
    return context[fn](...arg)
}

Function.prototype.bind = function (context, ...arg) {
    const self = this;

    function Fn() {
        return self.apply(this instanceof Fn ? this : context, [...arguments].concat(arg));
    }
    Fn.prototype = Object.create(this.prototype);
    return Fn;
}

// 定义 Function.prototype.bind2 方法
Function.prototype.bind2 = function (context) {

    // 检查当前对象是否为函数
    if (typeof this !== "function") {
        // 如果不是函数,则抛出错误
        throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    // 保存原始函数
    var self = this;

    // 收集除了第一个参数(context)之外的所有参数
    var args = Array.prototype.slice.call(arguments, 1);

    // 创建一个空函数,用于继承原始函数的原型属性
    var fNOP = function () {};

    // 创建绑定函数
    var fBound = function () {
        // 收集传递给 fBound 的所有参数
        var bindArgs = Array.prototype.slice.call(arguments);

        // 判断 fBound 是否作为构造函数被调用
        // 如果是构造函数调用,则 this 为新创建的对象
        // 如果不是构造函数调用,则 this 为 context
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    // 设置 fNOP 的原型为原始函数的原型
    fNOP.prototype = this.prototype;

    // 设置 fBound 的原型为 fNOP 的一个新实例
    // 这样当 fBound 作为构造函数被调用时,创建的对象具有正确的原型链
    fBound.prototype = new fNOP();

    // 返回绑定函数
    return fBound;
}

# 10、深拷贝(考虑到复制Symbol)

function isObject(val) {
    return typeof val === "object" && val !== null;
}

function deepClone(obj, hash = new WeakMap()) {
    if (!isObject(obj)) return obj;
    if (hash.has(obj)) {
        return hash.get(obj);
    }
    let target = Array.isArray(obj) ? [] : {};
    hash.set(obj, target);
    Reflect.ownKeys(obj).forEach((item) => {
        if (isObject(obj[item])) {
            target[item] = deepClone(obj[item], hash);
        } else {
            target[item] = obj[item];
        }
    });

    return target;
}

// var obj1 = {
// a:1,
// b:{a:2}
// };
// var obj2 = deepClone(obj1);
// console.log(obj1);

Last Updated: 6/22/2022, 9:31:27 AM