赞
踩
继续上一节介绍函数相关知识。
箭头函数(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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。