开启掘金成长之旅!这是我参与「掘金日新计划 · 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