news 2026/10/7 16:03:09

bind 之后函数属性为何消失?深入解析 JavaScript 函数绑定与函数对象属性

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
bind 之后函数属性为何消失?深入解析 JavaScript 函数绑定与函数对象属性
  • 文档
  • 教程
  • 前端

【免费下载链接】zh.javascript.info

现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。

项目地址:https://gitcode.com/gh_mirrors/zh/zh.javascript.info
点击查看免费下载

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 相关知识。

项目地址:https://gitcode.com/gh_mirrors/zh/zh.javascript.info
点击查看免费下载
上一篇:推荐文章:深入探索Kubernetes与Vault的无缝集成——Kubernetes-Vault项目解析
下一篇:5分钟入门Veaury:React项目嵌入Vue3组件的完整教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 15:58:14

YOLOv5 6.0吸烟检测实战:从数据集配置到边缘部署全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 15:58:13

LeetCode 64最小路径和:Java动态规划与滚动数组优化精讲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 15:57:33

RV1126B芯片解析:AI-ISP与AOV3.0如何重构边缘视觉智能

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 15:56:21

OpenHarmony上Flutter GridView实战与性能优化

在 OpenHarmony 设备上跑 Flutter 并不算难,难的是把它用到一个真实页面里:数据要动起来、图片要加载、滚动要够稳、异常不能直接崩掉。这篇就围绕我看得最多也最常用的一个场景——GridView 网格视图——把 Flutter for OpenHarmony 从环境准备到实战落…

作者头像 李华