javaScript基础之call、apply、bind实现原理

164 阅读4分钟

call、apply、bind是个啥

javaScript中,如果我们需要改变this的指向,那么我们可以借助call,apply,bind这是三个函数原型上的方法来实现。call和apply会立即执行该函数,call接收的第二个参数一个参数列表,而apply接收的第二个参数是一个数组,而bind会返回一个新函数。

模拟实现一个call

先写一个demo

let obj = {
    name:'chw'
}
function printName(){
    console.log(this.name)
}
printName.call(obj)
//chw

第一版实现

在上面的demo中我们可以看出,call是函数原型上的方法,执行call的时候做了两件事情

  1. 将this执行obj
  2. 立即执行printName方法 如何将this执行obj呐,我们知道如果将printName变成obj的方法,是不是就可以改变this,同时也可以执行该方法了呐,最后再将该函数删除,那么我们可以很轻松的写出代码
Function.prototype.myCall = function(context){
    //context 就是传入需要改变this指向的对象
    //这里的this可以取到执行的函数
    context.fn = this
    context.fn()
    delete context.fn
}
//test
let obj = {
    name:'chw'
}
function printName(){
    console.log(this.name)
}
printName.myCall(obj)
//chw

第二版,解决参数问题

由于我们还需要传递给执行函数参数,同时函数的参数长度是不固定的,这个我们可以通过arguments对象取出,同时我们还需要除去第一个传递进来context,这个时候你可能会提出可以利用es6的...语法可以收集参数,其实也是可以,我们还可以使用eval函数,不过这个是一个危险函数,项目中尽量避免使用,他会将传入的字符串当成js代码执行。

Function.prototype.myCall = function(context){
    //context 就是传入需要改变this指向的对象
    //这里的this可以取到执行的函数
    context.fn = this
    //收集参数
    let args = []
    for(let i=1;i<arguments.length;i++){
        args.push(arguments[i])
    }
    // eval('context.fn('+args+')') //args 会自动调用toSting()
    //or
    context.fn(...args)
    delete context.fn
}
//test
let obj = {
    name:'chw'
}
function printName(a,b){
    console.log(this.name,a,b)
}
printName.myCall(obj,12,13)
//chw,12,13

第三版,细节优化

  1. context可以传递null,这个时候this指向widow
  2. 有返回值时候,需要将返回值返回
Function.prototype.myCall = function(context){
    context = context || window
    context.fn = this
    let args = []
    for(let i=1;i<arguments.length;i++){
        args.push(arguments[i])
    }
    let result = eval('context.fn('+args+')')
    delete context.fn
    return result
}
//test
let obj = {
    name:'chw'
}
function printName(a,b){
    console.log(this.name,a,b)
    return {a:1}
}
printName.myCall(obj,12,13)
//chw,12,13
//{a:1}

模拟实现一个apply

apply和call的区别仅仅在参数传递的过程,我们可以很轻松的写出代码

Function.prototype.myApply = function(context,arr){
   context = context || widnow
   context.fn = this
   let result
   if(!arr){
       result = context.fn()
   }else{
       result = eval('context.fn('+arr+')')
   }
   delete context.fn
   return result
}
//test
let obj = {
 name:'chw'
}
function printName(a,b){
    console.log(this.name,a,b)
}
printName.myApply(obj,[12,13])

模拟实现一个bind

写一个demo

let obj = {
    name:'chw'
}
function printName(){
 console.log(this.name)
}
let res = printName.bind(obj)
res()
//chw

第一版

bind的定义:创建一个新函数,bind()函数执行的时候,将this指向第一个参数对象,其余的参数作为新函数的参数,供调用者使用。 那么这个过程有两步:

  1. 创建一个新函数并返回
  2. 将this 执行第一个参数
Function.prototype.myBind = function(context){
 //闭包保存执行的函数
 let self = this
 //返回一个函数
 return function(){
     //改变this指向
     return self.call(context)
 }
}
//test
let obj = {
    name:'chw'
}
function printName(){
 console.log(this.name)
}
let res = printName.myBind(obj)
res()
//chw

第二版,解决参数问题

let obj = {
    name:'chw'
}
function printName(age,work){
 console.log(this.name,age,work)
}
let res = printName.bind(obj,18)
res('coding')
//chw,18,coding

从上面demo可以看出参数可以分两次传递,那么我们需要将第一次bind传递的参数和返回的参数合并起来再执行

Function.prototype.myBind = function(context){
    //保存执行的函数
    let self = this
    //取出bind到第二个到最后一个参数
    let arg = [].slice.call(arguments,1)
    return function(){
        let args = [].slice.call(arguments)
        let allArg = arg.concat(args)
        return self.apply(context,allArg)
    }
}
//test
let obj = {
    name:'chw'
}
function print(age,work){
 console.log(this.name,age,work)
}
let res = print.myBind(obj,18)
res('coding')
//chw,18,coding

第三版,实现对bind返回的函数进行new操作(改变this优先级问题)

最难的点是,可以对放回的函数进行new构造对象,原来bind提供的对象会失效,这个时候的this是指向new出来的实例对象,看个demo

let obj = {
 value:'cc'
}
function print(name){
    this.habit = 'play'
    console.log(this.value,name)
}
let res = print.bind(obj)
let obj2 = new res('chw')
//undefined,chw //这个时候this不在指向obj,而是obj2
//obj2 === {habit:'paly'}

实现过程如下:如果返回函数是被new实例化过的,那么我们可以判断this的指向来决定绑定的this

Function.prototype.myBind = function(context){
    //保存执行的函数
    let self = this
    //取出bind到第二个到最后一个参数
    let arg = [].slice.call(arguments,1)
    let bunod = function(){
        let args = [].slice.call(arguments)
        let allArg = arg.concat(args)
        //如果this的instance指向bound,那么就被new实例化了
        //否则如果是被当成普通函数执行,this就指向全局window了
        let obj = this instanceof bunod ? bunod : context
        return self.apply(obj,allArg)
    }
    //这样可以继承绑定函数上的原型了
    bunod.prototype = this.prototype
    return bunod
}
//test
let obj = {
 value:'cc'
}
function print(name,age){
    this.habit = 'play'
    console.log(this.value,name,age)
}
let res = print.bind(obj,'cc')
let obj2 = new res(18)
//undefined , chw,18

至此已经完结call、apply、bind 的实现过程,有不对地方欢迎指出