在JavaScript ES6普及的今天,箭头函数几乎已经成为了前端开发日常编码的标配写法。但90%的前端开发者,其实都没能真正吃透箭头函数和传统普通函数的底层差异。
这也是大厂前端面试高频必出的经典考题:很多人只会简单说一句“箭头函数没有自己的this”,就再也讲不出更深的内容,直接错失面试加分项。
本文就从面试五层拆解、底层原理、代码实战、避坑指南、最佳实践等维度,一次性讲透二者区别,全文3000+字,附带完整可运行代码,看完面试可以直接流利背诵回答,写业务代码也能彻底避开深坑。
一、第一层核心区别:this 指向规则(最根本差异)
this的指向问题,是箭头函数和普通函数最本质、最核心的分水岭,也是所有其他差异的根源。
- 普通函数的 this 规则
普通函数(function声明/表达式)的this,是动态绑定的:
- 在函数调用执行的那一刻才确定指向
- 遵循“谁调用,this就指向谁”的规则
- 全局调用时:浏览器环境指向 window ,严格模式下为 undefined
- 可以通过 call() 、 apply() 、 bind() 手动强制修改this指向
代码示例
javascript
// 普通函数 function normalFunc() { console.log("普通函数this指向:", this); }
// 1. 全局直接调用 normalFunc(); // 非严格模式:this -> window // 严格模式:this -> undefined
// 2. 被对象调用,this指向调用者 const obj = { name: "豆包前端", func: normalFunc } obj.func(); // this 指向 obj 对象
// 3. call/apply/bind 可以修改this normalFunc.call({name: "手动绑定对象"}); // this 被修改为传入的新对象
- 箭头函数的 this 规则
箭头函数的this,是词法作用域(静态绑定):
- 箭头函数本身没有自己的this
- 只会继承自己外层最近一层普通函数/全局作用域的this
- 在函数定义声明的那一刻,this就已经固定死了
- 永远无法通过call/apply/bind修改this指向
代码示例
javascript
// 全局箭头函数 const arrowFunc = () => { console.log("箭头函数this指向:", this); }
arrowFunc(); // 全局定义,继承全局this,始终指向window/全局对象
// 嵌套场景对比 const testObj = { name: "测试对象", // 普通函数 normalSay: function() { console.log("普通函数内部this:", this); // 定时器回调 setTimeout(function() { // 普通回调函数,独立调用,this指向window console.log("普通定时器回调this:", this); }, 1000) }, // 箭头函数写法 arrowSay: function() { setTimeout(() => { // 箭头函数继承外层arrowSay的this,也就是testObj console.log("箭头定时器回调this:", this); }, 1000) } }
testObj.normalSay(); testObj.arrowSay();
// 尝试用call修改箭头函数this,完全无效 arrowFunc.call({newName: "强行修改"}); // this 依然还是原来继承的全局this,不会有任何变化
面试答题话术
普通函数this是运行时确定,谁调用指向谁,可手动修改;箭头函数没有自身this,词法继承外层作用域,定义时就固化,无法被改变。
二、第二层:能否作为构造函数 new 实例
- 普通函数
普通函数天生支持作为构造函数:
- 拥有 prototype 显式原型
- 可以使用 new 关键字调用
- new的时候会创建实例对象、绑定this、挂载原型链、返回实例
javascript
// 普通构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 原型上挂载方法
Person.prototype.sayHello = function() {
console.log(我是${this.name},今年${this.age}岁);
}
// new 创建实例 const p1 = new Person("张三", 18); p1.sayHello(); console.log(p1 instanceof Person); // true
- 箭头函数
箭头函数完全不能作为构造函数:
- 没有自己的 prototype 原型属性
- 不具备 [[Construct]] 内部方法
- 对箭头函数使用 new ,会直接抛出类型错误
javascript
const ArrowPerson = (name) => { this.name = name; }
// 直接报错:Uncaught TypeError: ArrowPerson is not a constructor const a1 = new ArrowPerson("李四");
原理补充
new 关键字执行的四步操作:
1. 创建一个全新的空对象 {} 2. 空对象的 proto 指向构造函数的 prototype 3. 构造函数内部this指向这个空对象 4. 如果函数没有返回对象,则返回创建的实例 箭头函数缺失第2、3步的底层能力,因此天生不能new。
三、第三层:arguments 参数对象差异
- 普通函数
普通函数内部,天生内置 arguments 类数组对象:
- 可以接收函数调用时传入的所有实参
- 不需要提前定义形参,也可以拿到全部传入参数
- 适合不定参数数量的场景
javascript
function sum() { console.log(arguments); // 类数组对象,包含所有调用传入的参数 let total = 0; for(let i = 0; i < arguments.length; i++) { total += arguments[i]; } return total; }
sum(1,2,3,4,5); // 可以正常累加所有参数
- 箭头函数
箭头函数自身没有arguments对象:
- 箭头函数内部访问arguments,会向上层作用域去继承外层的arguments
- 想要获取不定数量参数,必须使用ES6剩余参数 ...rest
javascript
// 箭头函数直接用arguments会继承外层 const arrowSum = (...args) => { console.log(args); // 真正的数组,比arguments更好用、更安全 return args.reduce((pre, cur) => pre + cur, 0); }
arrowSum(10,20,30,40);
// 错误示范:箭头函数原生无arguments const errorArrow = () => { console.log(arguments); // 会去外层找arguments,外层没有就直接报错 }
剩余参数优势: ...args 是纯正数组,可以直接调用数组所有API(map/filter/reduce等),而arguments只是类数组,需要手动转换才能使用数组方法。
四、第四层:原型与原型链区别
普通函数
- 普通函数天生自带 prototype 显式原型属性
- 函数实例自带 proto 隐式原型
- 可以在原型上批量挂载公共方法、实现原型继承
javascript
function Car() {} console.log(Car.prototype); // 存在原型对象 // 挂载原型方法 Car.prototype.run = function() { console.log("车正在行驶"); } const myCar = new Car(); myCar.run(); // 原型链上可以访问到方法
箭头函数
- 箭头函数没有 prototype 属性,输出为 undefined
- 不存在原型对象,自然无法挂载原型方法
- 完全不适合用来做原型继承、类相关的开发
javascript
const ArrowCar = () => {} console.log(ArrowCar.prototype); // undefined
// 给箭头函数加原型方法完全无效 ArrowCar.prototype.run = () => {} console.log(ArrowCar.prototype.run); // undefined
五、第五层:使用场景与避坑黑名单
✅ 箭头函数 最佳使用场景
1. 回调函数场景:数组遍历 map/forEach/filter/reduce 等 2. 定时器、延时函数:需要保留外层this上下文 3. 高阶函数封装、函数嵌套调用 4. 简短工具纯函数,代码简洁优雅
箭头函数正确示例
javascript
// 1. 数组高阶函数 const arr = [1,2,3,4,5]; const doubleArr = arr.map(item => item * 2);
// 2. 定时器保留this const page = { timer: 1000, startCount: function() { setTimeout(() => { console.log(this.timer); // 直接继承page的this,不用额外const _this = this }, this.timer) } }
❌ 箭头函数 绝对不能用的场景
1. 对象的方法定义:this会指向全局,不是对象本身 2. 原型方法定义:无原型,继承失效 3. 构造函数、实例创建:无法new 4. DOM事件监听回调:拿不到 event.target 事件源 5. 需要使用arguments的场景
箭头函数踩坑实战
javascript
// 坑1:对象方法用箭头函数 const badObj = { name: "前端开发", sayName: () => { console.log(this.name); // this继承全局,永远拿不到badObj的name } } badObj.sayName(); // 输出undefined
// 坑2:事件监听踩坑 // 错误写法 button.addEventListener("click", (e) => { console.log(this); // this不是button元素,拿不到事件源 })
// 正确写法 button.addEventListener("click", function(e) { console.log(this); // this正确指向被点击的button })
六、额外补充:其他细节差异
- 简写return规则
- 箭头函数单行省略大括号,默认自动return结果
- 普通函数必须手动写return才会返回值
javascript
// 箭头单行简写 const add = (a,b) => a + b; // 等价于 const add = (a,b) => { return a + b }
// 普通函数必须return function add2(a,b) { return a + b; }
- Generator 生成器
- 普通函数可以加 function* 声明Generator函数
- 箭头函数永远不能作为Generator,不支持 yield 关键字
- 变量提升
- 函数声明式普通函数,存在全局变量提升
- 箭头函数是变量赋值形式,不存在提升,必须先定义再使用
javascript
// 普通函数声明可以先调用再定义 func(); function func() {}
// 箭头函数必须先定义 arrowFunc(); // 报错 暂时性死区 const arrowFunc = () => {}
七、大厂标准满分面试回答模板
面试官问:说说箭头函数和普通函数的区别?
你可以分层流畅回答,直接惊艳面试官:
1. this指向不同:普通函数this运行时动态确定,谁调用指向谁,call/apply/bind可修改;箭头函数无自身this,词法继承外层作用域,定义时固化,无法修改。 2. 构造能力不同:普通函数可以作为构造函数new创建实例;箭头函数没有 [[Construct]] ,不能new,会直接报错。 3. arguments差异:普通函数内置arguments类数组接收全部参数;箭头函数无自身arguments,只能用剩余参数 ...rest 接收。 4. 原型结构不同:普通函数拥有prototype原型对象,支持原型挂载与继承;箭头函数无prototype,无法做原型扩展。 5. 适用场景不同:箭头函数适合回调、定时器、高阶函数,简化this处理;禁止用作对象方法、原型方法、构造函数、事件回调。 6. 其他细节:箭头函数支持单行隐式返回,不能作为Generator,不存在函数提升。
八、开发最佳实践总结
维度 普通函数 箭头函数 this指向 运行时动态绑定 词法静态继承外层 new构造 ✅ 支持 ❌ 不支持 arguments ✅ 内置 ❌ 无原生arguments prototype ✅ 拥有 ❌ 无原型 call修改this ✅ 可以 ❌ 无效 适合对象方法 ✅ ❌ 适合回调函数 需要手动存this ✅ 天然适配
结尾
箭头函数不是普通函数的简化替代品,二者底层设计定位完全不同:
- 箭头函数主打简洁、稳定的this绑定,用来消灭过去 var that = this 的冗余写法
- 普通函数主打灵活、强大的动态能力,支撑构造、原型、面向对象编程
很多新手只会盲目全程滥用箭头函数,踩了无数this的坑还找不到原因。吃透本文内容,不仅面试可以从容拿分,日常写代码也能写出更健壮、更优雅、零隐患的高质量JS代码。
如果觉得本文对你有帮助,欢迎点赞收藏,后续会更新更多前端面试核心底层原理干货~