本文你能学到什么
欢迎关注公众号《
前端界
》,文章会同步更新,也可快速加入前端交流群!
前言
最近在复习 JavaScript
手写代码。想搜一下 call/apply/bind
实现,发现搜的结果参差不齐,有的是不对的,有的长篇大论不够精简缺应用场景,于是自己手写总结分享下,希望对看到的同学有帮助,同时也是为了方便以后自己复习吧!
call 实现
call
函数内部做了什么
- 函数先通
__proto__
原型链找到Function.prototype
上的call
函数 - 确定
this
为执行的函数 - 接下来要执行函数,但是执行函数的上下文需要是传递进来的第一个参数,所以想办法修改函数执行上下文
- 正式执行函数,并返回执行结果
基础版代码实现
根据前面的分析开始编码
js
Function.prototype.call = (context,...arguments){
// 第一步:this 就是对应执行的函数,也就是调用函数 fun
const self = this;
// 第二步:想要执行函数时需要函数的上下文为传入的上下文,想要改变调用上下文最好的办法是直接用上下文对象调用函数,这时候函数内部的 this 就指向了上下文
// 一个小结论 xx1.xx2 xx2的 this 就指向了 xx1
// 使用 symbol 类型可以保证属性名的唯一性,而且不会被遍历枚举出来
const symbolKey = Symbol('fun');
context[symbolKey] = self;
// 第三步:执行函数
const result = context[symbolKey](...arguments);
// 删除 symbol 类型的 key
delete context[symbolKey];
// 返回结果
return result;
}
const fun = function(param){
let a = this.a;
console.log(a,param);
}
fun.call({a:234},'哈哈哈');
升级版代码实现
上面实现的代码有些问题
- 如果上下文传递的为
null
,会报错funA.call()
; - 如果上下文传递的是整数类型,会报错
funA.call(123)
;
针对以上两种情况,我们升级一下扩展代码
js
Function.prototype.callUpgrade = function(context,...arguments){
// 校验context是否为空,如果为空,默认值给 window
context === null ? context = window : null;
// 校验上下文是否为对象,对象基础类型情况需要创建一个对象
!/^(object|function)$/i.test(typeof context) ? context = Object(context) : null;
const self = this;
const symbolKey = Symbol('fun');
context[symbolKey] = self;
const result = context[symbolKey](...arguments);
delete context[symbolKey];
return result;
}
const fun = function(param){
const a = this.a;
console.log(a);
}
fun.call();// 空上下文情况不报错
fun.call(123);// 非对象情况上下文不报错
apply 实现
apply
不过多说明,因为他与 call
唯一的区别是传递参数不同。call
可以传递任意数量的参数,这些参数会作为函数的参数按顺序传递进去。apply
第二个参数只接受一个数组或者类数组对象。
代码实现
js
Function.prototype.bind = function(context,arguments){
context === null ? context = window : null;
!/^(object|function)$/i.test(typeof context) ? context = Object(context) : null;
const self = this;
const symbolKey = Symbol('fun');
context[symbolKey] = self;
const result = context[symbolKey](...arguments);
delete context[symbolKey];
return result;
}
手写时候使用箭头函数,箭头函数中的 this 指向上一级别
bind 实现
bind 内部做了什么
- 先通过
__proto__
原型连找到Function.prototype
上的bind
函数 bind
函数内部没有把函数立即执行,是将传来的信息通过必包方式存储起来bind
实际返回了一个新的函数。- 这个新函数内容不完成了函数执行,并把
this
上下文和参数改变为之前存储的内容
基础代码实现
js
// 使用 call 实现 bind 函数
Function.prototype.bind = function(context,...args){
let self = this;
return function Fun(...params){
self.call(context,...args,...params)
}
}
// 使用 apply 实现 bind 函数
Function.prototype.bind = function(context,...args){
let self = this;
return function Fun(...params){
self.apply(context,args.concat(params))
}
}
call/apply/bind 的几个有趣应用场景
apply 数组展开能力
开发过程中我想合并一个数组到目标数组(数据添加),但是不想生成新的数组。
js
let target = [1,2,3];
let source = [4,5,6];
- 扩展运算符
[...target,...source]
是做不到的,会生成新数组 target.concat(source)
也是做不到的,也是生成新数组- 难道我要
for
循环,然后每一个值push
进去吗?有没有优雅点的方式
可以使用 apply
函数
js
Array.prototype.push.apply(target,source)
apply
函数的第一个参数是调用的函数上下文(即 this
值),第二个参数是一个数组或类数组,会被展开并作为参数传递。在 push
函数内部会遍历将传递进来的参数放到数组末尾。其实和我们 for
循环遍历添加是相同的
因为
Array.prototype.push
本身支持多个参数,你也可以直接使用扩展运算符,target.push(...source)
bind 实现函数柯里化
首先理解一下什么是柯里化,是一种将接受多个参数的函数转化为一系列接受一个参数的函数的过程。这种转化使得函数更加灵活,可以通过部分应用来创建新的函数,从而在需要的时候传递剩余的参数。
函数柯里化有助于在函数式编程中实现复杂的函数组合和延迟求值,提高代码的可维护性和复用性。
举个例子来理解一下柯里化的概念
js
/**
* 原始的普通函数,接受两个参数并返回它们的乘积
* */
function multiply(x, y) {
return x * y;
}
/*
* 使用函数柯里化,将上述函数转化为接受一个参数的函数
* */
function curriedMultiply(x) {
return function(y) {
return x * y;
};
}
// 创建一个新函数,只需要传递一个参数
const double = curriedMultiply(2);
// 使用新函数来获取传递的参数的两倍
const result = double(5); // 2 * 5 = 10
函数柯里化的好处在于,您可以在需要的时候提供部分参数,然后在稍后的调用中提供剩余的参数。这对于实现通用的函数,避免重复代码以及构建复杂的函数组合非常有用。
介绍完基础概念,我们回到本文主题 bind
。更简洁的实现函数柯里化,上面的例子我们可以用 bind
来创建一个新函数
js
const multiply = (x,y)=>x*y;
const double = multiply.bind(null,2);
const result = double(5);// 2*5 = 10
有趣的小知识,为什么这种实现叫柯里化,柯里化的概念得名于数学家
Haskell Curry
,他是函数式编程的先驱之一。柯里化的概念最早可以追溯到Lambda
演算,这是一种数学形式化的方法,用于描述函数的运算和变换。Haskell Curry
提出了一种将多参数函数转化为一系列单参数函数的思想,这样就能更好地进行函数组合和变换。
柯里化事件绑定应用
柯里化在前端开发中有许多应用场景,特别是函数式编程和高阶函数的概念中。它可以帮助简化代码,提高可复用性,并促进更模块化和函数式的编程风格。 再举一个事件处理函数的例子;
在前端开发中 ,处理事件常常需要传递附加的数据和上下文信息。柯里化可以用于创建高阶事件处理函数,从而将特定的事件处理逻辑 与通用的事件逻辑绑定分离
js
// 使用柯里化创建高阶事件处理函数
const curriedEventHandler = eventData => event => {
console.log(`Event: ${event.type},Data: ${JSON.stringify(eventData)}`)
}
const button = document.getElementById('myButton');
const handleClick = curriedEventHandler({action:"click"});
button.addEventListener("click",handleClick);
鸭子类型实现类数组转换
鸭子类型(Duck Typing
) 是一种在动态语言中常见的类型检查方式。它关注的不是对象的具体类型,而是关注对象是否有特定的方法,属性或行为。如果一个对象的方法,属性或行为与某个类型的期望一致,那么就可以将该对象视为该类型。
类数组是一种具有索引和length
属性,但本身本身不可以调用 Array.prototype
上面的函数的,比如forEach reduce
等原型上的函数。这时候可以使用 bind/call/apply
等函数将数组方法应用于类数组函数。这样就拥有了数组原型上面的函数。
可以这么使用
js
function sum() {
return Array.prototype.reduce.call(arguments, (acc, val) => acc + val, 0);
}
const total = sum(1, 2, 3, 4, 5); // 15
也可以这么转换一下哦
js
const duckArr = Array.prototype.slice.call(arguments,0);
console.log(Array.isArray(duckArr));// true
🦆鸭子类型 在工作中应用场景和思想还有很多,再举个例子。 当我们谈到迭代起模式时,鸭子类型 的应用是指对象只要有迭代器的行为(如 next()
方法),那么这个对象就可以被视为迭代器,无论其实际类型是什么(符合前面提到的鸭子类型的概念)。这意味着你可以在不同类型的对象上使用统一的迭代方式,从而实现更加通用和灵活的代码。
鸭子类迭代器应用
下面举一个简单的例子,展示如何使用鸭子类型来实现迭代器模式:
js
/**
* 定义一个通用的迭代函数
* */
function iterate(iterator) {
let result = iterator.next();
while (!result.done) {
console.log(result.value);
result = iterator.next();
}
}
/**
* 定义一个模拟的迭代器对象
**/
const mockIterator = {
index: 0,
values: [1, 2, 3],
next() {
if (this.index < this.values.length) {
return { done: false, value: this.values[this.index++] };
} else {
return { done: true };
}
}
};
// 使用迭代函数遍历不同类型的对象
iterate(mockIterator); // 输出: 1, 2, 3
在上述示例中,我们首先定义了一个通用的 iterate
函数,该函数接受一个迭代器对象并使用其 next()
方法来遍历对象的值。然后,我们定义了一个模拟的迭代器对象 mockIterator
,它具有 next()
方法和要遍历的值。最后,我们使用 iterate
函数来遍历 mockIterator
,实现了迭代器模式。
值得注意的是,iterate
函数不关心传入的迭代器对象的具体类型,只要对象具有期望的 next()
方法,就可以进行遍历。这就是鸭子类型的应用,通过关注对象的行为而不是类型,实现了更灵活的代码。
哈哈哈,发现一个自己的问题,其实总是想要写一篇小文章,但是越写展开的越多,收一下。
欢迎关注公众号《
前端界
》,文章会同步更新,也可快速加入前端交流群!
本文就到这里如果有收获,欢迎大家点赞,转发支持下。