手写call、apply、bind方法

247 阅读4分钟

Function.prototype.call()

方法介绍

语法

function.call(thisArg, arg1, arg2, ...)

参数

thisArg

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

arg1, arg2, ...

指定的参数列表。

返回值

使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined

描述

call() 允许为不同的对象分配和调用属于一个对象的函数/方法。

call() 提供新的 this 值给当前调用的函数/方法。你可以使用 call 来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)。

手写思路总结

  • 接收的第一个参数如果为null或undefined,自动指定this为window
  • 调用call,在改变函数内部this指向后,立即执行函数,并将函数执行结果返回
  • 执行的函数为调用当前call方法的函数,可以理解为call方法内部默认的this
  • 在执行完毕后,将赋值的上下文对象清除

手写call方法

//方法挂载在Functin的原型上,这样所有的函数都可以调用
Function.prototype.myCall = function (context, ...arr) {
  if (context === null || context === undefined) {
    // 如果传入的this对象为 null 和 undefined ,则将函数内部的this自动指向全局对象(浏览器中为window)
    context = window
  } else {
      //防止指定的为原始值,将其转化为原始值对应的实例对象
    context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
  }
  const specialPrototype = Symbol('only') // 定义一个唯一的属性用于存放接下来被执行的函数
  //当前的this就是调用的函数
  //相当于context.specialPrototype = 需要被执行的函数。那么需要被执行的函数就是由context调用的,也就是说,需要执行的函数的内部的this变为了调用他的对象context
  context[specialPrototype] = this // 函数的this指向隐式绑定到context上。
  let result = context[specialPrototype](...arr) // 通过隐式绑定执行函数并传递参数
  delete context[specialPrototype] // 删除上下文对象的属性
  return result // 返回函数执行结果
}

Function.prototype.apply()

方法介绍

语法

func.apply(thisArg, [argsArray])

参数

thisArg

必选的。在 func 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。

argsArray

可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 null 或 undefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。 浏览器兼容性 请参阅本文底部内容。 返回值 调用有指定this值和参数的函数的结果。

描述

在调用一个存在的函数时,你可以为其指定一个 this 对象。 this 指当前对象,也就是正在调用这个函数的对象。 使用 apply, 你可以只写一次这个方法然后在另一个对象中继承它,而不用在新对象中重复写该方法。

applycall() 非常相似,不同之处在于提供参数的方式。apply 使用参数数组而不是一组参数列表。apply 可以使用数组字面量(array literal),如 fun.apply(this, ['eat', 'bananas']),或数组对象, 如 fun.apply(this, new Array('eat', 'bananas'))

你也可以使用 arguments对象作为 argsArray 参数。 arguments 是一个函数的局部变量。 它可以被用作被调用对象的所有未指定的参数。 这样,你在使用apply函数的时候就不需要知道被调用对象的所有参数。 你可以使用arguments来把所有的参数传递给被调用对象。 被调用对象接下来就负责处理这些参数

手写思路总结

  • 思路与call方法相似,只是apply的参数变成了数组
  • 要对数组做额外的判断,是否是一个复合需求的数组

手写apply方法

//方法挂载在Functin的原型上,这样所有的函数都可以调用
Function.prototype.myApply = function (context) {
 // 如果传入的this对象为 null 和 undefined ,则将函数内部的this自动指向全局对象(浏览器中为window)
  if (context === null || content === undefined) {
    context = window
  } else {
  // //防止指定的为原始值,将其转化为原始值对应的实例对象
    context = Object(context)
  }

// JavaScript权威指南判断是否为类数组对象
  function isArrayLike(o) {
    if (
      o &&                              // o不是null、undefined等     
      typeof o === 'object' &&          // o是对象
      isFinite(o.length) &&             // o.length是有限数值
      o.length >= 0 &&                  // o.length为非负值
      o.length === Math.floor(o.length) &&  // o.length是整数
      o.length < Math.pow(2, 32)           // o.length < 2^32
    )
      return true
    else return false
  }
  const specialPrototype = Symbol('only')   // 定义一个唯一的属性用于存放接下来被执行的函数
  context[specialPrototype] = this   // 隐式绑定this指向到context上
  let args = arguments[1]         // 获取参数数组
  let result
  if (args) {  // 是否传递第二个参数
    if (!Array.isArray(args) && !isArrayLike(args)) {
      throw new TypeError('参数类型错误')
    } else {
      args = Array.from(args)   // 转为数组
      result = context[specialPrototype](...args)  // 执行函数并展开数组,传递函数参数
    }
  } else {
    result = context[specialPrototype]()   // 执行函数
  }
  delete context[specialPrototype]   // 删除上下文对象的属性
  return result         // 返回函数执行结果
}

Function.prototype.bind()

方法介绍

语法

function.bind(thisArg[, arg1[, arg2[, ...]]])

参数

thisArg

调用绑定函数时作为 this 参数传递给目标函数的值。 如果使用new运算符构造绑定函数,则忽略该值。当使用 bind 在 setTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg。

arg1, arg2, ...

当目标函数被调用时,被预置入绑定函数的参数列表中的参数。

返回值

返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。

描述

bind() 函数会创建一个新的绑定函数(bound function,BF)。绑定函数是一个 exotic function object(怪异函数对象,ECMAScript 2015 中的术语),它包装了原函数对象。调用绑定函数通常会导致执行包装函数。 绑定函数具有以下内部属性:

  • [BoundTargetFunction] - 包装的函数对象
  • [BoundThis] - 在调用包装函数时始终作为 this 值传递的值。
  • [BoundArguments] - 列表,在对包装函数做任何调用都会优先用列表元素填充参数列表。
  • [Call] - 执行与此对象关联的代码。通过函数调用表达式调用。内部方法的参数是一个this值和一个包含通过调用表达式传递给函数的参数的列表。

当调用绑定函数时,它调用 BoundTargetFunction 上的内部方法 Call,就像这样 Call(boundThis, args)。其中,boundThisBoundThis,args 是 BoundArguments 加上通过函数调用传入的参数列表。

绑定函数也可以使用 new 运算符构造,它会表现为目标函数已经被构建完毕了似的。提供的 this 值会被忽略,但前置参数仍会提供给模拟函数。

手写思路总结

通过以上的介绍,我们总结到bind方法的以下特性:

  • 调用bind方法后,返回一个内部this改变的函数,函数不立即执行;
  • 接收参数中,第一个是执行的this对象,后面的的参数是可选参数,用于指定返回函数的默认参数;
  • 返回的函数在被调用的时候可能会继续传参,即二次传参
  • 如果返回的方法是以new进行构造,提供的this参数会被忽略,this指向当前的函数的实例对象

手写bind

Function.prototype.myBind = function (newThis, ...params) {
  const oldFn = this // 存储原函数
  //定义bind方最终返回的函数,这个函数在执行的时候可以接收参数
  let newFn = function (...secondParams) {
    //首先判断newFn是否是通过new方式构造执行的,判断的方式是判断newFn是否是this上的实例对象
    const isNew = this instanceof newFn
    const context = isNew ? this : Object(newThis) // 通过new调用的话,this指向不变,否则就改版this指向,即将该方法绑定到传入的newThis上
    //按照mdn上的介绍,内部可以使用call实现,改版this并执行函数
    //执行函数的时候,需要将两次的参数解构后一起传入
    return oldFn.call(context, ...params, ...secondParams)
  }
  if (oldFn.prototype) {
    // 复制原函数的prototype给newFn 一些情况下函数没有prototype,比如箭头函数
    newFn.prototype = oldFn.prototype
  }
  return newFn // 返回拷贝的函数
}

测试

let user = { name: 'Jason', age: 23, } 
function Fun(school) { 
    this.school = school; 
    console.log(this.name); 
    console.log(this.age); 
    console.log(this.school); 
} 
let fun = Fun.myBind(user, '江南大学'); // this -> user fun() // this -> 对象实例 
let obj = new fun(); 
console.log(obj.__proto__ === Fun.prototype) // ture