MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript函数的声明方式与特点

2021-04-231.6k 阅读

JavaScript函数的声明方式

在JavaScript中,函数是一等公民,这意味着函数可以像其他数据类型(如数字、字符串等)一样被赋值给变量、作为参数传递给其他函数以及从其他函数返回。而函数的声明方式决定了函数在代码中的行为和作用域。以下详细介绍JavaScript中常见的函数声明方式。

函数声明(Function Declaration)

函数声明是最传统也是最常用的声明函数的方式。其语法如下:

function functionName(parameters) {
    // 函数体
    return returnValue;
}
  • 示例
function addNumbers(a, b) {
    return a + b;
}
let result = addNumbers(3, 5);
console.log(result); // 输出 8
  • 函数名functionName 是函数的标识符,在函数声明所在的作用域内必须是唯一的。
  • 参数parameters 是函数接受的输入值,可以有多个,也可以没有。在函数体内部,这些参数作为局部变量使用。
  • 函数体:包含了在函数被调用时要执行的语句。
  • 返回值return 语句用于指定函数的返回值。如果没有 return 语句,函数将返回 undefined

函数声明提升

函数声明的一个重要特点是函数提升。这意味着在JavaScript引擎解析代码时,会将函数声明提升到其所在作用域的顶部。因此,函数可以在声明之前被调用。

// 调用函数在声明之前
let sum = add(2, 3); 
console.log(sum); 

function add(a, b) {
    return a + b;
}

在上述代码中,尽管 add 函数的调用在声明之前,但代码依然能够正常运行,这就是函数声明提升的效果。提升使得函数声明在整个作用域内都可访问,就好像函数声明被放在了作用域的最顶部一样。不过,需要注意的是,只有函数声明会被提升,函数表达式不会。

函数表达式(Function Expression)

函数表达式是将函数赋值给一个变量的方式。语法如下:

let functionVariable = function(parameters) {
    // 函数体
    return returnValue;
};
  • 示例
let multiplyNumbers = function(a, b) {
    return a * b;
};
let product = multiplyNumbers(4, 6);
console.log(product); // 输出 24

这里,function(a, b) {... } 是一个匿名函数(没有名字的函数),它被赋值给了变量 multiplyNumbers。此时,multiplyNumbers 就成为了一个指向该函数的变量。

匿名函数与具名函数表达式

在上述示例中,我们使用的是匿名函数表达式。不过,函数表达式也可以是具名的,语法如下:

let functionVariable = function functionName(parameters) {
    // 函数体
    return returnValue;
};

在具名函数表达式中,functionName 只能在函数体内部访问,外部依然通过 functionVariable 来调用函数。例如:

let sayHello = function greet() {
    console.log('Hello from greet function');
};
sayHello(); 
// console.log(greet()); // 这里会报错,greet 在外部不可访问

具名函数表达式在调试和递归等场景下很有用,因为它有一个可识别的名称,有助于调试信息的可读性,以及在递归调用时可以通过函数名调用自身。

函数表达式的作用域与提升

与函数声明不同,函数表达式不会被提升。变量会被提升,但变量的值(即函数)不会。例如:

// 这里会报错,因为 multiplyNumbers 此时是 undefined
let result = multiplyNumbers(2, 3); 

let multiplyNumbers = function(a, b) {
    return a * b;
};

在上述代码中,变量 multiplyNumbers 会被提升,但由于函数表达式本身没有提升,在调用 multiplyNumbers 时,它的值还是 undefined,因此会导致错误。

箭头函数(Arrow Function)

箭头函数是ES6引入的一种更简洁的函数声明方式。其基本语法如下:

let arrowFunction = (parameters) => {
    // 函数体
    return returnValue;
};
  • 示例
let square = (num) => {
    return num * num;
};
let result = square(5);
console.log(result); // 输出 25

如果函数只有一个参数,可以省略参数的括号;如果函数体只有一条语句,并且该语句是返回值,可以省略 return 关键字和花括号。例如:

let double = num => num * 2;
let result = double(4);
console.log(result); // 输出 8

如果没有参数,需要保留空括号:

let greet = () => console.log('Hello');
greet(); 

箭头函数的特点

  1. 没有自己的 this:箭头函数不会创建自己的 this 值,它的 this 是从其外层作用域继承而来的。这与传统函数不同,传统函数在调用时会根据调用的上下文来确定 this 的值。例如:
const obj = {
    name: 'John',
    regularFunction: function() {
        return function() {
            return this.name;
        };
    },
    arrowFunction: function() {
        return () => this.name;
    }
};
let regularResult = obj.regularFunction()(); 
// 这里的 this 在内部函数中指向全局对象(在浏览器环境中是 window),所以输出 '' 或 undefined
let arrowResult = obj.arrowFunction()(); 
// 这里的 this 继承自外层函数,指向 obj,所以输出 'John'
console.log(regularResult); 
console.log(arrowResult); 
  1. 没有 arguments 对象:箭头函数没有自己的 arguments 对象。如果需要访问函数的参数,可以使用剩余参数语法。例如:
let sum = (...args) => args.reduce((acc, val) => acc + val, 0);
let result = sum(1, 2, 3);
console.log(result); // 输出 6
  1. 不能用作构造函数:箭头函数不能使用 new 关键字调用,因为它们没有 prototype 属性,也不具备构建新对象实例的能力。例如:
let MyClass = () => {};
// 以下代码会报错
// let instance = new MyClass(); 

JavaScript函数声明方式的特点比较

作用域与提升特点

  1. 函数声明:函数声明具有提升特性,在其所在作用域内可以在声明之前被调用。这使得函数的调用顺序更加灵活,但也可能导致一些潜在的问题,比如在大型代码库中,如果函数声明混乱,可能会难以理解代码的执行逻辑。函数声明创建的是一个函数作用域,在函数内部定义的变量在函数外部无法访问。
function testScope() {
    let localVar = 'Inside function';
    console.log(localVar); 
}
// console.log(localVar); // 这里会报错,localVar 只在 testScope 函数内部可见
testScope(); 
  1. 函数表达式:函数表达式不会提升函数部分,只有变量会被提升。这使得代码的执行顺序更加清晰,因为函数必须在赋值之后才能被调用。函数表达式同样创建函数作用域,内部变量外部不可访问。
let funcExpression;
// funcExpression(); // 这里会报错,funcExpression 此时为 undefined
funcExpression = function() {
    let localVar = 'Inside function expression';
    console.log(localVar); 
};
funcExpression(); 
// console.log(localVar); // 这里会报错,localVar 只在函数表达式内部可见
  1. 箭头函数:箭头函数同样不存在函数提升。其作用域规则与函数表达式类似,不过箭头函数没有自己的 thisarguments 等特性,这使得它在处理一些需要特定上下文的场景时与传统函数有所不同。箭头函数的作用域也是封闭的,内部变量外部无法访问。
let arrowFunc = () => {
    let localVar = 'Inside arrow function';
    console.log(localVar); 
};
arrowFunc(); 
// console.log(localVar); // 这里会报错,localVar 只在箭头函数内部可见

this 指向特点

  1. 函数声明:函数声明中的 this 取决于函数的调用方式。如果作为对象的方法调用,this 指向该对象;如果作为普通函数调用,this 指向全局对象(在严格模式下为 undefined)。例如:
const person = {
    name: 'Alice',
    sayName: function() {
        console.log(this.name); 
    }
};
person.sayName(); // 输出 'Alice'

function globalSay() {
    console.log(this.name); 
}
// 假设在浏览器环境中,全局对象为 window,这里的 this.name 输出 '' 或 undefined
globalSay(); 

// 在严格模式下
function strictGlobalSay() {
    'use strict';
    console.log(this); // 输出 undefined
}
strictGlobalSay(); 
  1. 函数表达式:函数表达式中的 this 同样取决于调用方式,与函数声明类似。例如:
let obj = {
    value: 10,
    getValue: function() {
        let innerFunc = function() {
            console.log(this.value); 
        };
        innerFunc(); 
    }
};
// 这里的 innerFunc 作为普通函数调用,this 指向全局对象(在浏览器环境中为 window),输出 '' 或 undefined
obj.getValue(); 
  1. 箭头函数:箭头函数没有自己的 this,它的 this 继承自外层作用域。这在很多场景下使得代码更加简洁明了,尤其是在处理回调函数时。例如:
const outerObject = {
    count: 0,
    increment: function() {
        setTimeout(() => {
            this.count++;
            console.log(this.count); 
        }, 1000);
    }
};
outerObject.increment(); 
// 这里箭头函数的 this 继承自 increment 函数的 this,指向 outerObject,所以会正确输出 1

作为构造函数的特点

  1. 函数声明:函数声明可以用作构造函数,通过 new 关键字创建对象实例。当函数作为构造函数调用时,this 指向新创建的对象实例,并且函数内部可以使用 this 来定义对象的属性和方法。例如:
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayInfo = function() {
        console.log(`Name: ${this.name}, Age: ${this.age}`); 
    };
}
let person1 = new Person('Bob', 25);
person1.sayInfo(); 
  1. 函数表达式:普通的函数表达式也可以用作构造函数,与函数声明类似。例如:
let Animal = function(type, sound) {
    this.type = type;
    this.sound = sound;
    this.makeSound = function() {
        console.log(`${this.type} makes ${this.sound}`); 
    };
};
let dog = new Animal('Dog', 'Woof');
dog.makeSound(); 
  1. 箭头函数:箭头函数不能用作构造函数,因为它没有 prototype 属性,也不会为 this 创建新的绑定。试图使用 new 关键字调用箭头函数会抛出错误。例如:
let ArrowConstructor = () => {};
// 以下代码会报错
// let arrowInstance = new ArrowConstructor(); 

语法简洁性特点

  1. 函数声明:函数声明语法相对完整,有明确的函数名、参数列表和函数体,适合定义较为复杂、功能完整的函数。例如:
function calculateArea(radius) {
    return Math.PI * radius * radius;
}
  1. 函数表达式:函数表达式语法也较为常规,与函数声明类似,但在作为回调函数等场景下,匿名函数表达式可以使代码更加简洁。例如:
let numbers = [1, 2, 3, 4];
let squaredNumbers = numbers.map(function(num) {
    return num * num;
});
  1. 箭头函数:箭头函数语法最为简洁,特别是在处理简单的回调函数或单行逻辑的函数时。例如:
let numbers = [1, 2, 3, 4];
let squaredNumbers = numbers.map(num => num * num);

这种简洁性在链式调用等场景下尤为明显,使代码更加易读。

性能特点

在性能方面,一般来说,函数声明、函数表达式和箭头函数在现代JavaScript引擎中性能差异不大。JavaScript引擎经过优化,能够高效地处理各种函数声明方式。然而,在一些极端情况下,如大量的函数调用和复杂的递归场景下,函数声明和函数表达式可能会因为其完整的函数上下文和 this 绑定机制而产生一些额外的开销,相比之下,箭头函数由于没有 this 绑定等特性,可能在某些场景下性能稍好。但这种性能差异非常微小,在大多数实际应用中可以忽略不计。

适用场景特点

  1. 函数声明:适合定义在整个模块或作用域内都需要使用的通用函数,由于其提升特性,在代码组织上可以更加灵活。例如,在一个数学计算模块中定义各种计算函数:
function add(a, b) {
    return a + b;
}
function subtract(a, b) {
    return a - b;
}
// 其他代码可以在函数声明之前调用这些函数
let result1 = add(2, 3);
let result2 = subtract(5, 1);
  1. 函数表达式:常用于需要将函数作为值传递的场景,如作为回调函数。例如,在事件处理或数组的迭代方法中:
document.getElementById('myButton').addEventListener('click', function() {
    console.log('Button clicked');
});

let numbers = [1, 2, 3];
let doubled = numbers.map(function(num) {
    return num * 2;
});
  1. 箭头函数:非常适合用于简单的回调函数,尤其是当需要保持 this 的一致性时。例如,在 mapfilterreduce 等数组方法中,以及在 setTimeoutsetInterval 等异步操作中:
let numbers = [1, 2, 3];
let squared = numbers.map(num => num * num);

setTimeout(() => {
    console.log('Time elapsed');
}, 2000);

此外,在一些只需要简单执行一段逻辑且不需要自己的 thisarguments 的场景下,箭头函数也是很好的选择。

不同声明方式在实际项目中的应用案例

函数声明在模块中的应用

在一个JavaScript模块中,我们经常使用函数声明来定义一些公共的功能函数。例如,假设我们正在开发一个工具模块,用于处理字符串和数字的转换。

// utility.js
function convertStringToNumber(str) {
    return parseFloat(str);
}

function convertNumberToString(num) {
    return num.toString();
}

export { convertStringToNumber, convertNumberToString };

在另一个模块中,我们可以使用这些函数:

// main.js
import { convertStringToNumber, convertNumberToString } from './utility.js';

let num = convertStringToNumber('123');
let str = convertNumberToString(num);
console.log(str); 

这里,函数声明使得模块中的函数在整个模块内都可访问,并且通过提升机制,可以在导入后直接使用,无需担心函数声明的顺序。

函数表达式在事件处理中的应用

在Web开发中,函数表达式常用于处理DOM事件。例如,我们有一个按钮,当点击按钮时,需要执行一些逻辑。

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Function Expression in Event Handling</title>
</head>

<body>
    <button id="myButton">Click Me</button>
    <script>
        document.getElementById('myButton').addEventListener('click', function () {
            console.log('Button clicked!');
            // 可以在这里添加更多复杂的逻辑,如更新DOM等
        });
    </script>
</body>

</html>

在这个例子中,函数表达式作为回调函数传递给 addEventListener 方法,这种方式简洁明了,适合处理简单的事件逻辑。

箭头函数在数组操作中的应用

箭头函数在数组操作中非常常用,因为其简洁的语法可以使代码更加易读。例如,我们有一个数组,需要过滤掉小于10的数字,并对剩下的数字进行平方操作。

let numbers = [5, 12, 8, 15, 3];
let squaredGreaterThanTen = numbers.filter(num => num > 10).map(num => num * num);
console.log(squaredGreaterThanTen); 

通过使用箭头函数,我们可以在一行代码中完成复杂的数组操作,使得代码的逻辑更加紧凑和清晰。

综合应用案例

假设我们正在开发一个简单的音乐播放器应用,其中需要处理播放列表、歌曲信息以及用户交互。

// song.js
function Song(title, artist, duration) {
    this.title = title;
    this.artist = artist;
    this.duration = duration;
    this.getInfo = function () {
        return `${this.title} - ${this.artist} (${this.duration}s)`;
    };
}

// playlist.js
function Playlist() {
    this.songs = [];
    this.addSong = function (song) {
        this.songs.push(song);
    };
    this.getSongTitles = function () {
        return this.songs.map(song => song.title);
    };
    this.playAll = function () {
        this.songs.forEach(song => {
            console.log(`Now playing: ${song.getInfo()}`);
            // 实际应用中这里可能会调用音频播放API
        });
    };
}

// main.js
let song1 = new Song('Song A', 'Artist 1', 180);
let song2 = new Song('Song B', 'Artist 2', 240);

let playlist = new Playlist();
playlist.addSong(song1);
playlist.addSong(song2);

console.log(playlist.getSongTitles()); 
playlist.playAll(); 

在这个案例中,我们使用函数声明来定义 SongPlaylist 构造函数,用于创建歌曲和播放列表对象。在 Playlist 类的方法中,我们使用箭头函数来简化数组操作和回调函数的编写,展示了不同函数声明方式在实际项目中的协同应用。

通过以上对JavaScript函数声明方式及其特点的详细介绍,以及实际应用案例的展示,开发者可以根据具体的需求和场景,选择最合适的函数声明方式,从而编写出更高效、易读的JavaScript代码。无论是在小型脚本还是大型项目中,正确选择函数声明方式都是优化代码结构和性能的重要一环。