恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
箭头函数VS普通函数:从this绑定到arguments的本质差异
首页
资讯中心
/
箭头函数VS普通函数:从this绑定到arguments的本质差异
箭头函数VS普通函数:从this绑定到arguments的本质差异
发布时间:2026/10/10 15:30:59
从第一次在项目里用上箭头函数到现在我经历了从“这不就一个简写语法糖吗”到“原来它改了JavaScript函数的底层模型”的全过程。如果你的认知还停留在“箭头函数就是省去function和return”那么接下来这些内容极有可能颠覆你对函数的理解。——因为箭头函数Arrow Function和普通函数Function之间的差别实际上比你想的要大得多它不仅仅是写法上的精简还直接影响this的绑定方式、arguments对象的有无、能否被new构造甚至能不能当生成器用。这篇文章不聊教科书定义只讲我实际踩坑后的理解。无论你是刚学ES6的新手还是已经在全职项目里写了几万行代码的老手相信都能从下面这几个维度里重新认识“箭头函数”和“普通函数”到底差在哪。1. 箭头函数的语法拆解从最简写法到容易被忽略的边界情况1.1 第一眼看上去的差异省略了 function 和 return先来个最直观的对比。普通函数定义一个加法function add(a, b) { return a b; }箭头函数可以写成const add (a, b) a b;这里已经把function关键字、花括号和return全部省略了因为箭头右边的表达式就是隐式返回的结果。这就是最典型的“箭头函数简写”。但语法规则要捋清楚不要光记简写形式不然很容易在参数个数和函数体上面翻车没有参数必须写成() 括号不能省比如const rand () Math.random();只有一个参数可以省略参数外面的括号比如const double n n * 2;多个参数必须带括号比如(a, b) a b。函数体是“块语句”比如内部有多行逻辑时需要加花括号并且在块内部用return显式返回const processUser (user) { const name user.name.trim(); const age Number(user.age); return { name, age }; };这几条都不难记但我在不少面试和Code Review里看到有人把“一个参数时省略括号”和“函数体是简单表达式时省略return”这两件事混在一起写出了完全不是你本意的代码。比如const foo x, y x y; // 这是错误的语法正确的多个参数必须补括号const foo (x, y) x y;1.2 两个最容易踩的语法坑返回对象字面量和立即执行函数返回对象字面量必须加一层圆括号如果箭头函数想要返回一个对象看起来应该像是const getConfig () { key: value };但这样写并不会返回一个对象。花括号在箭头函数里被解析成函数体块key: value会被当成一个带标签的语句最终返回undefined。想返回对象必须用圆括号把对象包住const getConfig () ({ key: value });这层括号的作用就是告诉解析器“这里不是函数体而是一个表达式”。这个坑我见过太多次了新手几乎必踩有几个项目里就是因为漏了这层括号接口一直返回undefined排查了一下午。立即执行函数IIFE的写法要调整普通函数通常这么写立即执行(function() { console.log(普通 IIFE); })();箭头函数如果想立即执行最省事的写法是(() { console.log(箭头 IIFE); })();也有人喜欢这样(() console.log(箭头 IIFE))();注意如果前面有上一行代码且没有分号箭头IIFE可能被解析成调用前一行的返回值个人建议在前一行补分号或者把箭头IIFE整体放在括号里避免产生“Cannot read property of undefined”这种诡异报错。1.3 与解构、默认参数、rest参数搭配时的妙用箭头函数也可以使用默认参数和解构这在平时封装接口时特别顺手const getUserName ({ name 匿名 }) name; const getSum (...numbers) numbers.reduce((acc, cur) acc cur, 0); const foo (a 1, b 2) a b;这些用法和普通函数没有本质区别但注意一点箭头函数原生没有自己的arguments对象如果需要不定参数永远优先使用rest参数...args而不是尝试在箭头函数内部依赖arguments这个问题后面会详细说。2. this绑定的本质差异普通函数看调用者箭头函数看定义处2.1 普通函数的this由“怎么调用”决定大部分对箭头函数的误解都来源于对普通函数this机制的不够透彻。普通函数的this不是写代码时固定下来的而是在调用发生时才确定const obj { name: obj, greet: function() { console.log(this.name); } }; obj.greet(); // 这里this是obj输出obj const fn obj.greet; fn(); // 这里this取决于调用环境严格模式下是undefined非严格模式是window/global同样的函数在不同调用方式下this完全不同。这就是为什么以前经常有人在这类场景遇到问题把一个方法赋值给一个变量再去调用this就丢了用setTimeout传入一个对象方法this也丢了。为了修复this丢失我们过去会这样干const obj { name: obj, greet: function() { const self this; setTimeout(function() { console.log(self.name); }, 100); } };或者用bind显式绑定greet: function() { setTimeout(function() { console.log(this.name); }.bind(this), 100); }这两个方案都能生效但代码都显得绕。箭头函数就是用来解决这类场景的。2.2 箭头函数没有自己的this它直接沿用外层作用域的this箭头函数的this定义是“词法的”它不产生自己的this绑定而是捕获定义箭头函数时所在的外层普通函数或模块作用域的this。所谓“外层的this是谁箭头函数的this就是谁”。举一个我常用的演示例子const obj { name: obj, greet: function() { setTimeout(() { console.log(this.name); // 这里的this来自greet函数调用时绑定的this }, 100); } }; obj.greet(); // 输出 obj这里箭头函数定义在greet方法内部而greet方法调用时this是obj因此箭头函数内部的this也等于obj。换成普通函数的话this就是undefined了严格模式而这个差异正是区分两者的关键。再来看一個会误导人的场景——在外层作用域里直接定义箭头函数然后尝试像普通方法一样调用const obj { name: obj, greet: () { console.log(this.name); } }; obj.greet(); // 报错或者输出外层this对应的name这是因为obj这个对象字面量并不产生作用域箭头函数定义在最外层所以它内部的this是全局对象浏览器里是windowNode.js里是global。即便你用obj.greet()去调用它也不会改变this因为箭头函数压根不接收动态绑定。2.3 为什么这个差异能解决回调函数this丢失在没有箭头函数的时代最头疼的就是给数组方法、定时器、Promise、事件监听传递回调时this丢失。箭头函数在这种场景下几乎是一剂良药class Counter { constructor() { this.count 0; } start() { setInterval(() { this.count; // 这里的this永远指向Counter实例 }, 1000); } }在这个例子里如果用普通函数做setInterval的回调this会是undefined必须手动bind。箭头函数由于词法捕获了start方法里的this直接避免了bind。但注意一个前提start本身必须作为普通方法调用所以start内部this才正确指向实例如果start也是箭头函数反而会把this绑定到更外层导致问题。这一点在React类组件时代尤其常见——绑定事件处理器时很多人直接用箭头函数类字段结果依赖了组件实例的this其实没问题但也要注意方法定义的位置。3. 除了thisarguments、构造函数、prototype、yield等鲜为人知的差别3.1 箭头函数没有自己的arguments对象普通函数内部都可以通过arguments访问本次调用传入的所有参数function sum() { let total 0; for (let i 0; i arguments.length; i) { total arguments[i]; } return total; } console.log(sum(1, 2, 3)); // 6而箭头函数内部没有自己的arguments对象。如果你在箭头函数里访问arguments它会沿着作用域链向上找直到找到外层普通函数的arguments。最坑的情况是——你根本不知道这个arguments是谁的function outer() { const inner () { // 这里的arguments不是inner的参数而是outer的参数 console.log(arguments); // 输出outer传入的参数 }; inner(inner自身参数); } outer(outer参数); // 输出 [outer参数]这个行为经常导致诡异bug尤其是当初把普通函数重构为箭头函数时没有意识到内部使用了arguments。重构后要么报错要么拿到的参数不是自己想要的。处理方案是用rest参数替代argumentsconst inner (...args) { console.log(args); // 真正拿到inner自身被传入的参数 };3.2 箭头函数不能作为构造函数也没有prototype属性普通函数可以配合new创建实例因为普通函数自带[[Construct]]内部方法和prototype属性JS引擎允许它执行构造函数流程。但箭头函数不能const Person (name) { this.name name; }; const p new Person(前端); // 这里会抛 TypeError: Person is not a constructor原因在于箭头函数只绑定了[[Call]]没有绑定[[Construct]]所以它无法被new调用。同理因为不需要给实例挂原型链箭头函数也没有prototype属性。这意味着一件事箭头函数本身是用来做“计算”和“传递”的而不是用来创建对象的。如果你在代码中写过const A () {}然后试图通过A.prototype.xxx ...给实例添加方法这一行就会报错Cannot set property xxx of undefined。我一同事就因为在重构时把所有函数统一改成箭头函数结果构造函数被new不出来了修了一下午。3.3 没有super、没有new.target也不能使用yield这几个冷门差异在类继承和生成器场景中会带来明确限制super箭头函数内的super指的是外层函数或类的super而不是自身。所以你不能在箭头函数里通过super访问“自己的”父类成员。new.target普通函数内部可以通过new.target判断自己被new调用还是普通调用箭头函数没有自己的new.target它继承外层的new.target。yield箭头函数体内不能直接使用yield因此箭头函数不能作为Generator函数。想用生成器还是得老老实实写普通函数。这三个差异在日常业务中遇到的不多但如果写底层库或者编译工具容易在语法层面直接报错。比如写Koa中间件时如果误用了箭头函数包裹yield nextNode直接抛SyntaxError。3.4 call / apply / bind无法改变箭头函数的this普通函数可以通过call、apply、bind来改变this指向箭头函数却不行const obj { name: obj }; const fn () console.log(this); fn.call(obj); // 不会打印objthis仍然是定义时的外层this fn.bind(obj)(); // 同样无效这并不是说调用会报错而是调用时传入的this参数被忽略。箭头函数对外层this已经“铁了心”用什么动态绑定手段都拐不回来。所以如果你发现某个箭头函数里的this不对唯一的解决思路是看这个函数定义在哪个作用域里而不是想着靠bind去修正。4. 实战选型什么时候放心用箭头函数什么时候必须回头用普通函数4.1 先给出我的总体判断标准我在项目里一般基于四个问题来判断一个函数能不能写成箭头函数这个函数内部需要动态的this吗这个函数需要访问arguments对象吗这个函数需要被new调用吗这个函数需要作为生成器函数使用吗**只要上面任何一个答案是“是”就请用普通函数别图省事。**如果四个问题的答案都是“否”并且函数体足够短箭头函数通常能带来更干净的代码。我见过不少团队直接把ESLint里prefer-arrow-callback和func-style打开要求所有回调都改成箭头函数。但这并不代表所有函数都该改成箭头函数。下面几种场景我会明确坚持用普通函数。4.2 这些场景请坚持用普通函数对象方法需要动态this上面已经讲过对象方法用箭头函数this会取外层作用域根本拿不到当前调用对象。const counter { num: 0, // 错误 increase: () { this.num; }, // 正确 increase() { this.num; } };这里最迷惑人的一点是如果外层作用域恰好有一个this.num错误版本反而能“碰巧”工作导致你不知道问题出在哪。所以对象方法的定义脑子里要拉一根警戒线默认用method() {}简写除非你有明确的词法this需求。需要动态控制this的事件处理函数在DOM事件里普通函数作为监听器时this指向当前元素button.addEventListener(click, function() { console.log(this); // 输出button元素 });如果换成箭头函数button.addEventListener(click, () { console.log(this); // 输出外层this而不是button元素 });如果你想获取当前被点击的元素采用箭头函数时就要额外使用event.currentTarget比较绕。所以事件监听器里我一般保留普通函数。需要arguments对象的包装函数例如需要做一个参数转发函数function forward() { return anotherFn.apply(null, arguments); }如果改成箭头函数没有自己的arguments就不得不改成rest参数。虽然rest参数也很优雅但如果团队里其他人还不太熟悉rest参数这种“暗换行为”容易在合并分支时引发误解。4.3 箭头函数真正发光发热的场景数组函数式编程在回调中保持this例如在类里处理数组同时还想引用实例状态class OrderStore { constructor() { this.items []; } totalPrice() { return this.items .filter(item item.checked) .map(item item.price) .reduce((sum, price) sum price, 0); } }这些回调里用箭头函数简洁且没有this歧义。如果再用普通函数并复习bind反而很累赘。延迟执行时固定this比如定时器、请求回调、Promise链fetchData() .then(data this.process(data)) .catch(error this.logError(error));用箭头函数后this在定义时就绑定到实例不必再为每个回调写bind。纯函数计算与柯里化箭头函数配高阶函数挺顺滑const multiply (a) (b) a * b; const double multiply(2);这种链式柯里化如果用普通函数写光看括号层级就够头疼。4.4 一个让我印象深刻的真实踩坑复盘之前在维护一个老项目时需要给一个模块加计数功能。原本模块是一个对象字面量const tracker { count: 0, reset() { this.count 0; }, tick() { setInterval(function() { this.count; // 这里一直是undefinedcount加不上 }, 1000); } };我当时的改法是把setInterval里的回调改成了箭头函数然后自信满满地运行计数就正常了。因为tick()以tracker.tick()形式调用时箭头函数捕获了tick的this而这个this就是tracker对象本身所以this.count生效。但一个同事后来接手这个模块想“统一优化”一下把tick() {}也改成了箭头函数结果计数又不涨了。他跑来问我我一看代码就明白了tracker.tick从普通方法变成了箭头函数后tick内部的this也不再指向tracker而是最外层作用域因此setInterval里的箭头函数捕获到的this也变成了全局对象。一个看似“优化”的改动把整条链路的this全带偏了而且没有编译期报错。这给我留下一个非常重要的经验箭头函数的使用具有“传染性”。一旦某个关注this语义的方法改为箭头函数会影响它内部所有箭头函数的this绑定。所以不要孤立地看一个函数是否适合用箭头函数要看整条调用链上this是怎么流动的。4.5 最终判断口诀和写给团队的小建议我个人现在推荐团队用这样一条简单规则广泛使用箭头函数作为回调和高阶函数参数。对象方法、原型方法、类实例方法默认使用普通函数简写。构造函数、生成器函数、依赖arguments的函数必须使用普通函数。同时可以配置ESLint规则辅助{ rules: { prefer-arrow-callback: warn, func-style: [error, declaration, { allowArrowFunctions: true }] } }这样既能享受箭头函数的简洁又不会让this语义失控。我自己的习惯是在没有状态的纯计算逻辑里几乎一律用箭头函数但只要牵扯到this、arguments、new、yield就毫不犹豫切回普通函数。这不仅仅是语法偏好更是工程经验沉淀下来的安全红线。最后说一个小技巧如果你在调试一个箭头函数里的this最快捷的办法不是加console.log看this而是去看它定义位置的外层普通函数是用什么方式调用的。因为箭头函数永远复制外层的this向外找一层问题往往就清晰了。这种“顺着作用域链找this”的排查方法比瞎试call和bind管用得多。