call、apply、bind的原理(清新脱俗)

160 阅读4分钟

前言

本文主要是讲下call、apply、bind 的实现原理,我们都知道 call、apply、bind 三个方法是用来改变 this 指向的,但是也只局限于会用状态。具体的实现原理,却没有思考过。所以有必要研究一下。在明白起原理的情况下,才能用的更好。

请输入图片描述

call、apply 和 bind 是挂在 Function 对象上的三个方法,所以调用这三个方法的必须是一个函数。

调用示例

// thisArg  在 function 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格
//模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
fn.call(thisArg,arg1,arg2,....)
fn.apply(thisArg,[arg1,arg2,....])
fn.bind(thisArg,arg1,arg2,....)()

在这里先补充点this指向的相关知识

关于This指向补充

  • 在浏览器里,在全局范围内this 指向window对象
  • 在函数中,this永远指向最后调用他的那个对象;
  • 构造函数中,this指向new出来的那个新的对象;
  • callapplybind 中的this被强绑定在指定的那个对象上;
  • 箭头函数中this比较特殊,箭头函数this为父作用域的this,不是调用时的this.要知道前几种方式,都是调用时确定,也就是动态的,而箭头函数的this指向是静态的,指向是不可更改的

总结:谁调用了该函数,函数作用域就指向谁。箭头函数永远指向它的父级作用域 请输入图片描述

注意:因为这call、apply、bind这几个方法底层的源码都是通过c++来实现,我们这里只是用js来模拟实现

实现call方法

  1. call方法的使用是通过函数调用,第一个参数是我们需要指定的this指向,后面可以是单独给出的一个或多个参数。

  2. call方法的作用无非就是执行一个函数的时候,把这个函数的作用域对象指向新的作用域对象,接下来就可以围绕着这段话去实现一个自己的call方法


 Function.prototype.mycall = function (thisArg, ...params) {
       //定义对象的唯一属性值
       let fn=Symbol('fn')
       //传进来的作用域如果是null或者undefined自动替换为指向全局对象
       //传进来的作用域可能是Number、String等类型,但是我们又要它是对象,所以我们利用Object强制转换下
        let newArg =(thisArg !== undefined && thisArg !== null) ? Object(thisArg) : window;
       //this指向的就是调用mycall的那个函数,把this添加到新作用域的属性fn上。在新作用域对象上调用fn,fn的this 
       //指向就会指向newArg 
        newArg[fn] = this;
       //上面...params的意思是es6用的剩余参数,这时params是个数组,然后我们需要展开来传到fn里面,下面...params 
       //的作用是展开参数
        let result=newArg[fn](...params);
        //防止作用域污染,删除该属性
        delete newArg[fn];
        return result;
 };

let newObj={
     name:'小张'
}
let obj={
    name:'小王',
    fn:function(num){
        console.log(this.name)
        console.log(num)
    }
}
obj.fn.mycall(newObj,1)
上面打印的name就是小张,num是1

实现apply方法

  1. apply 和 call 的功能完全一致,唯一不同就是参数形式不同而已,只是apply方法后面的参数只能是一个数组。

 Function.prototype.myapply = function (thisArg, params) {
        let fn=Symbol('fn')
        let newArg =(thisArg !== undefined && thisArg !== null) ? Object(thisArg) : window;
        newArg[fn] = this;
        //必须为数组类型,否则抛出错误
        if(!Array.isArray(params)){
            throw new TypeError('params not Array')
        }
        let result=newArg[fn](...params);
        delete newArg[fn];
        return result;
 };

let newObj={
     name:'小张'
}
let obj={
    name:'小王',
    fn:function(num){
        console.log(this.name)
        console.log(num)
    }
}
obj.fn.mycall(newObj,[1])
上面打印的name就是小张,num是1

实现bind方法

  1. bind与call的不同是它先返回的是一个函数,再让我们调用这个函数。
  2. bind() 方法创建一个新的函数作为返回值,在 bind() 被调用时,这个新函数的 this 被bind的第一个参数指定,其余的参数将作为新函数的参数供调用时使用

 Function.prototype.mybind=function(thisArg,...params){
         let fn=Symbol('fn')
         let newArg=(thisArg!==undefined && thisArg !==null)?Object(thisArg):window
         newArg[fn]=this
         return function(...args){
               //拼接bind的参数列表和新函数的参数列表
               let newArr=[...params,...args]
               let proxy= newArg[fn](...newArr)
               delete newArg[fn]
               return proxy
         }
}

let newObj={
     name:'小张'
}
let obj={
    name:'小王',
    fn:function(num1,num2){
        console.log(this.name)
        console.log(num1,num2)
    }
}
obj.fn.mybind(newObj,1)(2)
上面打印的name就是小张,num1是1 num2是2

总结

call、apply 和 bind 的功能非常相似,那什么时候该使用 call、apply,什么时候使用 bind 呢?其实这个也没有明确的规定,只要知其原理,相互转化何其简单,主要的区别就是 call、apply 绑定后是立即执行,而 bind 绑定后是返回待调用的。