那些前端面试官让你手写的东西[0]——call和new的实现

90 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情

前端在面试的时候,经常会被面试官问到需要手写实现一些浏览器和框架已有的东西。虽然说在实际工作过程中,会用就已经解决了99%的问题了,不过,如果能知其然还能知其所以然的话,在某些特殊的边缘情况下,说不定能够帮上我们的忙。

今天先来搞两个点击率高的,callnew的实现、

实现call,apply和bind

call,apply,bind这三个方法相比每个前端开发者都不会陌生,它们的主要作用就是使得函数中this的指向改变:

function showFirstName(...others) {
    console.log(this.firstName,...others)
}
showFirstName();// undefined 1 2 3 4 因为目前的this指向window对象,没有firstName属性
var person = {firstName: "YuChen"}
showFirstName.apply(person,[1,2,3,4]);// YuChen 1 2 3 4
showFirstName.call(person,1,2,3,4);// YuChen 1 2 3 4
showFirstName.bind(person,1,2,3,4)();// YuChen 1 2 3 4

要手动实现这个功能,首先是需要从函数上触发这三个方法,所以需要从Function的原型链上动手:

Function.protoType.mycall = function() {
    //处理逻辑
}

我们以call为例(毕竟三者逻辑基本差不多,另二者是在取值和返回值上做一些不同处理),我们需要的参数是一个对象和一系列可迭代的参数:

Function.protoType.mycall = function(obj,...arg) {
    //处理逻辑
}

我们知道,要改变函数中this的指向需要在新的上下文中调用这个函数,对于一个对象来说,就是要让对象来调用这个方法,因此我们需要把方法附在这个对象上:

Function.prototype.mycall = function(obj,...arg) {
    obj.fun = this;
    obj.fun(...arg)
}
showFirstName.mycall(person,1,2,3,4); // YuChen 1 2 3 4

这就完成了?错误的,因为call方法调用后我们实际上并没有给对象新增方法,而我们这么一写,person对象仍然可以继续调用fun方法,这显然是不对的,因此需要将其删除:

Function.prototype.mycall = function(obj,...arg) {
    obj.fun = this;
    obj.fun(...arg)
    delete obj.fun
}
showFirstName.mycall(person,1,2,3,4); // YuChen 1 2 3 4
obj.fun(); // Uncaught ReferenceError: obj is not defined

再考虑一个情况,如果这个对象本来就有fun这个方法,我们这么一做覆盖而且删除了它,岂不是惹了大麻烦?那么生成随机数?那样也会有边缘情况会正好碰到已有属性,那么这个时候,Symbol()就要登场来解决我们的问题了。

每个从 Symbol() 返回的 symbol 值都是唯一的。一个 symbol 值能作为对象属性的标识符;这是该数据类型仅有的目的。

Symbol() === Symbol();// false

所以我们把刚才的fun属性替换成Symbol()就大功告成了:

Function.prototype.mycall = function(obj,...arg) {
    let symbol = Symbol()
    obj[symbol] = this;
    obj[symbol](...arg)
    delete obj[symbol]
}
var person = {firstName: "YuChen", fun: "JavaScript"}
showFirstName.mycall(person,1,2,3,4);// YuChen 1 2 3 4
console.log(person.fun);// JavaScript

可以看到,这时的fun属性没有被替换掉。

applybind做类似的处理:

Function.prototype.myapply = function(obj,args) {
    let symbol = Symbol()
    obj[symbol] = this;
    obj[symbol](...args)
    delete obj[symbol]
}
showFirstName.myapply(person,[1,2,3,4]);// YuChen 1 2 3 4
Function.prototype.mybind = function(obj,...arg) {
    return () => {
        let symbol = Symbol()
        obj[symbol] = this;
        obj[symbol](...arg)
        delete obj[symbol]
    }
}
showFirstName.mybind(person,1,2,3,4)(); // YuChen 1 2 3 4

实现new

在前端面试题中,我们往往会被问到这样的问题:

使用new方法构造一个对象时,new都做了什么?

首先回顾一下new是怎么用的:

function Father(name) {
    this.age = 35;
    this.gender = "male";
    this.whoseFather = name;
}
Father.prototype.showSonName = function() {return this.whoseFather}
var JackFather = new Father("Jack");
JackFather; // Father {age: 35, gender: 'male', whoseFather: 'Jack'}
JackFather.showSonName();// Jack

new将函数作为新对象的构造函数生成新的对象,并且继承了原函数的原型方法,但是有一种情况是例外,就是当函数有返回值且返回值是个对象时,生成的新对象是返回的对象(因此也不会有这个函数的原型方法)

function Father(name) {
    this.age = 35;
    this.gender = "male";
    this.whoseFather = name;
    
    return {
        error: "这样返回之后前面都白干了"
    }
}
Father.prototype.showSonName = function() {return this.whoseFather}
var JackFather = new Father("Jack");
JackFather; // {error: "这样返回之后前面都白干了"} 
JackFather.showSonName();// Uncaught TypeError: JackFather.showSonName is not a function

所以,上面那个问题的答案应该是:

  • 构造一个空对象
  • 将函数作为构造函数在这个对象的上下文中执行
  • 如果函数有对象类型的返回值则返回这个返回值,结束
  • 如果函数没有返回值或返回值不是对象类型,则让这个对象继承函数的原型方法并返回,结束

因此,要手写一个new方法按照这个思路来就可以了,我们依然在Function的原型方法上动手:

Function.prototype.mynew = function (...args) {
    var obj = {};
    var result = this.call(obj,...args);
    if (typeof result === 'object') {
        return result;
    }
    else {
        obj.__proto__ = this.prototype;
        return obj
    }
}
var JackFather = Father.mynew();
JackFather; // {error: "这样返回之后前面都白干了"} 
JackFather.showSonName();// Uncaught TypeError: JackFather.showSonName is not a function

对了,忘了把刚才测试的函数改回来了,这里走入了前一种情况。我们把函数改回之前的情况:

function Father(name) {
    this.age = 35;
    this.gender = "male";
    this.whoseFather = name;
    
    return ""; //只要不是返回对象类型,啥都行,另外别忘了null也是对象类型!我写文章的时候就搞错了!
}
Father.prototype.showSonName = function() {return this.whoseFather};
var JackFather = Father.mynew("Jack"); // Father {age: 35, gender: 'male', whoseFather: Jack}
JackFather.showSonName(); // Jack

一个new的方法就实现完成了!