欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JS基础之call、apply、bind

程序员文章站 2022-07-14 14:27:58
...

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

相关标签: call apply bind