Skip to content

call、apply、bind

对比

call

执行过程

在 JavaScript 中,callFunction 原型链上的一个方法,它的主要作用是改变函数内部的 this 指向,并立即执行这个函数。call 的执行过程可以分为以下几个步骤:

  1. 检查类型:首先,JavaScript 引擎会检查 call 的调用者是否为一个函数,因为只有函数才能调用 call 方法。如果调用者不是一个函数,JavaScript 会抛出一个类型错误。

  2. 确定 this 指向:然后,call 方法会将函数内部的 this 指向第一个参数。如果第一个参数是 nullundefined,或者没有提供参数,那么 this 会指向全局对象(在浏览器中是 window,在 Node.js 中是 global)。如果第一个参数是原始值(比如数字、字符串或布尔值),那么 this 会指向这个原始值的包装对象。如果第一个参数是一个对象,那么 this 就会指向这个对象。

  3. 执行函数call 方法会立即执行函数。如果 call 方法有多个参数,那么第一个参数之后的所有参数都会作为函数的参数传入。

下面是一个简单的例子来说明 call 的执行过程:

javascript
function greet(greeting, punctuation) {
  console.log(greeting + ", " + this.name + punctuation);
}

var person = { name: "John" };

greet.call(person, "Hello", "!"); // 输出:Hello, John!

在这个例子中,greet.call(person, 'Hello', '!') 的执行过程如下:

  1. JavaScript 引擎检查 greet 是否为一个函数。因为 greet 是一个函数,所以可以调用 call 方法。

  2. call 方法将 greet 函数内部的 this 指向 person 对象。

  3. call 方法立即执行 greet 函数,'Hello''!' 作为参数传入。

总结一下,call 方法的主要作用是改变函数内部的 this 指向,并立即执行这个函数。

模拟实现

在 JavaScript 中,我们可以模拟实现call方法的功能。以下是一个简单的实现:

javascript
Function.prototype.myCall = function (context) {
  // 如果没有传入 context 或者 context 是 null,设置 context 为全局对象
  if (context == null) {
    context = window || global;
  }

  // 如果传入的 context 不是对象,转化为对象
  if (typeof context !== "object") {
    context = Object(context);
  }

  // 创建一个唯一的属性名
  var uniqueId = "00" + Math.random();

  // 将函数设置为 context 的一个属性
  context[uniqueId] = this;

  // 获取参数
  var args = [];
  for (var i = 1; i < arguments.length; i++) {
    args.push("arguments[" + i + "]");
  }

  // 使用 eval 执行函数
  var result = eval("context[uniqueId](" + args + ")");

  // 删除添加的属性
  delete context[uniqueId];

  // 返回执行结果
  return result;
};

这个myCall方法首先检查传入的context,如果没有传入或者传入的是null,就设置context为全局对象。然后,将函数设置为context的一个属性,这样就可以改变函数内部的this指向。接着,获取call方法的参数,并使用eval函数执行函数。最后,删除添加的属性,并返回执行结果。

请注意,这个实现使用了eval函数,这个函数有一些安全问题。在实际的生产环境中,我们应该尽量避免使用eval函数。

使用场景

在 JavaScript 中,call 方法是所有函数对象的内置方法之一。它的主要作用是改变函数的执行上下文,即函数运行时 this 关键字的指向。call 方法接受的参数是一个参数列表,第一个参数是要设置为 this 的值,其余参数是要传递给函数的参数。

以下是一些 call 方法的使用场景:

在 JavaScript 中,call是一个非常有用的方法,它允许我们借用一个对象的方法或者属性。这在很多情况下都非常有用。以下是一些使用场景和示例:

  1. 改变函数的上下文(this 的指向):
javascript
function greet() {
  console.log(`Hello, my name is ${this.name}`);
}

let user = {
  name: "John",
};

greet.call(user); // 输出: "Hello, my name is John"

在这个例子中,我们使用call方法将greet函数的上下文改为user对象。

  1. 借用其他对象的方法

这是 call 最常见的用途。如果两个对象有相同的方法,你可以使用一个对象的方法来操作另一个对象。

javascript
let person1 = {
  fullName: function () {
    return this.firstName + " " + this.lastName;
  },
};

let person2 = {
  firstName: "John",
  lastName: "Doe",
};

console.log(person1.fullName.call(person2)); // John Doe

在这个例子中,person1 有一个 fullName 方法,而 person2 没有。我们使用 call 方法,将 this 设置为 person2,这样 person1fullName 方法就能操作 person2 的数据了。

  1. 使用参数列表调用函数

call 方法的其他参数是要传递给函数的参数,这意味着我们可以使用 call 方法将参数列表传递给函数。

javascript
function greet(greeting, punctuation) {
  console.log(greeting + ", " + this.name + punctuation);
}

let person = { name: "John Doe" };

greet.call(person, "Hello", "!"); // Hello, John Doe!

在这个例子中,greet 函数需要两个参数:greetingpunctuation。我们使用 call 方法,将 this 设置为 person,并传递 'Hello' 和 '!' 作为参数。

  1. 链式继承(多重继承):

JavaScript 并不直接支持多重继承,但我们可以通过call方法来实现类似的功能。

javascript
function Mammal(name) {
  this.name = name;
  this.isMammal = true;
}

function WingedAnimal(name, wings) {
  Mammal.call(this, name);
  this.wings = wings;
  this.isWingedAnimal = true;
}

function Bat(name, wings, isNocturnal) {
  WingedAnimal.call(this, name, wings);
  this.isNocturnal = isNocturnal;
}

let bat = new Bat("Bruce", 2, true);

console.log(bat); // 输出: Bat { name: 'Bruce', isMammal: true, wings: 2, isWingedAnimal: true, isNocturnal: true }

在这个例子中,我们通过call方法实现了链式继承。Bat继承了WingedAnimalMammal的属性。

  1. 将类数组对象转换为数组:
javascript
let pseudoArray = { 0: "a", 1: "b", 2: "c", length: 3 };

let realArray = Array.prototype.slice.call(pseudoArray);

console.log(realArray); // 输出: [ 'a', 'b', 'c' ]

在这个例子中,我们使用call方法和Array.prototype.slice将一个类数组对象转换为一个真正的数组。

  1. 使用 Math 函数处理数组:
javascript
let numbers = [5, 6, 2, 3, 7];

let max = Math.max.call(null, ...numbers);

console.log(max); // 输出: 7

在这个例子中,我们使用call方法和扩展运算符(...)来找到数组中的最大值。通常,Math.max不能直接用于数组,但是通过使用call,我们可以传递一个数组的所有元素作为参数。

这些是call方法的一些常见使用场景,希望这些示例能帮助你更好地理解call方法在 JavaScript 中的应用。

apply

执行过程

JavaScript 中的apply方法是所有函数对象都有的一个方法,它的主要作用是改变函数内部的this指向,并立即执行这个函数。apply方法接受两个参数,第一个参数是需要绑定的this值,第二个参数是一个数组或类数组对象,其中的元素会被作为单独的参数传给函数。

以下是apply方法的执行过程:

  1. 检查调用对象:首先,apply会检查它的调用对象,也就是它前面的函数对象。如果调用对象不是一个函数,JavaScript 会抛出一个错误。

  2. 设置this:然后,apply方法会设置函数内部的this值。如果apply的第一个参数是nullundefined,函数内部的this值会被设置为全局对象。如果apply的第一个参数是一个原始值(比如一个数字或字符串),函数内部的this值会被设置为这个原始值的包装对象。如果apply的第一个参数是一个对象,函数内部的this值会被设置为这个对象。

  3. 获取参数apply方法会从它的第二个参数(一个数组或类数组对象)中获取参数,并把这些参数作为单独的参数传给函数。如果apply没有第二个参数,或者第二个参数是nullundefined,函数将不会接收到任何参数。

  4. 执行函数:最后,apply方法会立即执行函数,并返回函数的执行结果。

以下是一个使用apply方法的例子:

javascript
function greet(greeting, punctuation) {
  console.log(greeting + ", " + this.name + punctuation);
}

var person = {
  name: "John",
};

greet.apply(person, ["Hello", "!"]); // 输出:Hello, John!

在这个例子中,apply方法改变了greet函数内部的this值,并立即执行了greet函数。

模拟实现

在 JavaScript 中,我们可以模拟实现 apply 方法的功能。以下是一个简单的实现:

javascript
Function.prototype.myApply = function (context, arr) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Apply must be called on a function");
  }

  context = context ? Object(context) : window; // 若没有传入this, 默认绑定window对象
  context.fn = this; // 将调用apply方法的函数添加到context的属性中

  let result;
  // 判断参数是否存在
  if (!arr) {
    result = context.fn();
  } else {
    if (!Array.isArray(arr)) {
      throw new TypeError("CreateListFromArrayLike called on non-object");
    }
    let args = [];
    for (let i = 0, len = arr.length; i < len; i++) {
      args.push("arr[" + i + "]");
    }
    result = eval("context.fn(" + args + ")");
  }

  delete context.fn; // 删除添加的属性
  return result; // 返回执行结果
};

这个 myApply 方法首先检查它的调用对象是否是一个函数,如果不是,就抛出一个错误。然后,它设置函数内部的 this 值,并将调用 apply 方法的函数添加到 context 的属性中。接着,它检查参数是否存在,如果存在,就把参数作为单独的参数传给函数;如果不存在,函数将不会接收到任何参数。最后,它删除添加的属性,并返回函数的执行结果。

下面是一个简单的例子来说明 myApply 方法的使用方法:

javascript
function greet(greeting, punctuation) {
  console.log(greeting + ", " + this.name + punctuation);
}

var person = {
  name: "John",
};

greet.myApply(person, ["Hello", "!"]); // 输出:Hello, John!

在这个例子中,myApply 方法改变了 greet 函数内部的 this 值,并立即执行了 greet 函数。

使用场景

在 JavaScript 中,apply 方法和 call 方法类似,都是用来改变函数的执行上下文,即函数运行时 this 关键字的指向。不同之处在于,apply 方法接受的是一个参数数组,而 call 方法接受的是一个参数列表。

以下是一些 apply 方法的使用场景:

  1. 借用其他对象的方法

这和 call 方法的用法类似。如果两个对象有相同的方法,你可以使用一个对象的方法来操作另一个对象。

javascript
let person1 = {
  fullName: function () {
    return this.firstName + " " + this.lastName;
  },
};

let person2 = {
  firstName: "John",
  lastName: "Doe",
};

console.log(person1.fullName.apply(person2)); // John Doe

在这个例子中,person1 有一个 fullName 方法,而 person2 没有。我们使用 apply 方法,将 this 设置为 person2,这样 person1fullName 方法就能操作 person2 的数据了。

  1. 使用参数数组调用函数

apply 方法的第二个参数是一个数组,这意味着我们可以使用 apply 方法将一个数组的元素作为参数传递给函数。

javascript
function greet(greeting, punctuation) {
  console.log(greeting + ", " + this.name + punctuation);
}

let person = { name: "John Doe" };

greet.apply(person, ["Hello", "!"]); // Hello, John Doe!

在这个例子中,greet 函数需要两个参数:greetingpunctuation。我们使用 apply 方法,将 this 设置为 person,并传递一个数组 ['Hello', '!'] 作为参数。

  1. 找出数组中的最大值或最小值

Math.maxMath.min 函数可以接受多个参数,但不能接受一个数组。我们可以使用 apply 方法解决这个问题。

javascript
let numbers = [5, 6, 2, 3, 7];

// 使用 apply 方法找出数组中的最大值
let max = Math.max.apply(null, numbers);

console.log(max); // 7

在这个例子中,我们使用 apply 方法将 numbers 数组的元素作为参数传递给 Math.max 函数。注意,因为 Math.max 函数不使用 this,所以我们将 this 设置为 null

当然,除了上述的使用场景,apply 还有其他的使用方式。以下是一些额外的 apply 方法的使用场景:

  1. 链接数组

apply 可以用于链接数组。例如,我们需要将一个数组添加到另一个数组的末尾,我们可以使用 Array.prototype.push.apply

javascript
let array1 = [1, 2, 3];
let array2 = [4, 5, 6];

// 使用 apply 方法链接数组
Array.prototype.push.apply(array1, array2);

console.log(array1); // [1, 2, 3, 4, 5, 6]

在这个例子中,我们使用 apply 方法将 array2 的元素作为参数传递给 array1.push 方法。这样 array2 的所有元素就被添加到 array1 的末尾了。

  1. 函数的柯里化

柯里化是一种将使用多个参数的函数转换成一系列使用一个参数的函数的技术。我们可以使用 apply 方法实现柯里化:

javascript
function sum(x, y, z) {
  return x + y + z;
}

// 柯里化函数
function curry(fn) {
  let args = Array.prototype.slice.call(arguments, 1);
  return function () {
    let innerArgs = Array.prototype.slice.call(arguments);
    let finalArgs = args.concat(innerArgs);
    return fn.apply(null, finalArgs);
  };
}

let curriedSum = curry(sum, 1, 2);

console.log(curriedSum(3)); // 6

在这个例子中,curry 函数接受一个函数 fn 和一些参数 args,并返回一个新的函数。这个新的函数会将 args 和它自己的参数 innerArgs 合并,然后使用 apply 方法将这些参数传递给 fn

  1. 在构造函数中使用 apply

在某些情况下,我们可能需要在构造函数中使用 apply。这通常涉及到动态参数的传递,或者继承其他构造函数的属性和方法:

javascript
function Person(name, age) {
  this.name = name;
  this.age = age;
}

function Employee(name, age, position) {
  Person.apply(this, [name, age]);
  this.position = position;
}

let employee = new Employee("John Doe", 30, "Developer");

console.log(employee); // { name: 'John Doe', age: 30, position: 'Developer' }

在这个例子中,Employee 构造函数使用 Person.apply(this, [name, age]) 调用 Person 构造函数,这样 Employee 就能继承 Person 的属性了。

bind

执行过程

JavaScript 中的bind方法是所有函数对象都有的一个方法,它的主要作用是创建一个新的函数,这个新的函数在调用时会有预设的初始参数,这些参数会加在新函数的参数列表前面,并且新函数的this值会被绑定到bind的第一个参数上。

以下是bind方法的执行过程:

  1. 检查调用对象:首先,bind会检查它的调用对象,也就是它前面的函数对象。如果调用对象不是一个函数,JavaScript 会抛出一个错误。

  2. 获取参数:然后,bind会从它的参数列表中获取参数。这些参数(除了第一个参数外)会被作为新函数的初始参数。

  3. 设置thisbind方法会设置新函数的this值。如果bind的第一个参数是nullundefined,新函数的this值会被设置为全局对象。如果bind的第一个参数是一个原始值(比如一个数字或字符串),新函数的this值会被设置为这个原始值的包装对象。如果bind的第一个参数是一个对象,新函数的this值会被设置为这个对象。

  4. 创建新函数:最后,bind会创建一个新的函数。这个新的函数在调用时会有预设的初始参数,这些参数会加在新函数的参数列表前面。并且新函数的this值会被绑定到bind的第一个参数上。

以下是一个使用bind方法的例子:

javascript
function greet(greeting, punctuation) {
  console.log(greeting + ", " + this.name + punctuation);
}

var person = {
  name: "John",
};

var boundGreet = greet.bind(person, "Hello");

boundGreet("!"); // 输出:Hello, John!

在这个例子中,bind方法创建了一个新的函数boundGreet,并预设了greet函数的this值和初始参数。当调用boundGreet函数时,它会使用预设的this值和初始参数,然后加上它自己的参数列表,最后执行greet函数。

模拟实现

JavaScript 中的 bind() 方法可以通过以下步骤来模拟实现:

  1. 首先,接收一个需要绑定的 this 上下文以及一些参数。
  2. 然后,返回一个新的函数。
  3. 在这个新函数中,调用原函数,并将 this 上下文以及参数传入。

这个过程可以用以下的代码来实现:

javascript
Function.prototype.myBind = function (context) {
  if (typeof this !== "function") {
    throw new Error(
      "Function.prototype.myBind - what is trying to be bound is not callable"
    );
  }
  var self = this;
  var args = Array.prototype.slice.call(arguments, 1);

  var fNOP = function () {};

  var fBound = function () {
    var bindArgs = Array.prototype.slice.call(arguments);
    return self.apply(
      this instanceof fNOP ? this : context,
      args.concat(bindArgs)
    );
  };

  fNOP.prototype = this.prototype;
  fBound.prototype = new fNOP();
  return fBound;
};

在这段代码中:

  • self 是原函数。
  • context 是需要绑定的 this 上下文。
  • argsbind 方法接收的参数,除了第一个参数(this 上下文)以外的其他参数。
  • fNOP 是一个空函数,它的作用是为了让 fBound 可以作为构造函数使用,同时不调用 self
  • fBound 是绑定后的新函数,它会调用 self,并根据是否使用 new 关键字来决定 this 的值,然后将 args 和新调用的参数一起传给 self

这样,我们就模拟实现了 JavaScript 的 bind() 方法。

使用场景

bind() 方法在 JavaScript 中有许多使用场景。以下是一些常见的例子:

  1. 事件监听器中的 this:在事件监听器中,this 通常指向触发事件的元素。如果你希望 this 指向其他对象,可以使用 bind()

    javascript
    var button = document.getElementById("myButton");
    var obj = {
      num: 10,
      onClick: function () {
        console.log(this.num);
      },
    };
    button.addEventListener("click", obj.onClick.bind(obj)); // 输出:10

    在这个例子中,如果不使用 bind()this.num 将会是 undefined,因为 this 将指向按钮元素,而不是 obj

  2. 部分应用函数(Partial Application)bind() 可以用来创建部分应用函数,即预先设置一些参数,返回一个新的函数,等待接收剩余的参数。

    javascript
    function multiply(a, b) {
      return a * b;
    }
    var multiplyByTwo = multiply.bind(null, 2);
    console.log(multiplyByTwo(4)); // 输出:8

    在这个例子中,multiplyByTwo 是一个部分应用的 multiply 函数,它的第一个参数已经被设置为 2

  3. 在定时器中保持 this:在 setTimeoutsetInterval 的回调函数中,this 通常指向全局对象(在浏览器中是 window)。如果你希望 this 指向其他对象,可以使用 bind()

    javascript
    var obj = {
      num: 10,
      startTimer: function () {
        setTimeout(
          function () {
            console.log(this.num);
          }.bind(this),
          1000
        );
      },
    };
    obj.startTimer(); // 输出:10

    在这个例子中,如果不使用 bind()this.num 将会是 undefined,因为 this 将指向 window,而不是 obj

这些都是 bind() 在 JavaScript 中的常见用途,通过使用 bind(),可以更灵活地控制函数的 this 和参数。

当然,除了上述的使用场景,bind() 还有其他的应用。以下是一些额外的例子:

  1. 在类(Class)方法中保持 this:在 React 类组件中,经常需要在构造函数中使用 bind() 来确保方法中的 this 正确指向组件实例。

    javascript
    class MyComponent extends React.Component {
      constructor(props) {
        super(props);
        this.handleClick = this.handleClick.bind(this);
      }
    
      handleClick() {
        // `this` 在这里正确地指向了组件实例
        console.log(this);
      }
    
      render() {
        return <button onClick={this.handleClick}>Click me</button>;
      }
    }

    在这个例子中,如果不使用 bind()handleClick 方法中的 this 将是 undefined,因为 React 事件处理函数的回调中 this 不会自动绑定到组件实例。

  2. 创建具有特定上下文的函数bind() 可以用来创建一个永久绑定到特定 this 上下文的函数。

    javascript
    var module = {
      x: 42,
      getX: function () {
        return this.x;
      },
    };
    
    var retrieveX = module.getX;
    console.log(retrieveX()); // 输出:undefined,因为在这种情况下,this 指向全局对象
    
    var boundGetX = retrieveX.bind(module);
    console.log(boundGetX()); // 输出:42,因为 this 已经被绑定到 module

    在这个例子中,retrieveX 函数的 this 默认指向全局对象,而 boundGetX 函数的 this 则被绑定到 module

以上就是 bind() 方法的一些常见使用场景。它提供了一种灵活的方式来控制函数的 this 上下文和参数,使得函数能够在各种不同的上下文中被重复使用。

hancenter808@outlook.com