前端面试必问系列·手写call、apply、bind

个人博客欢迎分享

大前端养成记欢迎StartFork

前言:

手写call、apply、bind是前端面试必问的问题,但是也不用当太当心,因为实现起来也并不算太难。

callapplybind都可以修改一个函数执行时候的this指向,虽然用法上略有差异,但是在实现的思想上如出一辙。

在实现之前,必须要知道这三个方法是如何使用的:

const obj = {
    language: "javascript"
}
function fn(...arg) {
    console.log("The current language is " + this.language)
    console.log(arg)
}
fn() // The current language is undefined,[]
fn.call(obj, "javascript", "java", "c++") // "The current language is javascript",["javascript", "java", "c++"]
fn.apply(obj, ["javascript", "java", "c++"]) // "The current language is javascript",["javascript", "java", "c++"]
const bindFn = fn.bind(obj, "javascript", "java", "c++")
bindFn() // "The current language is javascript",["javascript", "java", "c++"]
new bindFn() // The current language is undefined,["javascript", "java", "c++"]

从上面的代码明显的发现,callapplybind可以修改函数执行时内部的this指向,并且还能传参数到函数中。三者的使用方式都是通过函数点的方式使用,说明这三个方法都是在原型上(Function.prototype)。callapply不同之处就是传递的参数方式不同。最大的不同就是bindbind有两种用法,如果返回的函数当成普通函数调用的时候,里面的this还是传进去的obj,但是new的时候,函数内部的this指向window,返回值则是new的实例。

了解了这些知识后我们就可以撸代码。。。

一. Function.prototype.call实现

从上面可以知道,call的第一个参数是修改函数内部的this指向,从第二个起则是传到函数的参数。

Function.prototype.call = function(context = window) {
    // 创建一个唯一值 防止context或者window有相同的key
    const symbol = Symbol()
    context[symbol] = this // 这里的this是调用者 也就是函数
    const ret = context[symbol](...Array.from(arguments).slice(1))
    delete context[symbol]  // 删除我们添加的属性 不修改传进来的对象或者污染全局变量
    return ret
}

function fn() {
    console.log(this.name)
}
fn.call({name: "Little Boy"}, "arg1", "arg2") // Little Boy

看到这里很多小伙伴肯定有很多疑问:为什么要用到Symbol,为什么要在context上挂载一个调用者函数,接下来就一一来解答。

首先先来看这段代码:

const obj = {
    name: "Little Boy",
    fn: function() {
        console.log(this.name)
    }
}
obj.fn() // Little Boy

这段代码的意思就是对象点的形式去调用函数,this是指向当前对象的(如果这里还不明白的小伙伴,需要对this的指向好好复习了),那么利用这个套路我们就可以实现call,使用对象点的方式修改函数运行时内部的this指向,这就是为什么要在context上挂载一个调用者函数,既然要在context上挂载一个函数那么就必须要保证key唯一,因为Symbol可以生成一个唯一的值,所以这里用到了Symbol

二. Function.prototype.apply实现

如果看懂了call是如何实现了之后,apply就很好实现了,因为它们两者不同的地方就是传递的参数不同。

Function.prototype.apply = function(context = window) {
    // 创建一个唯一值 防止context或者window有相同的key
    const symbol = Symbol()
    context[symbol] = this // 这里的this是调用者 也就是函数
    const args = arguments[1] || []
    const ret = context[symbol](...args)
    delete context[symbol] // 删除我们添加的属性 不修改传进来的对象或者污染全局变量
    return ret
}

function fn() {
    console.log(this.name)
}

fn.apply({name: "Little Boy"}, []) // Little Boy

三. Function.prototype.bind实现

bind的方法有两种用法,一种是直接调用,另一种是new的方式调用。实现代码如下:

Function.prototype.bind = function(context = window) {
    // 创建一个唯一值 防止context或者window有相同的key
    const symbol = Symbol()
    context[symbol] = this // 这里的this是调用者 也就是函数
    const firstArgs = Array.from(arguments).slice(1) // 获取第一次调用的参数
    const bindFn = function() {
        const secondArgs = Array.from(arguments) // 获取第二次传入的参数
        const fn = context[symbol] // 获取调用函数
        return this instanceof bindFn ? fn(...[...firstArgs, ...secondArgs]) : context[symbol](...[...firstArgs, ...secondArgs])
    }
    bindFn.prototype = Object.create(this.prototype)
    return bindFn
}
const obj = {
    language: "javascript"
}

function fn(...arg) {
    console.log("The current language is " + this.language)
    console.log(arg)
}
const newFn = fn.bind(obj, 1, 2)
newFn(3, 4) // The current language is javascript,[1, 2, 3, 4]
new newFn(3, 4) // The current language is undefined,{}
console.log(new newFn(3, 4)) // 输出的时候会发现是这个样子的 bindFn {} 发现名称并不是预期的效果,目前我并没有想到好的方案,有知道的小伙伴可以在评论区大显身手哦。

代码看到这里,疑问也会非常的多,大概有如下问题:

  1. returnFn函数内部this instanceof returnFn为什么要这样判断,判断依据是什么。
  2. returnFn函数内部的fn,为什么要赋值给fn后再调用呢。

首先第一个问题,new与不new的区别就是函数内部的this不同,new的时候returnFn内部的this是指向实例的,不new的时候,returnFn内部的this是指向调用者的,这里是windowthis instanceof returnFn所以这样是为了判断是否new下执行的不同的逻辑。(这里对this指向有问题的小伙伴,需要去补充这方面的知识了)。

第二个问题,我们使用bind会发现,new的时候fn内部的this是指向window的,既然想达到这种效果,就必须在returnFn中定义个变量把函数取出来,这样相当于window.fnfn函数内部的this就是指向window,这就是赋值给fn后再调用的目的。

最后,希望这篇文章帮助大家对callapplybind的理解。

你可能感兴趣的:(前端面试必问系列·手写call、apply、bind)