资讯详情

资讯详情

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是多少答案undefinedsolution.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并设定thiscontext。按 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 的答案就不会是nullfunction 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 undefinedbound就是这样一个全新的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-bindbind与包装器的取舍和 6-ask-partial绑定参数的部分应用它们共同构成了对bind行为的完整认知闭环。赞分享文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载相关推荐深入解析 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 是 undefinedJavaScript 现代教程实战深入解析 bind 与函数属性为什么 bound.test 是 undefinedJavaScript 现代教程实战 Function.prototype文档/教程前端上一篇推荐文章深入探索Kubernetes与Vault的无缝集成——Kubernetes-Vault项目解析下一篇5分钟入门VeauryReact项目嵌入Vue3组件的完整教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →