🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6
🍨 阿珊和她的猫_CSDN个人主页
🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》
🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入门到实战全面掌握 uni-app》
文章目录
- 四、函数的参数传递
- 按值传递和按引用传递
- 可选参数和默认值
- 剩余参数和展开运算符
- 五、函数的递归
- 递归函数的定义和示例
- 递归的注意事项和优化
- 六、函数作为对象
- 函数的属性和方法
- 函数的调用和构造
- 函数的原型和原型链
四、函数的参数传递
按值传递和按引用传递
在 JavaScript 中,函数参数的传递方式有两种:按值传递和按引用传递。
按值传递是指将实参的值复制一份传递给函数,函数内部对参数的修改不会影响到实参。示例如下:
function changeValue(num) {
num = 100;
}
let num = 50;
changeValue(num);
console.log(num);
在上述代码中,定义了一个 changeValue
函数,它接收一个参数 num
。在函数内部,将 num
的值修改为 100
。然后,在函数外部定义了一个变量 num
,并将其初始化为 50
。最后,调用 changeValue
函数并传递 num
作为参数。输出结果仍然是 50
,而不是 100
。
按引用传递是指将实参的引用传递给函数,函数内部对参数的修改会影响到实参。在 JavaScript 中,基本数据类型(如字符串、数字、布尔值等)是按值传递的,而对象(包括数组、对象等)是按引用传递的。示例如下:
function changeObj(obj) {
obj.name = "张三";
}
let person = { name: "李四" };
changeObj(person);
console.log(person.name);
在上述代码中,定义了一个 changeObj
函数,它接收一个参数 obj
,并将其作为对象进行修改。在函数外部定义了一个对象 person
,并将其初始化为 { name: "李四" }
。最后,调用 changeObj
函数并传递 person
作为参数。输出结果为 张三
,说明函数内部对对象的修改会影响到实参。
需要注意的是,在 JavaScript 中,按引用传递只针对对象,而不是基本数据类型。对于基本数据类型,无论函数内部如何修改参数,都不会影响到实参。
可选参数和默认值
在 JavaScript 中,函数的可选参数允许在调用函数时省略一些参数,而默认值则是为可选参数提供的预定义值。当没有传递可选参数时,将使用默认值。
以下是一个示例,展示了如何定义和使用带有可选参数和默认值的函数:
function calculateSum(num1, num2, num3 = 0) {
return num1 + num2 + num3;
}
console.log(calculateSum(10, 20));
console.log(calculateSum(10, 20, 30));
在上述示例中,定义了一个名为 calculateSum
的函数,它接受三个参数:num1
、num2
和 num3
。其中,num3
是可选参数,并设置了默认值为 0
。
在调用 calculateSum
函数时,可以根据需要传递任意数量的参数。如果没有传递 num3
参数,它将使用默认值 0
。这样可以使函数更加灵活和易用。
你可以根据实际需求,在函数定义中设置可选参数及其默认值,以便在调用函数时提供更方便的参数传递方式。
剩余参数和展开运算符
剩余参数是指在函数定义中,在参数列表的最后一个参数之后使用三个点 ...
表示剩余参数。在函数调用时,剩余参数将收集所有未被命名的参数,并将它们作为一个数组传递给函数。
例如,以下代码定义了一个带有剩余参数的函数 add
:
const add = (x, y, z, ...args) => {};
在这个例子中,x
、y
和 z
是已命名的参数,而 args
是剩余参数。在函数体内,可以使用 args
来访问传递给函数的所有剩余参数。
展开运算符与剩余参数关联密切,它允许将一个数组分割,并将各个项作为分离的参数传给函数。当用在字符串或数组前面时称为扩展运算符。
例如,以下代码使用展开运算符将数组分割成多个参数传递给函数:
const arr = [1, 2, 3];
const result = Math.min(...arr);
在这个例子中,Math.min(...arr)
将数组 arr
展开为三个参数 1
、2
和 3
,并将它们传递给 Math.min
函数。
五、函数的递归
递归函数的定义和示例
递归函数是一种在函数定义中使用函数自身的函数。它通过反复调用自身来解决问题,直到达到某个终止条件。
递归函数的定义通常包括两个部分:递归步骤和终止条件。
以下是一个使用递归函数计算斐波那契数列的前 n
项的示例:
function fibonacci(n) {
if (n <= 1) {
return n;
} else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
在这个示例中,定义了一个名为 fibonacci
的递归函数,它接受一个整数参数 n
。如果 n
小于等于 1,则直接返回 n
,因为斐波那契数列的前两项都是 1。否则,通过调用自身来计算前两项的和,即 fibonacci(n - 1) + fibonacci(n - 2)
,然后返回这个和。
在使用递归函数时需要注意,由于递归函数会反复调用自身,可能会导致栈溢出。为了避免这种情况,可以使用迭代或其他更高效的算法来解决问题。
递归的注意事项和优化
在使用递归时,需要注意以下几点:
- 递归深度:递归函数可能会产生大量的调用,导致栈溢出。为了避免这种情况,需要限制递归的深度。
- 终止条件:递归函数必须有明确的终止条件,否则程序将无限循环并导致栈溢出。
- 递归效率:递归函数的效率可能较低,因为它需要重复执行相同的操作。在可能的情况下,尽量使用迭代或其他更高效的算法来替代递归。
- 内存消耗:递归函数可能会消耗大量的内存,因为每次调用都会创建新的栈帧。在处理大数据量时,需要注意内存使用情况。
为了优化递归函数,可以考虑以下几点:
- 尾递归优化:如果递归函数的最后一个操作是调用自身,可以使用尾递归优化来避免重复创建栈帧。许多编程语言(如 JavaScript)都支持尾递归优化。
记忆化搜索:对于一些递归问题,可以使用记忆化搜索来避免重复计算
。记忆化搜索将已经计算过的结果存储起来,以便在下次遇到相同的情况时直接返回结果,而不必再次递归计算。- 迭代替代:如果可能的话,尽量使用迭代来替代递归。迭代通常比递归更高效,并且可以避免栈溢出的问题。
总之,在使用递归时需要谨慎考虑,并根据具体情况进行优化。如果递归导致性能问题或栈溢出,可以考虑使用其他更高效的算法来解决问题。
六、函数作为对象
函数的属性和方法
在 JavaScript 中,函数作为一种对象,也具有一些属性和方法。以下是一些常见的函数属性和方法:
length
属性:返回函数的形参数量。name
属性:返回函数的名称。apply()
方法:调用一个函数,并将其参数作为一个数组进行传递。它可以改变函数的执行上下文。call()
方法:与apply()
方法类似,但它还可以指定函数的执行上下文。bind()
方法:创建一个新的函数,该函数的this
对象被绑定到指定的值,并将原始函数的参数作为新函数的参数。
以下是一个示例,展示了如何使用这些属性和方法:
function sum(num1, num2) {
return num1 + num2;
}
// 使用 length 属性
console.log(sum.length);
// 使用 name 属性
console.log(sum.name);
// 使用 apply() 方法
const result = sum.apply(null, [10, 20]);
console.log(result);
// 使用 call() 方法
const result = sum.call(null, 10, 20);
console.log(result);
// 使用 bind() 方法
const boundSum = sum.bind(null, 10);
const result = boundSum(20);
console.log(result);
在上述示例中,我们定义了一个名为 sum
的函数,它接受两个参数并返回它们的和。然后,我们使用 length
属性和 name
属性来获取函数的参数数量和名称。接下来,我们使用 apply()
方法和 call()
方法来调用函数,并将参数作为数组传递。最后,我们使用 bind()
方法创建了一个新的函数 boundSum
,它的 this
对象被绑定到 null
,并将第一个参数固定为 10
。然后,我们调用 boundSum
函数并传递 20
作为第二个参数。
函数的调用和构造
在 JavaScript 中,函数的调用和构造是两个不同的概念。
函数的调用是指执行函数的代码,传递所需的参数,并获取函数的返回值。调用函数的方式有以下几种:
- 直接调用:使用函数名后面跟着括号,传递所需的参数。
function sum(num1, num2) {
return num1 + num2;
}
const result = sum(10, 20);
console.log(result);
- 作为方法调用:如果函数是对象的方法,可以通过对象的点运算符调用。
const obj = {
sum: function(num1, num2) {
return num1 + num2;
}
};
const result = obj.sum(10, 20);
console.log(result);
- 作为构造函数调用:当使用
new
关键字调用函数时,它将创建一个新的对象。
function Person(name, age) {
this.name = name;
this.age = age;
}
const person = new Person("张三", 24);
console.log(person.name);
console.log(person.age);
函数的构造是指创建一个新的函数对象。在 JavaScript 中,可以使用 Function
构造函数或函数表达式来创建函数对象。
- 使用
Function
构造函数:
const sum = new Function("num1", "num2", "return num1 + num2");
console.log(sum(10, 20));
- 使用函数表达式:
const sum = function(num1, num2) {
return num1 + num2;
};
console.log(sum(10, 20));
无论是通过调用还是构造函数创建的函数,它们都可以执行相同的任务,并且具有相同的特性和行为。选择哪种方式取决于你的需求和代码结构。
函数的原型和原型链
在 JavaScript 中,每个函数都有一个原型对象(prototype
),它包含可以被特定类型的所有实例共享的属性和方法。当创建一个函数时,JavaScript 会自动为该函数创建一个原型对象,并将其赋值给函数的 prototype
属性。
原型对象上的属性和方法可以被实例继承。当调用实例的某个方法时,如果该方法在实例自身的属性上找不到,JavaScript 会自动沿着原型链向上查找,直到找到该方法为止。如果最终没有找到该方法,则会返回 undefined
。
以下是一个示例,展示了原型和原型链的工作原理:
function Person(name) {
this.name = name;
}
// 在原型对象上添加方法
Person.prototype.sayHello = function() {
console.log("Hello, my name is " + this.name);
}
const person1 = new Person("张三");
person1.sayHello();
// 修改原型对象上的方法
Person.prototype.sayHello = function() {
console.log("Hello, my name is " + this.name + "! How are you today?");
}
person1.sayHello();
在这个示例中,首先创建了一个名为 Person
的函数,它接收一个参数 name
,并在实例上创建了一个名为 name
的属性。然后,在原型对象上添加了一个名为 sayHello
的方法。接着,创建了一个 Person
实例 person1
,并调用了 sayHello
方法。
当修改原型对象上的方法时,所有的实例都会自动获取到修改后的方法。因此,当再次调用 person1.sayHello()
时,它将输出修改后的问候语