Back to Article

JavaScript 函数表达式与箭头函数

JavaScript 函数表达式与箭头函数

函数表达式(Function Expression)

function 关键字可以在一个表达式中用来定义一个函数。

你可以使用函数表达式来创建一个匿名函数(anonymous function)。一个函数表达式可以被用作一个 IIFE(立即执行函数表达式),它一旦定义就运行。

1. 语法

基本的函数表达式语法如下:

let function_expression = function [name]([param1[, param2[, ..., paramN]]]) {
    statements
};
  • name: 函数名称。可以省略,此时函数是匿名的。
  • paramN: 传递给函数的参数名称。
  • statements: 构成函数体的语句。

2. 函数表达式与函数声明的区别

函数表达式与函数声明非常相似,并且具有几乎相同的语法。它们之间的主要区别在于函数名称,在函数表达式中可以省略该名称以创建匿名函数

下面的例子展示了函数表达式和函数声明的不同:

// 函数声明
function add(a, b) {
    return a + b;
}

// 函数表达式 (匿名)
const add = function(a, b) {
    return a + b;
};

3. 函数表达式提升 (Hoisting)

JavaScript 中的函数表达式不会被提升,这与函数声明不同。你不能在创建函数表达式之前使用它。

请看下面这个例子:

notHoisted(); // TypeError: notHoisted is not a function

var notHoisted = function() {
    console.log("bar");
};

在上面的代码中,notHoisted 变量被提升了,但在赋值之前,它的值是 undefined,因此调用它会导致 TypeError。这与函数声明不同,函数声明会被整体提升,因此可以在声明之前被调用。

4. 具名函数表达式 (Named Function Expression)

如果你想在函数体内部引用当前函数,则需要创建一个具名函数表达式。函数名称将会(且只会)作为函数体的本地变量。

const factorial = function fac(n) {
    return n < 2 ? 1 : n * fac(n - 1);
};
console.log(factorial(3)); // 输出 6

在上面的例子中,递归函数 fac 的名称在函数体内部是可见的,但在外部作用域是不可见的。

5. 立即执行函数表达式 (IIFE)

函数表达式可以被用作一个IIFE,它一旦定义就立即运行。这种模式常用于创建一个独立的作用域,以避免变量污染全局作用域。

(function() {
    console.log("This runs immediately!");
})();

6. 函数表达式中的 this

与箭头函数不同,传统的函数表达式(包括匿名函数)拥有自己的 this 绑定。this 的值取决于函数被调用的方式,而不是它被定义的地方。例如,当函数作为对象的方法被调用时,this 会指向该对象。

7. 使用场景

函数表达式非常灵活,适用于多种场景:

  • 作为值的传递:将函数作为参数传递给其他函数(回调函数)。
  • 条件创建:根据条件在运行时动态创建并定义函数。
  • 立即执行:使用 IIFE 模式创建独立作用域,封装私有变量。

💎 总结

总的来说,函数表达式是 JavaScript 中一个核心且强大的特性:

  • 核心概念:函数表达式是指在表达式中定义函数的方式,可以将函数作为值赋给变量或属性。
  • 关键区别:与函数声明不同,函数表达式不会被提升;并且,它可以省略函数名,创建匿名函数。
  • 主要用途:它被广泛用于创建回调函数、实现递归的具名函数表达式,以及封装私有变量的立即执行函数表达式(IIFE)。
  • 实践建议:函数表达式为开发者提供了在运行时动态创建和使用函数的灵活性,是现代 JavaScript 开发(尤其是异步编程和模块化开发)中不可或缺的工具。

揭秘 JavaScript 箭头函数

原文作者:Ajo John
译者:子舒

箭头函数是函数表达式的替代方法,但在语法上有所不同,不能在所有情况下使用。

现在,让我们试着从语法、执行、作用域和提升以及代码示例方面来理解箭头函数。


1. 语法

const arrowFunctionSyntax = () => {
    console.log('Hi, I am an arrow function');
};
arrowFunctionSyntax();

在上面的代码示例中,我们可以看到箭头函数类似于函数表达式,因为它们被赋值为变量。主要区别在于函数的编写方式。

以下是我们可以根据其语法从上述代码中得出的观察结果:

  • 它不包含 function 关键字。
  • 它没有函数名,这意味着这些是匿名函数。
  • 引入了箭头 => 符号。
const arrowFunctionWithOneParam = (number) => number + 1;
const arrowFunctionWithMultipleParams = (numberOne, numberTwo) => {
    let sum = numberOne + numberTwo;
    return sum;
};
console.log(arrowFunctionWithOneParam(5));
console.log(arrowFunctionWithMultipleParams(5, 6));

如果我们同时观察 arrowFunctionWithOneParamarrowFunctionWithMultipleParams,我们可以发现三者的不同——括号 ()、块 {}return 关键字的使用。

根据箭头函数的语法,如果函数只接受一个参数,可以忽略括号 ()。如果函数只包含一条语句,则可以忽略块 {},最后其实也可以忽略 return,如果函数只包含一个语句。


2. 执行

const arrowFunctionExecution = () => {
    console.log('Hi, my execution is similar to normal function');
};
arrowFunctionExecution();

在执行箭头函数时,它们的执行方式与其他普通函数的执行方式相似。当 JS 引擎执行 arrowFunctionExecution() 时,它会创建一个函数,执行上下文并被推送到调用堆栈。一旦创建了执行上下文,它就会启动创建阶段。在这个阶段,它将创建参数对象并在其本地内存堆中声明所有变量。

与普通函数相比,主要区别在于 this 的声明。箭头函数没有自己的 this 变量;this 在箭头函数中使用时会得到词法解析。

在创建阶段之后不久,执行阶段开始。这时它开始执行语句 console.log(),并将 "Hi, my execution is similar to normal function" 打印到控制台。


3. 作用域

const arrowFunctionScope = () => {
    console.log('Hi, my scoping rules works similar to function expression');
};

这些函数遵循与其他函数表达式相同的作用域规则。这些函数有自己的作用域,函数内部声明的任何变量都不能在函数外部访问。这些函数也不适用于 callapplybind 方法,这些方法通常依赖于作用域。


4. 提升

amIGoingToBeHoisted();
var amIGoingToBeHoisted = () => {
    console.log('The answer is NO');
};

箭头函数没有被提升,因为它们也是函数表达式,其中函数被分配为变量的值。当 JavaScript 引擎执行上述代码时,在创建阶段,声明语句 var amIGoingToBeHoisted 将被移到顶部并使用值 undefined 进行初始化,而将初始值留在后面。在执行阶段,遇到语句 amIGoingToBeHoisted 时会抛出类型错误,因为它的值是 undefined,这不是提升后的函数类型。


5. this

普通函数中的 this

在 JavaScript 中,this 关键字指的是它所属的对象。

const myObject = {
    myMethod(items) {
        console.log(this); // myObject 对象
        const callback = function() {
            console.log(this); // 全局对象(或严格模式下的 undefined)
        };
        items.forEach(callback);
    },
};
myObject.myMethod([1, 2, 3]);

在上面的示例中,this 在回调函数内部丢失了与 myObject 的关联,因为回调函数有自己的 this 绑定。

箭头函数中的 this

const myObject = {
    myMethod(items) {
        console.log(this); // myObject 对象
        const callback = () => {
            console.log(this); // myObject 对象
        };
        items.forEach(callback);
    },
};
myObject.myMethod([1, 2, 3]);

由于箭头函数没有自己的 this 绑定,它们会从封闭作用域继承 this 的值。这解决了普通函数中 this 丢失的问题。


6. 不能用作构造函数

箭头函数不能用作构造函数。与普通函数不同,不能使用 new 关键字调用箭头函数。如果尝试这样做,会引发错误。

const ArrowFunction = () => {};
const arrowFunctionInstance = new ArrowFunction(); // TypeError: ArrowFunction is not a constructor

7. 没有 arguments 对象

箭头函数没有自己的 arguments 对象。在箭头函数内部访问 arguments 将解析为封闭作用域中的 arguments 绑定。

function regularFunction() {
    const arrowFunction = () => {
        console.log(arguments); // 外层函数的 arguments 对象
    };
    arrowFunction();
}
regularFunction(1, 2, 3);

8. 何时使用箭头函数

适合使用箭头函数的场景:

  • 数组方法(mapfilterreduce 等)的回调函数
  • 需要保留 this 上下文的事件处理程序
  • Promise 链(thencatch
  • 需要简洁语法的简单函数

不适合使用箭头函数的场景:

  • 作为对象的方法(因为会丢失 this 绑定)
  • 需要使用 arguments 对象时
  • 需要动态 this 绑定的场景
  • 需要作为构造函数时

9. 总结

箭头函数为 JavaScript 提供了一种更简洁的函数写法,但在语义上存在重要差异:

  1. 更简洁的语法:省略了 function 关键字,对于单语句函数可以省略 return 和大括号。
  2. 词法 this 绑定:这是箭头函数最重要的特性,它从封闭作用域继承 this,而不是绑定自己的 this
  3. 不能作为构造函数:箭头函数没有 [[Construct]] 内部方法,不能与 new 一起使用。
  4. 没有 arguments 对象:需要使用剩余参数(...args)作为替代。
  5. 没有原型:箭头函数没有 prototype 属性。
  6. 不能被提升:作为函数表达式,箭头函数遵循变量的提升规则。

理解这些差异将帮助您在正确的场景中使用箭头函数,编写更清晰、更可预测的 JavaScript 代码。