【面试】JS原型链手写题

153 阅读2分钟

题目1 手写Object.create

Object.create(obj): 创建以obj为原型的对象

function create(o) {
    function F() {};
    F.prototype = o;
    return new F();
}

这道题的知识点:原型以及原型链,既然涉及到原型,那我们就得画画图, 假如有如下代码:

function A() {}
const a = new A();

const o = {};

首先每个函数都有一个prototype属性,指向函数原型,而对象的实例的话是__proto__属性,这两个都指向Function.prototype。其次,每个原型对象中都有一个constructor指向构造函数。

prototype.png

图错了==

a.proto === A.prototype

一涉及到原型的题目就想起来js中鸡生蛋蛋生鸡的问题,为什么 Object instanceof Function,同时Function instanceof Object,如果要验证这个就要看它的__proto__属性的指向,上图已经画出来了,所以可以直接按照下面的写:

// 如果要解释就是Object是函数,都是由Function创建的实例,所以最关键的是Object的__proto__指向了函数原型
Object instanceof Function:
Object.__proto__ === Function.prototype


// 这里最关键的也是Function由自身构造出来😄,Function的__proto__也是指向的函数原型
// 而函数原型的__proto__指向对象原型,因为原型对象也是个对象
Function instanceof Object:
Function.__proto__ === Function.prototype
Function.prototype.__proto__ === Object.prototype

上面这里还涉及到2个面试中经常出现的问题,instanceof如何实现以及new 如何实现:

题目二 手写instanceof

instanceof用于判断后面的构造函数是否出现在对象的原型中。

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

题目三 手写new

  1. 创建一个新对象
  2. 新对象的原型指向构造函数
  3. 将构造函数this改为新对象,执行获取返回值
  4. 如果返回值是对象,则将返回值直接返回,否则返回新创建的对象
function myNew() {
    // 取出第一个参数,就是构造函数
    const constructor = Array.prototype.shift.call(arguments);
    if (typeof constructor !== 'function') {
        throw new Error('not function');
    }
    const o = new Object;
    // 原型指向构造函数原型
    o.__proto__ = constructor.prototype;
    // 改变构造函数的this,使它指向新创建的对象,o可以访问到构造函数的属性。apply参数是数组,call是一个一个的
    const res = constructor.apply(o, arguments);
    return typeof res === 'object' ? res : o;
}

写完这个题,又引出另外一个题,apply,call,bind的实现😄,这里还有个知识点 typeof 类型判断。

题目四 手写call

定义:Function.prototype.call1(context, ...args), context为要更改为的上下文,其余为其他参数

  1. context如果不存在,赋值为window
  2. 将函数赋值为context的一个属性fn
  3. 调用context.fn并且传入参数
  4. 删除fn属性
  5. 返回结果
Function.prototype.call2 = function(context) {
    if (typeof this !== 'function') {
        throw new Error('type error');
    }
    context = context || window;
    const args = [...arguments].slice(1);
    context.fn = this;
    const res = context.fn(...args);
    delete context.fn;
    return res;
}

// 如果不使用es6方法
Function.prototype.call2 = function(context) {
    if (typeof this !== 'function') {
        throw new Error('type error');
    }
    context = context || window;
    let args = [];
    // 结果是['arguments[1]', 'arguments[2]']
    for (let i = 1; i < arguments.length; i++) {
        args.push('arguments[' + i + ']');
    }
    context.fn = this;
    // args自动调用toString转换为'arguments[1],arguments[2]'
    const res = eval('context.fn('+ args + ')');
    delete context.fn;
    return res;
}

题目五 手写apply

和call基本一致,区别就是参数的传递,apply是传递数组,核心还是以下几步:

  1. context判空处理
  2. 函数赋值为context的属性
  3. 调用context.fn,传入arguments[1], 记录返回值
  4. 删除属性
Function.prototype.apply2 = function(context) {
    if (typeof this !== 'function') {
        throw new Error('type error');
    }
    context = context || window;
    context.fn = this;
    // 取第二个参数解构传进去
    const res = context.fn(...arguments[1]);
    delete context.fn;
    return res;
}

// 不使用es6版本
Function.prototype.apply2 = function(context) {
    context = context || window;
    context.fn = this;
    let result;
    if (arguments.length === 1) {
        result = context.fn();
    } else {
        resilt = eval('context.fn(' + arguments[1] + ')');
    }
    delete context.fn;
    return result;
}

题目六 手写bind

关键点:

  1. 返回一个新函数
  2. 参数都要收集起来
  3. 兼容构造函数场景:就是bind之后的函数又用了new来调用(这种有实际应用场景吗?。。。)这时候要忽略绑定的上下文。
    • 如何判断是new调用场景?就是调用时this是否是返回的bind函数的实例,是的话代表是new调用,要用当前this作为上下文,否则普通场景调用则用传入的context作为上下文。
    • 然后还要兼容原型,要把调用bind的原始函数的prototype赋值给新构造的函数,如果直接赋值会出现污染,所以用一个新函数中转,构造一个空函数,将原型赋值为空函数原型,再将新构造的函数原型初始化为F的实例。
/**
 * 返回一个新的函数
 * 函数参数接收bind的函数和调用的函数,bind函数拼接调用函数构成最后参数
 * 如果bind后的函数通过new来调用,则忽略bind绑定的context,使用原来构造函数上下文
 */
Function.prototype.bind2 = function(context) {
    const outArgs = [...arguments].slice(1);
    const func = this;
    function fBound() {
        const inArgs = [...arguments];
        return func.apply(this instanceof F ? this : context, outArgs.concat(inArgs));
    }
    function F() {}
    F.prototype = this.prototype;
    // 断开和原来的原型,防止污染
    fBound.prototype = new F();
    return fBound;
}

参考文档

juejin.cn/post/712533…

www.cnblogs.com/objectorl/a…

github.com/mqyqingfeng…

github.com/mqyqingfeng…

github.com/mqyqingfeng…