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

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

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

箭头函数(Arrow Function)

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

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

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

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

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

singleParam => expression

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

() => { statements }

☆简单箭头函数,例如:

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

☆省略花括号和return关键字

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

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

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

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

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

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

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

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

注意,箭头函数不绑定this

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

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

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

// 传统函数
function traditionalFunction() {
    console.log(this);
}

// 箭头函数
const arrowFunction = () => {
    console.log(this);
}

const obj = {
    value: 42,
    traditional: traditionalFunction,
    arrow: arrowFunction
};

// 作为对象方法调用
obj.traditional(); // 输出 obj 对象
obj.arrow(); // 输出 obj 对象

// 全局作用域中调用
traditionalFunction(); // 输出全局对象(浏览器中是 window)
arrowFunction(); // 输出全局对象(浏览器中是 window)

// 显式绑定
const explicitBindObj = { value: 99 };
traditionalFunction.call(explicitBindObj); // 输出 explicitBindObj 对象
arrowFunction.call(explicitBindObj); // 输出全局对象(浏览器中是 window)

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

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

函数参数和返回值说明

1 默认参数

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

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

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

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

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

console.log(add(2, 3)); // 输出 5
console.log(add(5)); // 输出 10,b的值默认为a的值,即5

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

2 剩余参数(Rest Parameters)

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

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

示例:

function sum(...numbers) {
    let total = 0;
    for (let number of numbers) {
        total += number;
    }
    return total;
}

console.log(sum(1, 2, 3, 4)); // 输出 10
console.log(sum(5, 10, 15)); // 输出 30

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

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

3 返回值

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

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

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

function multiply(a, b) {
    return a * b;
}

let result = multiply(3, 4);
console.log(result); // 输出 12

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

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

function getGreeting(name) {
    if (name) {
        return 'Hello, ' + name + '!';
    }
    return 'Hello, Guest!';
}

console.log(getGreeting('Alice')); // 输出 "Hello, Alice!"
console.log(getGreeting()); // 输出 "Hello, Guest!"

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

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

function doSomething() {
    // 没有return语句
}

let result = doSomething();
console.log(result); // 输出 undefined

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

function processData(data) {
    if (!data) {
        return; // 如果没有数据,立即返回
    }
    // 处理数据的逻辑
    console.log('Processing data...');
}

processData(null);

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

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

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

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

构造函数(Constructor Function)

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

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

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

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

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

例如:

function Person(name, age) {
  // 设置属性
  this.name = name;
  this.age = age;

  // 设置方法
  this.sayHello = function() {
    console.log("Hello, my name is " + this.name);
  };

  this.haveBirthday = function() {
    this.age++;
    console.log(this.name + " is now " + this.age + " years old.");
  };
}

// 创建 Person 实例
const john = new Person("John", 30);

console.log(john.name);  // 输出: John
console.log(john.age);   // 输出: 30
john.sayHello();         // 输出: Hello, my name is John
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语法:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayHello() {
    console.log("Hello, my name is " + this.name);
  }

  haveBirthday() {
    this.age++;
    console.log(this.name + " is now " + this.age + " years old.");
  }
}

关于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

示例源码:

function factorial(n) {
  if (n <= 1) return 1;  // 基本情况
  return n * factorial(n - 1);  // 递归情况
}

console.log(factorial(5));  // 输出: 120

递归函数注意事项:

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

大量递归可能导致栈溢出

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

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

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

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

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

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

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

闭包

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

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

function outerFunction() {
    let outerVariable = 100;  // 外部变量

    function innerFunction() {
        console.log(outerVariable);  // 访问外部变量
    }

    return innerFunction;
}

const myClosure = outerFunction();  // outerFunction执行完毕
myClosure();  // 输出: 100

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

闭包的基本特性

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

被赋值给变量

作为参数传递给其他函数

作为函数的返回值

示例:

// 函数赋值给变量
const greet = function(name) {
    return `Hello, ${name}!`;
};

// 函数作为参数
function executeFunction(fn, param) {
    return fn(param);
}
console.log(executeFunction(greet, "Alice")); // 输出: Hello, Alice!

// 函数作为返回值
function createMultiplier(factor) {
    return function(number) {
        return number * factor;
    };
}
const double = createMultiplier(2);
console.log(double(5)); // 输出: 10

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

let x = 10;

function createFunction() {
    let x = 20;
    return function() {
        console.log(x); // 这里的 x 引用的是 createFunction 中的 x,而不是全局的 x
    };
}

const f = createFunction();
f(); // 输出: 20

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

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

示例:

function outerFunction(x) {
    let y = 10;
    function innerFunction() {
        console.log(x + y);
    }
    return innerFunction;
}

const closure = outerFunction(5);
closure(); // 输出: 15

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

闭包主要特点

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

function outerFunction(x) {
    let y = 20;
    function innerFunction() {
        console.log(x + y);
    }
    innerFunction();
}

outerFunction(10); // 输出: 30

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

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

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

function createGreeter(name) {
    return function() {
        console.log(`Hello, ${name}!`);
    };
}

const greetAlice = createGreeter('Alice');
greetAlice(); // 输出: Hello, Alice!

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

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

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

function createBankAccount(initialBalance) {
    let balance = initialBalance;

    return {
        deposit: function(amount) {
            balance += amount;
            return balance;
        },
        withdraw: function(amount) {
            if (amount > balance) {
                return "Insufficient funds";
            }
            balance -= amount;
            return balance;
        },
        getBalance: function() {
            return balance;
        }
    };
}

const account = createBankAccount(100);
console.log(account.getBalance()); // 输出: 100
account.deposit(50);
console.log(account.getBalance()); // 输出: 150
console.log(account.withdraw(200)); // 输出: "Insufficient funds"
console.log(account.balance); // 输出: undefined

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

闭包的用途

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

☆惰性加载

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

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

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

let heavyComputation = (function() {
    let result;
    return function() {
        if (result === undefined) {
            console.log("Computing...");
            result = /* 复杂计算 */;
        }
        return result;
    };
})();

console.log(heavyComputation()); // 输出: Computing... 然后返回结果
console.log(heavyComputation()); // 直接返回结果,不再计算

☆记忆化函数

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

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

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

function memoize(fn) {
    const cache = {};
    return function(...args) {
        const key = JSON.stringify(args);
        if (key in cache) {
            return cache[key];
        }
        const result = fn.apply(this, args);
        cache[key] = result;
        return result;
    };
}

const slowFibonacci = memoize(function(n) {
    if (n <= 1) return n;
    return slowFibonacci(n - 1) + slowFibonacci(n - 2);
});

console.log(slowFibonacci(40)); // 快速计算,结果被缓存

☆柯里化:

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

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

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

function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        } else {
            return function(...args2) {
                return curried.apply(this, args.concat(args2));
            };
        }
    };
}

const sum = curry((a, b, c) => a + b + c);
console.log(sum(1)(2)(3)); // 6
console.log(sum(1, 2)(3)); // 6

闭包注意事项

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

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

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

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

  • 17
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学习&实践爱好者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值