JS-底搞懂箭头函数与普通函数的“爱恨纠葛”

38 阅读4分钟

前言

箭头函数(Arrow Function)是 ES6 最受欢迎的新特性之一。它不仅让代码更加简洁,更重要的是它解决了 JS 中长期存在的 this 指向混乱问题。但你知道箭头函数真的“没有” this 吗?为什么它不能作为构造函数?本文带你一探究竟。

一、 背景:为什么要发明箭头函数?

在箭头函数出现之前,普通函数的 this 指向是动态的,取决于谁调用了它。这在回调函数中经常导致令人困惑的 Bug。

1. 对象方法中的 this 指向

在对象方法中调用this时,this指向该对象的方法,但是将对象方法作为独立函数调用时,this就会变成全局对象

const obj = {
  a: 1,
  func: function() {
    console.log(this);
  }
};

const test = obj.func; 

obj.func(); // obj (正常,通过对象调用)
test();     // window (严格模式下为 undefined,this 丢失)

2. 构造函数的this指向

当使用 new 操作符调用构造函数时,this 指向新创建的实例对象。然而,如果不使用 new 而直接调用构造函数,this 就会指向全局对象

function MyObject() {
  this.value = 5;
}

const obj1 = new MyObject();
console.log(obj1.value); // 5 (正常)

// ❌ 错误演示
const obj2 = MyObject(); // 没有 new,函数返回 undefined,且 window.value 被置为 5
console.log(obj2); // undefined
console.log(obj2.value); // 报错!Cannot read properties of undefined

二、 箭头函数的核心特性

1. 静态的 this(Lexical Scoping)

箭头函数没有自己的 this。它的 this它的this是继承上层函数作用域的,也就是说,它在定义时就捕获了其所在上下文的 this 值,而不是在执行时。

  • 固定性:一旦绑定,永远不会改变。
  • 不可修改callapplybind 可以调用箭头函数。
const obj1 = {
  name: 'obj1',
  printName: function() {
    console.log(this);
  }
};

const obj2 = {
  name: 'obj2',
  printName: () => {
    // 这里的 this 继承自 obj2 定义时的外层作用域(通常是 window)
    console.log(this); 
  }
};

obj1.printName(); // {name: 'obj1', printName: ƒ}
obj2.printName(); //  window

// 即使赋值给其他变量
const func1 = obj1.printName;
func1(); // window
const func2 = obj2.printName; 
func2(); // window

2. 没有 arguments 对象

箭头函数中访问 arguments 会报错(或者引用外层函数的 arguments)。

解决方案:使用 ES6 的 剩余参数(Rest Parameters) ...args

function func1() {
  // 普通函数可以使用 arguments
  console.log('func1:', arguments);
}

const func2 = (...args) => {
  // 箭头函数使用剩余参数
  console.log('func2 args:', args); 
  console.log(arguments); // ❌ 报错:arguments is not defined
};

func1(1, 2, 3); // Arguments(3) [1, 2, 3, ...]
func2(4, 5, 6); // [4, 5, 6] (真数组)

三、 为什么箭头函数不能作为构造函数?

这是面试中最底层的考题之一。

原因解析:

  1. 没有 this:构造函数需要将 this 绑定到新创建的实例上。箭头函数没有自己的 this,它只能捕获外层的,无法被 new 内部的逻辑重新绑定。
  2. 没有 prototype:箭头函数对象上没有 prototype 属性,而 new 操作符的第二步是将实例的 __proto__ 指向构造函数的 prototype
const ArrowPerson = (name) => {
    this.name = name;
};
console.log(ArrowPerson.prototype); // undefined
const p = new ArrowPerson('Jack');  // TypeError: ArrowPerson is not a constructor

四、 避坑指南:哪些场景不适合用箭头函数?

  1. 定义对象的方法(如果方法内部需要操作对象属性)。
  2. DOM 事件回调(如果需要操作 this 指向当前元素)。
  3. 原型链上的方法
const button = document.getElementById('btn');
// ❌ 错误:this 指向 window,而不是 button 元素
button.addEventListener('click', () => {
    this.classList.toggle('on'); 
});

// ✅ 正确:普通函数
button.addEventListener('click', function() {
    this.classList.toggle('on');
});

五、 面试模拟题(挑战一下)

Q1:箭头函数的 this 指向哪里?

参考回答: 箭头函数没有自己的 this。它的 this继承上层函数作用域的,指向是固定的,指向定义该函数时所在的作用域中, 无法通过bind、call、apply来改变。

Q2:普通函数和箭头函数的主要区别有哪些?

参考回答:

  1. this 指向:普通函数看调用方式;箭头函数看定义位置(词法作用域)。
  2. 构造函数:箭头函数不能 new,因为没有 prototype 且无法绑定 this
  3. arguments:箭头函数没有 arguments,需用 rest 参数 ...args

Q3:如下代码输出什么?

var id = 'GLOBAL';
var obj = {
  id: 'OBJ',
  a: function(){
    console.log(this.id);
  },
  b: () => {
    console.log(this.id);
  }
};

obj.a(); 
obj.b(); 

参考回答:

  • obj.a() 输出 'OBJ'(普通函数,obj 调用)。
  • obj.b() 输出 'GLOBAL'(箭头函数,继承自全局作用域的 this)。