跳到主要内容
预计阅读 48 分钟

Javascript篇 | 箭头函数

前言

在前面的章节里,我们从闭包与作用域聊到了this 指向与执行上下文的五种绑定规则。如果你一路读下来,应该已经对”变量怎么查找、this 怎么绑定”有了一套完整的认知框架。

但你可能也发现了:在 this 那一章里,有一个角色总在”特例”的位置上出现——箭头函数。当时我们只用一句话概括了它的行为:

箭头函数没有自己的 this,它的 this 继承自定义时所在的外层执行上下文。

这句话虽然正确,但如果面试官继续追问:

  • “继承外层”到底是什么意思?为什么对象字面量不算”外层”?
  • 箭头函数为什么不能 new?从规范角度怎么解释?
  • 没有 arguments、没有 prototype,这些限制背后的设计逻辑是什么?
  • 在 React 类组件、事件回调、定时器里,箭头函数和普通函数分别该怎么选?

很多人就开始支支吾吾了。

本章,我们就把箭头函数从语法、语义、规范、实战四个层面彻底讲清楚。读完之后,你不仅能准确回答上面这些问题,还能在代码里做出更合理的选择。


诊断自测

在开始正文之前,先用几道题测测你目前对箭头函数的理解程度。答不上来也没关系,读完全文再回来看,效果更好。

Q1:下面代码输出什么?为什么?

const team = {
  members: ['Alice', 'Bob'],
  teamName: 'Alpha',
  listMembers: () => {
    return this.members?.map(m => `${this.teamName}: ${m}`);
  }
};

console.log(team.listMembers());
点击查看答案

输出 undefinedlistMembers 是箭头函数,而对象字面量 {} 不会创建新的执行上下文,所以 this 不是 team,而是外层的全局/模块上下文。this.membersundefined,调用 ?.map 短路返回 undefined

Q2:箭头函数能不能通过 callbind 改变 this 指向?说说原因。

点击查看答案

不能。箭头函数压根没有自己的 this 绑定——它的 this 是从外层作用域”捕获”过来的,存在闭包中。call/apply/bind 修改的是函数自身的 this 绑定,但箭头函数没有这个绑定可供修改,所以这些方法对箭头函数的 this 无效(虽然不报错,但 this 不会变)。

Q3:以下两种写法有什么区别?在什么场景下选择哪种?

class Logger {
  // 写法 A
  log() { console.log(this); }
  // 写法 B
  log = () => { console.log(this); };
}
点击查看答案

写法 A 是原型方法,所有实例共享同一份函数,但单独提取出来调用时 this 会丢失。写法 B 是实例属性(类字段),每个实例各有一份,this 永远指向实例。如果要把方法作为回调传递(如 onClick={this.log}),选写法 B 更安全;如果实例数量多、且不需要提取为回调,选写法 A 更节省内存。


一、箭头函数解决了什么问题?

在 ES6 之前,JavaScript 里最让人头疼的场景之一就是回调中的 this 丢失

看一个经典的例子:

function Timer() {
  this.seconds = 0;

  setInterval(function () {
    this.seconds++; // ❌ this 不是 Timer 实例
    console.log(this.seconds);
  }, 1000);
}

const t = new Timer();
// 输出 NaN,因为回调里的 this 指向了全局对象(非严格模式)

setInterval 的回调是一个普通函数,它被引擎独立调用,根据我们在 this 那章讲过的默认绑定规则,this 会指向全局对象(非严格模式)或 undefined(严格模式)。

为了绕过这个问题,ES6 之前常见的做法有两种:

方法一:var self = this

function Timer() {
  this.seconds = 0;
  var self = this;

  setInterval(function () {
    self.seconds++; // ✅ 通过闭包访问外层的 self
  }, 1000);
}

方法二:bind

function Timer() {
  this.seconds = 0;

  setInterval(
    function () {
      this.seconds++; // ✅ bind 绑定了 this
    }.bind(this),
    1000
  );
}

这两种方法都能解决问题,但都不够优雅——要么多一个变量,要么多一层 .bind(this)

ES6 的箭头函数,就是为了从语言层面彻底消除这类样板代码而设计的:

function Timer() {
  this.seconds = 0;

  setInterval(() => {
    this.seconds++; // ✅ 箭头函数的 this 就是 Timer 实例
  }, 1000);
}

一行 => 同时解决了两个问题:语法更短、this 更稳。

但要注意,箭头函数不仅仅是”语法糖”。它和普通函数在语言层面有本质的差异,下面我们就逐一拆解。


二、基础语法:几种写法和常见坑

在深入语义之前,先快速过一遍语法。箭头函数的语法比普通函数灵活得多,但也因此有几个容易踩的坑。

最基本的形式

const add = (a, b) => {
  return a + b;
};

简写:省略大括号和 return

如果函数体只有一个表达式,可以省略 {}return,表达式的值就是返回值:

const add = (a, b) => a + b;

简写:单参数省略括号

如果只有一个参数,可以省略参数列表的括号:

const double = x => x * 2;

但注意:无参数或多参数时,括号不能省

const greet = () => 'hello';
const add = (a, b) => a + b;

坑一:返回对象字面量要加小括号

这是笔试和面试中出场率极高的一个坑:

// ❌ 错误:大括号被解析成代码块,name 被当成了 label 语法
const buildUser = () => { name: 'Alice' };
console.log(buildUser()); // undefined

// ✅ 正确:用小括号包裹,告诉引擎这是一个表达式
const buildUser = () => ({ name: 'Alice' });
console.log(buildUser()); // { name: 'Alice' }

为什么会这样? 因为 { 在 JavaScript 里有两种含义:代码块和对象字面量。当 => 后面紧跟 { 时,引擎会优先把它解析为代码块。加上 () 后,引擎就知道这是一个需要求值的表达式。

坑二:换行问题

箭头符号 => 和参数之间不能换行

// ❌ SyntaxError
const add = (a, b)
  => a + b;

// ✅ 正确
const add = (a, b) =>
  a + b;

与解构、默认参数的组合

箭头函数的参数列表支持所有 ES6 参数特性:

// 解构 + 默认值
const greet = ({ name = 'Anonymous' } = {}) => `Hi, ${name}`;

// rest 参数
const sum = (...nums) => nums.reduce((a, b) => a + b, 0);

三、核心差异:箭头函数和普通函数到底差在哪?

语法只是表面,真正让箭头函数”特殊”的,是它在语言规范层面和普通函数的本质不同

3.1 没有自己的 this:从”运行时绑定”到”定义时捕获”

这是箭头函数最核心、也是面试中最常考的特性。

我们在 this 那章讲过,普通函数的 this 是在调用时根据调用方式动态绑定的。但箭头函数完全不同:

箭头函数没有自己的 this 绑定。它的 this 和普通变量一样,是从外层作用域中一层一层往上找的。

说人话:箭头函数在写出来那一刻,就”偷看”了外层的 this 并记住了,之后无论怎么调用(隐式绑定、显式绑定、甚至 new),this 都不会变。

从规范角度看,普通函数在被调用时会创建一个新的执行上下文,其中包含一个新的 this 绑定。而箭头函数在被调用时,不会创建新的 this 绑定——它直接沿着作用域链,使用外层执行上下文的 this

这就是为什么我们说箭头函数的 this 是”定义时确定”的——因为外层执行上下文的 this 在箭头函数定义的那一刻就已经存在了。

来看一个对比:

const obj = {
  name: 'obj',
  regular() {
    return function () {
      return this.name;
    };
  },
  arrow() {
    return () => this.name;
  }
};

const f1 = obj.regular();
const f2 = obj.arrow();

console.log(f1());  // undefined 或 window.name(默认绑定)
console.log(f2());  // 'obj'(继承了 arrow() 执行时的 this)

f1 是一个普通函数,独立调用时触发默认绑定。而 f2 是箭头函数,它”记住”了 obj.arrow() 执行时的 this(即 obj),所以无论后续怎么调用,都返回 'obj'

更关键的是:call / apply / bind 对箭头函数的 this 无效

console.log(f2.call({ name: 'hijack' })); // 仍然是 'obj'

这不是说 call “没生效”,而是箭头函数压根就没有自己的 this 可以被替换call 改的是函数自己的 this 绑定,但箭头函数没有这个绑定,所以改了个寂寞。

3.2 一个高频误区:对象字面量不是”外层作用域”

这个问题在面试中出现频率极高:

const obj = {
  name: 'obj',
  sayName: () => {
    console.log(this.name);
  }
};

obj.sayName(); // ❌ 不是 'obj',而是 undefined(全局)

很多人直觉上觉得”箭头函数写在 obj 里面,外层就是 obj 啊”。但这是错的。

关键概念:对象字面量 {} 不会创建新的执行上下文。

在 JavaScript 中,能创建新执行上下文(也就是能产生新 this 的)只有:

  • 函数调用(包括普通函数、方法调用、构造调用)
  • eval(直接调用)
  • 模块顶层

对象字面量只是一个表达式,写 { key: value } 的过程中,执行上下文并没有发生任何切换。所以箭头函数 sayName 的”外层”不是 obj,而是包含这个对象字面量的那个执行上下文——通常是全局上下文或模块上下文。

正确写法:

const obj = {
  name: 'obj',
  sayName() {
    // 普通方法,this 由调用方式决定
    console.log(this.name);
  }
};

3.3 不能作为构造函数:没有 [[Construct]] 内部方法

const Person = (name) => {
  this.name = name;
};

const p = new Person('Alice'); // ❌ TypeError: Person is not a constructor

在 ECMAScript 规范中,一个函数要能被 new 调用,必须拥有 [[Construct]] 内部方法。我们在 new 那章讲过,new 调用会触发 [[Construct]],执行”创建空对象 → 绑定原型 → 执行函数 → 返回”这四步。

箭头函数在规范层面没有 [[Construct]] 内部方法,只有 [[Call]]。所以用 new 调用时,引擎会直接抛出 TypeError

这也是为什么箭头函数没有 prototype 属性——既然不能当构造函数,也就不需要 prototype 来给实例建立原型链了:

const fn = () => {};
console.log(fn.prototype); // undefined

function normalFn() {}
console.log(normalFn.prototype); // { constructor: normalFn }

3.4 没有 arguments 对象

普通函数在被调用时,会在执行上下文中自动创建一个 arguments 对象,包含所有传入的参数。但箭头函数不会创建自己的 arguments

function outer() {
  const inner = () => {
    console.log(arguments); // 这里的 arguments 是 outer 的!
  };
  inner();
}

outer(1, 2, 3); // Arguments(3) [1, 2, 3]

如果箭头函数在全局作用域中使用 arguments,会直接报 ReferenceError(严格模式)或拿到全局的 arguments(非严格模式,如果存在的话)。

替代方案:rest 参数

const sum = (...args) => args.reduce((a, b) => a + b, 0);
console.log(sum(1, 2, 3)); // 6

rest 参数不仅可以替代 arguments,而且它是一个真正的数组,不需要像 arguments 那样用 Array.from[].slice.call 做转换。在现代代码中,即使是普通函数,也推荐用 rest 参数替代 arguments。

3.5 不能用作 Generator

箭头函数不能使用 yield 关键字,也不能用 function* 的箭头版本(不存在 *=> 这种语法)。

// ❌ SyntaxError
const gen = *() => {
  yield 1;
};

如果需要生成器,必须使用普通的 function* 声明。

3.6 没有 super 绑定

thisarguments 类似,箭头函数也没有自己的 super 绑定。如果在箭头函数中使用 super,它会沿作用域链查找外层的 super

这在 class 的方法中偶尔会用到,比如在回调里调用父类方法:

class Child extends Parent {
  start() {
    setTimeout(() => {
      super.doSomething(); // ✅ 箭头函数继承了 start() 的 super
    }, 1000);
  }
}

完整对比表

特性普通函数箭头函数
this 绑定调用时动态绑定定义时继承外层,无法改变
arguments有自己的 arguments 对象没有,沿作用域链查找
prototype没有
能否 new可以(有 [[Construct]])不可以
能否用作 Generator可以不可以
super 绑定有自己的继承外层
call/apply/bind 改 this有效无效

四、什么时候该用箭头函数?什么时候不该用?

知道差异还不够,更重要的是在实际写代码时做出正确的选择。这部分也是面试官考察”工程素养”的常见角度。

✅ 推荐:回调函数

这是箭头函数的”主战场”。任何需要在回调里保留外层 this 的场景,箭头函数都比普通函数更合适:

// 定时器
function Timer() {
  this.seconds = 0;
  setInterval(() => {
    this.seconds++;
  }, 1000);
}

// DOM 事件(注意:如果回调里需要通过 this 访问 DOM 元素本身,就不能用箭头函数)
button.addEventListener('click', () => {
  console.log('clicked'); // 如果不需要 this 指向 button,箭头函数更简洁
});

// Promise 链
fetch('/api/data')
  .then(res => res.json())
  .then(data => console.log(data));

✅ 推荐:数组的函数式方法

mapfilterreduceforEachfindsomeevery 等方法的回调,用箭头函数既简洁又不会有 this 问题:

const names = users.map(u => u.name);
const adults = users.filter(u => u.age >= 18);
const total = prices.reduce((sum, p) => sum + p, 0);

⚠️ 不推荐:对象方法

前面 3.2 已经详细解释过了。如果你需要在方法里通过 this 访问对象本身,必须用普通函数或方法简写

// ❌ 箭头函数:this 不是 counter
const counter = {
  count: 0,
  inc: () => { this.count++; }
};

// ✅ 方法简写:this 由调用方式决定
const counter = {
  count: 0,
  inc() { this.count++; }
};

⚠️ 不推荐:原型方法

原型方法需要通过 this 访问实例,箭头函数会导致 this 指向错误:

// ❌
Person.prototype.sayHi = () => {
  return `Hi, I am ${this.name}`; // this 不是实例
};

// ✅
Person.prototype.sayHi = function () {
  return `Hi, I am ${this.name}`;
};

⚠️ 不推荐:需要动态 this 的 DOM 事件处理器

有时候事件处理器需要通过 this 拿到触发事件的 DOM 元素:

// ❌ 箭头函数里 this 不是 button 元素
button.addEventListener('click', () => {
  this.classList.toggle('active'); // this 是全局对象
});

// ✅ 普通函数里 this 指向 button
button.addEventListener('click', function () {
  this.classList.toggle('active');
});

// ✅ 或者用 event.currentTarget 替代 this,这样也能用箭头函数
button.addEventListener('click', (e) => {
  e.currentTarget.classList.toggle('active');
});

五、实战场景深入:箭头函数 + class

在现代前端开发中,箭头函数和 class 的组合使用非常常见。这里有一个细节值得深入。

class 中的箭头函数属性

class Button {
  constructor(text) {
    this.text = text;
  }

  // 方法一:原型方法(普通函数)
  handleClick() {
    console.log(this.text);
  }

  // 方法二:实例属性(箭头函数)
  handleClickArrow = () => {
    console.log(this.text);
  };
}

这两种写法在使用上的关键区别是:

const btn = new Button('Submit');

// 原型方法:单独拿出来调用时,this 丢失
const fn1 = btn.handleClick;
fn1(); // ❌ undefined(this 不是 btn)

// 箭头函数属性:单独拿出来调用时,this 仍然是实例
const fn2 = btn.handleClickArrow;
fn2(); // ✅ 'Submit'

为什么?

handleClickArrow = () => { ... } 是一个类字段(class field),它的初始化等价于在 constructor 中写:

constructor(text) {
  this.text = text;
  this.handleClickArrow = () => {
    console.log(this.text);
  };
}

根据我们在 new 那章讲的,constructor 执行时 this 指向新创建的实例。箭头函数定义在 constructor 的执行上下文中,所以它”记住”的 this 就是这个实例。

但要注意权衡:

  • 箭头函数属性是每个实例各一份(挂在实例上),而原型方法是所有实例共享一份(挂在 prototype 上)
  • 如果创建大量实例,箭头函数属性会占用更多内存
  • 箭头函数属性不能被子类通过 super 调用(因为它不在原型上)

在 React 类组件中,这种写法曾经非常流行(避免在 constructor 里写一堆 this.handleClick = this.handleClick.bind(this))。但在函数组件 + Hooks 时代,这个问题已经不太常见了。


六、回到开头:热身题逐行分析

现在我们有了完整的知识体系,回来看开头的热身题:

const obj = {
  name: 'obj',
  fn1() {
    console.log('fn1 this:', this.name);
  },
  fn2: () => {
    console.log('fn2 this:', this.name);
  },
  fn3() {
    setTimeout(function () {
      console.log('fn3 timeout this:', this?.name);
    }, 0);
  },
  fn4() {
    setTimeout(() => {
      console.log('fn4 timeout this:', this.name);
    }, 0);
  }
};

obj.fn1()

obj.fn1(); // 输出: 'fn1 this: obj'

fn1 是普通方法,通过 obj.fn1() 调用,触发隐式绑定this 指向 obj

obj.fn2()

obj.fn2(); // 输出: 'fn2 this: undefined'

fn2 是箭头函数。对象字面量 {} 不创建执行上下文,所以 fn2 的外层是全局/模块上下文。非严格模式下可能是 window.name(通常是空字符串),模块环境下是 undefined

obj.fn3()

obj.fn3(); // 输出: 'fn3 timeout this: undefined'

fn3 是普通方法,但内部 setTimeout 的回调是普通函数。这个回调被引擎独立调用,触发默认绑定this 指向全局对象(非严格模式)或 undefined(严格模式)。

注意:即使 fn3 本身的 this 是 obj,也不影响 setTimeout 回调里的 this——因为回调是一个独立的普通函数,它有自己的 this 绑定规则。

obj.fn4()

obj.fn4(); // 输出: 'fn4 timeout this: obj'

fn4 是普通方法,内部 setTimeout 的回调是箭头函数。箭头函数没有自己的 this,它继承了外层 fn4 执行时的 this。而 obj.fn4() 通过隐式绑定,fn4 的 this 是 obj,所以箭头函数里的 this 也是 obj

这道题的精髓在于:fn3 和 fn4 唯一的区别就是 setTimeout 回调用的是普通函数还是箭头函数,但结果完全不同。 这恰恰体现了箭头函数设计的初衷——在回调中保留外层 this。


Bonus:箭头函数的 name 属性

一个很少有人提到但面试偶尔会问到的细节:箭头函数是匿名的,但它仍然有 name 属性。

const foo = () => {};
console.log(foo.name); // 'foo'

const obj = {
  bar: () => {}
};
console.log(obj.bar.name); // 'bar'

// 但直接传入的箭头函数没有名字
console.log((() => {}).name); // ''

这是因为 ES6 引入了函数名推断(name inference):当一个匿名函数被赋值给变量或属性时,引擎会自动推断出一个名字。这对调试很有帮助——在堆栈追踪中,有名字的函数比 <anonymous> 更容易定位问题。


常见误区

在面试和实际开发中,关于箭头函数有几个非常顽固的误解。这里逐一澄清,帮你避免踩坑。

误区一:“箭头函数就是普通函数的简写语法糖”

不是。语法糖意味着”换了个写法,本质完全一样”,比如 class 之于构造函数 + 原型。但箭头函数和普通函数在规范层面有本质差异:没有自己的 this、没有 arguments、没有 [[Construct]]、没有 prototype、不能做 Generator。这些不是语法层面的省略,而是语义层面的删减。把箭头函数当成”短一点的 function”来用,迟早会掉进 this 的坑里。

误区二:“箭头函数的 this 指向定义时所在的对象”

这句话看起来”差不多对”,但差的那一点恰好是面试重灾区。准确的说法是:箭头函数的 this 继承自定义时所在的外层执行上下文。“对象”和”执行上下文”是两个完全不同的概念——对象字面量 {} 不会创建执行上下文。看这个例子:

const config = {
  env: 'production',
  getEnv: () => this.env
};

console.log(config.getEnv()); // undefined,不是 'production'

getEnv 写在 config 对象里,但 config 不产生执行上下文,所以 this 是外层的全局/模块上下文,而不是 config

误区三:“箭头函数没有 this”

严格说,箭头函数不是”没有 this”,而是没有自己的 this 绑定。在箭头函数内部,this 这个关键字依然可以使用,只不过它引用的是外层作用域的 this,就像访问一个闭包变量一样。说”没有 this”容易让人以为箭头函数里写 this 会报错,但实际上不会——它只是不归箭头函数自己管。

function Outer() {
  this.value = 42;
  const inner = () => {
    console.log(this.value); // 42,this 可以正常使用,只不过是 Outer 的
  };
  inner();
}

new Outer();

误区四:“用了箭头函数就不用考虑 this 问题了”

恰恰相反,用箭头函数时更需要清楚 this 的来源。箭头函数只是把 this 的决定时机从”调用时”提前到了”定义时”,但如果你搞不清楚定义时外层的 this 是谁,照样会出 bug。比如在 forEach 的回调里用箭头函数,你得确认 forEach 所在的那个函数的 this 是你期望的值。箭头函数不是银弹,它只是把 this 的问题从”动态”变成了”静态”,让你在写代码的时候就能确定,而不是等到运行时才发现。


小结

本章我们从 ES6 之前”回调中 this 丢失”的痛点出发,完整拆解了箭头函数的设计动机、语法细节、和普通函数的核心差异、以及实战中的选择策略。

核心要点

  1. 箭头函数没有自己的 this:它的 this 从外层作用域继承,在定义时就确定了,之后 call / apply / bind 都无法改变
  2. 对象字面量不是”外层作用域”{} 不创建执行上下文,箭头函数会继续往上找
  3. 不能 new:没有 [[Construct]] 内部方法,也没有 prototype
  4. 没有 arguments:用 rest 参数 ...args 替代
  5. 不能做 Generator:不支持 yield
  6. 最佳使用场景:回调函数、数组方法、需要保留外层 this 的地方
  7. 谨慎使用场景:对象方法、原型方法、需要动态 this 的 DOM 事件处理器

记忆口诀

箭头函数四个”没有”:没有 this、没有 arguments、没有 prototype、没有 constructor。

一个”不能”:不能做 Generator。

箭头函数 语法简洁,但更重要的是 词法 this

一个”无效”:call / apply / bind 改不了它的 this。


本章思维导图

JS:箭头函数
  • 为什么会出现
    • ES6 之前回调 this 丢失(var self = this / bind)
    • 箭头函数从语言层面解决这个问题
  • 语法
    • 基本形式:(params) => expression
    • 省略规则:单参数省括号、单表达式省 return
    • 坑:返回对象要加小括号
  • 核心差异(vs 普通函数)
    • this:定义时继承外层,无法被 call/apply/bind 改变
    • 对象字面量不创建执行上下文(高频误区)
    • 没有 [[Construct]],不能 new
    • 没有 prototype
    • 没有 arguments,用 rest 参数替代
    • 没有 super 绑定(继承外层)
    • 不能做 Generator
  • 使用场景
    • ✅ 回调(定时器、Promise、事件)
    • ✅ 数组方法(map/filter/reduce)
    • ⚠️ 不推荐:对象方法、原型方法
    • ⚠️ 不推荐:需要动态 this 的 DOM 处理器
  • class 中的箭头函数
    • 类字段箭头函数 vs 原型方法
    • 权衡:this 稳定 vs 内存开销
  • 与 this 章的关系
    • 箭头函数不参与 this 优先级比较
    • 它是"五种绑定"之外的特例

练习挑战

从易到难,三道题帮你巩固本章内容。建议先自己写出答案,再展开对照。

第一题(基础):预测输出

const calculator = {
  value: 100,
  add: function(nums) {
    return nums.map(n => this.value + n);
  },
  subtract: function(nums) {
    return nums.map(function(n) {
      return this.value - n;
    });
  }
};

console.log(calculator.add([1, 2, 3]));
console.log(calculator.subtract([1, 2, 3]));
点击查看答案与解析
[101, 102, 103]
[NaN, NaN, NaN]  // 非严格模式下,如果全局没有 value 属性

add 方法中,map 的回调是箭头函数,this 继承自 add 执行时的上下文,即 calculator,所以 this.value 是 100。

subtract 方法中,map 的回调是普通函数,被 map 内部独立调用,触发默认绑定,this 是全局对象(非严格模式)或 undefined(严格模式)。全局对象上通常没有 value 属性,所以 undefined - n 得到 NaN

第二题(进阶):修复 Bug

下面的代码期望点击按钮后,延迟 500ms 打印出 "Clicked: submitBtn",但实际运行时打印的是 "Clicked: undefined"。请找出 Bug 并用最少的改动修复。

class Widget {
  constructor(name) {
    this.name = name;
  }

  bindEvents(button) {
    button.addEventListener('click', function() {
      setTimeout(function() {
        console.log('Clicked: ' + this.name);
      }, 500);
    });
  }
}

const w = new Widget('submitBtn');
w.bindEvents(document.querySelector('#submit'));
点击查看答案与解析

问题出在两层嵌套的普通函数回调上,this 在每一层都会被重新绑定。最简洁的修复方式是把两个回调都改成箭头函数:

bindEvents(button) {
  button.addEventListener('click', () => {
    setTimeout(() => {
      console.log('Clicked: ' + this.name);
    }, 500);
  });
}

外层箭头函数继承 bindEventsthis(即 Widget 实例),内层箭头函数继承外层箭头函数的 this(同样是 Widget 实例)。两层箭头函数层层传递,最终 this.name 就是 'submitBtn'

注意:如果你只改内层的 setTimeout 回调为箭头函数,还是不够——外层的 addEventListener 回调仍然是普通函数,它的 this 会被绑定为触发事件的 DOM 元素(button),而不是 Widget 实例。所以两层都要改。

第三题(综合):实现一个 pipeline 工具函数

请用箭头函数实现一个 pipeline 函数,接收一个初始值和任意数量的变换函数,依次将值传入每个函数并返回最终结果。要求:

  1. 使用 rest 参数收集变换函数
  2. 使用数组方法(reduce)实现管道
  3. 不能出现 function 关键字
// 用法示例:
const result = pipeline(
  '  Hello, World!  ',
  s => s.trim(),
  s => s.toLowerCase(),
  s => s.replace(/\s+/g, '-'),
  s => s.split('-'),
  arr => arr.length
);

console.log(result); // 2
点击查看参考实现
const pipeline = (initial, ...fns) => fns.reduce((val, fn) => fn(val), initial);

一行搞定。rest 参数 ...fns 收集所有变换函数为数组,reduce 依次将上一步的结果传入下一个函数。整个实现没有用到 this,所以箭头函数在这里既简洁又安全——这正是箭头函数最舒服的使用场景。


自我检测

读完本章后,对照下面的清单检验一下自己的掌握程度。每一项都能自信地打勾,说明你对箭头函数的理解已经足够扎实了。

  • 能说清楚 ES6 之前处理回调 this 丢失的两种常见方法(var self = this.bind(this)),以及箭头函数如何从语言层面解决这个问题
  • 能准确列出箭头函数和普通函数的至少 5 个核心差异,并解释每个差异背后的规范原因
  • 能解释”箭头函数的 this 继承自外层执行上下文”这句话的准确含义,特别是为什么对象字面量不算外层
  • 能区分 class 中的原型方法类字段箭头函数的差异,并说出各自的适用场景和内存影响
  • 能在给定的代码片段中准确判断箭头函数的 this 指向谁
  • 能说出箭头函数返回对象字面量时需要加小括号的原因
  • 能在实际代码中合理选择使用箭头函数还是普通函数,并说出选择的理由
  • 能解释为什么 call/apply/bind 无法改变箭头函数的 this(不是”规定如此”,而是从”箭头函数没有自己的 this 绑定”这个角度解释)

购买课程解锁全部内容

大厂前端面试通关:71 篇构建完整知识体系

¥89.90