五、js手写系列

184 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

根据前面,我们已经知道了怎么去改变this的指向,其中硬式绑定apply,call,bind。下面看一下如何自己实现这三个函数

一 、apply方法实现

我们先要知道apply方法如何使用,第一个参数是要改变的this对象,第二个参数是传递的参数数组。那我们来实现一下。

/**
 * kbApply
 * @param {*} thisArg
 * @param {*} array
 * @returns
 */
Function.prototype.kbApply = function (thisArg, array) {
  // 谁调用call,根据隐式绑定,this指向谁
  let fn = this
  // 将传入要改变的this对象转成对象
  let _this =
    thisArg === null || thisArg === undefined ? window : Object(thisArg)
  _this.fn = fn

  return _this.fn(...array)
}

function foo(num1, num2) {
  console.log(this.name, num1 + num2)
}

foo.kbApply({ name: 'wkb' }, [10, 20])

这里没有做很多的边界处理。其中的原理就是用到了隐式绑定。

1.  如何在自定义的apply函数中获取到当前的调用函数?我们在上一篇文章中知道了this的隐式绑定会改变this的指向。函数也是对象,函数调用apply函数,那apply函数中的this不就是指向自定义函数了嘛。
2.  获取到当前调用的函数后,怎么改变当前调用函数的this指向为传入的参数呢?这里,我们还是用this的隐式绑定来实现。首先,我们先把传入的参数用Object()转成对象。然后把调用的函数加入参数对象,用参数转来的对象来执行该函数。根据隐式绑定,函数中的this的将变成传入的参数对象,这样就实现了apply功能

二、call方法实现

同理,根据apply实现call。与apply实现不一样的是,call的传参是剩余参数。

/**
 * kbCall
 * @param {*} thisArg
 * @param  {...any} args
 * @returns
 */
Function.prototype.kbCall = function (thisArg, ...args) {
  let fn = this
  let _this =
    thisArg === null || thisArg === undefined ? window : Object(thisArg)
  _this.fn = fn

  return _this.fn(...args)
}

function foo(num1, num2) {
  console.log(this.name, num1 + num2)
}

foo.kbCall({ name: 'wkb' }, 10, 20)

三、bind方法实现

bind的实现方式与call和apply不同。因为call和apply会立即执行,并且返回结果,但bind是返回一个函数,函数执行后返回结果。

我们来构思一下实现过程:

1.  首先还是先拿到调用函数对象,把传入的this参数转成对象,并且把调用对象加入参数对象中
2.  返回一个函数,函数中接受参数
3.  在函数中利用闭包,传入两次参数变量并调用函数
4.  返回函数执行结果

好了,我们在call的实现代码上做一些修改。

/**
 * kbBind
 * @param {*} thisArg
 * @param  {...any} args
 * @returns
 */
Function.prototype.kbBind = function (thisArg, ...args) {
  let fn = this
  let _this =
    thisArg === null || thisArg === undefined ? window : Object(thisArg)
  _this.fn = fn

  return function (...remain) {
    return _this.fn(...args, ...remain)
  }
}

function foo(num1, num2) {
  console.log(this.name, num1 + num2)
}

const fn = foo.kbBind({ name: 'wkb' }, 10)
fn(10)

四、函数柯里化实现

柯里化技术,主要体现在函数里面返回函数。就是将多变量函数拆解为单变量(或部分变量)的多个函数并依次调用。

再直白一点:利用闭包,可以形成一个不销毁的私有作用域,把预先处理的内容都存在这个不销毁的作用域里面,并且返回一个函数,以后要执行的就是这个函数。

下面来看一个案例,如果我们要想实现一个简单的加法复用方法

function add(a, b){
	return function(c){
		return a + b + c
  }
}

const add10 = add(5, 5);
add10(2);

这样就利用了函数的柯里化来实现了函数复用,但我们每次都要写一个高阶函数来实现,可不可以实现一个函数来生成高阶函数呢?当然是可以的。

我们先构思一下:

1.  首先我们需要一个函数,参数是接受一个函数,返回值是一个函数
2.  如果返回的函数传入的参数比需要柯里化函数需要的参数少,则递归返回函数
3.  如果传入的参数等于需要柯里化函数需要的参数时,则执行开始传入的函数,
    并且传入参数,返回结果

好了,我么先实现一下

function kbCurring(fn) {
  if (typeof fn !== 'function') {
    throw Error('请传入函数')
  }
  return function curring(...params) {
    if (params.length >= fn.length) {
      return fn.call(this, ...params)
    } else {
      // 参数没传完
      return function (...restArgs) {
        return curring.call(this, ...params, ...restArgs)
      }
    }
  }
}

看着很简单,我们校验一下

function sum(a, b, c) {
  return a + b + c
}

const fn = kbCurring(sum)
const add20 = fn(10, 10)

console.log(add20(1))

没有问题,函数柯里化就实现了。

五、compose函数合并实现

在平时的开发中,我们会经常遇到这样的情况:把一个函数的结果当做另外一个函数的参数使用。这时我们一般需要手动调用一下函数,然后在把结果传入另一个函数,这样太麻烦了,既然这种情况很常见,那不如封装成 一个函数。

有了实现函数柯里化的经验,我想这个应该不是很难

function compose(...fns) {
  // 1. 参数校验
  if (fns.length <= 1) {
    throw Error('参数长度要大于1')
  }
  fns.forEach((fn) => {
    if (typeof fn !== 'function') {
      throw Error('数组内参数必须是函数')
    }
  })

  // 2.返回函数
  return function (...params) {
    // 3.  获取第一个函数结果
    let res = fns[0].call(this, ...params)
    // 4. 遍历执行函数
    for (let i = 1; i < fns.length; i++) {
      res = fns[i].call(this, res)
    }		
		// 5. 返回最终结果
    return res
  }
}

看着很简单,我们来验证一下

function double(m) {
  return m * 2
}

function square(n) {
  return n ** 2
}

let composed = compose(double, square)

console.log(composed(10)) // 400