ARTICLE DETAIL

资讯详情

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

JavaScript 函数绑定实战:修复 `askPassword` 中丢失的 `this`(zh.javascript.info 深度解析)

JavaScript 函数绑定实战:修复 `askPassword` 中丢失的 `this`(zh.javascript.info 深度解析) 文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载this丢失是 JavaScript 回调传递中最经典的坑当你把对象方法当作函数传入另一个函数时方法内部的this会与对象分离变成undefined或window导致代码静默出错。本篇以 1-js/06-advanced-functions/10-bind/5-question-use-bind 一节的练习题与解答为骨架结合本仓库函数绑定章节的完整讲义与同章其他习题解答讲清错误的成因、bind修复方案、包装器方案的隐患以及bind在外来对象、硬绑定、部分应用等层面的底层原理帮助读者在真实项目中一次彻底掌握安全传递对象方法的实战技巧。任务场景askPassword为何报错题目见 task.md给出的代码如下askPassword()先弹出密码输入框然后根据输入结果调用ok()或fail()回调而调用方把user.loginOk / user.loginFail两个对象方法直接传了进去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);运行时会报错loginOk/loginFail内部的this.name取不到John在浏览器中通常表现为输出undefined或抛 TypeError。题目的要求是只修改高亮的那一行调用让所有内容正常运作其余行不动。错误根源方法被分离传递this变为undefined答案见 solution.md指出错误是因为ask获得的是没有绑定对象的loginOk/loginFail函数。当askPassword在内部执行ok()/fail()时这两个函数是被裸调用的它们自然认定this undefined于是this.name必然取不到值。这与本章讲义 article.md 中丢失 this一节描述的机制完全一致一旦方法被传递到与对象分开的某个地方this就丢失。讲义用setTimeout演示了同样的现象let user { firstName: John, sayHi() { alert(Hello, ${this.firstName}!); } }; setTimeout(user.sayHi, 1000); // Hello, undefined!原因在于setTimeout拿到的是函数user.sayHi本身它已经和对象分离了。最后一行等价于let f user.sayHi; setTimeout(f, 1000); // 丢失了 user 上下文讲义还提醒浏览器中的setTimeout比较特殊会给回调设定this window所以this.firstName实际上在读取不存在的window.firstName而在其他类似场景如本题的askPassword中this通常直接是undefined。理解这一点后修复思路就清晰了——必须在传递之前把方法的this固定到user上。修复方案一用bind绑定上下文推荐solution.md给出的标准答案是使用Function.prototype.bind把两个方法分别绑定到userfunction 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.bind(user), user.loginFail.bind(user));现在两个回调内部执行时this恒等于user输出John logged in或John failed to log in。bind的基本语法是let boundFunc func.bind(context);func.bind(context)返回一个特殊的、类似函数的外来对象exotic objectECMAScript 规范中的 Bound Function Exotic Object它可以像普通函数一样被调用并且透明地把调用转发给func同时固定this context。换句话说boundFunc就是绑定了this的func。更完整的语法还支持预绑定参数let bound func.bind(context, [arg1], [arg2], ...);即除了this还能把部分参数一并固化详见下文部分应用小节。对于对象方法的使用形态讲义给出了典型示例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 还是会使用预先绑定的值该值是对旧的 user 对象的引用 user { sayHi() { alert(Another user in setTimeout!); } };在(*)行sayHi已经是绑定后的方法无论单独调用还是传给setTimeout函数上下文始终正确并且它持有的是对旧user对象的引用后续变量被重新赋值也不会影响调用结果。这正是bind方案相比包装器方案的核心优势。如果对象有大量方法需要批量绑定可以在循环中完成for (let key in user) { if (typeof user[key] function) { user[key] user[key].bind(user); } }讲义指出JavaScript 库如 lodash 的_.bindAll(object, methodNames)也提供了现成的批量绑定工具本仓库中该章的 article.md 头部还声明依赖 lodash可作为对照参考。修复方案二包装器箭头函数——注意它的隐患solution.md同时给出了另一个可行的替换答案askPassword(() user.loginOk(), () user.loginFail());箭头函数从外部词法环境捕获user再以常规方式调用user.loginOk()此时this由方法调用语法user.loginOk()决定自然指向user所以通常也能正常工作看起来也挺好的。但solution.md特别提醒了一个隐患这个方案可能会在更复杂的场景下失效。例如变量user在调用askPassword之后、但在访问者应答并执行() user.loginOk()之前被修改——因为箭头函数每次执行时读取的是当前的user变量而不是捕获那一刻的对象引用。讲义 article.md 用setTimeout版本演示了这个漏洞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在创建时刻就把this固化天然免疫后续变量被改写的问题包装器方案代码更简洁却把正确性寄托在回调执行时user未被改写这一前提上。这正是本题推荐bind的根本原因。同章的 6-ask-partial 解答 也印证了这一取舍它给出了askPassword(() user.login(true), () user.login(false))与askPassword(user.login.bind(user, true), user.login.bind(user, false))两种写法后者借助bind同时固化this与首个参数更加稳健。深入原理bind的外来对象、硬绑定与属性缺失顺着bind方案继续深挖本仓库同章的三道附加习题从不同侧面揭示了绑定函数的底层行为值得一并掌握1. 绑定上下文是硬绑定无法再被修改见 2-write-to-object-after-bind/solution.mdfunction f() { alert( this ); // null } let user { g: f.bind(null) }; user.g(); // 仍然输出 null即使把绑定函数作为user.g通过user.g()调用源方法内的this依然是创建时绑定的null——对象方法调用语法无法覆盖bind已经固化的上下文。2. 绑定函数一次成型不能被重新绑定见 3-second-bind/solution.mdfunction f() { alert(this.name); } f f.bind( {name: John} ).bind( {name: Pete} ); f(); // Johnf.bind(...)返回的外来绑定函数对象仅在创建时记忆上下文与参数再次对它调用bind产生的是以该绑定函数为基座的新的外来对象而原始绑定中的John上下文早已固化因此最终输出John而非Pete。3. 绑定结果是另一个对象不继承原函数属性见 4-function-property-after-bind/solution.mdfunction f() { /* ... */ } f.test 5; let bound f.bind(null); bound.test; // undefinedbind的结果是一个全新的外来对象它并不携带原函数上挂载的自定义属性。这提醒我们在实际工程中不要依赖给绑定函数传递原函数的元数据这类写法。这些行为共同刻画了bind的语义边界它是一次性的、创建即固化的上下文绑定适合把稳定的对象方法安全地交付出让给外部调用方回调、定时器、事件处理器。延伸部分应用partial——bind绑定参数的另一面bind不仅能修this还能预绑定参数这在本章讲义 article.md 的部分应用函数一节有系统讲解。例如基于乘法函数创建doublefunction 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 function application它的价值在于能创建double、triple这类可读性好、参数更少的独立函数let triple mul.bind(null, 3); alert( triple(3) ); // mul(3, 3) 9 alert( triple(5) ); // mul(3, 5) 15注意即使完全用不到thisbind也要求第一个参数是上下文所以此时惯例是传null。当不想绑定this、只想绑定参数时原生的bind不允许跳过上下文讲义给出的通用partial实现利用 rest/spread 轻松搞定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}!); } }; user.sayNow partial(user.say, new Date().getHours() : new Date().getMinutes()); user.sayNow(Hello); // 类似于 [10:00] John: Hello!包装器(*)调用func时this沿用调用方传入的上下文对user.sayNow(Hello)而言即user参数依次为partial预绑定的10:00与调用时传入的Hello。回到本文的askPassword场景把参数绑定与this绑定结合还能得到更紧凑的写法见 6-ask-partial/solution.mdaskPassword(user.login.bind(user, true), user.login.bind(user, false));总结与实战要点围绕askPassword丢失this这道题可以提炼出以下可复用的工程结论丢失this的成因对象方法被当作独立函数传递并裸调用时this变为undefined浏览器setTimeout场景则为window方法内部对this.xxx的访问随之失效。首选bind修复askPassword(user.loginOk.bind(user), user.loginFail.bind(user))——bind在创建时刻固化this回调何时、被谁调用都无关紧要它返回外来绑定对象上下文硬绑定、不可重绑、不继承原函数属性。包装器箭头函数方案要谨慎() user.loginOk()依赖回调执行时user未被改写一旦user变量在askPassword调用后、回调执行前被重新赋值就会调用错误对象bind持有的是旧对象的引用天然免疫此问题。bind的另一半能力是参数绑定func.bind(context, arg1, ...)可用于部分应用如user.login.bind(user, true)结合 article.md 中的partial实现可在不绑定this时仅固化参数。如需进一步练习可依次完成本仓库同章的 2-write-to-object-after-bind、3-second-bind、4-function-property-after-bind 与 6-ask-partial将本文的原理逐一验证。赞分享文档教程前端【免费下载链接】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文档/教程前端Modern JavaScript Tutorial用 bind 修复丢失 this 的函数——askPassword 实战解析Modern JavaScript Tutorial用 bind 修复丢失 this 的函数——askPassword 实战解析 askPassword us文档/教程前端JavaScript 函数绑定bind完全指南解决 this 丢失、绑定参数与偏函数实战zh.javascript.info 现代 JavaScript 教程JavaScript 函数绑定bind完全指南解决 this 丢失、绑定参数与偏函数实战zh.javascript.info 现代 JavaScript文档教程前端上一篇Nginx服务器配置恢复指南配置错误后的快速恢复方法下一篇打造专属业务系统WebVella-ERP插件开发实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表