JS基础之call、apply、bind

Function.prototype.call()

call() 方法调用一个函数, 其具有一个指定的this值和分别地提供的参数(参数的列表)。

fun.call(thisArg, arg1, arg2, ...)
  • thisArg:在fun函数运行时指定的this值*。*
  • arg1, arg2, …:指定的参数列表。

需要注意的是,指定的this值并不一定是该函数执行时真正的this值,如果这个函数处于非严格模式下,则指定为nullundefinedthis值会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象。

  1. 用法

    • 使用call调用父构造函数

      function Person(name, time) {
        this.name = name
        if (time < 6) {
          throw RangeError(
            this.name + ' is sleep'
          );
        }
      }
      
      function Man(name, time) {
        Person.call(this, name, time);
        this.category = 'man';
      }
      
      //等同于
      function Woman(name, time) {
        this.name = name;
        this.time = time;
        if (time < 6) {
          throw RangeError(
            this.name + ' is sleep'
          );
        }
        this.category = 'woman';
      }
      
      var yang = new Man('yang', 5);
      var an = new Woman('an', 8);
      
    • 使用call调取匿名函数

      var person = [
        {name: 'yang', age: '24'},
        {name: 'an', age: '12'}
      ];
      
      for (var i = 0; i < person.length; i++) {
        (function (i) { 
          this.print = function () { 
            console.log('#' + i  + ' ' + this.name + ': ' + this.age); 
          } 
          this.print();
        }).call(person[i], i);
      }
      

      在上面例中的for循环体内,我们创建了一个匿名函数,然后通过调用该函数的call方法,将每个数组元素作为指定的this值执行了那个匿名函数。这个匿名函数的主要目的是给每个数组元素对象添加一个print方法,这个print方法可以打印出各元素在数组中的正确索引号。当然,这里不是必须得让数组元素作为this值传入那个匿名函数(普通参数就可以),目的是为了演示call的用法。

    • 使用call方法调用函数并指定上下文中的this

      function Person() {
        var hello = [this.name, ' say ', this.word].join(' ');
        console.log(hello);
      }
      
      var man = {
        name: 'yang', word: 'hello'
      };
      
      Person.call(man); // yang  say  hello
      
    • 使用call调用函数并且没有确定第一个参数

      // 非严格模式下
      var an = 'an'
      function say(){
         console.log('name is %s ',this.an)
      }
      say.call()   //name is an
      
      // 严格模式下--注意:在严格模式下this的值将会是undefined. 
      'use strict'
      var an = 'an'
      function say(){
         console.log('name is %s ',this.an)
      }
      say.call()  // Uncaught TypeError: Cannot read property 'an' of undefined
      

Function.prototype.apply()

apply() 方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数。

call()apply()的区别在于,call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组

func.apply(thisArg, [argsArray])
  • thisArg:在fun函数运行时指定的this值*。*
  • arg1, arg2, …:可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。

需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象。如果传入类数组对象,它们会抛出异常。

  1. 用法

    • 用apply将数组添加到另一数组

      var array = ['a', 'b']
      var elements = [0, 1, 2]
      array.push.apply(array, elements)
      console.info(array) // ["a", "b", 0, 1, 2]
      
    • 使用apply和内置函数

      /* 找出数组中最大/小的数字 */
      var numbers = [5, 6, 2, 3, 7]
      
      /* 应用(apply) Math.min/Math.max 内置函数完成 */
      var max = Math.max.apply(null, numbers) /* 基本等同于 Math.max(numbers[0], ...) 或 Math.max(5, 6, ..) */
      var min = Math.min.apply(null, numbers)
      // max: 7
      // min: 2
      
      /* 代码对比: 用简单循环完成 */
      max = -Infinity, min = +Infinity
      for (var i = 0; i < numbers.length; i++) {
        if (numbers[i] > max)
          max = numbers[i]
        if (numbers[i] < min) 
          min = numbers[i]
      }
      

      但是:如果用上面的方式调用apply,会有超出JavaScript引擎的参数长度限制的风险。更糟糕的是其他引擎会直接限制传入到方法的参数个数,导致参数丢失。

      所以,当数据量较大时

      function minOfArray(arr) {
        var min = Infinity
        var QUANTUM = 32768 // JavaScript 核心中已经做了硬编码  参数个数限制在65536
      
        for (var i = 0, len = arr.length; i < len; i += QUANTUM) {
          var submin = Math.min.apply(null, arr.slice(i, Math.min(i + QUANTUM, len)))
          min = Math.min(submin, min)
        }
        return min
      }
      var min = minOfArray([5, 6, 2, 3, 7])
      
    • 使用apply来链接构造器

      Function.prototype.construct = function (aArgs) {
        var oNew = Object.create(this.prototype);
        this.apply(oNew, aArgs);
        return oNew;
      };
      

Function.prototype.bind()

bind() 方法会创建一个新绑定函数,当这个新绑定函数被调用时,this键值为其提供的值,其参数列表前几项值为创建时指定的参数序列,绑定函数与被调函数具有相同的函数题(ES5中)。

var module = {
    x: 42,
    getX: function() {
        return this.x
    }
}
var unbindGetX = new module.getX
console.log(unbindGetX())// 在这种情况下,“this” 指向全局作用域
// output: undefined

var bindGetX = unbindGetX.bind(module)// 创建一个新函数,将“this”绑定到 module 对象
console.log(bindGetX())
// output: 42

注意:绑定函数也可以使用new运算符构造:这样做就好像已经构造了目标函数一样。提供的this值将被忽略,而前置参数将提供给模拟函数。

this.value = 11
var module = {
    value: 42
}
function ubx() {
    console.log("ubv-")
    console.log(this.value)
    console.log("-ubv")
}
var bindv = ubv.bind(module)
console.log(bindv()) 
// ubv-
// 42
// -ubv
console.log(new bindv()) 
// ubv-
// undefined
// -ubv

上面例子中,运行结果this.value 输出为 undefined,这不是全局value, 也不是ubx对象中的value,这说明 bindthis 对象失效了,new 的实现中生成一个新的对象,这个时候的 this指向的是 obj

  1. 用法:

    • 创建绑定函数

      bind() 最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的 this 值。JavaScript新手经常犯的一个错误是将一个方法从对象中拿出来,然后再调用,希望方法中的 this 是原来的对象(比如在回调中传入这个方法)。如果不做特殊处理的话,一般会丢失原来的对象。从原来的函数和原来的对象创建一个绑定函数,则能很漂亮地解决这个问题

      this.x = 9; 
      var module = {
        x: 81,
        getX: function() { return this.x; }
      };
      
      module.getX(); // 返回 81
      
      var retrieveX = module.getX;
      retrieveX(); // 返回 9, 在这种情况下,"this"指向全局作用域
      
      // 创建一个新函数,将"this"绑定到module对象
      var boundGetX = retrieveX.bind(module);
      boundGetX(); // 返回 81
      
    • 偏函数

      function list() {
        return Array.prototype.slice.call(arguments);
      }
      
      var list1 = list(1, 2, 3); // [1, 2, 3]
      
      // Create a function with a preset leading argument
      var leadingThirtysevenList = list.bind(undefined, 37);
      
      var list2 = leadingThirtysevenList(); // [37]
      var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]
      
    • 配合setTimeout

      function LateBloomer() {
        this.petalCount = Math.ceil(Math.random() * 12) + 1;
      }
      
      // Declare bloom after a delay of 1 second
      LateBloomer.prototype.bloom = function() {
        window.setTimeout(this.declare.bind(this), 1000);
      };
      
      LateBloomer.prototype.declare = function() {
        console.log('I am a beautiful flower with ' +
          this.petalCount + ' petals!');
      };
      
      var flower = new LateBloomer();
      flower.bloom();  // 一秒钟后, 调用'declare'方法
      
    • 作为构造函数使用的绑定函数

      function Point(x, y) {
        this.x = x;
        this.y = y;
      }
      
      Point.prototype.toString = function() { 
        return this.x + ',' + this.y; 
      };
      
      var p = new Point(1, 2);
      p.toString(); // '1,2'
      
      var emptyObj = {};
      var YAxisPoint = Point.bind(emptyObj, 0/*x*/);
      // 以下这行代码在 polyfill 不支持,
      // 在原生的bind方法运行没问题:
      //(译注:polyfill的bind方法如果加上把bind的第一个参数,即新绑定的this执行Object()来包装为对象,Object(null)则是{},那么也可以支持)
      var YAxisPoint = Point.bind(null, 0/*x*/);
      
      var axisPoint = new YAxisPoint(5);
      axisPoint.toString(); // '0,5'
      
      axisPoint instanceof Point; // true
      axisPoint instanceof YAxisPoint; // true
      new Point(17, 42) instanceof YAxisPoint; // true
      
  2. Polyfill

    bind() 最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的 this 值。JavaScript新手经常犯的一个错误是将一个方法从对象中拿出来,然后再调用,希望方法中的 this 是原来的对象(比如在回调中传入这个方法)。如果不做特殊处理的话,一般会丢失原来的对象。从原来的函数和原来的对象创建一个绑定函数,则能很漂亮地解决这个问题

    if (!Function.prototype.bind) {
      Function.prototype.bind = function(oThis) {
        if (typeof this !== 'function') {
          // closest thing possible to the ECMAScript 5
          // internal IsCallable function
          throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
        }
    
        var aArgs   = Array.prototype.slice.call(arguments, 1),
            fToBind = this,
            fNOP    = function() {},
            fBound  = function() {
              // this instanceof fNOP === true时,说明返回的fBound被当做new的构造函数调用
              return fToBind.apply(this instanceof fNOP
                     ? this
                     : oThis,
                     // 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
                     aArgs.concat(Array.prototype.slice.call(arguments)));
            };
    
        // 维护原型关系
        if (this.prototype) {
          // Function.prototype doesn't have a prototype property
          fNOP.prototype = this.prototype; 
        }
        // 下行的代码使fBound.prototype是fNOP的实例,因此
        // 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例
        fBound.prototype = new fNOP();
    
        return fBound;
      };
    }
    
  3. 然而实际使用时会碰到这样的问题:

    function Person(name) {
        this.name = name
        this.hello = function(){
            setTimeout(function(){
                console.log('Hello, ', this.name)
            }, 1000)
        }
    }
    
    var an = new Person('An')
    an.hello() // 1s后output: Hello,
    

    这个时候输出的this.name是null,原因是this指向是在运行函数时确定的,而不是定义函数时候确定的,再因为setTimeout在全局环境下执行,所以this指向setTimeout的上下文:window

    • 解决方法一: 缓存this

      function Person(name) {
          this.name = name
          this.hello = function(){
              var self = this // 缓存this
              setTimeout(function(){
                  console.log('Hello, ', self.name)
              }, 1000)
          }
      }
      
      var an = new Person('An')
      an.hello()// 1s后output: Hello,An
      
    • 解决方法二: bind

      function Person(name) {
          this.name = name
          this.hello = function(){
              setTimeout(function(){
                  console.log('Hello, ', this.name)
              }.bind(this), 1000)
          }
      }
      
      var an = new Person('An')
      an.hello()// 1s后output: Hello,An
      

call、apply、bind区别与实现

call、apply都是为了解决 this 的指向。作用是相同的,只是传参的方式不同。

除了第一个参数外,call 可以接收一个参数列表,apply 只能接收一个参数数组。

let a = {
    value: 1
}
function getValue(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value)
}
getValue.call(a, 'yck', '24')
getValue.apply(a, ['yck', '24'])

模拟实现call、apply

可以从一下几点考虑实现

  • 不传入第一个参数,那么默认为 window

  • 改变了 this 指向,让新的对象可以执行该函数,那么思路是否可以变成新的对象添加一个函数,然后再执行完成后删除

    Function.prototype.myCall = function(context) {
        var context = context || windows
        // 给 context 添加一个属性
        // getValue.call(a, 'yck', '24') => a.fn = gatValue
        context.fn = this
        // 将 context 后面的参数取出来
        var args = [...arguments].slice(1)
        // getValue.call(a, 'yck', '24') => a.fn('yck', '24')
        var result = context.fn(...args)
        // 删除 fn
        delete context.fn
        return result
    }
    

    以上就是 call 的思路, apply的实现也类似

    Function.prototype.myApply = function(context) {
        var context = context || window
        context.fn = this
        
        var result
        // 需要判断是否存储第二个参数
        // 如果存在,就将第二个参数展开
        if (arguments[1]) {
            result = context.fn(...arguments[1])
        } else {
            result = context.fn()
        }
        
        delete context.fn
        return result
    }
    

    bind 和其他两个方法作用是一致的,只是该方法会返回一个函数,并且我们可以通过bind来实现柯里化。

    调用绑定函数通常会导致执行包装函数,绑定函数有以下内部属性:

    • [[BoundTargetFunction]]:包装的函数(function)
    • [[BoundThis]]:调用包装函数的this值
    • [[BoundArguments]]:值列表,其元素用于对包装函数调用的第一个参数
    • [[Call]]:执行与此对象关联的代码。通过函数调用表达式调用,内部方法的参数是this值和参数列表

    当调用绑定函数时,它调用[[BoundTargetFunction]]上的内部方法[[Call]],后跟参数Call(boundThis, args)。其中,boundThis是[[BoundThis]],args是[[BoundArguments]],后跟函数调用传递的参数。

    Function.prototype.myBind = function(context) {
        if (typeof this !== 'function') {
            throw new TypeError('error')
        }
        var _this = this
        var args = [...arguments].slice(1)
        // 返回一个函数
        return function Fun() {
            // 因为返回一个函数, 我们可以 new Fun(), 所以需要判断
            if (this instanceof Fun) {
                return new _this(...args, ...arguments)
            }
            return _this.call(context, ...args, ...arguments)
        }
    }
    

柯里化

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。

var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

add(1)(2);
// 3

这里定义了一个 add 函数,它接受一个参数并返回一个新的函数。调用 add 之后,返回的函数就通过闭包的方式记住了 add 的第一个参数。所以说 bind 本身也是闭包的一种使用场景。
文章参考自MDN

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值