
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 当作安全沙箱。
十三、怎样选择:一个实用判断流程
- 需要现在就执行函数吗?如果不需要,优先考虑
bind或包装函数。 - 需要现在执行,参数已经逐个列出吗?使用
call。 - 需要现在执行,参数在数组或类数组对象中吗?可以使用
apply;数组可迭代时也可以用展开语法。 - 需要创建固定上下文或固定部分参数的可复用函数吗?使用
bind。 - 目标是箭头函数吗?不要期待这三种方法改变它的
this。 - 参数集合特别大吗?避免一次性通过
apply或展开语法传入,改用循环或分块算法。 - 回调以后要注销吗?保存
bind返回的函数引用。
十四、总结
call、apply 和 bind 都可以用于普通函数的显式 this 绑定,但它们在调用时机和参数形式上有清楚的分工:
call:立即执行,参数逐个传入。apply:立即执行,参数通过数组或类数组对象传入。bind:返回新函数,预先固定this,也可以预先绑定部分参数。
实际使用时,还要留意严格模式下 this 的处理、箭头函数的词法 this、绑定函数的构造行为、回调函数引用是否保存,以及引擎对单次调用参数数量的实现限制。把这些边界考虑清楚,才能在正确的场景中使用这三个方法,而不是只记住它们的语法形式。








