关于javascript:那些经常被面试官问到的手写题

10次阅读

共计 15042 个字符,预计需要花费 38 分钟才能阅读完成。

1. 实现 instanceof 运算符

instanceof 运算符用于检测构造函数的 prototype 属性是否呈现在某个实例对象的原型链上,运算符左侧是实例对象,右侧是构造函数。

const iInstanceof = function (left, right) {let proto = Object.getPrototypeOf(left);
     while (true) {if (proto === null) return false;
        if (proto === right.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
};

这是常见的实现,咱们也能够用 isPrototypeOf 实现

const iInstanceof = function (left, right) {return right.prototype.isPrototypeOf(left)
};

2. 实现 new 操作符

new 执行过程如下:

  1. 创立一个新对象;
  2. 新对象的 [[prototype]] 个性指向构造函数的 prototype 属性;
  3. 构造函数外部的 this 指向新对象;
  4. 执行构造函数;
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创立的新对象;
const iNew = function (fn, ...rest) {let instance = Object.create(fn.prototype);
    let res = fn.apply(instance, rest);
    return res !== null && (typeof res === 'object' || typeof res === 'function') ? res : instance;
};

3. 实现 Object.assign 办法

浅拷贝办法,只会拷贝源对象本身的且可枚举的属性(包含以 Symbol 为 key 的属性)到指标对象

const iAssign = function (target, ...source) {if (target === null || target === undefined) {throw new TypeError('Cannot convert undefined or null to object');
    }
    let res = Object(target);
    for (let i = 0; i < source.length; i++) {let src = source[i];
        let keys = [...Object.keys(src), ...Object.getOwnPropertySymbols(src)];
        for (const k of keys) {if (src.propertyIsEnumerable(k)) {res[k] = src[k];
            }
        }
    }
    return res;
};
// 放弃 assign 的数据属性统一
Object.defineProperty(Object, 'iAssign', {
    value: iAssign,
    configurable: true,
    enumerable: false,
    writable: true
});

4. bind 办法

扭转函数内 this 的值并且传参,返回一个函数

const iBind = function (thisArg, ...args) {
    const originFunc = this;
    const boundFunc = function (...args1) {
        // 解决 bind 之后对返回函数 new 的问题
        if (new.target) {if (originFunc.prototype) {boundFunc.prototype = originFunc.prototype;}
            const res = originFunc.apply(this, args.concat(args1));
            return res !== null && (typeof res === 'object' || typeof res === 'function') ? res : this;
        } else {return originFunc.apply(thisArg, args.concat(args1));
        }
    };
    // 解决 length 和 name 属性问题
    const desc = Object.getOwnPropertyDescriptors(originFunc);
    Object.defineProperties(boundFunc, {
        length: Object.assign(desc.length, {value: desc.length < args.length ? 0 : (desc.length - args.length)
        }),
        name: Object.assign(desc.name, {value: `bound ${desc.name.value}`
        })
    });
    return boundFunc;
};
// 放弃 bind 的数据属性统一
Object.defineProperty(Function.prototype, 'iBind', {
    value: iBind,
    enumerable: false,
    configurable: true,
    writable: true
});

5. call 办法

用指定的 this 值和参数来调用函数

const iCall = function (thisArg, ...args) {thisArg = (thisArg === undefined || thisArg === null) ? window : Object(thisArg);
    let fn = Symbol('fn');
    thisArg[fn] = this;
    let res =  thisArg[fn](...args);
    delete thisArg[fn];
    return res;
};
// 放弃 call 的数据属性统一
Object.defineProperty(Function.prototype, 'iCall', {
    value: iCall,
    configurable: true,
    enumerable: false,
    writable: true
});

6. 函数柯里化

将一个多参数函数转化为多个嵌套的单参数函数。

const curry = function (targetFn) {return function fn (...rest) {if (targetFn.length === rest.length) {return targetFn.apply(null, rest);
        }  else {return fn.bind(null, ...rest);
        }
    };
};
// 用法
function add (a, b, c, d) {return a + b + c + d;}
console.log('柯里化:', curry(add)(1)(2)(3)(4)); 
// 柯里化:10

7. 函数防抖 debounce 办法

const debounce = function (func, wait = 0, options = {
    leading: true,
    context: null
}) {
    let timer;
    let res;
    const _debounce = function (...args) {options.context || (options.context = this);
        if (timer) {clearTimeout(timer);
        }
        if (options.leading && !timer) {timer = setTimeout(() => {timer = null;}, wait);
            res = func.apply(options.context, args);
        } else {timer = setTimeout(() => {res = func.apply(options.context, args);
               timer = null;
           }, wait);
        }
        return res;
    };
    _debounce.cancel = function () {clearTimeout(timer);
        timer = null;
    };
    return _debounce;
};

leading 示意进入时是否立刻执行,如果在 wait 工夫内触发事件,则会将上一个定时器革除,并从新再设置一个 wait 工夫的定时器。

8. 函数节流 throttle 办法

const throttle = function (func, wait = 0, options = {
    leading: true,
    trailing: false,
    context: null
}) {
    let timer;
    let res;
    let previous = 0;
    const _throttle = function (...args) {options.context || (options.context = this);
        let now = Date.now();
        if (!previous && !options.leading) previous = now;
        if (now - previous >= wait) {if (timer) {clearTimeout(timer);
                timer = null;
            }
            res = func.apply(options.context, args);
            previous = now;
        } else if (!timer && options.trailing) {timer = setTimeout(() => {res = func.apply(options.context, args);
                previous = 0;
                timer = null;
           }, wait);
        }
        return res;
    };
    _throttle.cancel = function () {
        previous = 0;
        clearTimeout(timer);
        timer = null;
    };
    return _throttle;
};

函数节流就像水龙头滴水一样,距离 wait 工夫就会触发一次,这里相比函数防抖新增了 trailing 选项,示意是否在最初额定触发一次。

9. 事件公布订阅(EventBus 事件总线)

class EventBus {constructor () {
        Object.defineProperty(this, 'handles', {value: {}
        });
    }
    on (eventName, listener) {if (typeof listener !== 'function') {console.error('请传入正确的回调函数');
            return;
        }
        if (!this.handles[eventName]) {this.handles[eventName] = [];}
        this.handles[eventName].push(listener);
    }
    emit (eventName, ...args) {let listeners = this.handles[eventName];
        if (!listeners) {console.warn(`${eventName}事件不存在 `);
            return;
        }
        for (const listener of listeners) {listener(...args);
        }
    }
    off (eventName, listener) {if (!listener) {delete this.handles[eventName];
            return;
        }
        let listeners = this.handles[eventName];
        if (listeners && listeners.length) {let index = listeners.findIndex(item => item === listener);
            listeners.splice(index, 1);
        }
    }
    once (eventName, listener) {if (typeof listener !== 'function') {console.error('请传入正确的回调函数');
            return;
        }
        const onceListener = (...args) => {listener(...args);
            this.off(eventName, listener);
        };
        this.on(eventName, onceListener);
    }
}

自定义事件的时候用到,留神一些边界的查看

10. 深拷贝

const deepClone = function (source) {if (source === null || typeof source !== 'object') {return source;}
    let res = Array.isArray(source) ? [] : {};
    for (const key in source) {if (source.hasOwnProperty(key)) {res[key] = deepClone(source[key]);
        }
    }
    return res;
};

这个是深拷贝的很根底版本,其中存在一些问题,比方循环援用,比方递归爆栈,前面我会专门写一篇文章来展开讨论。

11. 实现 ES6 的 Class

用构造函数模仿,class 只能用 new 创立,不能够间接调用,另外留神一下属性的描述符

const checkNew = function (instance, con) {if (!(instance instanceof con)) {throw new TypeError(`Class constructor ${con.name} cannot be invoked without 'new'`);
    }
};
const defineProperties = function (target, obj) {for (const key in obj) {
        Object.defineProperty(target, key, {
            configurable: true,
            enumerable: false,
            value: obj[key],
            writable: true
        });
    }
};
const createClass = function (con, proto, staticAttr) {proto && defineProperties(con.prototype, proto);
    staticAttr && defineProperties(con, staticAttr);
    return con;
};

// 用法
function Person (name) {checkNew(this, Person);
    this.name = name;
}
var PersonClass = createClass(Person, {getName: function () {return this.name;}
}, {getAge: function () {}});

12. 实现 ES6 的继承

ES6 外部应用寄生组合式继承,首先用 Object.create 继承原型,并传递第二个参数以将父类构造函数指向本身,同时设置数据属性描述符。

而后用 Object.setPrototypeOf 继承动态属性和静态方法。

const inherit = function (subType, superType) {
     // 对 superType 进行类型判断
    if (typeof superType !== "function" && superType !== null) {throw new TypeError("Super expression must either be null or a function");
    }
    subType.prototype = Object.create(superType && superType.prototype, {
        constructor: {
            configurable: true,
            enumerable: false,
            value: subType,
            writable: true
        }
    });
    // 继承静态方法
    superType && Object.setPrototypeOf(subType, superType);
};

// 用法
function superType (name) {this.name = name;}
superType.staticFn = function () {console.log('staticFn');
}
superType.prototype.getName = function () {console.log('name:' + this.name);
}
function subType (name, age) {superType.call(this, name);
    this.age = age;
}
inherit(subType, superType);
// 必须在继承之后再往 subType 中增加原型办法,否则会被笼罩掉
subType.prototype.getAge = function () {console.log('age:' + this.age);
}
let subTypeInstance = new subType('Twittytop', 29);
subType.staticFn();
subTypeInstance.getName();
subTypeInstance.getAge();

13. 图片懒加载

// 获取窗口高度
function getWindowHeight () {return window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;}

function getTop (e) {
    let t = e.offsetTop;
    while (e = e.offsetParent) {t += e.offsetTop;}
    return t;
}

const delta = 30;
let count = 0;
function lazyLoad (imgs) {const winH = getWindowHeight();
    const s = document.documentElement.scrollTop || document.body.scrollTop;
    for (let i = 0, l = imgs.length; i < l; i++) {if (winH + s + delta > getTop(imgs[i]) && getTop(imgs[i]) + imgs[i].offsetHeight + delta > s) {if (!imgs[i].src) {imgs[i].src = imgs[i].getAttribute('data-src');
                count++;
            }
            if (count === l) {window.removeEventListener('scroll', handler);
                window.removeEventListener('load', handler);
            }
        }
    }    
}
const imgs = document.querySelectorAll('img');
const handler = function () {lazyLoad(imgs);
};
window.addEventListener('scroll', handler);
window.addEventListener('load', handler);

当然你也能够用 getBoundingClientRect 办法:

// 获取窗口高度
function getWindowHeight () {return window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;}

const delta = 30;
let count = 0;
function lazyLoad (imgs) {const winH = getWindowHeight();
    for (let i = 0, l = imgs.length; i < l; i++) {const rect = imgs[i].getBoundingClientRect();
        if (winH + delta > rect.top && rect.bottom > -delta) {if (!imgs[i].src) {imgs[i].src = imgs[i].getAttribute('data-src');
                count++;
            }
            if (count === l) {window.removeEventListener('scroll', handler);
                window.removeEventListener('load', handler);
            }
        }
    }    
}
const imgs = document.querySelectorAll('img');
const handler = function () {lazyLoad(imgs);
};
window.addEventListener('scroll', handler);
window.addEventListener('load', handler);

当然你也能够用 IntersectionObserver 办法:

function lazyLoad (imgs) {
    let options = {rootMargin: '30px'};
    let count = 0;
    let observer = new IntersectionObserver(entries => {
        entries.forEach(entry => {if (entry.intersectionRatio > 0) {entry.target.src = entry.target.getAttribute('data-src');
                count++;
                observer.unobserve(entry.target);
                if (count === imgs.length) {window.removeEventListener('load', handler);
                }
            }
        });
    }, options);
    for (let i = 0; i < imgs.length; i++) {observer.observe(imgs[i]);
    }
}
const imgs = document.querySelectorAll('img');
const handler = function () {lazyLoad(imgs);
};
window.addEventListener('load', handler);

14. 实现 Object.is 办法

Object.is() 和 === 的区别是 Object.is(0, -0) 返回 false, Object.is(NaN, NaN) 返回 true。

const iIs = function (x, y) {if (x === y) {return x !== 0 || 1 / x === 1 / y;} else {return x !== x && y !== y;}
}
// 放弃 is 的数据属性统一
Object.defineProperty(Function.prototype, 'iIs', {
    value: iIs,
    configurable: true,
    enumerable: false,
    writable: true
});

15. 工夫切片

把长工作切割成多个小工作,应用场景是避免一个工作执行工夫过长而阻塞线程

function ts (gen) {if (typeof gen === 'function') gen = gen();
    if (!gen || typeof gen.next !== 'function') return;
    (function next() {const start = performance.now();
        let res = null;
        do {res = gen.next();
        } while(!res.done && performance.now() - start < 25)
        if (res.done) return;
        setTimeout(next);
    })();}

// 用法
ts(function* () {const start = performance.now();
    while (performance.now() - start < 1000) {yield;}
    console.log('done!');
});

16. CO(协程)实现

function co (gen) {return new Promise(function (resolve, reject) {if (typeof gen === 'function') gen = gen();
        if (!gen || typeof gen.next !== 'function') return resolve(gen);
        onFulfilled();
        
        function onFulfilled (res) {
            let ret;
            try {ret = gen.next(res);
            } catch (e) {return reject(e);
            }
            next(ret);
        }
        
        function onRejected (err) {
            let ret;
            try {ret = gen.throw(err);
            } catch (e) {return reject(e);
            }
            next(ret);
        }
        
        function next (ret) {if (ret.done) return resolve(ret.value);
            let val = Promise.resolve(ret.value);
            return val.then(onFulfilled, onRejected);
        }
    });
}

// 用法
co(function* () {let res1 = yield Promise.resolve(1);
    console.log(res1);
    let res2 = yield Promise.resolve(2);
    console.log(res2);
    let res3 = yield Promise.resolve(3);
    console.log(res3);
    return res1 + res2 + res3;
}).then(value => {console.log('add:' + value);
}, function (err) {console.error(err.stack);
});

co 承受一个生成器函数,当遇到 yield 时就暂停执行,交出控制权,当其余程序执行结束后,将后果返回并从中断的中央继续执行,如此往返,始终到所有的工作都执行结束,最初返回一个 Promise 并将生成器函数的返回值作为 resolve 值。

咱们将 * 换成 async,将 yield 换成 await 时,就和咱们常常用的 async/await 是一样的,所以说 async/await 是生成器函数的语法糖。

17. 单例模式

const getSingleton = function (fn) {
    let instance;
    return function () {return instance || (instance = new (fn.bind(this, ...arguments)));
    };
};
// 用法
function Person (name) {this.name = name;}
let singleton = getSingleton(Person);
let instance1 = new singleton('Twittop1');
let instance2 = new singleton('Twittop2');
console.log(instance1 === instance2); // true

当然你也能够用 ES6 的 Proxy 实现:

const getSingleton = function (fn) {
    let instance;
    const handler = {construct (target, argumentsList) {return instance || (instance = Reflect.construct(target, argumentsList)); 
        }
    }
    return new Proxy(fn, handler);
};
// 用法
function Person (name) {this.name = name;}
let singleton = getSingleton(Person);
let instance1 = new singleton('Twittop1');
let instance2 = new singleton('Twittop2');
console.log(instance1 === instance2); // true

18. Promise

function isFunction (obj) {return typeof obj === 'function';}
function isObject (obj) {return !!(obj && typeof obj === 'object');
}
function isPromise (obj) {return obj instanceof Promise;}
function isThenable (obj) {return (isFunction(obj) || isObject(obj)) && 'then' in obj;
}
function transition (promise, state, result) {
    // 一旦变成非 pending 状态,就不可逆
    if (promise.state !== 'pending') return;
    promise.state = state;
    promise.result = result;
    setTimeout(() => promise.callbacks.forEach(callback => handleCallback(callback, state, result)));
}
function resolvePromise (promise, result, resolve, reject) {if (promise === result) {return reject(new TypeError('Chaining cycle detected for promise'));
    } 
    if (isPromise(result)) {return result.then(resolve, reject);
    } 
    if (isThenable(result)) {
      try {
        let then = result.then;
        if (isFunction(then)) {return new Promise(then.bind(result)).then(resolve, reject);
        }
      } catch (error) {return reject(error);
      }
    }
    resolve(result);
}
function handleCallback (callback, state, result) {let { onFulfilled, onRejected, resolve, reject} = callback;
    try {if (state === 'fulfilled') {isFunction(onFulfilled) ? resolve(onFulfilled(result)) : resolve(result);
        } else if (state === 'rejected') {isFunction(onRejected) ? resolve(onRejected(result)) : reject(result);
        }
    } catch (e) {reject(e);
    }
}
class Promise {constructor (executor) {
        this.state = 'pending';
        this.result = undefined;
        this.callbacks = [];
        let onFulfilled = value => transition(this, 'fulfilled', value);
        let onRejected = reason => transition(this, 'rejected', reason);
        // 保障 resolve 或 reject 只有一次调用
        let flag = false;
        let resolve = value => {if (flag) return;
            flag = true;
            resolvePromise(this, value, onFulfilled, onRejected);
        };
        let reject = reason => {if (flag) return;
            flag = true;
            onRejected(reason);
        };
        try {executor(resolve, reject); 
        } catch (e) {reject(e);
        }
    }
    then (onFulfilled, onRejected) {return new Promise((resolve, reject) => {let callback = { onFulfilled, onRejected, resolve, reject};
            if (this.state === 'pending') {this.callbacks.push(callback);
            } else {setTimeout(() => {handleCallback(callback, this.state, this.result);
                });
            }
        });
    }
    catch (onRejected) {this.then(undefined, onRejected);
    }
    // 无论胜利还是失败都会执行,个别都会传递前一个 promise 的状态,只有在 onFinally 抛出谬误(显示抛出或 reject)的时候才会返回一个 rejected 的 promise
    finally (onFinally) {
        return this.then(val => Promise.resolve(onFinally()).then(() => val),
            rea => Promise.resolve(onFinally()).then(() => { throw rea;})
        );
    }
    static resolve (value) {if (isPromise(value)) return value;
        return new Promise ((resolve, reject) => resolve(value));
    }
    static reject (reason) {return new Promise ((resolve, reject) => reject(reason));
    }
    // 当所有 promise 都返回 fulfilled 的时候,它才会返回一个 fulfilled 的 promise,外面蕴含了对应后果的数组,否则只有一个 promise 返回 rejected,它就会返回一个 rejected 的 promise,其中蕴含第一个 rejected 的 promise 抛出的错误信息
    static all (iterable) {return new Promise ((resolve, reject) => {
            let count = 0;
            let arr = [];
            for (let i = 0, l = iterable.length; i < l; i ++) {iterable[i].then(val => {
                    count++;
                    arr[i] = val;
                    if (count === l) {reresolve(arr);
                    }
                }, reject);
            }
        });
    }
    // 只有有一个 promise 返回 fulfilled 或 rejected,它就会返回一个 fulfilled 或 rejected 的 promise
    static race (iterable) {return new Promise ((resolve, reject) => {for (const p of iterable) {p.then(resolve, reject);
            }
        });
    }
    // 当所有 promise 都 fulfilled 或 rejected 后,返回一个蕴含对应后果的数组
    static allSettled (iterable) {return new Promise ((resolve, reject) => {
            let count = 0;
            let arr = [];
            function handle (state, index, result) {arr[index] = {
                    status: state,
                    [state === 'fulfilled' ? 'value' : 'reason']: result
                };
                count++;
                if (count === iterable.length) {resolve(arr);
                }
            }
            for (let i = 0, l = iterable.length; i < l; i ++) {iterable[i].then(val => handle ('fulfilled', i, val), rea => handle ('rejected', i, rea));
            }
        });
    }
    // 只有有一个 promise 胜利,就会返回一个胜利的 promise,否则返回一个 AggregateError 类型实例的失败 promise
    static any (iterable) {return new Promise ((resolve, reject) => {
            let count = 0;
            let arr = [];
            for (let i = 0, l = iterable.length; i < l; i ++) {iterable[i].then(resolve, rea => {
                    count++;
                    arr[i] = rea;
                    if (count === l) {reject(new AggregateError(arr));
                    }
                });
            }
        });
    }
}

Promise 有三种状态 pending、fulfilled 和 rejected,pending 是最后的状态,一旦落定为 fulfilled 或 rejected 状态,就不可逆。且一旦执行 resolve 或 reject,前面的 resolve 或 reject 就不会失效。then 传入的回调函数有可能提早执行,所以需放到 callbacks 数组中,等状态变更的时候再取出执行。

参考资料

https://juejin.cn/post/684490…
https://github.com/berwin/tim…
CO 模块
100 行代码实现 Promises/A+ 标准

正文完
 0