当前位置:   article > 正文

JavaScript青少年简明教程:函数及其相关知识(下)

JavaScript青少年简明教程:函数及其相关知识(下)

JavaScript青少年简明教程:函数及其相关知识(下)

继续上一节介绍函数相关知识。

箭头函数(Arrow Function)

箭头函数是 ES6(ECMAScript 2015)及更高版本中引入的语法,用于简化函数定义。箭头函数使用=>符号来定义函数:

JavaScript箭头函数(arrow functions)是一种简洁的函数定义方式。它们提供了一种更简洁的语法来创建匿名函数,同时不绑定自己的this值,非常适合回调函数或需要保持上下文的场景。

箭头函数的基本语法如下:

(param1, param2, ..., paramN) => { statements }

或者,对于只有一个参数和单一表达式的箭头函数,可以省略参数括号和花括号:

singleParam => expression

对于没有参数的箭头函数,需要使用空括号:

() => { statements }

☆简单箭头函数,例如:

  1. const add = (a, b) => {
  2. return a + b;
  3. };
  4. console.log(add(2, 3)); // 输出 5

☆省略花括号和return关键字

当函数体只有一个表达式时,可以省略花括号和return关键字,例如:

  1. const multiply = (a, b) => a * b;
  2. console.log(multiply(2, 3)); // 输出 6

☆单一参数时省略参数括号,例如:

  1. const square = x => x * x;
  2. console.log(square(4)); // 输出 16

【提示:
const square = x => x * x;
等同于:
const square = function(x) {
    return x * x;
}; 】

☆没有参数的箭头函数,要有空括号,例如:

  1. const greet = () => console.log('Hello, World!');
  2. greet(); // 输出 Hello, World!

【提示:
const greet = () => console.log('Hello, World!');
等同于:
const greet = function() {
    console.log('Hello, World!');
}; 】
 

注意,箭头函数不绑定this

箭头函数不会创建自己的this值,它会捕获其所在上下文的this值。

箭头函数不绑定自己的this,而是继承定义时的上下文this。这意味着箭头函数的this是静态的,不会因为调用方式的不同而改变。

当与传统函数进行对比时,可以更清楚地理解箭头函数不绑定自己的this的行为。下面通过对比来说明这一点:

  1. // 传统函数
  2. function traditionalFunction() {
  3. console.log(this);
  4. }
  5. // 箭头函数
  6. const arrowFunction = () => {
  7. console.log(this);
  8. }
  9. const obj = {
  10. value: 42,
  11. traditional: traditionalFunction,
  12. arrow: arrowFunction
  13. };
  14. // 作为对象方法调用
  15. obj.traditional(); // 输出 obj 对象
  16. obj.arrow(); // 输出 obj 对象
  17. // 全局作用域中调用
  18. traditionalFunction(); // 输出全局对象(浏览器中是 window)
  19. arrowFunction(); // 输出全局对象(浏览器中是 window)
  20. // 显式绑定
  21. const explicitBindObj = { value: 99 };
  22. traditionalFunction.call(explicitBindObj); // 输出 explicitBindObj 对象
  23. arrowFunction.call(explicitBindObj); // 输出全局对象(浏览器中是 window)

从上面的例子可以看出,在传统函数中,this的值取决于函数的调用方式,而在箭头函数中,无论是作为对象的方法调用还是在全局作用域中调用,它们的this都是继承定义时的上下文,而不是根据调用方式而改变。

在对象方法调用中,传统函数的this指向调用该方法的对象,而箭头函数的this仍然是继承定义时的上下文。在全局作用域中调用时,传统函数的this是全局对象,而箭头函数的this也是继承自全局作用域的。

函数参数和返回值说明

1 默认参数

在JavaScript中,函数可以使用默认参数(Default Parameters)来指定在调用函数时没有提供某个参数值时的默认值。从ECMAScript 2015(ES6)开始,这个功能就被引入了。

默认参数在函数定义中的参数列表内部设置,如果调用函数时没有提供该参数的值,则使用默认值。以下是默认参数的基本用法:

  1. function greet(name = 'World') {
  2. console.log(`Hello, ${name}!`);
  3. }
  4. // 调用函数时没有提供name参数
  5. greet(); // 输出: Hello, World!
  6. // 调用函数时提供了name参数
  7. greet('Alice'); // 输出: Hello, Alice!

还可以在函数定义时使用先前定义的参数作为默认参数的值,例如:

  1. function add(a, b = a) {
  2. return a + b;
  3. }
  4. console.log(add(2, 3)); // 输出 5
  5. console.log(add(5)); // 输出 10,b的值默认为a的值,即5

在上述示例中,参数b的默认值使用了参数a的值,当只传递一个参数时,b将自动取a的值,实现了参数间的默认关联。

2 剩余参数(Rest Parameters)

这点前面已提到过。在JavaScript中,特别是从ECMAScript 2015(ES6)开始,你可以使用剩余参数(Rest Parameters)来收集一个函数中被视为单个参数的多个值到一个数组中。这在你不知道将有多少个参数传递给函数时非常有用。

剩余参数使用三个点(...)语法,并且必须作为函数参数的最后一个参数。这允许你将一个不定数量的参数作为数组处理。

示例:

  1. function sum(...numbers) {
  2. let total = 0;
  3. for (let number of numbers) {
  4. total += number;
  5. }
  6. return total;
  7. }
  8. console.log(sum(1, 2, 3, 4)); // 输出 10
  9. console.log(sum(5, 10, 15)); // 输出 30

在上述示例中,sum函数使用剩余参数...numbers来收集所有传入的参数,并将它们存储在名为numbers的数组中。然后,通过遍历数组中的元素,可以对参数进行处理。

需要注意的是,剩余参数只能出现在函数的最后一个参数位置,因为它会将所有未匹配的参数收集到一个数组中。如果在剩余参数之后还定义了其他参数,将会引发语法错误。

3 返回值

JavaScript函数的返回值是指函数执行完毕后返回给调用者的值。使用return语句可以指定函数的返回值。当函数执行到return语句时,函数将停止执行,并将指定的值返回给调用者。

以下是一些关于JavaScript函数返回值的要点:

1)使用return语句指定返回值:

  1. function multiply(a, b) {
  2. return a * b;
  3. }
  4. let result = multiply(3, 4);
  5. console.log(result); // 输出 12

在上述示例中,multiply函数使用return语句返回两个参数的乘积。调用函数时,返回值被赋给变量result。

2)函数可以有多个return语句,但只有一个会被执行:

  1. function getGreeting(name) {
  2. if (name) {
  3. return 'Hello, ' + name + '!';
  4. }
  5. return 'Hello, Guest!';
  6. }
  7. console.log(getGreeting('Alice')); // 输出 "Hello, Alice!"
  8. console.log(getGreeting()); // 输出 "Hello, Guest!"

在上述示例中,getGreeting函数根据传入的参数name返回不同的问候语。如果name存在,则返回个性化的问候语;否则,返回默认的问候语。

3)如果函数没有显式的return语句,或者return语句后面没有指定值,函数将返回undefined:

  1. function doSomething() {
  2. // 没有return语句
  3. }
  4. let result = doSomething();
  5. console.log(result); // 输出 undefined

4)函数执行完return语句后会立即停止执行,return语句后面的代码不会被执行:

  1. function processData(data) {
  2. if (!data) {
  3. return; // 如果没有数据,立即返回
  4. }
  5. // 处理数据的逻辑
  6. console.log('Processing data...');
  7. }
  8. processData(null);

在上述示例中,如果data为假值(例如null或undefined),函数会立即返回,不会执行后续的数据处理逻辑。

5)返回值可以是任意类型,包括基本类型(如数字、字符串、布尔值等)和引用类型(如对象、数组、函数等)。

返回值允许函数将计算结果、处理后的数据或其他有意义的值传递给调用者,使函数具有更强的可重用性和灵活性。

提示,本节中后面的内容,初学者对可以先不必深究,作为完整性,有一个初步认识即可。

构造函数(Constructor Function)

JavaScript的构造函数是一种特殊的函数,用于创建和初始化对象。它们通常与new关键字一起使用,以创建对象的新实例。

构造函数可以定义对象的属性和方法。使用this关键字来设置对象的属性和方法。

在构造函数内部,this 指向新创建的对象。

构造函数通常不需要返回值,它会自动返回创建的对象。

构造函数名称通常以大写字母开头(这是一个命名约定)。

例如:

  1. function Person(name, age) {
  2. // 设置属性
  3. this.name = name;
  4. this.age = age;
  5. // 设置方法
  6. this.sayHello = function() {
  7. console.log("Hello, my name is " + this.name);
  8. };
  9. this.haveBirthday = function() {
  10. this.age++;
  11. console.log(this.name + " is now " + this.age + " years old.");
  12. };
  13. }
  14. // 创建 Person 实例
  15. const john = new Person("John", 30);
  16. console.log(john.name); // 输出: John
  17. console.log(john.age); // 输出: 30
  18. john.sayHello(); // 输出: Hello, my name is John
  19. john.haveBirthday(); // 输出: John is now 31 years old.

在这个例子中:

this.name 和 this.age 设置了对象的属性。

this.sayHello 和 this.haveBirthday 设置了对象的方法。

需要注意的几点:

1)这种方法在每次创建新实例时都会创建新的函数对象,可能会占用更多内存。为了更高效地共享方法,通常会使用原型:
Person.prototype.sayHello = function() {
  console.log("Hello, my name is " + this.name);
};

2)在 ES6 及以后,可以使用类语法来实现相同的功能,这提供了一种更清晰的class语法:

  1. class Person {
  2. constructor(name, age) {
  3. this.name = name;
  4. this.age = age;
  5. }
  6. sayHello() {
  7. console.log("Hello, my name is " + this.name);
  8. }
  9. haveBirthday() {
  10. this.age++;
  11. console.log(this.name + " is now " + this.age + " years old.");
  12. }
  13. }

关于class语法,后面还将介绍。

JavaScript中的构造函数和普通函数在语法上是相同的,但它们的用途和使用方式有明显的区别。构造函数主要用于创建对象并设置初始状态,而普通函数用于执行特定的任务或计算。让我们来比较一下:

用途方面

普通函数:执行特定任务或计算

构造函数:创建和初始化对象实例

返回值方面

普通函数:可以返回任何值,或者不返回值(返回 undefined)

构造函数:通常不显式返回值,默认返回创建的对象实例

this 关键字方面

普通函数:this 的值取决于函数如何被调用

构造函数:this 指向新创建的对象实例

另外,命名约定方面(这只是约定,不是强制的)

普通函数:通常以小写字母开头

构造函数:通常以大写字母开头

递归函数

递归函数是一种在函数体内调用自身的函数。这是一种强大的编程技术,特别适合解决可以被分解成相似子问题的问题。

基本概念:

递归函数包含两个主要部分:

基本情况(Base case):停止递归的条件

递归情况(Recursive case):函数调用自身的部分

基本结构:

function recursiveFunction(parameters) {
  if (/* base case condition */) {
    return /* base case value */;
  } else {
    // 递归调用
    return recursiveFunction(/* modified parameters */);
  }
}

示例 - 计算阶乘

阶乘是一个数学上的重要概念,它表示从1乘到某个数n的所有整数的乘积。阶乘用符号n!来表示。定义如下:

n! = n × (n-1) × (n-2) × ... × 3 × 2 × 1

特别地,0! = 1,这是阶乘的基础情况。

例如:

3! = 3 × 2 × 1 = 6

示例源码:

  1. function factorial(n) {
  2. if (n <= 1) return 1; // 基本情况
  3. return n * factorial(n - 1); // 递归情况
  4. }
  5. console.log(factorial(5)); // 输出: 120

递归函数注意事项:

必须有基本情况以避免无限递归,即要确保有正确的终止条件

大量递归可能导致栈溢出

有时可能比迭代解决方案效率低

计算1至5的累计和,就是1+2+3+4+5。源码:

  1. function add(n) {
  2. // 当 n === 1 的时候要结束
  3. if (n === 1) {
  4. return 1
  5. } else {
  6. // 不满足条件的时候,就是当前数字 + 比自己小 1 的数字
  7. return n + add(n - 1) ;
  8. }
  9. }
  10. add(5)

递归函数的优点是可以将复杂的问题分解成更小的子问题,使问题解决过程更加清晰和简洁。它可以提供一种自然的思考方式,并且在某些情况下可以使代码更易于理解和维护。

然而,递归函数也有一些缺点需要考虑。首先,递归函数可能会占用大量的内存空间,因为每次递归调用都会在内存堆栈中创建一个新的函数调用帧。这可能导致堆栈溢出的问题,特别是当递归的层级非常深时。

其次,递归函数的性能可能不如迭代函数。递归函数需要频繁地进行函数调用和返回操作,这会增加函数调用的开销。相比之下,迭代函数通常可以通过循环结构实现相同的功能,并且在某些情况下可以更高效地执行。

另外,递归函数的设计需要小心处理边界条件和递归结束条件,否则可能导致无限递归的情况,造成程序崩溃或死循环。

闭包

闭包(Closure)是一个非常重要的概念,它允许函数记住并访问其词法环境,即使该函数在其词法环境之外执行。简单来说,闭包就是函数和声明该函数的词法环境的组合。

闭包通常在你创建了一个内部函数,并且这个内部函数访问了外部函数的变量时自然形成。内部函数会保留对外部函数变量的引用,即使外部函数执行完成后,这些变量不会被垃圾回收机制(Garbage Collection)清除。例如:

  1. function outerFunction() {
  2. let outerVariable = 100; // 外部变量
  3. function innerFunction() {
  4. console.log(outerVariable); // 访问外部变量
  5. }
  6. return innerFunction;
  7. }
  8. const myClosure = outerFunction(); // outerFunction执行完毕
  9. myClosure(); // 输出: 100

在这个例子中,outerVariable是outerFunction中的一个局部变量。innerFunction访问了这个变量,尽管outerFunction在调用myClosure()时已经执行完毕,outerVariable的值仍然被innerFunction保留访问。

闭包的基本特性

1)函数是第一类对象:在 JavaScript 中,函数是对象,可以像其他对象一样被传递和操作。这意味着函数可以:

被赋值给变量

作为参数传递给其他函数

作为函数的返回值

示例:

  1. // 函数赋值给变量
  2. const greet = function(name) {
  3. return `Hello, ${name}!`;
  4. };
  5. // 函数作为参数
  6. function executeFunction(fn, param) {
  7. return fn(param);
  8. }
  9. console.log(executeFunction(greet, "Alice")); // 输出: Hello, Alice!
  10. // 函数作为返回值
  11. function createMultiplier(factor) {
  12. return function(number) {
  13. return number * factor;
  14. };
  15. }
  16. const double = createMultiplier(2);
  17. console.log(double(5)); // 输出: 10

2)词法作用域:JavaScript 使用词法作用域(也称为静态作用域),这意味着作用域是基于函数在哪里被声明来确定的,而不是基于函数在哪里被调用。示例:

  1. let x = 10;
  2. function createFunction() {
  3. let x = 20;
  4. return function() {
  5. console.log(x); // 这里的 x 引用的是 createFunction 中的 x,而不是全局的 x
  6. };
  7. }
  8. const f = createFunction();
  9. f(); // 输出: 20

3)内部函数可以访问外部函数的变量:当一个函数内部定义了另一个函数时,内部函数可以访问外部函数的变量,即使外部函数已经执行完毕。

这是闭包最关键的特性。即使外部函数已经执行完毕,内部函数仍然可以访问外部函数的变量。

示例:

  1. function outerFunction(x) {
  2. let y = 10;
  3. function innerFunction() {
  4. console.log(x + y);
  5. }
  6. return innerFunction;
  7. }
  8. const closure = outerFunction(5);
  9. closure(); // 输出: 15

在这个例子中,即使 outerFunction 已经执行完毕,返回的 innerFunction 仍然可以访问 outerFunction 的参数 x 和局部变量 y。

闭包主要特点

a)内部函数可以访问外部函数的变量。这个特点是闭包的核心,内部函数不仅可以访问自己的变量,还可以访问外部函数的变量和参数。示例:

  1. function outerFunction(x) {
  2. let y = 20;
  3. function innerFunction() {
  4. console.log(x + y);
  5. }
  6. innerFunction();
  7. }
  8. outerFunction(10); // 输出: 30

在这个例子中,innerFunction 可以访问 outerFunction 的参数 x 和局部变量 y。

b)即使外部函数已经返回,内部函数仍然可以访问这些变量

这个特点让闭包变得特别强大。内部函数会保留对外部函数作用域的引用,即使外部函数已经执行完毕。示例:

  1. function createGreeter(name) {
  2. return function() {
  3. console.log(`Hello, ${name}!`);
  4. };
  5. }
  6. const greetAlice = createGreeter('Alice');
  7. greetAlice(); // 输出: Hello, Alice!

在这个例子中,即使 createGreeter 函数已经返回,返回的函数仍然可以访问 name 参数。

c)可以用来创建私有变量和方法

闭包允许我们创建在外部不可直接访问的变量和方法,从而实现数据的封装和私有性。示例:

  1. function createBankAccount(initialBalance) {
  2. let balance = initialBalance;
  3. return {
  4. deposit: function(amount) {
  5. balance += amount;
  6. return balance;
  7. },
  8. withdraw: function(amount) {
  9. if (amount > balance) {
  10. return "Insufficient funds";
  11. }
  12. balance -= amount;
  13. return balance;
  14. },
  15. getBalance: function() {
  16. return balance;
  17. }
  18. };
  19. }
  20. const account = createBankAccount(100);
  21. console.log(account.getBalance()); // 输出: 100
  22. account.deposit(50);
  23. console.log(account.getBalance()); // 输出: 150
  24. console.log(account.withdraw(200)); // 输出: "Insufficient funds"
  25. console.log(account.balance); // 输出: undefined

在这个例子中:balance 是一个私有变量,外部无法直接访问或修改。deposit、withdraw 和 getBalance 是公共方法,可以操作私有变量 balance。外部代码只能通过这些公共方法来与账户余额交互,保证了数据的安全性和一致性。

闭包的用途

闭包允许我们创建有状态的函数,同时又不暴露内部状态,这在很多编程模式中非常有用,比如惰性加载(Lazy Loading)、记忆化函数(Memoization)、柯里化(Currying)等。

☆惰性加载

定义:延迟初始化或计算,直到实际需要时才执行。

目的:优化性能,减少初始加载时间。

闭包可以用于延迟初始化,只在首次需要时执行某些操作。例子:

  1. let heavyComputation = (function() {
  2. let result;
  3. return function() {
  4. if (result === undefined) {
  5. console.log("Computing...");
  6. result = /* 复杂计算 */;
  7. }
  8. return result;
  9. };
  10. })();
  11. console.log(heavyComputation()); // 输出: Computing... 然后返回结果
  12. console.log(heavyComputation()); // 直接返回结果,不再计算

☆记忆化函数

定义:缓存函数的计算结果,以便在后续调用中快速返回。

目的:优化计算密集型函数的性能。

使用闭包来缓存函数的计算结果,提高性能。例子:

  1. function memoize(fn) {
  2. const cache = {};
  3. return function(...args) {
  4. const key = JSON.stringify(args);
  5. if (key in cache) {
  6. return cache[key];
  7. }
  8. const result = fn.apply(this, args);
  9. cache[key] = result;
  10. return result;
  11. };
  12. }
  13. const slowFibonacci = memoize(function(n) {
  14. if (n <= 1) return n;
  15. return slowFibonacci(n - 1) + slowFibonacci(n - 2);
  16. });
  17. console.log(slowFibonacci(40)); // 快速计算,结果被缓存

☆柯里化:

定义:将接受多个参数的函数转换成一系列使用一个参数的函数。

目的:增加函数的灵活性和可复用性。

闭包使得函数柯里化成为可能,柯里化函数使用闭包来保存部分应用的参数。例子:

  1. function curry(fn) {
  2. return function curried(...args) {
  3. if (args.length >= fn.length) {
  4. return fn.apply(this, args);
  5. } else {
  6. return function(...args2) {
  7. return curried.apply(this, args.concat(args2));
  8. };
  9. }
  10. };
  11. }
  12. const sum = curry((a, b, c) => a + b + c);
  13. console.log(sum(1)(2)(3)); // 6
  14. console.log(sum(1, 2)(3)); // 6

闭包注意事项

内存泄漏:由于闭包会保持对外部变量的引用,这可能导致内存无法释放,形成内存泄漏。适当的生命周期管理和及时的解除引用是必要的。

性能考虑:创建闭包可能会稍微消耗更多的内存,因为需要保存外部函数的活动对象。

进一步还需要学习回调函数、高阶函数、异步函数等。较少用到就不涉及了。

附录、JavaScript函数 https://blog.csdn.net/cnds123/article/details/109405136

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/天景科技苑/article/detail/898272
推荐阅读
相关标签
  

闽ICP备14008679号