- 文档
- 教程
- 前端
【免费下载链接】zh.javascript.info
现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。
bind是 JavaScript 中解决"丢失this"问题的内建手段,但很多开发者在使用它时会遇到一个隐蔽的坑:绑定之后,原函数身上自定义的属性(如计数器、辅助方法)统统不见了,访问到的是undefined。本文将结合《现代 JavaScript 教程》中的练习题"bind 后的函数属性"(task.md 与 solution.md),从函数对象本质、ECMAScript 的绑定函数外来对象(Bound Function Exotic Object)机制入手,讲清"属性为什么丢"以及"真需要时怎么办",读完你将对bind的返回值和函数属性存储模型有完整的认知。
题目回顾:bind 之后函数属性会改变吗
题目来自 task.md,核心代码如下:
function sayHi() { alert( this.name ); } sayHi.test = 5; let bound = sayHi.bind({ name: "John" }); alert( bound.test ); // 输出将会是什么?为什么?问的是:函数sayHi身上挂着一个自定义属性test = 5,经过bind得到bound之后,bound.test是多少?
答案:undefined
solution.md 给出的答案非常干脆:
答案:
undefined。bind的结果是另一个对象。它并没有test属性。
也就是说,bound.test既不是5,也不是报错,而是undefined——bound是一个与sayHi完全不同的对象,原函数身上的自定义属性一个都没有带过来。
这个结论初看反直觉:bound()调用起来明明就像sayHi一样工作(this还被替换成了{name: "John"}),为什么属性却"蒸发"了?要理解这一点,需要先回到函数对象的本质。
第一层原理:函数本质上是可调用的对象
在 JavaScript 中,函数的类型是对象(详细讲解见 06-function-object/article.md 中的"函数对象,NFE"章节)。这意味着函数不仅是"可调用的代码",还可以被当作对象来使用:
- 可以给它添加/删除自定义属性,例如
sayHi.test = 5; - 可以按引用传递它;
- 它自带一些内建属性,如
name(函数名)和length(形参个数,rest 参数不计入)。
自定义属性经常被用于"在函数身上附带状态"。教程中就有典型例子:用函数属性counter替代闭包来记录调用次数:
function sayHi() { alert("Hi"); sayHi.counter++; // 计算调用次数 } sayHi.counter = 0; sayHi(); sayHi(); alert( `Called ${sayHi.counter} times` ); // Called 2 times这里的关键是:counter、test这类属性是存储在sayHi这个函数对象自身的属性表里的,它们不是局部变量,也不存在于任何词法环境中。所以"属性跟着函数对象走"——对象是谁,属性就在谁身上。
第二层原理:bind 返回的是全新的"外来绑定函数对象"
既然属性存储在函数对象自身,那么问题就变成:bind返回的对象到底是不是原来的函数对象?
答案是否定的。bind章节(article.md)中明确说明:
func.bind(context)的结果是一个特殊的类似于函数的"外来对象(exotic object)",它可以像函数一样被调用,并且透明地将调用传递给func并设定this=context。
按 ECMAScript 规范,bind返回的是一个绑定函数外来对象(Bound Function Exotic Object),它内部只保存了三样东西:
[[BoundTargetFunction]]:被绑定的目标函数(即原func);[[BoundThis]]:绑定的this上下文;[[BoundArguments]]:绑定的前置参数列表。
当bound(...)被调用时,引擎会取出这三项,把[[BoundThis]]作为this、把[[BoundArguments]]与本次调用参数拼接后,Call到[[BoundTargetFunction]]上。除此之外,这个新对象和原函数之间没有任何属性共享——原函数自定义的test、counter、name等属性都不会出现在绑定函数上。
同一章节的练习题 3-second-bind 及其 solution.md 也从侧面印证了这一点:
function f() { alert(this.name); } f = f.bind( {name: "John"} ).bind( {name: "Pete"} ); f(); // John,不是 Pete连续两次bind,结果是第二次绑定完全无效。原因正如该答案所说:f.bind(...)返回的绑定函数外来对象仅在创建时记忆上下文和参数,一个函数不能被重新绑定(re-bound)。第一次bind生成的新对象已经"固化"了{name: "John"},第二次bind只是拿这个新对象再包一层,调用链最终仍落到最内层的目标函数和最初绑定的this上。
如果绑定函数的上下文还可以被外部环境改写,那么 2-write-to-object-after-bind 的答案就不会是null:
function f() { alert( this ); // null } let user = { g: f.bind(null) }; user.g(); // 输出 null即便通过user.g()调用,this依然是硬绑定的null。这说明绑定函数是一个独立的、硬编码了内部状态的新对象——它既不继承原函数属性,也不允许事后修改绑定上下文。
源码佐证:bind 的底层实现就是"包一层新函数"
在仓库的 head.html 中,教程提供了一个兼容旧引擎的bind模拟实现(ES5 写法),可以直观地看到"新对象从何而来":
function bind(func, context /*, args*/) { let bindArgs = [].slice.call(arguments, 2); // (1) 收集绑定的前置参数 function wrapper() { // (2) 创建一个全新的 wrapper 函数 let args = [].slice.call(arguments); let unshiftArgs = bindArgs.concat(args); // (3) 前置参数 + 本次调用参数 return func.apply(context, unshiftArgs); // (4) 以 context 作为 this 调用原函数 } return wrapper; }可以看到:
bind内部创建的是一个全新的wrapper函数对象并返回它,原函数的属性当然不会"复制"过来;- 调用
wrapper时,通过func.apply(context, ...)把this固定为context,参数则按"绑定参数在前、调用参数在后"的顺序拼接; - 这正好解释了本练习中
bound.test === undefined:bound就是这样一个全新的wrapper对象,它身上只实现了"按固定上下文调用目标函数"这件事。
原生Function.prototype.bind的实现细节虽然更复杂(涉及规范中的绑定函数内部槽),但"创建一个新函数对象、不继承原函数属性"这一行为特征是等价的。
工程影响:哪些场景最容易踩中这个坑
理解了上面的机制,就能预判以下真实场景中的坑:
1. 用函数属性存状态/辅助功能时
比如把counter属性挂在函数上统计调用次数(06-function-object/article.md 中的经典写法),一旦把函数bind后传给setTimeout、事件监听器或回调,计数器就会"归零"——因为绑定函数身上根本没有这个属性,访问到的永远是undefined。
2. 把带属性函数传给第三方库时
lodash 的_.bindAll(object, methodNames)(article.md 中有介绍)会在循环里用user[key].bind(user)批量重写对象方法。如果这些方法身上挂了自定义属性,绑定后属性同样会丢失。
3. 属性与变量的混淆
教程在函数对象章节特意提醒过:函数属性(如sayHi.counter)不是函数内的局部变量,二者互不影响、是完全平行的两条线。绑定函数丢失的是"属性"这条线,而闭包捕获的变量(词法环境中的user等)则不受影响——这也是为什么 5-question-use-bind 的解决方案里,bind和箭头包装器都能修复this,但只有bind能"硬固定"住对象引用:askPassword(user.loginOk.bind(user), user.loginFail.bind(user))绑定的是当时的user对象,而() => user.loginOk()每次调用时才去读外部变量user,若变量中途被改写就会调用到错误对象。
实战补充:如果确实需要保留属性怎么办
bind不会复制属性,但我们可以手动"补"回来,常见做法有三种:
方式一:绑定后重新挂载属性
function sayHi() { alert( this.name ); } sayHi.test = 5; let bound = sayHi.bind({ name: "John" }); bound.test = 5; // 手动把需要的属性挂到绑定函数上 alert( bound.test ); // 5方式二:封装一层返回"绑定 + 属性"的工具函数
function bindWithProps(func, context, props) { const bound = func.bind(context); return Object.assign(bound, props); // 把原函数的属性合并到绑定函数上 }方式三:改用闭包/词法变量承载状态
如果状态只是为了计数或标记,可以把状态放到函数外部词法环境中(或用对象this承载),这样bind只固定this,状态仍由闭包变量或对象属性保存,天然不受影响。函数属性与闭包变量的取舍,教程在 06-function-object/article.md 的"自定义属性"一节有详细对比。
总结
bind返回的是全新的绑定函数外来对象,调用时透明转发给目标函数并固定this与前置参数;- 原函数对象自身的属性(
test、counter等)存储在原对象上,不会被绑定函数继承,访问结果为undefined; - 绑定函数的
this是硬绑定的,无法二次绑定、也无法被后续的调用方式改写(参见 3-second-bind 与 2-write-to-object-after-bind); - 需要属性时,手动在绑定函数上重新挂载,或用闭包/对象属性承载状态。
想继续巩固这个知识点,可以顺带完成同章节的其余练习:5-question-use-bind(bind与包装器的取舍)和 6-ask-partial(绑定参数的部分应用),它们共同构成了对bind行为的完整认知闭环。
- 文档
- 教程
- 前端
【免费下载链接】zh.javascript.info
现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。
相关推荐
深入解析 JavaScript 中 `bind` 之后函数属性为何丢失:Function Property After Bind
深入解析 JavaScript 中 bind 之后函数属性为何丢失:Function Property After Bind 本篇围绕 Modern JavaS
文档/教程前端深入解析 JavaScript 中 bind 与函数属性:为什么绑定后 sayHi.test 变成了 undefined
深入解析 JavaScript 中 bind 与函数属性:为什么绑定后 sayHi.test 变成了 undefined Function.prototype.
文档教程前端深入解析 bind 与函数属性:为什么 bound.test 是 undefined(JavaScript 现代教程实战)
深入解析 bind 与函数属性:为什么 bound.test 是 undefined(JavaScript 现代教程实战) Function.prototype
文档/教程前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考