bind函数和 call、apply 有什么区别

bind函数和 call、apply 有什么区别

bind 函数和 call、apply 有什么区别

在 JavaScript 中,bind、call 和 apply 都与函数调用时的 this 有关,但它们的作用并不相同:

  • call:立即调用函数,逐个传入参数。
  • apply:立即调用函数,把参数作为数组或类数组对象传入。
  • bind:不立即调用函数,而是返回一个绑定了 this 和部分参数的新函数。

可以先记住一个简化版本:

fn.call(thisArg, arg1, arg2);
fn.apply(thisArg, [arg1, arg2]);
const boundFn = fn.bind(thisArg, arg1, arg2);

前两种方式执行 fn 并返回它的结果;后一种方式返回一个新函数,之后调用新函数时才执行原函数。理解这个区别,基本就能判断大多数场景该用哪一个。

一、先看一个完整示例

下面的代码可以直接在浏览器控制台或 Node.js 中运行:

function introduce(greeting, punctuation) {
  return `${greeting},我是 ${this.name}${punctuation}`;
}

const person = { name: "小林" };

console.log(introduce.call(person, "你好", "!"));
// 你好,我是 小林!

console.log(introduce.apply(person, ["你好", "。"]));
// 你好,我是 小林。

const boundIntroduce = introduce.bind(person, "早上好");
console.log(boundIntroduce("。"));
// 早上好,我是 小林。

call 和 apply 在调用时都指定了 this,并马上执行 introduce。bind 则提前指定了 this,同时固定了第一个参数;返回的 boundIntroduce 后续只需要接收剩余参数。

二、三者的基本区别

方法是否立即调用参数传递方式返回值典型用途
call是逐个传入被调用函数的返回值立即调用,并指定 this
apply是一个数组或类数组对象被调用函数的返回值已有参数集合,需要立即调用
bind否可预先绑定部分参数,调用时再传剩余参数一个新函数延迟调用、固定上下文或创建偏函数

这张表也提醒我们:bind 的返回值不是原函数的运行结果,而是一个新函数。常见错误之一,正是把它和 call、apply 当作可以互换的写法。

三、call:立即调用,参数逐个传入

call 的语法如下:

function.call(thisArg, arg1, arg2, ...)

第一个参数是本次调用时希望使用的 this 值,后面的参数按函数定义所需的顺序逐个传入。例如:

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

const result = add.call(null, 3, 4);
console.log(result); // 7

add 并没有使用 this,所以示例里的 null 只是占位。实际开发中经常用 call 为需要读取 this 的函数指定上下文:

const user = {
  name: "小周"
};

function getName(prefix) {
  return prefix + this.name;
}

console.log(getName.call(user, "用户:")); // 用户:小周

如果参数是在数组中动态收集的,逐个写在 call 里通常不方便,可以用展开语法:

const values = [2, 5];

console.log(Math.max.call(null, ...values)); // 5

这里 ...values 是 JavaScript 的展开语法,它会在调用前把数组元素展开成独立参数。它和 apply 的参数数组形式不是同一种语法,不过对于很多调用场景,两者都能实现“把一组值作为多个参数传给函数”的效果。

四、apply:立即调用,参数作为数组或类数组传入

apply 的语法是:

function.apply(thisArg, argsArray)

第二个参数可以是数组,也可以是类数组对象。类数组对象指有非负整数形式的 length 属性,并能通过数字索引取值的对象。例如浏览器中的某些 DOM 集合、函数的 arguments,以及手动构造的类数组对象。

function listItems(first, second, third) {
  return `${first} / ${second} / ${third}`;
}

const args = ["苹果", "梨", "葡萄"];

console.log(listItems.apply(null, args));
// 苹果 / 梨 / 葡萄

const arrayLike = {
  0: "红",
  1: "绿",
  length: 2
};

console.log(
  function (a, b) {
    return `${a}、${b}`;
  }.apply(null, arrayLike)
);
// 红、绿

传统代码中,apply 常用于已有参数数组的情形。现代 JavaScript 也可以写成 fn(...args),通常更直观:

const args = [4, 8, 15];

console.log(Math.max.apply(null, args)); // 15
console.log(Math.max(...args)); // 15

展开语法要求被展开的值是可迭代对象,例如数组、字符串或 Set。apply 的参数位置则接受数组或类数组对象。两者在一般数组场景下很像,但对输入类型的要求并不完全相同。如果手里是任意类数组对象,现代写法可以先转换:

function collect() {
  const args = Array.from(arguments);
  return Math.max(...args);
}

console.log(collect(6, 2, 9)); // 9

五、bind:返回绑定后的新函数

bind 的基本形式是:

const newFunction = function.bind(thisArg, arg1, arg2, ...)

调用 bind 时,原函数不会立即执行。它会返回一个新函数;调用这个新函数时,绑定的 this 和预先提供的参数才会生效。

function greet(greeting, name) {
  return `${greeting},${name}`;
}

const greetInChinese = greet.bind(null, "你好");

console.log(greetInChinese("小王")); // 你好,小王
console.log(greetInChinese("小陈")); // 你好,小陈

这个例子中,"你好" 是预先绑定的第一个参数;每次调用 greetInChinese 时传入的参数,会接在预绑定参数之后。

因此,bind 也可以用于创建“偏函数”:把一个多参数函数的部分参数固定下来,得到一个更专用的函数。

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

const double = multiply.bind(null, 2);

console.log(double(5)); // 10
console.log(double(11)); // 22

六、常见适用场景

1. 把对象方法作为回调传递

普通函数调用时,this 通常取决于函数怎样被调用,而不取决于函数定义的位置。把对象方法直接传给计时器或其他回调接口时,原来的对象调用形式可能丢失:

const counter = {
  value: 0,

  increase() {
    this.value += 1;
    console.log(this.value);
  }
};

setTimeout(counter.increase, 0);

上面的写法传递的是函数本身,不是“带着 counter 上下文的方法”。在回调中,this 不一定是 counter,所以可能得到错误结果,或者在严格模式下读取到未定义的 this。可以使用 bind:

setTimeout(counter.increase.bind(counter), 0);
// 输出 1

如果只需要立即调用,也可以显式使用 call:

counter.increase.call(counter); // 立即输出并增加

但是 setTimeout(counter.increase.call(counter), 0) 并不能延迟执行:它会先立即执行 increase,再把返回值交给定时器。需要延迟时,可以传入绑定后的函数,或者传入一个箭头函数:

setTimeout(() => counter.increase(), 0);

选用 bind 还是箭头函数,取决于代码风格和参数处理需要。箭头函数能在调用时读取外围作用域的 this;bind 则创建一个明确绑定上下文的新函数。

2. 事件监听中的上下文处理

在 DOM 事件监听器中,普通函数的 this 通常指向注册监听器的元素(事件处理方式不同会影响具体语义)。如果希望调用对象的方法并让它使用对象本身,可以预先绑定:

const panel = {
  count: 0,

  handleClick() {
    this.count += 1;
    console.log(`点击次数:${this.count}`);
  }
};

const button = document.querySelector("#saveButton");

if (button) {
  button.addEventListener("click", panel.handleClick.bind(panel));
}

实际项目还要注意:每次调用 bind 都会创建一个新的函数对象。若后续需要移除事件监听器,应保存绑定后的函数引用:

const handler = panel.handleClick.bind(panel);

button.addEventListener("click", handler);
// 需要移除时必须传入同一个函数对象
button.removeEventListener("click", handler);

下面的写法无法移除先前注册的监听器,因为两次 bind 返回的是不同函数:

button.addEventListener("click", panel.handleClick.bind(panel));
button.removeEventListener("click", panel.handleClick.bind(panel));

3. 用 apply 处理动态参数集合

有些代码先收集参数,之后再决定调用哪个函数。这时 apply 可以表达“把这组参数作为函数参数传入”。例如把函数收到的参数交给另一个函数:

function logAndRun(fn) {
  const args = Array.prototype.slice.call(arguments, 1);
  return fn.apply(null, args);
}

function join(a, b) {
  return `${a}-${b}`;
}

console.log(logAndRun(join, "前", "后")); // 前-后

在较新的 JavaScript 中可以改用剩余参数和展开语法,避免手动处理 arguments:

function run(fn, ...args) {
  return fn(...args);
}

console.log(run(join, "左", "右")); // 左-右

4. 创建长期复用的固定上下文函数

当同一个函数要交给多个调用方,且每次都应使用固定对象时,可以预先绑定:

const config = {
  prefix: "[系统]"
};

function printMessage(message) {
  console.log(`${this.prefix} ${message}`);
}

const printSystemMessage = printMessage.bind(config);

printSystemMessage("服务已启动");
printSystemMessage("服务已停止");

这种方式让调用点不用反复传入上下文。要留意,绑定只适用于普通函数的动态 this,不能改变箭头函数的 this。

七、thisArg 的边界:严格模式、原始值与箭头函数

1. 严格模式下的 null 和 undefined

对普通函数使用 call、apply 或 bind 时,传入的 thisArg 会受到函数是否处于严格模式的影响。严格模式下,null 和 undefined 会作为函数内部的 this 值保留,不会自动替换成全局对象。

"use strict";

function inspectThis() {
  return this;
}

console.log(inspectThis.call(null)); // null
console.log(inspectThis.call(undefined)); // undefined

非严格模式的普通函数会对 this 做转换:null 和 undefined 通常会转换为该环境的全局对象;原始值则会被装箱为对应的对象。具体全局对象与宿主环境有关。在浏览器中通常是 window,在 Node.js 中不能简单地按浏览器规则理解。

function inspectThisNonStrict() {
  return this;
}

// 在非严格模式的普通脚本中运行时,null 通常会被替换为全局对象
console.log(inspectThisNonStrict.call(null));

严格模式的代码示例适合用于明确说明规则。需要注意:ES 模块默认采用严格模式;普通脚本是否严格则取决于代码是否使用了 "use strict" 以及所在环境。

2. 传入原始值时的差别

严格模式的普通函数可以在 this 中直接观察到传入的原始值;非严格模式会将原始值转换为包装对象。为了减少兼容性判断,生产代码通常不应依赖这种转换行为,尤其不要假设传入数字后 this 一定是数字对象。

3. 箭头函数的 this 不能被这些方法改写

箭头函数没有自己的 this,它从定义时所在的词法作用域读取 this。因此,call、apply 和 bind 都不能为箭头函数设置一个新的 this。

const obj = { name: "对象" };

const arrow = () => this;

console.log(arrow.call(obj) === obj); // false:call 不会改写箭头函数的 this

const boundArrow = arrow.bind(obj);
console.log(boundArrow === arrow); // false:bind 返回新函数
console.log(boundArrow()); // 仍取决于箭头函数定义时的词法 this

这里 bind 仍会返回一个函数,但新函数并不能改变箭头函数内部的词法 this。如果函数必须由调用方指定上下文,应使用普通函数,而不是依赖箭头函数。

八、bind 还能预先绑定参数

传给 bind 的第一个参数用于绑定 this,后续参数会预先绑定到函数参数的前面。调用绑定后的函数时,后来传入的参数排在它们之后。

function format(prefix, value, suffix) {
  return `${prefix}${value}${suffix}`;
}

const formatCurrency = format.bind(null, "金额:");

console.log(formatCurrency(35, " 元"));
// 金额:35 元

绑定参数是“固定一部分参数”,不是“替代所有后续参数”。还可以多次对绑定后的函数使用 bind。后一次绑定的参数会继续追加,但对普通函数来说,后一次传入的 thisArg 不会覆盖第一次绑定的 thisArg:

function describe(a, b, c) {
  return `${this.name}: ${a}, ${b}, ${c}`;
}

const first = describe.bind({ name: "第一次绑定" }, "A");
const second = first.bind({ name: "第二次绑定" }, "B");

console.log(second("C"));
// 第一次绑定: A, B, C

这条规则适用于普通绑定函数的调用行为,不应误解为所有情况下绑定的 this 都不可改变:构造调用有单独规则,后文会说明。

九、bind 与构造调用:常见但容易忽略的边界

如果 bind 的目标是可构造的普通函数,绑定后的函数通常仍可以用 new 调用。此时,绑定时指定的 thisArg 会被忽略,因为构造调用会创建新的实例作为 this;预绑定的参数仍然生效。

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

const BoundPerson = Person.bind({ name: "不会使用的 this" }, "小吴");
const instance = new BoundPerson("编辑");

console.log(instance.name); // 小吴
console.log(instance.role); // 编辑
console.log(instance instanceof Person); // true

这并不意味着 bind 可以把任意函数变成构造函数。箭头函数、方法简写函数等不可构造的函数,不能因为使用了 bind 就获得构造能力;对绑定后的不可构造目标使用 new 会报错。

绑定函数本身没有通常意义上供实例继承的自有 prototype 属性,但可构造的绑定函数依然可以构造实例。它的构造行为会基于目标函数的构造能力处理。日常业务中,不建议把 bind 当作复杂继承工具;如果是在定义类或构造器,应清楚区分函数绑定与原型继承。

同时,不能在构造函数上使用 call 或 apply 来代替 new 创建实例。它们只是按普通函数调用规则执行函数:

function User(name) {
  this.name = name;
}

const target = {};
User.call(target, "小赵");

console.log(target.name); // 小赵
console.log(target instanceof User); // false

上例把属性写到了 target 上,但没有执行实例创建和原型关联。要创建实例,应使用 new User("小赵")。

十、参数数量限制:不要依赖无限参数

call、apply 和展开语法最终都需要把参数传给函数。JavaScript 引擎对单次函数调用可接收的实参数量存在实现相关的限制;限制值取决于引擎和运行环境,不应把某个固定数字当作所有浏览器与 Node.js 版本都适用的标准上限。参数过多可能导致异常或调用失败。

因此,下面这种写法只适用于参数集合大小适中的情况:

const hugeList = getVeryLargeList();

// 数据量很大时,不要假设这行一定能在所有引擎中成功
const maximum = Math.max(...hugeList);

如果要对大量数据求最大值,可以分块处理,而不是一次把所有元素展开成参数:

function maxInChunks(values, chunkSize = 10000) {
  let result = -Infinity;

  for (let i = 0; i < values.length; i += chunkSize) {
    const chunk = values.slice(i, i + chunkSize);
    result = Math.max(result, ...chunk);
  }

  return result;
}

console.log(maxInChunks([3, 1, 8, 2])); // 8

这里的 chunkSize 是示例中的应用层选择,不是 JavaScript 规范规定的安全上限。面对特别大的数据,使用循环、归约或其他分块算法,往往比依赖一次性参数展开更稳妥。

十一、常见错误与排查方法

错误一:把 bind 当成立即调用

const result = greet.bind(null, "你好");

result 是函数,不是问候语字符串。要执行它,必须再调用一次:

const sayHello = greet.bind(null, "你好");
console.log(sayHello("小吴"));

错误二:把 call 和 bind 的参数位置混淆

fn.call(context, [a, b]);

这会把数组当成一个参数传给 fn,不会自动把数组元素拆开。若希望逐个传入,可以写成 fn.call(context, a, b);如果已有数组,则用 fn.apply(context, [a, b]) 或 fn.call(context, ...[a, b])。

错误三:以为 bind 会修改原函数

fn.bind(context);
fn();

bind 不会原地修改 fn。第一行创建了一个新函数,但代码没有保存它,后面的 fn() 仍然按原来的方式调用。正确写法是:

const boundFn = fn.bind(context);
boundFn();

错误四:以为 call、apply 能改写箭头函数的 this

箭头函数使用词法 this。如果代码依赖 call 或 apply 设置上下文,应该确认目标函数是普通函数。

错误五:反复 bind 后期待 this 被覆盖

对于普通绑定函数,第一次绑定的 thisArg 不会被后续绑定覆盖。要切换上下文,应从原始函数重新创建绑定函数,而不是在已有绑定函数上再次绑定另一个对象。

错误六:用 bind 后没有保留回调引用

需要移除事件监听器、取消订阅或比较回调函数时,要保存 bind 的返回值。每次调用 bind 都会返回一个新的函数对象,两个绑定函数即使参数完全一样,也不相等。

十二、安全用途:能做什么,不能做什么

在安全敏感代码中,保存某些内建方法的引用并绑定到预期对象,可以减少后续受到属性遮蔽或替换的影响。例如,应用代码可能不希望某个对象自己的 hasOwnProperty 属性遮蔽同名方法。可以从原型上取得方法,并为预期接收者绑定:

const hasOwn = Function.prototype.call.bind(
  Object.prototype.hasOwnProperty
);

const record = {
  value: 1,
  hasOwnProperty: "这个属性遮蔽了常规写法"
};

console.log(hasOwn(record, "value")); // true
console.log(hasOwn(record, "missing")); // false

在现代代码中,也可以直接使用 Object.hasOwn(record, "value")。它能避免调用对象自身可能被遮蔽的 hasOwnProperty 方法。不过,当运行环境不支持该方法时,需要按项目的兼容性策略处理。

类似地,在受控的初始化阶段保存可信方法引用,有时可以降低之后全局属性被意外替换带来的影响。但这不是完整的安全边界,也不能仅靠 bind 构建沙箱或隔离不可信代码。

  • bind 不会验证输入数据、权限或调用者身份。
  • 绑定一个对象不会让这个对象变成只读或不可篡改。
  • 如果绑定时拿到的函数本身已经被替换或有恶意行为,绑定并不会使它变可信。
  • 页面中的其他代码可能仍能通过其他路径影响数据、原型或共享状态。

处理不可信输入仍需进行校验、权限控制、输出编码和适当的隔离。对于真正的隔离需求,应使用符合威胁模型的安全机制,而不是把 call、apply 或 bind 当作安全沙箱。

十三、怎样选择:一个实用判断流程

  1. 需要现在就执行函数吗?如果不需要,优先考虑 bind 或包装函数。
  2. 需要现在执行,参数已经逐个列出吗?使用 call。
  3. 需要现在执行,参数在数组或类数组对象中吗?可以使用 apply;数组可迭代时也可以用展开语法。
  4. 需要创建固定上下文或固定部分参数的可复用函数吗?使用 bind。
  5. 目标是箭头函数吗?不要期待这三种方法改变它的 this。
  6. 参数集合特别大吗?避免一次性通过 apply 或展开语法传入,改用循环或分块算法。
  7. 回调以后要注销吗?保存 bind 返回的函数引用。

十四、总结

call、apply 和 bind 都可以用于普通函数的显式 this 绑定,但它们在调用时机和参数形式上有清楚的分工:

  • call:立即执行,参数逐个传入。
  • apply:立即执行,参数通过数组或类数组对象传入。
  • bind:返回新函数,预先固定 this,也可以预先绑定部分参数。

实际使用时,还要留意严格模式下 this 的处理、箭头函数的词法 this、绑定函数的构造行为、回调函数引用是否保存,以及引擎对单次调用参数数量的实现限制。把这些边界考虑清楚,才能在正确的场景中使用这三个方法,而不是只记住它们的语法形式。

© 版权声明

相关文章