JS call apply bind原理与实现和函数柯理化总结
以下方法来自于各种资料大厂面试题收集。
添加了一些个人理解的注释和解释。
对下面概念熟悉的小可爱,不喜勿喷。
侵删
本人脑回路略慢,写的是我认为的比较详细的版本
call,apply,bind的区别
相同点:能够改变this指向
不同点:
call apply在使用的时候会触发运行一次,而bind只是绑定了函数,不会立即执行
call、apply因为要立即执行函数,所以第二个参数或之后的参数都是当前的真实参数,bind是“预设参数”
call 与 apply 传参不同:call可以多参数;apply 是一个arguments
const obj = {
name:'obj name'
}
function getName(para1,para2){
return this.name +'-'+ para1 +'-' +para2;
}
/*
call apply bind 使用
*/
console.log(getName.call(obj,'from','call'));
console.log(getName.apply(obj,['from','apply']));
var b = getName.bind(obj)
console.log(b('from','bind'));
call,apply,bind的实现
手动实现call()
call使用
先看一下call的使用
const obj = {
name:'obj name'
}
function getName(){
return this.name;
}
console.log(getName.call(obj)); //obj name
思考:
1.如何调用getName.func?
2.除了obj.name还能如何取到 name= 'obj name'
这里废话一下:
说我买了一台冰箱,我想试试冰箱的制冷能力,应该如何验证?
很简单,我只需要把大象放冰箱里一段时间,拿出来看一下大象结冰了没有就可以了
思考解答:
1.getName是一个function ,getName的构造函数是Function 。
验证:getName.__ proto__ == Function.prototype
在控制台看到 Function.call,就是说call 是构造函数给每个函数的标配。我们可以在
Function的原型上给一个mycall我们自己的mycall
所以 看到了 **Function.prototype.mycall= funciton(){}**
后面我们可以这样调用 getName.mycall()
2.调用obj里面的getName方法,如下:
var obj = {
name:"obj name",
getName:function(){
return this.name;
}
}
console.log(obj.getName())
看了上面 我们只需要把这两步整合下。把方法放到obj里面一下取到值就可以啦。
const obj = {
name:'obj name'
}
function getName(){
return this.name;
}
Function.prototype.mycall= function(obj,){//(1)原型上绑定自己的mycall
//this 指向了调用mycal 的函数 函数原型上有name length属性
const fnName = this.name //(2)getName
obj[fnName] = this; //(3)this指向了调用mycall的对象 即getName
const result = obj[fnName](); //(4)运行obj的getName方法
delete obj[fnName]; // (5) 删除obj中的 getName方法 使得obj不受影响
return result; // (6) 返回 this.name
}
console.log(getName.mycall(obj));
思路:把大象放进冰箱
(1)Function原型上绑定自己的mycall
(2)this指向了调用mycall的对象 即getName 可以获得func 以及func名(getName)
(3)把func放进obj
(4)调用一次 把结果存起来
(5) 不可以让这个obj[func] 一直存在 delete 删除对象属性
(6)返回运行结果
你品,你细品 3,4,5 是不是把大象放进冰箱 又拿了出来,大象身上的结晶就是我们要的结果, 是不是感觉 到这里就变成了 很简单的 一步两步三步
底层实现也没那么高大上 okok,我真是个话痨,
实现传参
上面是没有传参数的情况,我们需要改进下
最终代码
const obj = {
name:'obj name'
}
function getName(para1,para2){
return this.name +'-'+ para1 +'-' +para2;
}
//(1)原型上绑定自己的mycall
Function.prototype.mycall= function(obj,para1,para2){
//this 指向了调用mycal 的函数 函数原型上有name length属性
let fnName = this.name //(2)getName
obj[fnName] = this; //(3)this指向了调用mycall的对象 即getName
const result = obj[fnName](para1,para2); //(4)运行obj的getName方法
delete obj[fnName]; // (5) 删除obj中的 getName方法 使得obj不受影响
return result; // (6) 返回 this.name
}
console.log(getName.mycall(obj,'from','mycall'));
ok 写完了call,那么apply当然就只是传参变成一个arguments啦 不多赘述。
手动实现bind
由于bind 是先绑定。可以随时调用随时取,封装上有一点区别
const obj = {
name:'obj name'
}
function getName(para1,para2){
return this.name +'-'+ para1 +'-' +para2;
}
const bindFn = getName.bind(obj)
console.log(bindFn('from','bind')
思考:
这里提一句,封装的时候,先考虑使用方式,
能够随时访问到别的函数内部变量的函数,是什么?
没错是闭包
//第一种
Function.prototype.mybind(obj,para1,para2){
const _this = this;
function inner(para1,para2){
return _this.call(obj)
}
inner.prototype = Object.create(_this.prototype) //修正原型链
return inner;
}
//第二种
Function.prototype.mybind = function(obj,para1,para2){
const _this = this;
function inner(para1,para2){ //这是一个闭包 obj在bind的时候 绑定到了inner
// return _this.call(obj)
//等同于 上面call的实现原理
let fnName = _this.name
obj[fnName] = _this;
const result = obj[fnName](para1,para2);
delete obj[fnName];
return result;
}
inner.prototype = Object.create(_this.prototype) //修正原型链
return inner;
}
let mybindFn = getName.bind(obj)
console.log(mybindFn('from','bind')
注意:如果不修正原型链的话 mybindFn 指向了func inner
最终版本
okok,封装完成,整理代码
<script>
const obj = {
name:'obj name'
}
function getName(para1,para2){
return this.name +'-'+ para1 +'-' +para2;
}
/*
call apply bind 使用
*/
console.log(getName.call(obj,'from','call'));
console.log(getName.apply(obj,['from','apply']));
var b = getName.bind(obj)
console.log(b('from','bind'));
/*
mycall
*/
//(1)原型上绑定自己的mycall
Function.prototype.mycall= function(obj,para1,para2){
//this 指向了调用mycal 的函数 函数原型上有name length属性
let fnName = this.name //(2)getName
obj[fnName] = this; //(3)this指向了调用mycall的对象 即getName
const result = obj[fnName](para1,para2); //(4)运行obj的getName方法
delete obj[fnName]; // (5) 删除obj中的 getName方法 使得obj不受影响
return result; // (6) 返回 this.name
}
console.log(getName.mycall(obj,'from','mycall'));
/*
myapply
*/
Function.prototype.myapply = function(obj,arguments){
let fnName = this.name;
obj[fnName] = this;
const result = obj[fnName](arguments)
delete obj[fnName];
return result;
}
console.log(getName.mycall(obj,'from','myapply'));
/*
mybind
*/
Function.prototype.mybind = function(obj,para1,para2){
const _this = this;
function inner(para1,para2){ //这是一个闭包 obj在bind的时候 绑定到了inner
// return _this.call(obj)
//等同于 上面call的实现原理
let fnName = _this.name
obj[fnName] = _this;
const result = obj[fnName](para1,para2);
delete obj[fnName];
return result;
}
inner.prototype = Object.create(_this.prototype) //修正原型链
return inner;
}
let bindFn = getName.mybind(obj)
console.log(bindFn('from','mybind'));
bindFn = getName.bind({name:'new name'})
console.log(bindFn('from','mybind'));
</script>
函数柯理化
柯理化
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术
个人理解:
sum(1,2,3) <=> currySum(1)(2)(3)
看到这个表达式第一印象是
初级return 嵌套
function sum(a,b,c){
return (a + b + c)
}
function currySum(a,b,c){
return function(b){
return function(c){
return a+b+c
}
}
}
递归版本
上面这种方式 太low了,我们来换一种实现,思考一下哈,如果我们有n个参数[…a,b,c,d,e]等等。我们就要无限的return里面return 函数。这个时候用到了什么???没错,假装听到你的回答,递归。看下面
let add = (a,b,c) => a + b + c
function curry(fn,args){
let _this = this;
let _args = args || [];
let len = fn.length;
return function(){
let args = Array.prototype.slice.apply(arguments) //伪数组转为真数组
args = Array.prototype.concat.call(_args,args)//合并数组
if(args.length < len){
// return curry(fn,args) // 直接调用自己 也可以
return curry.call(_this,fn,args) //call可以传递 多个参数
}
// console.log(args); // 整合了 所有传参 在args
return fn.apply(this,args) //注意这里 没有用call,为什么???
//因为需要传递一个数组吖
}
}
let currySum = curry(add)
console.log(currySum(1)(2)(3));
这样的话呢,我们就可以写成currySum(1)(2)(3) 这种形式
存在问题
以下方式是无法输出的
currySum(1)
currySum(1,2)(3)
最终版本
代码
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
var _args = Array.prototype.slice.call(arguments);
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
var _adder = function () {
_args.push(...arguments);
console.log('111');
return _adder;
};
// console.log(_adder);
// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_adder.toString = function () {
console.log('222'); //在浏览器输出 会打印两次
return _args.reduce(function (a, b) {
return a + b;
});
};
return _adder;
}
console.log(add(1)(2)(3) instanceof Function);
console.log(add(1)(2)(3));
console.log(add(1, 2, 3)(4));
console.log(add(1)(2)(3,4)(4)(5,6));
console.log(add(1)(2)(3,4)(4)(5,6).toString());//解释
这样就可以无限输出了
代码是赋值来的 链接在文末
解释:
toString是挂载在 func _adder下面的属性,可以在func的constructor下面查看到,还有func自带的length,name等属性。所以如果不加toString的话 得到的是 f num,toString之后得到方法的返回值。
关于 222 会输出两次的问题,个人用node 运行js 是没有被输出的,浏览器打开会被浏览器运行打印两次。这个地方小作者我也不是很懂,希望有幸被大神读到,指点一下,不胜感激。
柯理化应用
这里没有个人原创,来自各种大佬
函数复用
比较常见的 正则复用
柯理化前
function reg(reg,str){
return reg.test(str)
}
console.log(reg(/\d+/g,'kk'));
console.log(reg(/[a-z]+/g,'kk'));
柯理化后
//柯理化函数
function curryReg(reg){
return function(str){
return reg.test(str)
}
}
let cRegNumber = curryReg(/[0-9]+/g)
let cRegString = curryReg(/[a-z]+/g)
console.log(cRegNumber(1314));
console.log(cRegString('kk'))
总结
项目中可以把这些可复用的函数封装出来,需要用到的页面引入使用即可,这样的好处是:你不用每次都传入一次正则,在公共的地方规定一次就好,对于后期的维护也非常方便
提前确认
由于浏览器的兼容性,一些操作可能需要不同的初始化api
var on = (function(){
if(document.addEventListener){
return function(element,event,handler){
if(element && event && handler){
element.addEventListener(event,handler,false)
}
}
}else{
return function(element,event,handler){
if(element && event&&handler){
element.attachEvent('on'+event,handler)
}
}
}
})()
const btn = document.getElementById('btn')
on(btn,'click',function(){ //调用
alert('PianistK')
})
延迟运行
说到 延迟 一定能想到 bind 只是预先绑定,这里有些柯理化的意思
Function.prototype.mybind = function(obj,para1,para2){
const _this = this;
function inner(para1,para2){ //这是一个闭包 obj在bind的时候 绑定到了inner
// return _this.call(obj)
//等同于 上面call的实现原理
let fnName = _this.name
obj[fnName] = _this;
const result = obj[fnName](para1,para2);
delete obj[fnName];
return result;
}
inner.prototype = Object.create(_this.prototype) //修正原型链
return inner;
}
let mybindFn = getName.bind(obj)
console.log(mybindFn('from','bind')
参考资料
链接: 函数柯理化.
全力以赴PianistK
本文地址:https://blog.csdn.net/lemaktub/article/details/107360072
上一篇: 吃螺蛳粉的危害有什么