深入理解 JavaScript 函数:提升编程技能的必备知识(中)

发布时间:2023年12月22日

在这里插入图片描述

🤍 前端开发工程师(主业)、技术博主(副业)、已过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 的函数,它接受三个参数:num1num2num3。其中,num3 是可选参数,并设置了默认值为 0

在调用 calculateSum 函数时,可以根据需要传递任意数量的参数。如果没有传递 num3 参数,它将使用默认值 0。这样可以使函数更加灵活和易用。

你可以根据实际需求,在函数定义中设置可选参数及其默认值,以便在调用函数时提供更方便的参数传递方式。

剩余参数和展开运算符

剩余参数是指在函数定义中,在参数列表的最后一个参数之后使用三个点 ... 表示剩余参数。在函数调用时,剩余参数将收集所有未被命名的参数,并将它们作为一个数组传递给函数。

例如,以下代码定义了一个带有剩余参数的函数 add

const add = (x, y, z, ...args) => {};

在这个例子中,xyz 是已命名的参数,而 args 是剩余参数。在函数体内,可以使用 args 来访问传递给函数的所有剩余参数。

展开运算符与剩余参数关联密切,它允许将一个数组分割,并将各个项作为分离的参数传给函数。当用在字符串或数组前面时称为扩展运算符。

例如,以下代码使用展开运算符将数组分割成多个参数传递给函数:

const arr = [1, 2, 3];
const result = Math.min(...arr);

在这个例子中,Math.min(...arr) 将数组 arr 展开为三个参数 123,并将它们传递给 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),然后返回这个和。

在使用递归函数时需要注意,由于递归函数会反复调用自身,可能会导致栈溢出。为了避免这种情况,可以使用迭代或其他更高效的算法来解决问题。

递归的注意事项和优化

在使用递归时,需要注意以下几点:

  1. 递归深度:递归函数可能会产生大量的调用,导致栈溢出。为了避免这种情况,需要限制递归的深度。
  2. 终止条件:递归函数必须有明确的终止条件,否则程序将无限循环并导致栈溢出。
  3. 递归效率:递归函数的效率可能较低,因为它需要重复执行相同的操作。在可能的情况下,尽量使用迭代或其他更高效的算法来替代递归。
  4. 内存消耗:递归函数可能会消耗大量的内存,因为每次调用都会创建新的栈帧。在处理大数据量时,需要注意内存使用情况。

在这里插入图片描述

为了优化递归函数,可以考虑以下几点:

  1. 尾递归优化:如果递归函数的最后一个操作是调用自身,可以使用尾递归优化来避免重复创建栈帧。许多编程语言(如 JavaScript)都支持尾递归优化。
  2. 记忆化搜索:对于一些递归问题,可以使用记忆化搜索来避免重复计算。记忆化搜索将已经计算过的结果存储起来,以便在下次遇到相同的情况时直接返回结果,而不必再次递归计算。
  3. 迭代替代:如果可能的话,尽量使用迭代来替代递归。迭代通常比递归更高效,并且可以避免栈溢出的问题。

总之,在使用递归时需要谨慎考虑,并根据具体情况进行优化。如果递归导致性能问题或栈溢出,可以考虑使用其他更高效的算法来解决问题。

六、函数作为对象

函数的属性和方法

在 JavaScript 中,函数作为一种对象,也具有一些属性和方法。以下是一些常见的函数属性和方法:

  1. length 属性:返回函数的形参数量。
  2. name 属性:返回函数的名称。
  3. apply() 方法:调用一个函数,并将其参数作为一个数组进行传递。它可以改变函数的执行上下文。
  4. call() 方法:与 apply() 方法类似,但它还可以指定函数的执行上下文。
  5. 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 中,函数的调用和构造是两个不同的概念。

函数的调用是指执行函数的代码,传递所需的参数,并获取函数的返回值。调用函数的方式有以下几种:

  1. 直接调用:使用函数名后面跟着括号,传递所需的参数。
function sum(num1, num2) {
  return num1 + num2;
}

const result = sum(10, 20);
console.log(result); 
  1. 作为方法调用:如果函数是对象的方法,可以通过对象的点运算符调用。
const obj = {
  sum: function(num1, num2) {
    return num1 + num2;
  }
};

const result = obj.sum(10, 20);
console.log(result); 
  1. 作为构造函数调用:当使用 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 构造函数或函数表达式来创建函数对象。

  1. 使用 Function 构造函数:
const sum = new Function("num1", "num2", "return num1 + num2");
console.log(sum(10, 20)); 
  1. 使用函数表达式:
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() 时,它将输出修改后的问候语

文章来源:https://blog.csdn.net/weixin_42554191/article/details/135160319
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。