箭头函数与this绑定全解析:原理、场景与面试考点

箭头函数与this绑定全解析:原理、场景与面试考点 写在前面面试官为什么总爱问箭头函数如果你准备过前端面试大概率遇到过这类问题“箭头函数和普通函数有什么区别”“箭头函数里的this指向哪里”说实话箭头函数Arrow Function在ES6新特性里属于看起来简单、问起来扎心的一类。语法上三分钟能学会但真要把它讲清楚能讲明白的人不多。我从面试官的角度拆解过这个问题。大部分候选人能说出来“箭头函数没有自己的this”“不能做构造函数”这两条但再往下问“为什么setTimeout里用箭头函数能拿到外层this”“什么时候不该用箭头函数”很多人就卡住了。这说明大家只是背了关键词没有真正理解箭头函数在JavaScript运行机制里的位置。这篇文章就把箭头函数的来龙去脉一次讲透。从语法写法到this绑定规则从能用在哪到不该用在哪从面试题的常见坑到实际项目里的取舍全部过一遍。不管你是刚学ES6的新手还是准备跳槽想复习一轮的老手这篇文章都值得你花十五分钟读完。读完以后再有面试官拿箭头函数考你你可以反手给他讲一遍原理。1. 箭头函数到底解决了什么问题1.1 从一段“丑陋”的回调代码说起在ES6出现之前JavaScript里写回调函数是一件很痛苦的事情。先不说嵌套层级的问题光是一个this指向就能让人头皮发麻。举一个非常典型的场景在一个对象方法里使用setTimeout想延迟执行后访问这个对象的属性。var person { name: 小明, delayShow: function () { setTimeout(function () { console.log(你好我是 this.name); }, 1000); } }; person.delayShow(); // 输出你好我是undefined这段代码的输出是undefined原因很简单setTimeout回调里的this不再指向person对象而是在调用时由运行时环境决定的浏览器里指向window严格模式下是undefined。当时的主流解决方案是缓存this也就是传说中的var self thisvar person { name: 小明, delayShow: function () { var self this; setTimeout(function () { console.log(你好我是 self.name); }, 1000); } };还有一种方案是用.bind(this)把回调里的this绑回去。这两种方案都能解决问题但都显得笨重。每写一个异步回调都要先声明一个变量去“保存”外层this代码里充满了self、_this、that这种临时变量。箭头函数从语言层面解决了这个问题它自身没有this访问this时直接从定义位置的外层作用域拿。用箭头函数重写上面的代码整个逻辑就像呼吸一样自然var person { name: 小明, delayShow: function () { setTimeout(() { console.log(你好我是 this.name); }, 1000); } }; person.delayShow(); // 输出你好我是小明这个例子基本概括了箭头函数最大的价值它让你不再为了this的传递问题费尽心思。回调函数里想用外层的this写箭头函数就完了。1.2 语法层面的一次净化除了this问题的改善箭头函数在语法层面也做了大量简化。很多人第一次看到箭头函数的时候觉得这个写法很新奇其实它就是“函数表达式”的一种更简洁的替代。普通函数定义一个求和函数最完整的写法是这样的function add(a, b) { return a b; }用函数表达式写是var add function (a, b) { return a b; };改成箭头函数以后中间的过程变量都可以省略var add (a, b) a b;一行搞定。当函数体只有一个表达式时箭头函数可以隐式返回这个表达式的值不需要写return关键词。当参数只有一个时参数外层的括号也可以省略var double x x * 2;这种精简体现在代码里最直观的感受就是函数式编程里的链式调用变得清爽了很多。举个例子把一组数字里大于10的挑出来再乘以2最后求和const nums [5, 12, 18, 3, 9, 22]; const result nums .filter(n n 10) .map(n n * 2) .reduce((sum, n) sum n, 0); console.log(result); // 104如果用普通函数写每个回调都要写上function关键字和return代码量翻倍不说阅读时视线的跳跃感也非常明显。箭头函数让每一个回调都变得足够短短到你可以一眼看完整条数据流的处理逻辑。很多面试题会问“箭头函数相比普通函数的优势”。语法简洁只是表象this机制上的改进才是根本优势。回答时可以按“语法 this 适用场景”三层递进比干巴巴背三条区别要有深度得多。2. 必须吃透的核心规则this的绑定逻辑2.1 this绑定定义时确定而非调用时确定箭头函数与普通函数在this上最本质的差异可以浓缩成一句话普通函数的this在调用时确定箭头函数的this在定义时确定。这句话怎么理解普通函数里的this取决于你怎么调用它。作为对象方法调用this指向对象作为普通函数调用this指向window或undefined用call/apply/bind调用this指向你绑定的目标。同一个函数在不同调用方式下this指向可以完全不同。箭头函数不一样。它在定义的时候就“继承”了当前作用域的this之后无论你怎么调用它、在哪里调用它this都不会再变。你可以把箭头函数理解为它的this是创建时从周围环境“捕获”过来的快照。从JavaScript引擎的角度来说箭头函数的this其实就是外层词法作用域的this所以它在规范里被称为“Lexical this”。这也是为什么箭头函数不能用call/apply/bind去强行改变this的原因——这三个方法确实会被调用但改变不了箭头函数的this指向参数倒是可以正常传进去。看一下这个例子对比会非常直观const obj { name: obj, normalFn: function () { console.log(this.name); }, arrowFn: () { console.log(this.name); } }; obj.normalFn(); // obj因为normalFn作为obj的方法被调用 obj.arrowFn(); // undefined因为arrowFn定义时外层作用域是全局this指向window一个常见的误解是“箭头函数里面this一定指向window”。准确的说法是箭头函数的this取决于定义它的外层作用域的this。如果外层作用域是一个对象方法比如上面提到的delayShow那这个对象方法的this指向person里面的箭头函数自然也能拿到person。我把这个规则总结成一句话箭头函数不决定this它只是this的“搬运工”。所以面试时问“箭头函数的this指向哪里”标准答案不是“指向window”而是“指向定义时外层执行上下文的this”。2.2 不可变this带来的连锁反应因为箭头函数没有自己的this所以它天然不能作为构造函数使用。new的过程会发生什么JavaScript引擎会创建一个新对象然后把构造函数里的this绑定到这个新对象上最后返回这个新对象。但箭头函数没有this可以绑定所以直接对它new引擎会直接抛错const Foo () { this.name x; }; const f new Foo(); // TypeError: Foo is not a constructor同理箭头函数也不能用prototype。普通函数天生带有prototype属性作为构造函数时新对象的原型链会指向函数的prototype。箭头函数根本没有prototype属性const fn () {}; console.log(fn.prototype); // undefined这两个特点在面试中经常被放在一起问“箭头函数能不能作为构造函数为什么不能”答案是“不能因为箭头函数没有自己的this无法绑定实例”。除了不能用new箭头函数还有一个被很多人忽视的特性函数内没有arguments对象。普通函数内部有arguments里面按顺序装着所有调用参数。但箭头函数里访问arguments拿到的是外层作用域的arguments不是自己的参数列表。const arrowFn () { console.log(arguments); // 指向外层作用域的arguments }; function normalFn() { const innerArrow () { console.log(arguments); // 指向normalFn的arguments }; innerArrow(1, 2, 3); } normalFn(a, b); // 输出 [a, b]而不是 [1, 2, 3]如果确实需要在箭头函数里拿到参数列表ES6提供了rest参数剩余参数语法const arrowFn (...args) { console.log(args); }; arrowFn(1, 2, 3); // [1, 2, 3]这块内容也值得在面试时主动补充因为很多候选人只知道“箭头函数没有arguments”但说不清楚“访问arguments会发生什么”。说清楚这一层回答的含金量立刻就上来了。2.3 三个容易记混的边界条件有些边界条件如果没搞清楚面试现场很容易翻车。第一个是关于call/apply/bind能不能改变箭头函数的this。能调用但改变不了this指向const fn () { console.log(this); }; const target { name: target }; fn.call(target); // 输出全局对象而不是target fn.apply(target); // 同样的规则第二个边界条件是箭头函数定义时的this也有层级关系。如果外层作用域的this本身是改变的那箭头函数里的this也会跟着变。看这个例子var name window; const obj { name: obj, fn: function () { return () { console.log(this.name); }; } }; const innerFn obj.fn(); // 此刻this指向obj箭头函数捕获了obj innerFn(); // obj const innerFn2 obj.fn; const fn2 innerFn2(); // 此刻this指向window箭头函数捕获了window fn2(); // window非严格模式下第三个边界条件是箭头函数的优先级问题。面试里有一种经典陷阱题给对象某个方法赋值为普通函数方法内部再返回箭头函数此时箭头函数的this到底是谁答案就是上面例子里的逻辑看返回箭头函数的那一刻外层函数的this是谁。理解到这一层你就能应对绝大多数this相关面试题了。3. 花样繁多的箭头函数写法这个部分就是热搜词“箭头函数写法”需要重点展开的内容。箭头函数之所以让一些初学者犯迷糊就是因为它的语法有太多“可变省略”的规则。我把所有写法变体整理成一个体系你照着这个体系捋一遍就不会乱了。3.1 从完整写法到最短写法先看一个箭头函数的完整结构包含四个部分(参数列表) { 函数体 }。里面的每一个部分都有对应的省略规则。完整写法const fn (a, b) { const sum a b; return sum; };省略规则第一条函数体只有一个表达式时可以去掉花括号和return结果自动作为返回值const fn (a, b) a b;省略规则第二条参数只有一个时可以去掉参数括号const double x x * 2;省略规则第三条零个参数时参数位不能空着写一对空括号即可但也可以用一个下划线占位。语感上因人而异我个人的习惯是直接用空括号因为更直观const fn1 () Math.random(); const fn2 _ Math.random(); // 这种写法合法但可读性一般综合起来一个函数的写法可以一路从最啰嗦变成最短// 原始版 function multiply(a, b) { return a * b; } // 转换版一完整箭头函数 const multiply (a, b) { return a * b; }; // 转换版二隐式返回 const multiply (a, b) a * b;看到这里你应该明白了箭头函数本质上是对“函数表达式”的一种语法压缩。压缩的目的是让短小的回调函数在阅读时更聚焦而不是让你把所有函数都写成一行。3.2 最容易踩坑的返回对象技巧有一个写法细节在面试和实际开发中都特别容易出问题用箭头函数直接返回一个对象字面量。因为对象字面量和函数体的花括号冲突了直接这么写会报错const getObj () { name: x }; // 语法错误或得不到预期结果JavaScript引擎看到{会认为这是函数体的开始然后解析name:发现这是一条标签语句后面的x是表达式最终函数没有返回值得到undefined。注意这行代码在部分浏览器里不报错但返回的是undefined因为name: x被解析成了带标签的语句不是对象字面量。实际开发中这种写法极其容易埋雷。解决办法是用一对圆括号把对象包起来告诉引擎这里是一个表达式const getObj () ({ name: x });加上括号以后引擎会知道花括号里是对象字面量而不是函数体。这一点必须刻在脑子里很多人在实际代码里踩过一次但没有理解背后的原因下次照样错。3.3 高阶用法解构参数与默认参数箭头函数的参数位同样支持解构和默认值这些特性组合在一起时写法会变得非常简洁且强大。最经典的应用是处理数组里的对象数据const users [ { name: 张三, age: 25 }, { name: 李四, age: 30 }, { name: 王五, age: 28 } ]; const names users.map(({ name }) name); console.log(names); // [张三, 李四, 王五]参数位置直接解构对象的name属性不用在函数体里多写一行const name item.name。这种感觉就像是专门为处理集合数据而生的。默认参数同样支持const greet (name 陌生人) 你好${name}; console.log(greet()); // 你好陌生人 console.log(greet(小明)); // 你好小明箭头函数和模板字符串配合起来代码可以从几行压缩到一行。这种写法在数据处理场景下非常常见面试时如果被问到“箭头函数在实际项目里怎么用”举一个map配合解构参数的例子比背十条理论都有说服力。4. 哪些场景该用哪些场景必须避雷4.1 适合箭头函数的场景箭头函数最适合的场景是那些不需要自己的this、只是作为“纯函数”被传递的地方。我把日常开发中最典型的四个场景列出来。第一个场景是数组遍历和变换。map、filter、reduce、forEach这些方法接受的回调函数大部分情况下只依赖参数和返回值不需要this用箭头函数写最简单const prices [100, 200, 300, 400]; const discounted prices.map(price price * 0.8);第二个场景是定时器和异步操作。比如setTimeout、setInterval、Promise链式调用里需要访问外层的this时用箭头函数可以省去bind或者self缓存class Countdown { constructor(start) { this.count start; } start() { setInterval(() { this.count--; console.log(this.count); }, 1000); } }第三个场景是事件监听回调。在类或组件里注册事件处理函数需要访问实例成员时箭头函数能保证this指向正确class Button { constructor(dom) { this.dom dom; this.dom.addEventListener(click, () { this.handleClick(); }); } handleClick() { console.log(clicked); } }第四个场景是函数式编程风格的工具函数。很多工具库的设计思路就是传递无状态函数箭头函数天然契合const compose (f, g) x f(g(x)); const addOne x x 1; const double x x * 2; const addOneAndDouble compose(double, addOne); console.log(addOneAndDouble(3)); // 8先1变成4再*2变成84.2 不能用箭头函数的场景箭头函数虽然好用但绝不意味着所有地方都该用它。有三个场景箭头函数会带来非常隐蔽的问题实际开发里我踩过不止一次。第一个场景是对象方法。用箭头函数给对象定义方法时this不会指向对象本身。这个问题在面试中经常被拿来当陷阱题const counter { count: 0, increment: () { this.count; // this指向全局对象count undefined } };为什么会有这个问题因为箭头函数定义时外层作用域的this是全局对象所以对象里的箭头函数拿到的也是全局this根本拿不到counter。想给这个对象定义方法必须用普通函数。第二个场景是原型链上的方法。给构造函数或类的原型定义方法如果用箭头函数方法内部的this也拿不到实例function Person(name) { this.name name; } Person.prototype.sayName () { console.log(this.name); // 错误this不指向实例 };这个场景在React类组件的旧代码里经常能看到使用箭头函数定义类方法在实例上是可以的后面会详细说但定义在prototype上就会出问题两者要区分清楚。第三个场景是需要动态this的地方。比如用arguments、用call/apply动态控制this或者用户事件回调里需要this指向触发元素这些场景下箭头函数都会“帮倒忙”button idbtn点击/button script const btn document.getElementById(btn); btn.addEventListener(click, function () { console.log(this); // 指向button元素 }); btn.addEventListener(click, () { console.log(this); // 指向window按钮的this丢失了 }); /script判断一个场景该不该用箭头函数就一个标准如果这个函数需要动态this用普通函数如果不需要自己的this或需要继承外层this用箭头函数。这个标准能覆盖九成以上的场景。4.3 类与React组件里的特殊用法在ES6的Class里给类定义方法时默认用的是普通函数方法里的this在调用时确定。如果方法被当作回调传递出去this就会丢失class Counter { constructor() { this.count 0; // 这种写法保证了this被绑定 this.increment () { this.count; }; } add() { this.count; } } const c new Counter(); const fn c.increment; fn(); // 没问题箭头函数定义了就绑定了thisReact早期的类组件写法大量使用这种模式在构造函数或类属性位置用箭头函数定义方法保证事件回调里的this指向实例。这个设计思路和ES6 class里方法绑定是同一个问题的不同解法。这里注意一个容易混淆的点类里用箭头函数定义方法本质上是给实例添加了一个属性这个属性是一个箭头函数而箭头函数定义时所在的作用域是构造函数内部所以this会绑定到实例。和直接定义在prototype上的方法角色不同。前者每个实例都会有一份函数副本后者是所有实例共享一份。所以用箭头函数给实例加方法确实可以解决this绑定问题代价是内存占用略高。如果类实例数量不多一般无感如果实例数量巨大就要权衡一下。5. 实战拆解5道高频面试题的正确打开方式5.1 经典这道题循环里的事件绑定面试中有一道经久不衰的题目给一组按钮绑定点击事件点击第几个按钮就弹出它的索引。用var写会出问题for (var i 0; i 5; i) { document.getElementById(btn i).addEventListener(click, function () { console.log(i); // 永远输出5 }); }原因在于var声明的i是函数级作用域循环结束后i已经变成5所有回调访问的都是同一个i。用箭头函数和let组合可以优雅解决for (let i 0; i 5; i) { document.getElementById(btn i).addEventListener(click, () { console.log(i); // 正确输出0, 1, 2, 3, 4 }); }这里有两个机制在起作用let让每次循环创建了独立的块级作用域变量箭头函数隐式捕获了这个变量。有些面试官会追问“能不能用var加箭头函数解决”答案是var不产生块级作用域单靠箭头函数也不行。所以这道题考察的其实是作用域链和闭包箭头函数只是顺带用到的工具。5.2 setTimeout里this指向的连环问这道题的变体特别多核心就一个打印出来的this是谁var name window; const obj { name: obj, fn1: function () { setTimeout(function () { console.log(this.name); }, 100); }, fn2: function () { setTimeout(() { console.log(this.name); }, 100); }, fn3: () { setTimeout(() { console.log(this.name); }, 100); } }; obj.fn1(); // window obj.fn2(); // obj obj.fn3(); // window逐个拆解fn1里的普通函数在setTimeout里被调用this指向windowfn2里的箭头函数在定义时继承了fn2的this而fn2是作为obj的方法调用的this指向objfn3本身是箭头函数定义时外层this是全局对象所以即使里面再套箭头函数拿到的仍是window。答这道题的时候把每一步的this推导过程说清楚面试官会认为你是真的懂而不是背过答案。5.3 第二个陷阱对象方法装饰器有一道题稍微有点难度涉及对象方法被取出再调用的场景var name window; const obj { name: obj, method: function () { return () { console.log(this.name); }; } }; const fn obj.method(); fn(); // obj const fn2 obj.method; fn2()(); // window第一行输出obj很好理解obj.method()执行时this指向obj返回的箭头函数捕获了这个this。第二行把obj.method从对象里取出来再调用此时方法内部的this不再指向obj而是指向window所以箭头函数捕获的也是window。这个例子说明箭头函数的this取决于定义它的那一刻外层函数的this是什么。外层函数的this由调用方式决定而不是由箭头函数决定。这个观念如果建立起来了箭头函数的this问题基本就是一通百通。5.4 面试反问箭头函数可以被bind吗这道题考察的是对bind底层逻辑的理解。很多人背了结论“不能改变箭头函数的this”但说不清为什么。其实更准确的说法是bind在改变this之前会先判断函数是不是箭头函数如果是则忽略this绑定只传递参数。const arrowFn (a, b) { console.log(this, a, b); }; const obj { name: obj }; const boundFn arrowFn.bind(obj, 1, 2); boundFn(); // 输出全局对象12所以理论上你可以用bind给箭头函数传参数但this不会改变。追问一句“为什么设计上要这样限制”答案是箭头函数的卖点就是this词法绑定如果允许bind改变this就和设计初衷矛盾了也会让开发者难以预测代码行为。5.5 综合题什么时候不能用箭头函数最后这道题偏开放没有标准答案需要你结合场景谈理解。一个完整的回答应该包含以下几点对象方法不能用因为this指向不对。构造函数不能用因为没有自己的this不能被new。想要动态this的场景不能用比如arguments对象使用时。事件监听器里需要访问当前元素时用箭头函数会丢掉this指向。需要函数提升的场景不能用因为箭头函数是表达式不参与提升。同时你也应该补一句箭头函数适合用在不需要this的地方比如数据处理函数、工具函数、回调函数。这样组织结构面试官会认为你既了解限制也清楚边界。“谈缺点不是坏事能清楚地指出工具的边界说明你真的用过它。面试答这类开放题重点不是展示你记了多少条而是展示你的判断标准是什么。”6. 常见误区与避坑经验6.1 五个常见理解误区自查表整理了几个最容易出错的点你可以对照自查一下。误区真实情况箭头函数没有this准确说是不绑定自己的this会继承外层this箭头函数的this指向window不绝对取决于定义时外层作用域的this箭头函数不能用new对准确原因是它没有内部this可以绑定新实例箭头函数不能用call/apply/bind能用传参正常但this改不了箭头函数不能获取参数可以通过rest参数拿到只是没有自己的arguments牢记这张表笔试和面试里的选择题基本不会翻车。6.2 我在项目中踩过的真实坑点第一坑隐式返回导致对象被误判。有一次项目中要写一个返回配置对象的函数新同事随手写成了() { key: value }结果所有地方拿到的都是undefined。排查了半天才发现是花括号被当成函数体的问题。后来团队规定了两条返回对象必须加圆括号函数体超过一行就不要用隐式返回。第二坑类属性箭头函数导致实例方法不可被bind。有一次在React类组件里用箭头函数定义了一个事件处理方法结果测试里simulate event的时候发现方法里读不到实例属性。原因是对应的组件实例被框架代理了方法里的this被React绑定到了组件实例上看起来正常但一旦方法被当成普通函数传出你无法再用bind修正。所以涉及测试代码或外部调用类方法时要谨慎使用类属性箭头函数。第三坑在接口API边界用了箭头函数导致作用域判断复杂。比如Vue项目里在methods里用箭头函数定义方法结果方法里访问this.xxx拿到了undefined。Vue初始化时会把methods里的函数直接挂到实例上调用时this应该指向实例但箭头函数不会随调用方式改变this于是所有数据访问全部失效。后来排查发现问题是Vue官方的methods设计逻辑里方法内部确实需要动态this不能一刀切用箭头函数。正确做法的原则就是凡是框架层面会帮你把函数绑定到实例上的位置例如Vue的methods请老老实实用普通函数。避免踩坑的核心原则就一句话框架替你处理this的场景用普通函数需要你自己传递回调和函数作为值的场景优先用箭头函数。搞清楚this是谁在控制再决定怎么定义函数。6.3 代码审查时的quick check清单代码审查时我一般会快速过一遍这些点对象字面量里的方法是否误用了箭头函数原型方法是否用箭头函数定义导致this丢失隐式返回返回的是不是对象有没有加圆括号回调函数里使用this是不是真的需要外层this新写的箭头函数组件的生命周期里是否绑定正确这五个点扫一遍基本上能过滤掉大部分箭头函数引起的问题。写在最后箭头函数在JavaScript里不是多么高深的概念但它牵扯到this机制、作用域链、函数式编程习惯属于前端基础知识里“牵一发动全身”的节点。面试考它不只是考你的记忆更是考你对JavaScript运行模型的理解深度。给准备面试的朋友一个建议不要只背“箭头函数和普通函数的区别”这种列表型答案而是去理解设计者的出发点——怎么让函数的书写和使用更符合人的直觉怎么减少bug产生的可能性。当你能从设计意图的角度回答问题时很多原本需要死记的答案都会变得非常自然。我个人在实际带团队的过程中还有一个习惯代码审查时看到箭头函数都会多问一句“这个位置用箭头函数是想要什么效果”大部分情况下答案要么是“继承外层this”要么是“简洁”。只有当你清楚自己为什么选它箭头函数才会真正成为你手里的利器而不是面试题里的一个得分点。