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

JavaScript函数的定义与调用方式

2024-08-034.2k 阅读

JavaScript函数的定义方式

函数声明定义方式

在JavaScript中,函数声明是最常见的定义函数的方式之一。它使用 function 关键字,后面跟着函数名,函数名后面是一对括号,括号内可以定义参数,最后是函数体,用花括号包裹。

function addNumbers(num1, num2) {
    return num1 + num2;
}

在上述代码中,addNumbers 就是函数名,num1num2 是函数的参数。函数体中的 return 语句用于返回计算结果。当函数执行到 return 语句时,函数会立即停止执行,并将 return 后面的值返回给调用者。

函数声明具有函数提升的特性。这意味着在代码执行之前,JavaScript引擎会将函数声明提升到当前作用域的顶部。所以,即使函数声明在调用之后,也能正常调用。例如:

console.log(addNumbers(2, 3));

function addNumbers(num1, num2) {
    return num1 + num2;
}

在这段代码中,虽然 addNumbers 函数的调用在函数声明之前,但仍然能够正常输出结果 5。这是因为函数声明被提升到了作用域的顶部,就好像代码是这样写的:

function addNumbers(num1, num2) {
    return num1 + num2;
}
console.log(addNumbers(2, 3));

函数表达式定义方式

函数表达式是将函数定义作为一个表达式的值。有两种常见的函数表达式形式:匿名函数表达式和具名函数表达式。

匿名函数表达式

匿名函数,即没有函数名的函数。它通常被赋值给一个变量。

const multiplyNumbers = function(num1, num2) {
    return num1 * num2;
};

在上述代码中,function(num1, num2) {... } 就是一个匿名函数表达式,它被赋值给了 multiplyNumbers 变量。与函数声明不同,函数表达式不会被提升。如果在变量声明之前调用函数,会导致 ReferenceError。例如:

console.log(multiplyNumbers(2, 3)); // 报错:ReferenceError: multiplyNumbers is not defined
const multiplyNumbers = function(num1, num2) {
    return num1 * num2;
};

这是因为变量声明会被提升,但变量的赋值并不会被提升。这里变量 multiplyNumbers 虽然被提升到了作用域顶部,但在调用时它的值还未被赋值,所以会报错。

具名函数表达式

具名函数表达式是给匿名函数表达式一个名字。虽然它有名字,但这个名字只能在函数内部使用。

const divideNumbers = function innerDivide(num1, num2) {
    if (num2 === 0) {
        return '不能除以零';
    }
    return num1 / num2;
};

在这个例子中,innerDivide 是函数的名字。它可以在函数内部用于递归调用等场景。例如:

const factorial = function innerFactorial(n) {
    if (n === 0 || n === 1) {
        return 1;
    }
    return n * innerFactorial(n - 1);
};
console.log(factorial(5)); // 输出:120

这里 innerFactorial 函数在自身内部调用自己,实现了阶乘的计算。具名函数表达式的名字在函数外部是不可见的,主要用于函数内部的自我引用。

使用Function构造函数定义函数

Function 构造函数允许通过传递字符串形式的参数和函数体来创建函数。语法如下:

const greet = new Function('name', 'console.log("Hello, " + name + "!");');
greet('John'); // 输出:Hello, John!

在上述代码中,new Function('name', 'console.log("Hello, " + name + "!");') 创建了一个新的函数。第一个参数 'name' 是函数的参数,第二个参数是函数体。虽然这种方式可以创建函数,但并不推荐在实际开发中大量使用。因为使用字符串来定义函数体,会失去代码编辑器的语法检查和代码提示功能,并且性能相对较差。例如:

// 错误示范,很难发现语法错误
const wrongFunction = new Function('a', 'b', 'return a + b);'); 
wrongFunction(2, 3); // 会报错,因为函数体少了一个括号

在实际开发中,除非有特殊需求,比如动态生成函数代码等场景,一般不会使用 Function 构造函数来定义函数。

JavaScript函数的调用方式

作为普通函数调用

当函数作为普通函数调用时,直接在函数名后面跟上括号,并在括号内传入相应的参数(如果有参数的话)。前面提到的函数声明和函数表达式定义的函数都可以这样调用。

function sayHello(name) {
    console.log('Hello, ' + name + '!');
}
sayHello('Alice'); // 输出:Hello, Alice!

const sayGoodbye = function(name) {
    console.log('Goodbye, ' + name + '!');
};
sayGoodbye('Bob'); // 输出:Goodbye, Bob!

在这种调用方式下,函数内部的 this 指向全局对象(在浏览器环境中是 window,在Node.js环境中是 global)。例如:

function showThis() {
    console.log(this);
}
showThis(); // 在浏览器环境中输出 window 对象

这是因为在普通函数调用中,this 的绑定是在函数调用时确定的,并且默认指向全局对象。但需要注意的是,在严格模式下,普通函数调用时 this 会是 undefined。例如:

function strictShowThis() {
    'use strict';
    console.log(this);
}
strictShowThis(); // 输出 undefined

作为对象方法调用

当函数作为对象的属性时,它被称为对象的方法。调用对象方法时,使用对象名加上点号,再跟上方法名和参数。

const person = {
    name: 'Charlie',
    greet: function() {
        console.log('Hello, I am ' + this.name + '!');
    }
};
person.greet(); // 输出:Hello, I am Charlie!

在这种调用方式下,函数内部的 this 指向调用该方法的对象。即 person.greet() 调用时,greet 函数内部的 this 指向 person 对象。这使得方法可以访问对象的属性。例如:

const calculator = {
    num1: 5,
    num2: 3,
    add: function() {
        return this.num1 + this.num2;
    },
    subtract: function() {
        return this.num1 - this.num2;
    }
};
console.log(calculator.add()); // 输出:8
console.log(calculator.subtract()); // 输出:2

这里 addsubtract 方法通过 this 访问了 calculator 对象的 num1num2 属性。

使用call()方法调用函数

call() 方法允许显式地设置函数内部 this 的指向,并立即调用该函数。语法为 function.call(thisArg, arg1, arg2, ...),其中 thisArg 是要设置为函数内部 this 的值,后面的参数是要传递给函数的参数。

function introduce() {
    console.log('Hi, I am ' + this.name + '.');
}
const user1 = { name: 'David' };
const user2 = { name: 'Eve' };
introduce.call(user1); // 输出:Hi, I am David.
introduce.call(user2); // 输出:Hi, I am Eve.

在上述代码中,通过 call() 方法,将 introduce 函数内部的 this 分别指向了 user1user2 对象。call() 方法的一个常见用途是借用其他对象的方法。例如,数组有一个 push 方法用于向数组末尾添加元素。我们可以借用 push 方法来向类数组对象添加元素。

const arrayLike = {
    0: 'apple',
    1: 'banana',
    length: 2
};
Array.prototype.push.call(arrayLike, 'cherry');
console.log(arrayLike); // 输出:{0: "apple", 1: "banana", 2: "cherry", length: 3}

这里通过 Array.prototype.push.call(arrayLike, 'cherry'),借用了数组的 push 方法,将 'cherry' 添加到了 arrayLike 对象中,并且更新了 length 属性。

使用apply()方法调用函数

apply() 方法与 call() 方法类似,也是用于显式设置函数内部 this 的指向并调用函数。不同之处在于,apply() 方法的第二个参数是一个数组(或类数组对象),包含要传递给函数的参数。语法为 function.apply(thisArg, [arg1, arg2, ...])

function sumNumbers() {
    let sum = 0;
    for (let i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }
    return sum;
}
const numbers = [1, 2, 3, 4, 5];
console.log(sumNumbers.apply(null, numbers)); // 输出:15

在上述代码中,sumNumbers.apply(null, numbers)numbers 数组作为参数传递给 sumNumbers 函数。null 作为 thisArg,在这种情况下,sumNumbers 函数内部的 this 指向全局对象(在非严格模式下)。如果在严格模式下,this 会是 nullapply() 方法也常用于借用其他对象的方法,特别是当参数是数组时,使用 apply() 会更方便。例如,我们要找到数组中的最大值,可以借用 Math.max 方法。

const values = [10, 20, 30, 40];
const maxValue = Math.max.apply(null, values);
console.log(maxValue); // 输出:40

这里通过 Math.max.apply(null, values),将 values 数组中的值作为参数传递给 Math.max 方法,从而得到数组中的最大值。

使用bind()方法调用函数

bind() 方法用于创建一个新的函数,这个新函数内部的 this 被绑定到指定的值。语法为 function.bind(thisArg, arg1, arg2, ...)bind() 方法不会立即调用函数,而是返回一个新的函数,新函数在调用时,内部的 this 会指向 bind() 方法传入的 thisArg

function printDetails() {
    console.log('Name: ' + this.name + ', Age: ' + this.age);
}
const user = { name: 'Frank', age: 30 };
const boundFunction = printDetails.bind(user);
boundFunction(); // 输出:Name: Frank, Age: 30

在上述代码中,printDetails.bind(user) 创建了一个新的函数 boundFunctionboundFunction 内部的 this 被绑定到了 user 对象。所以当调用 boundFunction() 时,会输出正确的用户信息。bind() 方法还可以用于部分应用,即预先设置函数的一些参数。例如:

function multiply(a, b) {
    return a * b;
}
const multiplyByTwo = multiply.bind(null, 2);
console.log(multiplyByTwo(5)); // 输出:10

这里 multiply.bind(null, 2) 创建了一个新的函数 multiplyByTwo,它将 multiply 函数的第一个参数固定为 2。所以当调用 multiplyByTwo(5) 时,实际上是调用 multiply(2, 5),得到结果 10

自调用函数(立即调用函数表达式 - IIFE)

自调用函数,也称为立即调用函数表达式(IIFE),是一种在定义后立即执行的函数。它有两种常见的形式:

// 形式一
(function() {
    console.log('This is an IIFE');
})();

// 形式二
(function() {
    console.log('This is also an IIFE');
})();

在形式一中,函数表达式被包裹在括号内,然后紧跟一对括号用于调用该函数。在形式二中,整个函数表达式和调用括号被包裹在括号内。这两种形式的效果是一样的,都是在定义后立即执行函数。IIFE常用于创建一个独立的作用域,避免变量污染全局作用域。例如:

(function() {
    let localVar = 'This is a local variable';
    console.log(localVar);
})();
// console.log(localVar); // 报错:localVar is not defined

在上述代码中,localVar 变量只在IIFE内部有效,外部无法访问,从而避免了与全局变量的冲突。此外,IIFE还可以接受参数。例如:

(function(message) {
    console.log(message);
})('Hello from IIFE'); // 输出:Hello from IIFE

这里 ('Hello from IIFE') 就是传递给IIFE的参数,在IIFE内部可以使用这个参数。

递归调用

递归是指函数在其函数体内调用自身的一种编程技巧。递归函数通常需要有一个终止条件,否则会导致无限循环。例如,计算阶乘可以使用递归实现:

function factorial(n) {
    if (n === 0 || n === 1) {
        return 1;
    }
    return n * factorial(n - 1);
}
console.log(factorial(5)); // 输出:120

在上述代码中,factorial 函数在 n 大于 1 时,会调用自身并传入 n - 1,直到 n 等于 01 时返回 1,从而终止递归。递归在处理一些具有递归结构的数据,如树结构时非常有用。例如,遍历二叉树可以使用递归实现:

function TreeNode(val) {
    this.val = val;
    this.left = this.right = null;
}

function inorderTraversal(root) {
    if (!root) {
        return [];
    }
    let result = [];
    result = result.concat(inorderTraversal(root.left));
    result.push(root.val);
    result = result.concat(inorderTraversal(root.right));
    return result;
}

// 创建一个简单的二叉树
let root = new TreeNode(1);
root.right = new TreeNode(2);
root.right.left = new TreeNode(3);

console.log(inorderTraversal(root)); // 输出:[1, 3, 2]

在这个例子中,inorderTraversal 函数递归地遍历二叉树的左子树、访问根节点、再递归地遍历右子树,从而实现中序遍历。递归虽然强大,但如果处理不当,可能会导致栈溢出错误。例如,如果递归层次过深,JavaScript引擎的调用栈可能会被填满。为了避免这种情况,可以考虑使用迭代(循环)来替代递归,或者使用尾递归优化(在支持的环境中)。

作为构造函数调用

在JavaScript中,函数可以作为构造函数来创建对象。使用 new 关键字调用函数时,该函数就作为构造函数。构造函数内部的 this 指向新创建的对象。构造函数通常用于初始化对象的属性。例如:

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayHello = function() {
        console.log('Hello, I am ' + this.name + ' and I am ' + this.age + ' years old.');
    };
}
const tom = new Person('Tom', 25);
tom.sayHello(); // 输出:Hello, I am Tom and I am 25 years old.

在上述代码中,Person 函数作为构造函数,使用 new 关键字调用它创建了一个新的 Person 对象 tomPerson 函数内部通过 this 为新对象添加了 nameage 属性和 sayHello 方法。当使用 new 关键字调用构造函数时,会发生以下几件事:

  1. 创建一个新的空对象。
  2. 这个新对象的 __proto__ 属性会指向构造函数的 prototype 对象。
  3. 构造函数内部的 this 指向这个新创建的对象。
  4. 执行构造函数的函数体,为新对象添加属性和方法。
  5. 如果构造函数没有显式返回一个对象,那么 new 表达式会返回这个新创建的对象。如果构造函数显式返回了一个对象,那么 new 表达式会返回这个显式返回的对象。例如:
function AnotherPerson(name) {
    this.name = name;
    return { message: 'This is a different object' };
}
const person1 = new AnotherPerson('Jerry');
console.log(person1.message); // 输出:This is a different object

在这个例子中,AnotherPerson 构造函数显式返回了一个对象,所以 new AnotherPerson('Jerry') 返回的是这个显式返回的对象,而不是通过 this 创建的对象。

箭头函数的调用方式

箭头函数是ES6引入的一种新的函数定义方式,它有更简洁的语法。箭头函数没有自己的 thisargumentssupernew.target。它的 this 继承自外层作用域。箭头函数的调用方式与普通函数类似,但由于其 this 的特殊性,在一些场景下需要特别注意。

作为普通函数调用

const square = num => num * num;
console.log(square(5)); // 输出:25

这里 num => num * num 是一个箭头函数,它接受一个参数 num,并返回 num 的平方。直接调用 square(5) 就可以得到结果。

作为对象方法调用

箭头函数作为对象方法时,由于其 this 继承自外层作用域,可能会导致一些与预期不符的行为。例如:

const obj = {
    name: 'Alex',
    greet: () => console.log('Hello, ' + this.name + '!')
};
obj.greet(); // 输出:Hello, undefined!

这里期望 obj.greet() 输出 Hello, Alex!,但实际上输出了 Hello, undefined!。这是因为箭头函数的 this 指向外层作用域(这里是全局作用域,在浏览器中 this.nameundefined),而不是指向 obj 对象。如果要在对象方法中使用箭头函数并正确访问对象属性,可以将 this 保存到一个变量中,然后在箭头函数中使用这个变量。例如:

const obj2 = {
    name: 'Ben',
    greet: function() {
        const self = this;
        return () => console.log('Hello, ' + self.name + '!');
    }
};
const greeting = obj2.greet();
greeting(); // 输出:Hello, Ben!

在这个例子中,obj2.greet 是一个普通函数,它内部的 this 指向 obj2 对象。通过 const self = thisthis 保存到 self 变量中,然后在箭头函数中使用 self.name,就可以正确访问 obj2 对象的 name 属性。

箭头函数与其他调用方式的结合

箭头函数同样可以与 call()apply()bind() 等方法结合使用,但由于其 this 是继承自外层作用域,这些方法实际上不会改变箭头函数内部的 this 指向。例如:

const multiplyArrow = (a, b) => a * b;
const result1 = multiplyArrow.call(null, 2, 3);
const result2 = multiplyArrow.apply(null, [2, 3]);
const boundMultiply = multiplyArrow.bind(null, 2);

console.log(result1); // 输出:6
console.log(result2); // 输出:6
console.log(boundMultiply(3)); // 输出:6

这里 call()apply()bind() 方法只是传递参数的作用,并没有改变箭头函数内部的 this 指向。因为箭头函数的 this 已经由外层作用域决定了。

通过对JavaScript函数的定义与调用方式的详细了解,开发者可以更好地运用函数来构建复杂而高效的JavaScript程序,无论是在前端网页开发还是后端Node.js开发中,都能更加灵活地实现各种功能需求。同时,理解函数定义和调用方式中的细节,如 this 的指向、函数提升等特性,对于避免编程错误和优化代码也非常关键。在实际项目中,应根据具体的场景选择合适的函数定义和调用方式,以提高代码的可读性、可维护性和性能。