当前位置: 移动技术网 > IT编程>开发语言>JavaScript > JS call apply bind原理与实现和函数柯理化总结

JS call apply bind原理与实现和函数柯理化总结

2020年07月22日  | 移动技术网IT编程  | 我要评论
以下方法来自于各种资料大厂面试题收集。添加了一些个人理解的注释和解释。对下面概念熟悉的小可爱,不喜勿喷。侵删本人脑回路略慢,写的是我认为的比较详细的版本call,apply,bind的区别相同点:能够改变this指向不同点:call apply在使用的时候会触发运行一次,而bind只是绑定了函数,不会立即执行call、apply因为要立即执行函数,所以第二个参数或之后的参数都是当前的真实参数,bind是“预设参数”call 与 apply 传参不同:call可以多参数;apply 是一个

以下方法来自于各种资料大厂面试题收集。
添加了一些个人理解的注释和解释。
对下面概念熟悉的小可爱,不喜勿喷。
侵删

本人脑回路略慢,写的是我认为的比较详细的版本

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

如您对本文有疑问或者有任何想说的,请点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网