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