原生JS实现call、apply、bind(超详细!)

想要实现实现一个方法,必然要先了解它。其实call、apply、bind都是咱们平常开发中比较经常使用的一些方法。他们都有一个共同的做用:改变this指向数组

call和apply本质上来讲并无什么区别,他们的做用是同样,区别仅在于param的传参方式。call接受连续传参,apply接受数组传参。markdown

bind和call\apply的区别想对来讲就大一些,call\apply都是当即执行的,它们的返回结果就是fn的执行结果;而bind并不当即执行,它的返回结果是fn的拷贝,改变this指向后不会当即执行,须要自行调用这个新函数。app

/** @params: targetThis (可选) fn的this的目标指向,默认指向window @params: param (可选) 传入fn的参数 */
fn.call(targetThis, param1, param2, param3 ...)
fn.apply(targetThis, [param1, param2, param3 ...])
fn.bind(targetThis, param1, param2, param3..)
(PS: 为了统一,下面文章都用fn、targetThis、params来叙述
复制代码
  • call的实现函数

    call的做用只说一句改变this指向就太笼统了,它的执行能够大体拆分为以下几个步骤:ui

    1. 将传入的fn做为targetThis的私有方法,使fn能够在this指向targetThis的前提下执行
    2. 经过arguments截取连续传入的不定量的参数
    3. 执行fn、记录执行结果
    4. 删除临时挂在targetThis的fn(不改变原来的targetThis)
    5. 返回执行结果
Function.prototype._call = function (context = window) {
  // 首先了解各个参数的值
  console.log('this', this) // foo
  console.log('context', context) // target {value: 1}
  console.log('arguments', arguments) // { {value: 1}, 'param1', 'param2'}
  let _context = context // 第一个参数不传默认为window
  _context.fn = this // 将foo做为target的私有方法, this改变
  const args = [...arguments].slice(1) // 截取下标从1开始的参数 {'param1', 'param2'}
  const result = _context.fn(...args) // 当即执行
  delete _context.fn // 删除fn 不改变target
  return result
}
// eg
let target = {
  value: 1
}
function foo(param1, param2) {
  console.log(param1)
  console.log(param2)
  console.log(this.value)
}
foo._call(target, 'param1', 'param2') // param1 param2 1
复制代码
  • apply的实现

apply的实现与call基本上是一致的,区别仅在于params的形式上,apply的参数为数组形式。this

Function.prototype._apply = function (context = window, arr) {
  // 首先了解各个参数的值
  console.log('this', this) // fn
  console.log('context', context) // target {value: 1}
  let _context = context// 第一个参数不传默认为window
  _context.fn = this // 将fn做为target的私有方法, this改变
  const result = arr.length ? _context.fn(...arr) : _context.fn() // 判断是否传入arr
  delete _context.fn // 删除fn 不改变target
  return result
}
// eg
let target = {
  value: 1
}
function fn(param1, param2) {
  console.log(param1)
  console.log(param2)
  console.log(this.value)
}
fn._apply(target, ['param1', 'param2']) // param1 param2 1
复制代码
  • bind的实现

因为ES5内置的Function.prototype.bind(..)实现过于复杂,这里咱们借助call/apply来实现bind,在实现bind以前咱们明确几个重点:spa

  1. bind并非当即执行的,它会返回一个函数,调用返回的函数改变this指向
  2. 在this的绑定规则中, new绑定的优先级高于硬绑定(划重点,待会要考)
  3. MDN中的原话:

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

(划重点加感叹号)code

咱们来解读一下, “绑定函数也可使用new运算构造 ” 就是说会在new中使用硬绑定,而new绑定的优先级又高于硬绑定,咱们须要对有new的状况作特殊处理;“前置参数仍会提供给模拟函数”就是说,在new中使用硬绑定函数能够预先设置一些参数,在使用new进行初始化时能够传入其余参数,是柯里化的一种。orm

Function.prototype._bind = function (context = window) {
    // 首先搞清楚各个参数的含义
    console.log('this', this) // foo
    console.log('context', context) // target {value: 1}
    console.log('argument', arguments) // { {value1: 1}, 'param1', 'param2'}
    const _this = this // 保存this
    // 这里arguments为foo的arguments
    const args = [...arguments].slice(1) // 截取下边为1开始的参数 ['param1', 'param2']
    var fn =  function () {
        const bindArgs = [...arguments] // 这里的arguments为fn的
        // 做为构造函数时,this指向实例fn this instanceof fn 返回true
        // 不做为构造函数时, this指向window, this instanceof fn 返回false 须要将this指向改变为context
        // [...args, ...bindArgs] 合并预先传入的参数和new 实例化时传入的参数
        return _this.apply( this instanceof fn ? this : context , [...args, ...bindArgs ])
    }
    // 修改绑定函数的prototype为foo的prototype,继承foo的属性
    // 建立一个空对象,让空对象__proto__指向_this.prototype, 作到修改fn的prototye不会影响到foo
    fn.prototype = Object.create(_this.prototype)
    return fn
}
// eg
let target = {
    value: 1
}
function foo(param1, param2, param3) {
    console.log(param1)
    console.log(param2)
    console.log(param3)
    console.log(this.value)
}
const bindFn = foo._bind(target, 'param1', 'param2') 
bindFn() // param1 param2 undefined 1

const newBindFn = new bindFn('param3') // param1 param2 param3 undefined
复制代码
相关文章
相关标签/搜索