JavaScript手写new, call, apply, bind

在前端面试时,经常考察手写 JavaScript 的 new, call, apply 和 bind。 这里把我的一些理解整理到这里,方便以后复习。

实现自己的new

思路是 1) 新建一个对象,把构造函数的原型传给新对象; 2) 然后用新对象为上下文,调用构造函数,如果此时有多余的参数,也一并传入; 3)如果构造函数返回了一个对象,那么就用返回的对象,舍弃新对象;否则,返回新对象。

function Student(name, id) {
    this.name = name;
    this.id = id;
}
Student.prototype.sayHello = () => { console.log('hello'); }

// ES6
function myNew(Constructor, ...rest) {
    const newObj = Object.create(Constructor.prototype);
    const result = Constructor.apply(newObj, rest);
    return result instanceof Object ? result : newObj;
}

// ES5
function myNew() {
    var newObj = {};
    var Constructor = Array.prototype.shift.call(arguments);
    newObj.__proto__ = Constructor.prototype;
    var result = Constructor.apply(newObj, arguments);
    return result instanceof Object ? result : newObj;
}

// Usage
const student1 = myNew(Student, 'Tom', 1);

注意,myNew的最后一行不能使用typeof result === 'object',因为如果构造函数返回值是null,那么typeof null === 'object'为true,就会导致myNew返回null。而浏览器里原生的new是会返回newObj的。对比见下图:

myNew-comparison

其中,myNew2返回了null,跟原生的new结果不同。具体对比typeofinstanceof见本文的扩展部分。

实现自己的bind

最简单版本:

Function.prototype.myBind = function(context, ...args) {
    if (!(this instanceof Function)) {
        throw new Error('Must bind to a function');
    }
    return (...newArgs) => {
        return this.apply(context, [...args, ...newArgs]);
    }
}

注意,myBind不能是箭头函数,否则内部的this会是创建时候的this,而不是调用myBind时候的this。但,return的function可以用箭头函数,这样就不用额外保存外部的this为fn了。

但是,如果想要支持new,就还是用传统的function。因为箭头函数既没有prototype,也不能调用call、apply、bind,这样就没办法在new的时候用新的obj来替换上下文。(见上文中实现自己的new时调用了Constructor.apply。)

Function.prototype.myBind = function(asThis, ...args) {
    const fn = this;
    if (!(fn instanceof Function)) {
        throw new Error('Must bind to a function');
    }
    function resultFn(...newArgs) {
        return fn.apply(
            resultFn.prototype.isPrototypeOf(this) ? this : asThis,  // 用来绑定this
            [...args, ...newArgs]
        )
    }
    // 把返回函数的原型指向被绑定函数的原型。
    resultFn.prototype = fn.prototype;
    return resultFn;
}

实现自己的callapply

Function.prototype.myCall = function(context, ...args) {
    context = context || window;

    if (!(this instanceof Function)) {
        throw new Error('Must be a function');
    }
    const key = Symbol();
    context[key] = this;
    const result = context[key](...args);
    delete context[key];
    return result;
}

// 如下的例子,表明为啥需要检查 `this`是不是Function。
// 如果不检查,会抛出 "context[key] is not a function"的错误。
// 注意,用户不应该知道context[key]这种内部实现的细节,
// 所以,需要抛出一个定制的Error。
const obj = {}
obj.myCall = Function.prototype.myCall;
obj.myCall(null)  // 这时的this是obj,不是个function

手写apply的方法基本一样,只需要额外检查第二个参数是不是个Array。

Function.prototype.myApply = function(context, argArray) {
    context = context || window;

    if (!(this instanceof Function)) {
        throw new Error('Must be a function');
    }
    if (!Array.isArray(argArray)) {
        throw new Error('The second arg must be an array');
    }
    const key = Symbol();
    context[key] = this;
    const result = context[key](...argArray);
    delete context[key];
    return result;
}

扩展

typeof vs instanceof

见这个Stack Overflow问题:typeof vs. instanceof。 简单来说:只有在判断简单的built-in types(string,boolean,number, symbol, bigint, undefined) 的时候,使用typeof,而不用instanceof + 大写的构造函数。其他时候都可以使用instanceof。其中,typeof null是object,需要额外小心。

typeof-vs-instanceof

其实还有三个办法可以判断类型,一个是用来判断子类与父类的关系:Foo.prototype.constructor === Base 以及 Object.prototype.toString.call([1]) === '[object Array]'。前者因为constructor可能会被不小心修改或者忘记修改,那么判断会出错。后者通常用来判断是不是Array,但已经有了Array.isArray,用处也没有那么大。另外,Object.prototype.isPrototypeOf 可以检查一个对象是否在另一个对象的原型链上,理论上来说也可以判断类型。

new与bind的优先级

在上面bind的代码中,一个bind后的function也能new,但问题就是这个new的时候的this是什么。

通过bind的代码看出,new绑定this的优先级大于bind,所以函数内部在new的时候会忽略掉bind传入的context。下面代码来自美团笔试题第二题,就考察了这个点:

var name = 'global';
var obj = {
    name: 'local',
    foo: function() {
        this.name = 'foo';
    }.bind(window)
};
var bar = new obj.foo();
console.log(bar.name);  // ==> 'foo'
console.log(name);  // ==> 'global'