☰
在 JavaScript 教程仓库中为所有函数扩展 Function.prototype.defer:从延迟执行到装饰器封装
2026/10/6 2:07:58 网站建设 项目流程
  • 文档/教程
  • 前端

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

Modern JavaScript Tutorial

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

导读

本文基于《Modern JavaScript Tutorial》(en.javascript.info)仓库中「原生原型(Native prototypes)」一节的配套任务,讲解如何通过修改Function.prototype,为 JavaScript 中所有函数统一添加defer(ms)延迟执行能力。任务分为两档难度:第一档用setTimeout(this, ms)实现最简单的"延迟后调用原函数";第二档要求返回一个装饰器(wrapper),把延迟与参数透传、this绑定融为一体。读完本文,你将掌握原生原型修改的合法姿势、this在原型方法中的指向规律,以及用f.apply(this, args)保持对象方法上下文的关键技巧,并理解为什么"修改原生原型"在业界通常被视为危险操作、而 polyfill 是其唯一公认的合理场景。

任务背景:方法存储在原型里,函数也不例外

在开始动手写defer之前,需要先明确一个底层事实:JavaScript 所有内置构造函数都把自己的方法存放在prototype属性上,对象实例本身只保存数据。

正如仓库章节 native-prototypes 所阐述的:

  • Object.prototype处于所有原型链的最顶端,它提供了toString等方法;Object.prototype.__proto__为null;
  • 数组实例继承Array.prototype,再向上继承Object.prototype,例如arr.__proto__ === Array.prototype与arr.__proto__.__proto__ === Object.prototype均为true;
  • 函数是内置Function构造函数的对象,它们的call/apply等方法都取自Function.prototype。

可以用仓库中的验证代码印证函数的原型链:

function f() {} alert(f.__proto__ == Function.prototype); // true alert(f.__proto__.__proto__ == Object.prototype); // true, 函数最终也从对象继承

下图展示了几个内置原型在链上的整体关系(图来自 native-prototypes-classes.svg):

正因如此,"给所有函数加方法"这件事有一个天然且统一的挂载点:Function.prototype。只要往它上面添加一个属性,全部函数都能立刻使用,无需逐个函数打补丁——这正是本任务的核心思路。

任务一:为所有函数添加f.defer(ms)

仓库中的任务文档 task.md(importance 5,属核心练习)给出的需求非常明确:

在所有函数的原型上添加defer(ms)方法,使其在ms毫秒之后执行该函数。

期望的使用效果:

function f() { alert("Hello!"); } f.defer(1000); // 1 秒后弹出 "Hello!"

标准解法:一行代码挂到 Function.prototype

仓库配套的 solution.md 给出的解法简洁得近乎优雅:

Function.prototype.defer = function(ms) { setTimeout(this, ms); }; function f() { alert("Hello!"); } f.defer(1000); // shows "Hello!" after 1 sec

剖析这行代码背后的this

这段代码看起来只有三行,却同时踩中了两个 JavaScript 核心机制:

  1. 原型方法中的this指向调用者。f.defer(1000)在调用时,defer内部的this就是函数f本身——方法调用语法obj.method()会把obj作为this传入,这里的obj是函数对象f。因此setTimeout(this, ms)等价于setTimeout(f, ms)。

  2. setTimeout的第一个参数可以直接接收函数引用。当ms毫秒计时结束后,引擎会以普通函数调用的方式执行f,弹窗如期出现。这里没有任何箭头函数或参数透传的负担,因为原始函数f本身不接受参数。

从原型链角度看,Function.prototype.defer的查找过程是:f自身没有defer属性 → 沿[[Prototype]]找到Function.prototype→ 命中新增的defer。这与仓库章节 prototype-inheritance 中"读取属性时,若对象自身缺失则自动从原型上取"的规则完全一致。

边界提醒:该方法不接受参数

第一版defer有一个明显短板:如果原函数需要参数,setTimeout(this, ms)无法把它们传递过去。例如function f(a, b) { alert(a + b) }配合f.defer(1000)(1, 2)就会失败。这正是仓库中紧接着布置的"进阶版"任务要解决的问题。

任务二:把defer()升级为装饰器,支持参数透传

仓库紧接着的任务 2-defer-to-prototype-extended/task.md(importance 4)把需求升级为:

为所有函数的原型添加defer(ms),它返回一个包装函数(wrapper),把真实调用推迟ms毫秒。

期望效果是链式调用:

function f(a, b) { alert( a + b ); } f.defer(1000)(1, 2); // 1 秒后显示 3

注意:参数必须原封不动地传给原始函数,而且调用时还不能丢失this。

进阶解法:rest 参数 + f.apply 保持 this

仓库配套的 solution.md 给出了完整实现:

Function.prototype.defer = function(ms) { let f = this; return function(...args) { setTimeout(() => f.apply(this, args), ms); } }; // check it function f(a, b) { alert( a + b ); } f.defer(1000)(1, 2); // shows 3 after 1 sec

逐层拆解这条调用链,可以看到它完成了三件事:

  1. let f = this先把原始函数保存下来。defer返回的是一个新的闭包函数,如果不先把this暂存,等到闭包真正执行时,this早已不是原始函数了。保存f是装饰器模式的经典手法。

  2. function(...args)用 rest 参数收集调用时传入的所有实参。f.defer(1000)(1, 2)这一步中,(1, 2)会被收集进args数组,从而解决了第一版无法传参的问题。

  3. setTimeout(() => f.apply(this, args), ms)双保险传参。外层箭头函数没有自己的this,它会从包裹它的普通函数继承this;f.apply(this, args)再把收集到的参数数组展开传给原始函数f,同时把当前的this原样传递。

关键点:为什么要用f.apply(this, args)?

仓库的官方解答特别强调了一句话:

我们在f.apply中使用this,是为了让装饰器对对象方法也能正常工作。

看这个官方给出的对象方法场景验证:

Function.prototype.defer = function(ms) { let f = this; return function(...args) { setTimeout(() => f.apply(this, args), ms); } }; let user = { name: "John", sayHi() { alert(this.name); } } user.sayHi = user.sayHi.defer(1000); user.sayHi(); // 1 秒后弹出 "John"

这里的执行顺序值得细品:

  • user.sayHi.defer(1000):defer内部的this是user.sayHi函数本身,被暂存为f;
  • 返回值(包装函数)被重新赋回user.sayHi;
  • 调用user.sayHi()时,包装函数作为对象方法被调用,因此它的this是user;
  • 包装函数内部setTimeout(() => f.apply(this, args), ms)中,箭头函数捕获了包装函数的this(即user),于是f.apply(user, args)让原始sayHi在调用时this.name === "John"。

如果这里偷懒写成f.apply(null, args)或直接f(args),sayHi里的this就会丢失(非严格模式下退化为全局对象),输出将不再是"John"。正是apply(this, args)这一行,让装饰器在"普通函数"与"对象方法"两种形态下都能正确工作,这也是它与第一版解法最大的进阶点。

原理纵深:为什么可以把方法挂到 Function.prototype?

从仓库章节 F.prototype 可以知道,每个函数天生都带一个prototype属性,默认值为{ constructor: F }。但我们在这里并不是给某个构造函数设置原型,而是直接往内置的Function.prototype对象上添加新属性:

  • 所有函数(包括构造函数、普通函数、箭头函数之外的具名函数等)的[[Prototype]]都指向Function.prototype;
  • 因此一次赋值Function.prototype.defer = ...,全库所有函数立即获得defer方法,这正是"原型共享方法、实例只存数据"这一内存高效设计的直接红利;
  • 从调用方视角看,f.defer(1000)的查找路径是:f自身 →Function.prototype,命中新增方法。

这一机制与仓库章节 native-prototypes 中String.prototype.show的例子同构——给String.prototype加方法后"BOOM!".show()立即可用,给Function.prototype加方法后所有函数立即可用。

重要提醒:修改原生原型是"双刃剑"

任务在练习层面展示了修改原生原型的强大,但仓库正文 native-prototypes 紧接着给出了严肃的工程警告,切莫把练习当成生产环境的默认做法:

原型是全局的,很容易产生冲突。如果两个库都添加了String.prototype.show,其中一个就会覆盖另一个的方法。因此,修改原生原型通常被认为是坏主意。

同时,正文也明确指出现代编程中唯一被认可的修改原生原型场景是 polyfill(垫片)——即规范中已定义、但当前引擎尚未实现的方法,可以手动实现并挂到内置原型上,让旧环境也能运行新语法。仓库给出的经典例子:

if (!String.prototype.repeat) { // 如果引擎还没有这个方法 String.prototype.repeat = function(n) { // 按规范算法重复字符串 n 次 // (完整算法见规范,这里的不完美实现通常也够用) return new Array(n + 1).join(this); }; } alert( "La".repeat(3) ); // LaLaLa

把这一原则映射到本文的defer任务上:setTimeout是早已普及的 API,不存在 polyfill 的必要;为Function.prototype添加自定义的defer属于"扩展非标准方法",会污染全局命名空间,若与未来标准方法或第三方库同名即可能互相覆盖。在练习和教学演示中体验原型机制没有问题,但在真实项目中,更稳妥的做法是使用独立的工具函数或装饰器库,避免触碰原生原型。

与仓库其余部分的呼应

本文的两个任务处于 03-native-prototypes 目录下,与同章正文 article.md 的 "Changing native prototypes" 一节直接对应;进阶版"返回包装函数延迟调用"的思想,又与仓库中专门讲解函数包装的 09-call-apply-decorators 一章(spy、delay、debounce、throttle 等装饰器)一脉相承——Function.prototype.defer的进阶解法本身就是最精简的"延迟装饰器"原型。建议读者按顺序完成 任务一 与其 解答、任务二 与其 解答,再回到正文巩固原生原型、polyfill 与原型修改的完整知识闭环。

小结

  • 第一版解法:Function.prototype.defer = function(ms) { setTimeout(this, ms); },利用原型方法中this指向调用函数这一规则,一行代码实现"延迟执行",但不支持传参。
  • 进阶版解法:返回包装函数,用let f = this暂存原函数、...args收集实参、setTimeout(() => f.apply(this, args), ms)同时完成延迟、参数透传与this保持,可安全用于对象方法。
  • 底层原理:函数的方法统一存放在Function.prototype,改一处、全局生效;属性查找沿原型链自动完成。
  • 工程红线:修改原生原型易引发全局冲突,除 polyfill 外不被推荐;练习场景中理解机制,生产环境中优先选用不污染全局的方案。
  • 文档/教程
  • 前端

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

Modern JavaScript Tutorial

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

相关推荐

上一篇:快速上手VRM4U:终极解决PMX动画重定向问题的完整指南
下一篇:APKMirror安卓应用下载工具完整使用教程

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询