真实面经题目 · 原创解析
箭头函数和普通函数有什么区别?
箭头函数是 ES6 引入的一种更简洁的函数写法,但它不只是语法糖。它最大的特点是没有自己的 this、arguments、super 和 new.target,其中 this 会按词法作用域从外层函数或模块环境中捕获,因此特别适合回调、数组方法和需要保留外层 this 的场景;但它不能作为构造函数,也没有 prototype,不适合对象方法、原型方法、构造器、事件处理器中依赖动态 this 的场景。
真实面经题目 · 原创解析
箭头函数是 ES6 引入的一种更简洁的函数写法,但它不只是语法糖。它最大的特点是没有自己的 this、arguments、super 和 new.target,其中 this 会按词法作用域从外层函数或模块环境中捕获,因此特别适合回调、数组方法和需要保留外层 this 的场景;但它不能作为构造函数,也没有 prototype,不适合对象方法、原型方法、构造器、事件处理器中依赖动态 this 的场景。
箭头函数可以理解为一种语法更简洁、this 绑定方式更固定的函数。普通函数的 this 取决于调用方式,比如谁调用它,this 就通常指向谁;而箭头函数没有自己的 this,它会从定义时所在的外层作用域继承 this,所以常说箭头函数的 this 是 lexical this。这个特点让它很适合写回调,比如 setTimeout、Promise、数组的 map/filter/reduce,能避免以前常见的 self = this 或 bind(this)。不过也正因为它没有自己的 this,所以 call、apply、bind 不能真正改变箭头函数的 this。除此之外,箭头函数也没有自己的 arguments,如果需要参数列表,应该用剩余参数。箭头函数不能用 new 调用,不是构造函数,也没有 prototype 属性,因此不能用来定义构造器或需要挂载实例方法的原型方法。实际使用时,可以把它用在短小、无状态、需要继承外层 this 的函数里;如果函数需要动态 this、arguments、作为构造函数,或者要作为对象/类原型上的方法,就应该选择普通函数。
箭头函数的基本形式是 参数 => 表达式 或 参数 => 函数体。当函数体只有一个表达式时,可以省略大括号并隐式返回表达式结果;如果使用大括号,就需要显式写 return。它的价值不只是少写 function,而是改变了函数内部若干绑定行为,尤其是 this。理解箭头函数时,不能只把它当作普通函数的简写,否则很容易在对象方法、构造函数和事件回调里写出错误代码。
普通函数的 this 通常由调用方式决定,例如 obj.fn() 中 this 指向 obj,单独调用时则可能是 undefined 或全局对象,取决于是否处于严格模式。箭头函数没有自己的 this,它的 this 来自定义时外层词法作用域,也就是代码写在哪里,就沿着作用域链向外找 this。这个规则让箭头函数非常适合需要保留外层 this 的回调场景,例如类方法内部的 setTimeout 回调。需要注意的是,箭头函数的 this 不是永远指向外层对象,而是指向外层作用域里的 this;对象字面量本身不会创建 this 作用域。
普通函数可以通过 call、apply、bind 显式改变 this,例如 fn.call(obj) 会让 fn 执行时的 this 指向 obj。箭头函数的 this 已经在定义时由外层作用域决定,因此 call、apply、bind 不能改变它的 this,只能传递参数或创建一个形式上的包装函数。这个差异在封装工具函数时很重要:如果函数的设计目标是允许调用方动态指定 this,就不应该使用箭头函数。
普通函数内部有自己的 arguments 对象,可以拿到调用时传入的全部参数。箭头函数没有自己的 arguments,如果在箭头函数里访问 arguments,拿到的是外层函数的 arguments;如果外层也不存在,就会报错或不可用。因此在箭头函数中需要收集参数时,应该使用剩余参数语法。剩余参数比 arguments 更清晰,也是真数组,能直接使用 map、forEach 等数组方法。
箭头函数不能使用 new 调用,因为它没有 [[Construct]] 内部能力,也没有自己的 new.target。普通函数可以作为构造函数,通过 new 创建实例,并把 this 指向新创建的对象;箭头函数没有自己的 this,自然也无法完成这种构造过程。尝试用 new 调用箭头函数会抛出错误。因此凡是用于创建实例、初始化对象状态、配合 instanceof 使用的函数,都应该使用普通函数或 class。
普通函数默认拥有 prototype 属性,当它作为构造函数使用时,实例可以通过原型链访问 prototype 上的方法。箭头函数没有 prototype 属性,所以无法承担构造函数和原型方法载体的角色。比如需要写 User.prototype.say 这类方法时,通常不应该改成箭头函数,因为原型方法往往依赖调用者实例作为 this。把原型方法写成箭头函数,通常会导致 this 指向定义位置的外层环境,而不是实例。
箭头函数适合短小的回调函数、数组方法回调、Promise 链、定时器回调,以及类方法内部需要继承外层 this 的逻辑。比如在一个类方法中写 setTimeout 回调,箭头函数会继承类方法执行时的 this,从而访问实例属性。它也适合表达纯计算逻辑,例如列表映射取 id,代码简洁且意图清楚。简单说,当函数不需要自己的 this、arguments、构造能力时,箭头函数通常是合适的。
箭头函数不适合作为对象方法、原型方法、构造函数、需要动态 this 的事件处理器,或者需要 arguments 的函数。比如在 DOM 事件处理中,如果希望 this 指向触发事件的元素,使用普通函数更合适;如果写成箭头函数,this 会来自外层环境,而不是事件元素。对象字面量中的方法也要谨慎,箭头函数通常不会让 this 指向该对象。判断是否适合使用箭头函数的核心标准是:这个函数是否需要自己的调用上下文。
箭头函数的 this 是由定义时所在的外层词法作用域决定的,不是由调用方式决定的。准确说,它自己没有 this,执行时会沿着作用域链去找外层的 this。因此即使用 call、apply、bind 调用箭头函数,也不能改变它内部 this 的指向。
对象方法通常期望 this 指向调用它的对象,例如 obj.fn() 中希望 this 是 obj。但箭头函数不会因为作为对象属性被调用就绑定到 obj,它的 this 仍然来自定义位置的外层作用域。这样很容易出现 this.xxx 取不到对象属性的问题。
不能真正绑定 this。对箭头函数调用 bind 会返回一个新函数,但新函数内部箭头函数的 this 仍然是原来词法作用域中的 this。bind 对箭头函数的意义主要只剩下预置参数,而不是改变 this。
箭头函数设计上没有自己的 arguments 绑定,这和它没有自己的 this 类似,都是为了让它更适合作为轻量回调。如果在箭头函数中写 arguments,它会去访问外层函数的 arguments。现代写法中,应该用剩余参数来接收可变参数,这样语义更明确,也能直接使用数组方法。
构造函数需要能被 new 调用,并在调用时创建新对象、绑定 this、连接原型链。箭头函数没有自己的 this,也没有构造函数所需的内部构造能力,所以不能被 new 调用。它也没有 prototype,因此不适合承担创建实例和定义实例共享方法的职责。
类方法内部的回调很适合用箭头函数,因为它能继承当前类方法里的 this,从而访问实例属性。例如定时器、Promise 回调、数组遍历回调中使用箭头函数可以避免 this 丢失。但定义类的原型方法本身时,通常还是使用标准方法语法;如果把实例方法写成类字段箭头函数,要意识到它会成为每个实例自己的函数,可能带来额外内存开销。