ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JavaScript 函数绑定(bind)完全指南:解决 this 丢失、绑定参数与偏函数实战(zh.javascript.info 现代 JavaScript 教程)

JavaScript 函数绑定(bind)完全指南:解决 this 丢失、绑定参数与偏函数实战(zh.javascript.info 现代 JavaScript 教程) 文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载导读在 JavaScript 中把对象方法作为回调传递给setTimeout、事件监听器或其他调度器时最常见的坑就是this意外丢失。本文以现代 JavaScript 教程zh.javascript.info中《函数绑定》一章为骨架系统讲解this丢失的成因、包装器方案、func.bind(context, ...args)的完整语法与底层原理并延伸讲解偏函数partial application与不带上下文的部分应用实现最后结合本仓库的配套练习与示例代码给出可验证的实战结论。问题起点把方法传出去this 就丢了在对象方法内部this指向调用该方法的对象。但一旦方法被与对象分离开地传递例如传递给setTimeoutthis就会丢失。下面是最经典的演示let user { firstName: John, sayHi() { alert(Hello, ${this.firstName}!); } }; setTimeout(user.sayHi, 1000); // Hello, undefined!输出不是Hello, John!而是Hello, undefined!。原因在于setTimeout拿到的是函数user.sayHi本身它与user对象已经脱钩。上面最后一行等价于let f user.sayHi; setTimeout(f, 1000); // 丢失了 user 上下文在浏览器中setTimeout对回调的调用会设定thiswindowNode.js 中则是计时器对象所以this.firstName实际读取的是window.firstName自然不存在在其他类似场景下this通常会变为undefined。这类需求非常典型我们想把一个对象方法传递到别处比如调度程序并在那里调用同时保证它在正确的上下文中执行。解决方案 1包装器函数最直接的修复是包一层——用一个普通函数把方法调用包起来使其从外部词法环境捕获userlet user { firstName: John, sayHi() { alert(Hello, ${this.firstName}!); } }; setTimeout(function() { user.sayHi(); // Hello, John! }, 1000);用箭头函数可以写得更简洁setTimeout(() user.sayHi(), 1000); // Hello, John!但包装器方案存在一个隐患它捕获的是变量user本身而不是当时的对象。如果在setTimeout触发之前user被重新赋值就会调用到错误的对象let user { firstName: John, sayHi() { alert(Hello, ${this.firstName}!); } }; setTimeout(() user.sayHi(), 1000); // ……user 的值在不到 1 秒的时间内发生了改变 user { sayHi() { alert(Another user in setTimeout!); } }; // Another user in setTimeout!这正是原生bind所要解决的问题。解决方案 2func.bind(context) 绑定 this函数对象有一个内建方法bind它能把this固定下来。基本语法// 稍后将会有更复杂的语法 let boundFunc func.bind(context);func.bind(context)的返回值是一个特殊的、类似函数的外来对象exotic object——在 ECMAScript 规范中称为Bound Function Exotic Object。它可以像函数一样被调用并把调用透明地转发给func同时强制设定thiscontext。换句话说boundFunc调用起来就像绑定了this的func。看一个最简例子let user { firstName: John }; function func() { alert(this.firstName); } let funcUser func.bind(user); funcUser(); // Johnfunc.bind(user)生成的绑定函数是func的一个绑定变体this被固定为user。调用时传入的所有参数都会原样转发给原始函数let user { firstName: John }; function func(phrase) { alert(phrase , this.firstName); } // 将 this 绑定到 user let funcUser func.bind(user); funcUser(Hello); // Hello, John参数 Hello 被传递并且 thisuser回到最初的setTimeout场景绑定对象方法后一切正常let user { firstName: John, sayHi() { alert(Hello, ${this.firstName}!); } }; let sayHi user.sayHi.bind(user); // (*) // 可以在没有对象与对象分离的情况下运行它 sayHi(); // Hello, John! setTimeout(sayHi, 1000); // Hello, John! // 即使 user 的值在不到 1 秒内发生了改变 // sayHi 还是会使用预先绑定pre-bound的值该值是对旧的 user 对象的引用 user { sayHi() { alert(Another user in setTimeout!); } };在(*)行我们把方法user.sayHi绑定到了user上。此后sayHi就是一个绑定后bound的方法无论单独调用还是传给setTimeout上下文都保持正确——因为它绑定的是一开始那个user对象的引用而不是可变的user变量。参数同样原样传递let user { firstName: John, say(phrase) { alert(${phrase}, ${this.firstName}!); } }; let say user.say.bind(user); say(Hello); // Hello, John!参数 Hello 被传递给了 say say(Bye); // Bye, John!参数 Bye 被传递给了 say便捷方法bindAll如果一个对象有很多方法都要传递出去可以用一个循环批量绑定for (let key in user) { if (typeof user[key] function) { user[key] user[key].bind(user); } }JavaScript 生态中也有现成的批量绑定工具例如 lodash 的_.bindAll(object, methodNames)。深入原理bind 到底做了什么本仓库在 head.html 中提供了一个手写bind的参考实现可以直观看到绑定函数的内部工作机制function bind(func, context /*, args*/) { let bindArgs [].slice.call(arguments, 2); // (1) function wrapper() { // (2) let args [].slice.call(arguments); let unshiftArgs bindArgs.concat(args); // (3) return func.apply(context, unshiftArgs); // (4) } return wrapper; }这段实现揭示了绑定的四个关键步骤收集预绑定参数(1)把调用bind时除func和context之外的参数收集为bindArgs生成包装函数(2)返回一个普通wrapper合并参数(3)把预绑定参数与调用时传入的参数拼接bindArgs.concat(args)用 apply 固定上下文(4)通过func.apply(context, ...)强制以context作为this调用原始函数。原生bind的行为正是如此上下文被硬绑定、参数被预先拼接调用被转发给原始函数。理解了这一点就不难解释下文几个反直觉的练习结论。bind 的三个边界行为配套练习实证本仓库为本章配了多道习题它们分别验证了绑定函数的三个重要特性1. 绑定函数的 this 无法被覆盖题目 作为方法的绑定函数如果把f.bind(null)作为对象方法user.g调用this是什么function f() { alert( this ); // ? } let user { g: f.bind(null) }; user.g();答案null。绑定函数的上下文是硬绑定的无法再修改。即使通过user.g()调用源方法仍以thisnull执行。2. 函数不能被重复绑定re-bound题目 二次 bindfunction f() { alert(this.name); } f f.bind( {name: John} ).bind( {name: Ann } ); f();答案John。f.bind(...)返回的外来绑定函数对象只在创建时记忆上下文与参数。对绑定函数再次bind只是在它之上又包了一层内层绑定的this不会改变——一个函数不能被重绑定。3. 绑定结果是新对象不继承原函数属性题目 bind 后的函数属性function sayHi() { alert( this.name ); } sayHi.test 5; let bound sayHi.bind({ name: John }); alert( bound.test ); // 输出将会是什么为什么答案undefined。bind的结果是另一个对象它没有test属性。绑定函数与原始函数是两个独立的对象原函数的自定义属性不会带过去。部分应用函数Partial functionsbind不仅能绑定this还能绑定参数。完整语法let bound func.bind(context, [arg1], [arg2], ...);它允许同时绑定上下文this和函数的部分参数。以乘法函数为例function mul(a, b) { return a * b; }基于mul创建double翻倍函数function mul(a, b) { return a * b; } let double mul.bind(null, 2); alert( double(3) ); // mul(2, 3) 6 alert( double(4) ); // mul(2, 4) 8 alert( double(5) ); // mul(2, 5) 10mul.bind(null, 2)创建的新函数double会把调用转发给mul以null为上下文、2为第一个参数其余参数原样传递。这种通过预先绑定已有函数的部分参数来创建新函数的技术称为函数的部分应用partial application。注意这里实际上没有用到this但bind的第一个参数必须是上下文所以只能传null或undefined占位。同理可以做出triplefunction mul(a, b) { return a * b; } let triple mul.bind(null, 3); alert( triple(3) ); // mul(3, 3) 9 alert( triple(4) ); // mul(3, 4) 12 alert( triple(5) ); // mul(3, 5) 15部分应用的价值在于其一我们能得到一个可读性强、语义明确的独立函数double、triple调用时不必每次都重复那个固定参数其二当我们有一个非常灵活、参数很多的函数却需要一个不那么灵活的特定变体时偏函数尤为有用。例如有个send(from, to, text)函数在user对象内部可以借助偏函数得到从当前用户发送的sendTo(to, text)变体。在没有上下文的情况下做 partial原生bind不允许跳过上下文只绑参数——你不能省略this直接跳到arguments。但如果想为一个对象方法绑定部分参数、同时保留调用方提供的this就需要手写一个partial。好消息是它非常简单function partial(func, ...argsBound) { return function(...args) { // (*) return func.call(this, ...argsBound, ...args); } } // 用法 let user { firstName: John, say(time, phrase) { alert([${time}] ${this.firstName}: ${phrase}!); } }; // 添加一个带有绑定时间的 partial 方法 user.sayNow partial(user.say, new Date().getHours() : new Date().getMinutes()); user.sayNow(Hello); // 类似于这样的一些内容 // [10:00] John: Hello!partial(func[, arg1, arg2...])返回的包装器(*)调用func时this与调用者一致对user.sayNow(Hello)而言this就是user通过func.call(this, ...)透传先给...argsBound来自partial调用时绑定的参数如10:00再给...args调用包装器时传入的参数如Hello。利用 spread 语法这个实现非常简洁。lodash 也提供了现成的_.partial实现功能与此一致。实战案例回调中的方法绑定与偏函数本章的两道综合练习完整展现了bind在真实代码中的两种用法。案例 1修复丢失 this 的回调题目 修复丢失了 this 的函数askPassword()校验密码后调用user.loginOk/loginFail但直接传入对象方法会导致this丢失function askPassword(ok, fail) { let password prompt(Password?, ); if (password rockstar) ok(); else fail(); } let user { name: John, loginOk() { alert(${this.name} logged in); }, loginFail() { alert(${this.name} failed to log in); }, }; askPassword(user.loginOk, user.loginFail); // this 丢失报错错误根源askPassword拿到的是未绑定对象的loginOk/loginFail调用时thisundefined。修复方式是用bind绑定上下文askPassword(user.loginOk.bind(user), user.loginFail.bind(user));另一种替代方案是用箭头函数包装askPassword(() user.loginOk(), () user.loginFail());它通常也能工作但在更复杂的场景下可能失效——例如askPassword被调用之后、访问者应答触发回调之前变量user被重新赋值箭头函数就会调用错误的对象。这正是包装器捕获变量、bind 捕获引用差异的实战体现。案例 2偏函数在登录中的应用题目 偏函数在登录中的应用 是上一个任务的变体对象只有一个方法user.login(true/false)需要向askPassword传入参数使user.login(true)表示成功、user.login(false)表示失败。方案一包装器 箭头函数askPassword(() user.login(true), () user.login(false));方案二bind 绑定上下文 预绑定参数——即偏函数askPassword(user.login.bind(user, true), user.login.bind(user, false));第二种方案正是绑定上下文 绑定参数的典型应用user.login.bind(user, true)生成的函数被调用时等价于user.login(true)。总结func.bind(context, ...args)返回func的绑定变体同时固定this与...args参数最常见的用途是把对象方法的this绑定好再安全地传递到setTimeout、事件监听等外部位置预先绑定部分参数得到的、通用性较低的绑定函数称为partially applied部分应用函数或partial绑定函数的this是硬绑定的不可重绑定绑定结果是新的外来对象不继承原函数属性可参见 作为方法的绑定函数、二次 bind、bind 后的函数属性 三道配套练习当只需要绑定参数、不想绑定上下文时原生bind无法胜任可参考文中partial的手写实现对应源码思路见 head.html或使用 lodash 的_.partial当不想反复重复相同参数时偏函数非常有用——就像send(from, to)中from固定时只需创建并使用它的一个部分应用变体。与之相关的概念可以继续阅读本教程的 闭包、箭头函数 与 函数对象Function 对象的属性与方法 等章节它们共同构成对 JavaScript 函数机制的完整认知。赞分享文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载相关推荐JavaScript 教程修复丢失 this 的 askPassword 函数 —— 深入理解 bind 绑定机制JavaScript 教程修复丢失 this 的 askPassword 函数 —— 深入理解 bind 绑定机制 导读 在 Modern JavaScrip文档/教程前端MXNet ndarray.linalg 线性代数算子 API 全解析从 GEMM 到特征分解的完整指南MXNet ndarray.linalg 线性代数算子 API 全解析从 GEMM 到特征分解的完整指南 本文是 MXNet 旧式legacy线性代数算子文档教程前端0元起步做开源dotnet官方免费工具与云服务清单全解析IDE、CI、性能分析器全覆盖0元起步做开源dotnet官方免费工具与云服务清单全解析IDE、CI、性能分析器全覆盖 想做开源却担心工具费钱这篇指南带你完整解析 dotnet 官方免文档教程前端上一篇终极Windows风扇控制指南用Fan Control实现专业级散热管理下一篇探秘Backend.ai一款全方位的云端开发平台创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表