Skip to content

想象你有一个通用的工具函数,但你希望在不同的上下文中使用它。JavaScript 的 callapplybind 方法就像是三个魔法师,可以让你精确地控制函数中 this 的指向,让你的代码更加灵活和可复用。

让我们先看一个简单的场景:

javascript
const person = {
  name: "Alice",
  age: 30,
  introduce() {
    return `Hi, I'm ${this.name} and I'm ${this.age} years old`;
  },
};

const anotherPerson = {
  name: "Bob",
  age: 25,
};

console.log(person.introduce()); // "Hi, I'm Alice and I'm 30 years old"

// 如何让 person.introduce 在 anotherPerson 的上下文中执行?
console.log(person.introduce.call(anotherPerson)); // "Hi, I'm Bob and I'm 25 years old"

通过 call 方法,我们成功让 introduce 函数在 anotherPerson 的上下文中执行了!

Call 方法:直接调用与参数传递

call 方法允许你调用一个函数,并明确指定函数运行时的 this 值以及参数。

基本语法

javascript
functionName.call(thisArg, arg1, arg2, ..., argN)
  • thisArg:函数运行时使用的 this
  • arg1, arg2, ..., argN:传递给函数的参数列表

使用场景

1. 函数借用

javascript
const arrayLike = {
  0: "apple",
  1: "banana",
  2: "orange",
  length: 3,
};

// 借用 Array.prototype.slice 方法
const realArray = Array.prototype.slice.call(arrayLike);
console.log(realArray); // ['apple', 'banana', 'orange']
console.log(Array.isArray(realArray)); // true

2. 改变 this 指向

javascript
function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`;
}

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

console.log(greet.call(person, "Hello", "!")); // "Hello, John!"
console.log(greet.call(person, "Hi", ".")); // "Hi, John."

3. 构造函数继承

javascript
function Parent(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

function Child(name, age) {
  // 继承 Parent 的属性
  Parent.call(this, name);
  this.age = age;
}

const child = new Child("Emma", 12);
console.log(child.name); // 'Emma'
console.log(child.colors); // ['red', 'blue', 'green']
console.log(child.age); // 12

Apply 方法:数组参数的魔法

apply 方法与 call 类似,但它接受一个数组(或类数组对象)作为参数。

基本语法

javascript
functionName.apply(thisArg, [argsArray]);
  • thisArg:函数运行时使用的 this
  • argsArray:包含要传递给函数参数的数组或类数组对象

使用场景

1. 数组操作

javascript
const numbers = [5, 2, 8, 1, 9, 3];

// 找出最大值
const max = Math.max.apply(null, numbers);
console.log(max); // 9

// 找出最小值
const min = Math.min.apply(null, numbers);
console.log(min); // 1

2. 函数参数组装

javascript
function calculateTotal(price, tax, discount, shipping) {
  return price + tax - discount + shipping;
}

const costs = [100, 8, 10, 15];
const total = calculateTotal.apply(null, costs);
console.log(total); // 113

3. 链式调用

javascript
const methods = ["push", "pop", "shift", "unshift"];
const array = [1, 2, 3];

methods.forEach((method) => {
  if (method === "push") {
    Array.prototype[method].apply(array, [4]);
  }
});

console.log(array); // [1, 2, 3, 4]

Bind 方法:永久绑定的艺术

bind 方法创建一个新函数,当被调用时,其 this 值为 bind 的第一个参数,其余参数将作为新函数的参数供调用时使用。

基本语法

javascript
const newFunction = functionName.bind(thisArg, arg1, arg2, ..., argN)
  • thisArg:新函数运行时使用的 this
  • arg1, arg2, ..., argN:预设的参数

使用场景

1. 永久绑定 this

javascript
const counter = {
  count: 0,
  increment() {
    this.count++;
    console.log(this.count);
  },
};

const increment = counter.increment;

// 不使用 bind,this 指向全局对象
increment(); // NaN (全局对象没有 count 属性)

// 使用 bind 绑定正确的 this
const boundIncrement = counter.increment.bind(counter);
boundIncrement(); // 1
boundIncrement(); // 2

2. 预设参数(柯里化)

javascript
function multiply(a, b) {
  return a * b;
}

// 创建固定第一个参数的函数
const double = multiply.bind(null, 2);
const triple = multiply.bind(null, 3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

// 预设多个参数
function discountPrice(price, discountRate, tax) {
  return price * (1 - discountRate) + tax;
}

const applyTenPercentDiscount = discountPrice.bind(null, 0.1);
const finalPrice = applyTenPercentDiscount(100, 5);
console.log(finalPrice); // 95

3. 事件处理器中的 this

javascript
const button = {
  text: "Click me",
  clicks: 0,

  init() {
    const buttonElement = document.getElementById("myButton");

    // 不使用 bind,this 指向 button 元素
    buttonElement.addEventListener("click", function () {
      this.clicks++; // 错误:this 指向 button 元素
      console.log(this.text); // undefined
    });

    // 使用 bind 正确绑定 this
    buttonElement.addEventListener(
      "click",
      function () {
        this.clicks++;
        console.log(`${this.text} clicked ${this.clicks} times`);
      }.bind(this)
    );
  },
};

4. 定时器中的 this

javascript
const timer = {
  seconds: 0,
  start() {
    setInterval(
      function () {
        this.seconds++;
        console.log(`Timer: ${this.seconds} seconds`);
      }.bind(this),
      1000
    );
  },
};

timer.start(); // Timer: 1 seconds, Timer: 2 seconds, ...

Call vs Apply vs Bind 的对比

语法对比

javascript
function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`;
}

const person = { name: "Sarah" };

// Call:逐个传递参数
console.log(greet.call(person, "Hello", "!")); // "Hello, Sarah!"

// Apply:通过数组传递参数
console.log(greet.apply(person, ["Hi", "."])); // "Hi, Sarah."

// Bind:创建新函数,可以后续调用
const boundGreet = greet.bind(person, "Hey");
console.log(boundGreet("?")); // "Hey, Sarah?"

执行时机对比

javascript
function showTime() {
  console.log(new Date().toLocaleTimeString());
}

// Call 和 Apply:立即执行
showTime.call(null); // 立即显示时间
showTime.apply(null); // 立即显示时间

// Bind:创建新函数,延迟执行
const delayedShowTime = showTime.bind(null);
setTimeout(delayedShowTime, 1000); // 1秒后显示时间

高级应用技巧

1. 函数方法链

javascript
const calculator = {
  value: 0,

  add(num) {
    this.value += num;
    return this; // 返回自身以支持链式调用
  },

  multiply(num) {
    this.value *= num;
    return this;
  },

  subtract(num) {
    this.value -= num;
    return this;
  },

  getValue() {
    return this.value;
  },
};

const result = calculator.add(5).multiply(2).subtract(3).getValue();
console.log(result); // 7

2. 借用构造函数

javascript
function EventEmitter() {
  this.events = {};
}

EventEmitter.prototype.on = function (event, callback) {
  if (!this.events[event]) {
    this.events[event] = [];
  }
  this.events[event].push(callback);
};

EventEmitter.prototype.emit = function (event, ...args) {
  if (this.events[event]) {
    this.events[event].forEach((callback) => callback.apply(this, args));
  }
};

function User(name) {
  EventEmitter.call(this); // 借用 EventEmitter 的构造函数
  this.name = name;
}

// 继承原型方法
User.prototype = Object.create(EventEmitter.prototype);
User.prototype.constructor = User;

const user = new User("John");
user.on("login", function () {
  console.log(`${this.name} logged in`);
});

user.emit("login"); // "John logged in"

3. 数组操作工具函数

javascript
const arrayUtils = {
  // 借用 Array.prototype 方法
  slice: Array.prototype.slice,
  map: Array.prototype.map,
  filter: Array.prototype.filter,

  // 转换类数组为真正数组
  toArray(list) {
    return this.slice.call(list);
  },

  // 过滤对象数组
  filterByProperty(array, property, value) {
    return this.filter.call(array, function (item) {
      return item[property] === value;
    });
  },
};

const nodeList = document.querySelectorAll("div");
const divs = arrayUtils.toArray(nodeList);

const users = [
  { name: "Alice", age: 25, role: "admin" },
  { name: "Bob", age: 30, role: "user" },
  { name: "Charlie", age: 35, role: "admin" },
];

const admins = arrayUtils.filterByProperty(users, "role", "admin");
console.log(admins); // [{ name: 'Alice', age: 25, role: 'admin' }, { name: 'Charlie', age: 35, role: 'admin' }]

性能考虑

1. 避免过度使用 bind

javascript
// 不推荐:每次渲染都创建新函数
function Component() {
  this.handleClick = function () {
    console.log("clicked");
  }.bind(this);

  // 每次 Component 实例化都会创建新的 handleClick 函数
}

// 推荐:在原型上定义方法
function Component() {
  // 构造函数
}

Component.prototype.handleClick = function () {
  console.log("clicked");
};

// 使用时绑定
const component = new Component();
element.addEventListener("click", component.handleClick.bind(component));

2. 使用 apply 处理大量参数

javascript
// 处理可变数量的参数
function sum() {
  return Array.prototype.reduce.call(
    arguments,
    function (total, num) {
      return total + num;
    },
    0
  );
}

// 使用 apply 传递参数数组
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const total = sum.apply(null, numbers);
console.log(total); // 55

实际项目应用

1. React 组件中的事件处理

javascript
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };

    // 在构造函数中绑定,避免每次渲染都创建新函数
    this.increment = this.increment.bind(this);
    this.decrement = this.decrement.bind(this);
  }

  increment() {
    this.setState((prevState) => ({ count: prevState.count + 1 }));
  }

  decrement() {
    this.setState((prevState) => ({ count: prevState.count - 1 }));
  }

  render() {
    return (
      <div>
        <button onClick={this.increment}>+</button>
        <span>{this.state.count}</span>
        <button onClick={this.decrement}>-</button>
      </div>
    );
  }
}

2. 函数式编程中的组合

javascript
// 创建通用的函数组合器
function compose() {
  const funcs = Array.prototype.slice.call(arguments);

  return function () {
    const args = Array.prototype.slice.call(arguments);

    return funcs.reduceRight(function (acc, func) {
      return func.apply(null, Array.isArray(acc) ? acc : [acc]);
    }, args);
  };
}

const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
const square = (x) => x * x;

const addAndSquare = compose(square, add);
const multiplyAndAdd = compose(add, multiply);

console.log(addAndSquare(3, 4)); // 49 ((3 + 4) * (3 + 4))
console.log(multiplyAndAdd(3, 4)); // 12 (3 * 4 + 0)

3. 借用 Array 方法处理字符串

javascript
String.prototype.reverse = function () {
  return Array.prototype.reduce.call(
    this,
    function (acc, char) {
      return char + acc;
    },
    ""
  );
};

String.prototype.uniqueChars = function () {
  return Array.prototype.filter
    .call(this, function (char, index, arr) {
      return arr.indexOf(char) === index;
    })
    .join("");
};

console.log("hello".reverse()); // "olleh"
console.log("banana".uniqueChars()); // "ban"

常见陷阱与解决方案

1. bind 不能被重新绑定

javascript
function greet() {
  return this.name;
}

const person1 = { name: "Alice" };
const person2 = { name: "Bob" };

const boundGreet = greet.bind(person1);
console.log(boundGreet()); // "Alice"

// 尝试重新绑定会失败
const reboundGreet = boundGreet.bind(person2);
console.log(reboundGreet()); // "Alice",仍然是 Alice

// 解决方案:创建原始函数的新绑定
const newBoundGreet = greet.bind(person2);
console.log(newBoundGreet()); // "Bob"

2. 构造函数中的 bind

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

Person.prototype.sayHello = function () {
  return `Hello, ${this.name}`;
};

const boundSayHello = Person.prototype.sayHello.bind({ name: "Stranger" });

const person = new Person("John");
console.log(boundSayHello.call(person)); // "Hello, Stranger",不会改变绑定的 this

总结

callapplybind 是 JavaScript 中控制 this 指向的三大魔法师:

  • call:立即执行函数,逐个传递参数
  • apply:立即执行函数,通过数组传递参数
  • bind:创建新函数,预设 this 和参数,延迟执行

这三个方法为 JavaScript 函数式编程提供了强大的灵活性,让你能够:

  • 借用其他对象的方法
  • 改变函数执行上下文
  • 预设函数参数
  • 处理可变参数列表
  • 实现函数组合和柯里化

掌握这三个方法,你就掌握了 JavaScript 中 this 指向控制的核心技能,能够写出更加灵活和可复用的代码。