实现call、apply、bind方法

1,998 阅读4分钟

image.png

前言

现在已经来到了春招白热化了,陆陆续续地大厂暑期实习开放了,不搞定这些大厂路上的拦路虎怎么行呢~

话不多说,直接进入主题吧

Call

用法

简单来说:call 接收多个参数,第一个为函数上下文也就是this,后边参数为函数本身的参数。

举例

let Name = {
    name: '大眼睛图图',
    say() {
        console.log(this, 'this')
        console.log(`我叫${this.name}`)
    }
}

Name1 = {
    name: '大耳朵图图'
}

Name.say()
// { name: '大眼睛图图', say: [Function: say] } this      
//  我叫大眼睛图图

Name.say.call(Name1)
// { name: '大耳朵图图' } this
// 我叫大耳朵图图

实现

call方法,我们最容易想到的有两个功能

  • 将调用它的函数立即执行
  • 改变其this指向

这两大核心功能该如何实现呢?

Function.prototype.Mycall = function(context){
    // context是Name1传入的对象
    context.say = this  // this执行调用Mycall的say()函数
    context.say() // 执行conext.say()方法
}

Name.say.Mycall(Name1)

可以看到,在这里,我们创建了context的say属性(context.say)等于调用Mycall的函数(Name.say),然后执行context中say方法的(context.say)同时也直接把调用Mycall的函数(Name.say)执行了。

带入我们的测试用例,可以通过!!! image.png

两行代码就写完了?没错,核心功能就是两行代码,当然还是有不少的细节值得我们注意:

  • call没有指定参数,call中的this会指向window
  • call中增加的say方法可能会被改写
  • call中增加say方法,会增加原先call函数中的属性,改变call函数的结构
  • call函数可以传入多个参数

接下来,让我们一一解决

Function.prototype.MyCall = function (context, ...arg) {

    // 如果MyCall没有传入参数,context就是window
    // context是Name1传入的对象
    context = context || window
   // 用Symbol来创建唯一的fn,不被之前的变量污染
    let fn = Symbol()
    context[fn] = this
   // 传入MyCall的多个参数
    context[fn](...arg)
    // 应及时将增加的fn方法删除
    delete context[fn]
}

示例: image.png 可以看到这里还是得到了我们想要的效果

当MyCall不传参数时 代码:

let Name = {
    name: '大眼睛图图',
    say() {
        console.log(this, 'this')
        console.log(`我叫${this.name}`)
    }
}

Name1 = {
    name: '大耳朵图图'
}


Function.prototype.MyCall = function (context, ...arg) {
    // 如果MyCall没有传入参数,context就是window
    // context是Name1传入的对象
    context = context || window
    // 用Symbol来创建唯一的fn,不被之前的变量污染
    let fn = Symbol()
    context[fn] = this
    // 传入MyCall的多个参数
    context[fn](...arg)
    // 应及时将增加的fn方法删除
    delete context[fn]
}
// 测试
Name.say.MyCall()

image.png 怎么报错了呢?

注意注意,这里是node环境下,我们要拿到浏览器环境下运行 image.png 这样就对到了~

这个call方法的手写我们就拿下了!

😁😁😁

apply

用法

apply接收两个参数

  • 第一个参数为函数上下文this

  • 第二个参数为函数参数只不过是通过一个数组的形式传入的。

如下

Name.say.apply(Name1,['大眼睛','大耳朵'])

实现

是不是我们又要重新来一遍类似call的写法呢?

NO NO NO

我们可以发现,它其实和call很像,就是参数的接收形式变成了数组而已,仅仅对参数进行调整就行了

let Name = {
    name: '大眼睛图图',
    say(a) {
        console.log(this, 'this')
        console.log(`我叫${this.name}${a}`)
    }
}

Name1 = {
    name: '大耳朵图图'
}

Function.prototype.MyApply = function (context, arg) {
    // 如果MyCall没有传入参数,context就是window
    // context是Name1传入的对象
    context = context || window
    // 用Symbol来创建唯一的fn,不被之前的变量污染
    let fn = Symbol()
    context[fn] = this
    // 传入MyCall的多个参数
    context[fn](...arg)
    // 应及时将增加的fn方法删除
    delete context[fn]
}
// 测试
Name.say.MyApply(Name1, ['1'])

(这里我们对say方法增加了一个参数,以便看到后面的效果)

执行结果

image.png OK!

实现了call还不小心实现了apply,直接买一送一了~

Bind

用法

function.bind接收多个参数

  • 第一个参数是要绑定的this值
  • 剩余的参数就是传入的参数。
  • 并且bind函数调用时,被调用的函数不会立即执行,这一点和call,apply很不一样

示例:

let Name = {
    name: '大眼睛图图',
    say() {
        console.log(this, 'this')
        console.log(`我叫${this.name}`)
    }
}

Name1 = {
    name: '大耳朵图图'
}

// 测试
const fn = Name.say.bind(Name1)
fn()
// { name: '大耳朵图图' } this
// 我叫大耳朵图图

实现

来规矩,先实现核心功能吧

  • 改变this执行
  • 函数(say)不会立即执行

上代码!

Function.prototype.myBind = function (context) {
    
    let fn = Symbol()
    fn = this  // this 还是指向调用它的函数

    // 返回Fn函数,等到下次调用Fn函数时才会调用fn函数
    return function Fn() {

        return fn.apply(context) 

    }
}

这里myBind函数直接返回Fn()函数,很好的解决了当执行myBind函数时say方法不会调用的问题。

并且这里我们直接用上面造好的轮子——apply函数直接改变fn函数中的this指向问题

后面再把我们可能要传入的参数补上

Function.prototype.myBind = function (context, ...args) {
    let fn = Symbol()
    fn = this

    return function Fn() {
        // 这里arguments的作用是拿到Fn中传入的参数
        return fn.apply(context, args.concat(arguments))
    }
}

测试:

image.png

没错,是我们想要的~



new bind

bind这就实现了?

不要又上当了,我们才实现了一半呢~

bind 还有一个特点

一个绑定函数也能使用new操作符创建对象
这种行为就像把原函数当成构造器。
提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

看个例子再感受一下

function bar() {
   this.habit = 'shopping';
}

var bindFoo = bar.bind();

var obj = new bindFoo();

console.log(obj.habit); // shopping

这个new bindFoo的行为,就像把原函数bar当成构造器new了出来

那么问题就变成了bindFoo这个构造函数有没有被new呢?

如果被new了那么它的效果和直接new bar构造函数的效果是一致的,直接new bar就好了

所以在这里我们要通过instanceof这个方法来实现~

Function.prototype.myBind = function (context, ...args) {

   let fn = Symbol('fn')
   fn = this

   return function Fn() {
       如果是通过 new 调用的,绑定 this 为实例对象
       if (this instanceof Fn) {
           return new fn(...args, ...arguments)
       }
       return fn.apply(context, args.concat(arguments))

   }
}

测试:

image.png 这下,可算终于实现了~🎉🎉🎉

另外,有些初学者可能对arguments这个函数参数的类数组对象不太了解,所以下面笔者也想简单介绍一下

arguments

arguments 是一个对应于传递给函数的参数的类数组对象。

function fun(a, b, c) {
  console.log(arguments[0]); // 1
  console.log(arguments[1]); // 2
  console.log(arguments[2]); // 3
}

arguments 对象不是一个 Array 

它类似于 Array,但除了 length 属性和索引元素之外没有任何 Array 属性。

总结

真的还是挺不容易的,不管怎么样,看懂永远只是学习的第一步,后面还得不断巩固,自己手写和不断复习才能真的掌握它们。

希望能够帮助到屏幕前的你~

本文正在参加「金石计划」