call/apply/bind

12 阅读6分钟

一文彻底搞懂 call/apply/bind:从原理到手写实现,面试再也不怕

callapplybind 是 JavaScript 中最核心的三个方法,也是面试的必考题。它们的唯一作用就是改变函数执行时的 this 指向,但很多人只知道怎么用,却不知道它们的底层原理和区别。

本文将从this 绑定规则→核心用法→底层原理→手写实现→真实应用五个维度,带你彻底搞懂这三个方法,让你不仅会用,更能在面试中从容应对所有相关问题。


一、为什么我们需要改变 this 指向?

在讲这三个方法之前,我们必须先搞懂一个前提:JavaScript 中的 this 是动态绑定的,不是在函数定义时确定,而是在函数调用时确定。

这是 JavaScript 最灵活也最容易踩坑的特性。很多时候,函数执行时的 this 并不是我们预期的那个值:

const person = {
  name: '张三',
  sayHello() {
    console.log(`你好,我是${this.name}`);
  }
};

// 正常调用:this 指向 person
person.sayHello(); // "你好,我是张三"

// 把方法赋值给变量后调用:this 丢失了
const say = person.sayHello;
say(); // "你好,我是undefined" ❌

上面的例子中,当我们把 person.sayHello 赋值给变量 say 后再调用,this 就不再指向 person 了,而是指向了全局对象(浏览器中是 window)。

callapplybind 就是为了解决这个问题而生的:它们允许我们手动指定函数执行时的 this 指向


二、先回顾:this 的四种绑定规则

callapplybind 属于显式绑定,是四种 this 绑定规则中优先级最高的(仅次于 new 绑定)。

绑定规则说明this 指向
默认绑定函数独立调用全局对象(严格模式下是 undefined
隐式绑定作为对象的方法调用调用方法的对象
显式绑定使用 call/apply/bind 调用手动指定的对象
new 绑定使用 new 关键字调用新创建的实例对象

优先级:new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定


三、call:立即执行函数并绑定 this

1. 核心定义

call 方法会立即执行函数,并将函数的 this 指向第一个参数,后续参数作为函数的参数逐个传递。

2. 基本语法

function.call(thisArg, arg1, arg2, ...);
  • thisArg:函数执行时的 this 指向
  • arg1, arg2, ...:传递给函数的参数,逐个传递
  • 返回值:函数执行后的返回值

3. 基本用法

function sayHello(greeting, punctuation) {
  console.log(`${greeting},我是${this.name}${punctuation}`);
}

const person = { name: '张三' };

// 调用 sayHello,将 this 绑定到 person
sayHello.call(person, '你好', '!'); // "你好,我是张三!"

4. 特殊情况

  • 如果 thisArgnullundefined,在非严格模式下会自动替换为全局对象(window
  • 如果 thisArg 是基本类型(数字、字符串、布尔),会自动转换为对应的包装对象
// 非严格模式下,this 指向 window
sayHello.call(null, '你好', '!'); // "你好,我是undefined"

// 基本类型会被包装成对象
function showThis() {
  console.log(this);
}
showThis.call(123); // Number {123}

四、apply:立即执行函数并绑定 this(数组传参)

1. 核心定义

apply 方法和 call 方法几乎完全相同,唯一的区别是:

  • call逐个传递参数
  • apply将参数放在一个数组或类数组中传递

类数组对象 = 有数字下标 + 有 length属性 + 不是数组 + 不能用数组方法

  • 必须拥有非负整数的数字索引属性:属性名必须是 0、1、2、3... 这样的非负整数(字符串形式的数字也可以,如 "0"、"1")
  • 必须拥有 length 属性:值为非负整数,表示元素的个数
  • 不能是数组本身:obj instanceof Array 必须返回 false
  • 不继承数组原型方法:没有 push、pop、forEach、map 等数组专属方法

2. 基本语法

function.apply(thisArg, [argsArray]);
  • thisArg:函数执行时的 this 指向
  • argsArray:传递给函数的参数数组或类数组
  • 返回值:函数执行后的返回值

3. 基本用法

function sayHello(greeting, punctuation) {
  console.log(`${greeting},我是${this.name}${punctuation}`);
}

const person = { name: '张三' };

// 用数组传递参数
sayHello.apply(person, ['你好', '!']); // "你好,我是张三!"

4. 经典应用场景:数组方法借用

这是 apply 最常用的场景,因为它可以将数组展开成逐个参数:

// 求数组的最大值
const arr = [1, 3, 5, 2, 4];
// Math.max 不接受数组参数,只接受逐个参数
console.log(Math.max.apply(null, arr)); // 5 ✅

// 等价于
console.log(Math.max(1, 3, 5, 2, 4)); // 5

五、bind:返回新函数并永久绑定 this

1. 核心定义

bind 方法和前两者最大的区别是:它不会立即执行函数,而是返回一个新的函数,这个新函数的 this 会被永久绑定到第一个参数,后续参数会作为新函数的预置参数。

2. 基本语法

const newFunction = function.bind(thisArg, arg1, arg2, ...);
  • thisArg:新函数执行时的 this 指向
  • arg1, arg2, ...:预置参数,会在调用新函数时先传递
  • 返回值:一个新的绑定了 this 的函数

3. 基本用法

function sayHello(greeting, punctuation) {
  console.log(`${greeting},我是${this.name}${punctuation}`);
}

const person = { name: '张三' };

// 返回一个新函数,this 永久绑定到 person
const boundSayHello = sayHello.bind(person);

// 调用新函数
boundSayHello('你好', '!'); // "你好,我是张三!"

// 也可以预置参数
const boundSayHi = sayHello.bind(person, '嗨');
boundSayHi('~'); // "嗨,我是张三~"

4. 重要特性:永久绑定

bind 绑定的 this永久的,一旦绑定就无法再被 callapply 改变:

const person1 = { name: '张三' };
const person2 = { name: '李四' };

const boundSayHello = sayHello.bind(person1);
boundSayHello('你好', '!'); // "你好,我是张三!"

// 尝试用 call 改变 this,无效
boundSayHello.call(person2, '你好', '!'); // "你好,我是张三!" ❌

六、三者核心区别对比表

对比维度callapplybind
执行时机立即执行立即执行不立即执行,返回新函数
参数传递逐个传递数组/类数组传递逐个传递,支持预置参数
this 绑定临时绑定临时绑定永久绑定,无法改变
返回值函数执行结果函数执行结果新的绑定函数
适用场景大多数场景需要数组传参的场景回调函数、事件处理、预置参数

七、最常见的 5 个应用场景

1. 借用其他对象的方法(最经典)

这是 callapply 最常用的场景,可以让一个对象借用另一个对象的方法:

// 类数组对象借用数组的 slice 方法
function sum() {
  // arguments 是类数组,没有 slice 方法
  const args = Array.prototype.slice.call(arguments);
  return args.reduce((a, b) => a + b, 0);
}

console.log(sum(1, 2, 3, 4)); // 10

2. 改变回调函数的 this 指向

在定时器、事件监听器等回调函数中,this 经常会丢失,用 bind 可以解决:

const person = {
  name: '张三',
  sayHello() {
    console.log(`你好,我是${this.name}`);
  }
};

// ❌ 错误:this 指向 window
setTimeout(person.sayHello, 1000); // "你好,我是undefined"

// ✅ 正确:用 bind 绑定 this
setTimeout(person.sayHello.bind(person), 1000); // "你好,我是张三"

3. 实现继承

在 ES6 之前,我们可以用 call 来实现构造函数的继承:

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

function Student(name, age, grade) {
  // 调用父类构造函数,绑定 this 到子类实例
  Person.call(this, name, age);
  this.grade = grade;
}

const student = new Student('张三', 18, '高三');
console.log(student.name); // "张三"
console.log(student.age); // 18
console.log(student.grade); // "高三"

4. 函数柯里化

利用 bind 的预置参数特性,可以实现函数柯里化:

function add(a, b) {
  return a + b;
}

// 预置第一个参数为 5,得到一个新函数
const add5 = add.bind(null, 5);

console.log(add5(3)); // 8
console.log(add5(7)); // 12

5. 绑定事件处理函数的 this

在 React 类组件中,我们经常需要用 bind 来绑定事件处理函数的 this

class Button extends React.Component {
  constructor(props) {
    super(props);
    // 在构造函数中绑定 this
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    console.log(this); // 指向组件实例
  }

  render() {
    return <button onClick={this.handleClick}>点击我</button>;
  }
}

八、手写实现 call/apply/bind(面试必考题)

手写实现这三个方法是前端面试的高频考点,下面我们一步步实现它们的完善版本。

1. 手写 call

Function.prototype.myCall = function(thisArg, ...args) {
  // 1. 处理 thisArg 为 null/undefined 的情况
  thisArg = thisArg || window;
  
  // 2. 创建一个唯一的 Symbol 属性,避免覆盖原有属性
  const key = Symbol();
  
  // 3. 将当前函数(this)挂载到 thisArg 上
  thisArg[key] = this;
  
  // 4. 执行函数并获取结果
  const result = thisArg[key](...args);
  
  // 5. 删除临时属性
  delete thisArg[key];
  
  // 6. 返回结果
  return result;
};

// 测试
const person = { name: '张三' };
function sayHello(greeting) {
  return `${greeting},我是${this.name}`;
}
console.log(sayHello.myCall(person, '你好')); // "你好,我是张三"

2. 手写 apply

applycall 几乎一样,只是参数处理不同:

Function.prototype.myApply = function(thisArg, argsArray) {
  thisArg = thisArg || window;
  const key = Symbol();
  thisArg[key] = this;
  
  // 处理参数数组
  const result = argsArray ? thisArg[key](...argsArray) : thisArg[key]();
  
  delete thisArg[key];
  return result;
};

// 测试
console.log(sayHello.myApply(person, ['你好'])); // "你好,我是张三"

3. 手写 bind

bind 是最复杂的,需要处理永久绑定、预置参数和 new 绑定的情况:

Function.prototype.myBind = function(thisArg, ...boundArgs) {
  const fn = this;
  
  // 创建一个中间函数,用来处理原型链
  //隔离原型:修改 boundFn.prototype 不会影响原函数的原型
  const Fn = function() {};
  
  const boundFn = function(...args) {
    // 如果是 new 调用,this 指向新创建的实例
    // 否则指向绑定的 thisArg
    const context = this instanceof Fn ? this : thisArg;
    
    // 合并预置参数和调用时的参数
    return fn.apply(context, [...boundArgs, ...args]);
  };
  
  // 维护原型链
  Fn.prototype = fn.prototype;
  boundFn.prototype = new Fn();
  //new boundFn() → boundFn.prototype → new Fn() → Fn.prototype → 原函数.prototype
  
  return boundFn;
};

// 测试
const boundSayHello = sayHello.myBind(person);
console.log(boundSayHello('你好')); // "你好,我是张三"

九、常见误区与注意事项

1. 误区1:bind 可以多次绑定

错! bind 绑定的 this 是永久的,多次绑定只有第一次有效:

const fn = function() { console.log(this.name); };
const bound1 = fn.bind({ name: '张三' });
const bound2 = bound1.bind({ name: '李四' });
bound2(); // "张三" ❌ 不是李四

2. 误区2:箭头函数可以被 call/apply/bind 改变 this

错! 箭头函数没有自己的 this,它的 this 继承自外层作用域,无法被 call/apply/bind 改变:

const arrowFn = () => console.log(this);
arrowFn.call({ name: '张三' }); // window ❌

3. 误区3:apply 的第二个参数可以是任何值

错! apply 的第二个参数必须是数组或类数组对象,否则会抛出 TypeError

sayHello.apply(person, '你好'); // ❌ TypeError: CreateListFromArrayLike called on non-object

4. 误区4:call/apply 会改变原函数的 this

错! callapply 只是在本次调用时临时改变 this,不会影响原函数本身:

const fn = function() { console.log(this); };
fn.call({ name: '张三' }); // { name: '张三' }
fn(); // window ✅ 原函数的 this 没有改变

十、面试高频考点总结

  1. call、apply、bind 的区别是什么?

    • callapply 立即执行函数,bind 返回新函数
    • call 逐个传参,apply 数组传参
    • bind 永久绑定 this,callapply 临时绑定
  2. 如何实现 call/apply/bind? 参考上面的手写实现,重点掌握 callbind 的实现。

  3. bind 多次绑定会怎么样? 只有第一次绑定有效,后续绑定会被忽略。

  4. 箭头函数可以被 call/apply/bind 改变 this 吗? 不可以,箭头函数的 this 继承自外层作用域,无法被改变。

  5. apply 的第二个参数有什么要求? 必须是数组或类数组对象。

  6. new 绑定和 bind 绑定哪个优先级更高? new 绑定优先级更高,当用 new 调用 bind 绑定的函数时,this 会指向新创建的实例。


写在最后

callapplybind 是 JavaScript 函数式编程的基础,也是理解 this 指向的关键。掌握它们不仅能让你写出更优雅、更健壮的代码,更是面试中脱颖而出的必备技能。

希望这篇文章能帮你彻底搞懂这三个方法,让你在实际开发和面试中都能游刃有余。

如果觉得这篇文章对你有帮助,欢迎点赞、收藏、关注,有任何问题可以在评论区留言讨论!