你好同学,我是沐爸,欢迎收藏、点赞、评论和关注!
JavaScript中的函数是一个核心概念,它涵盖了多个知识点,这些知识点对于理解和使用JavaScript至关重要。
以下是对JavaScript函数涉及的主要知识点的全面汇总,知识点包括:函数的定义、创建、调用、参数、返回值、作用域和作用域链、闭包、构造函数、原型和原型链、箭头函数、this指向、立即执行函数表达式、回调函数、递归函数、迭代函数。
本期分享内容较多,建议先收藏,有空了再看全篇。
一、函数的基本概念
- 定义:函数是一段可以被执行的JavaScript代码块,用于实现特定的功能。
- 类型:在JavaScript中,函数也是对象类型,属于Function类型。
- 作用:函数最大的作用就是代码封装,提高代码的复用性。
二、函数的创建
JavaScript提供了多种方式来创建函数,主要包括:
1.函数声明
function 函数名(参数列表) {
// 函数体
}
使用function
关键字声明函数,这种方式创建的函数会被提升(hoisting),可以先使用后声明。
2.函数表达式
var 函数名 = function(参数列表) {
// 函数体
};
将函数赋值给一个变量,这种方式创建的函数不会被提升,只能先声明后使用。
3.箭头函数
const 函数名 = (参数列表) => {
// 函数体
};
箭头函数是ES6中引入的一种更简洁的函数写法,它不会创建自己的this
上下文,而是继承自外围作用域的this
值。
三、函数的调用
1.直接调用
直接调用函数是最常见、最普通的方式。这种方式直接以函数名加括号并传入参数来调用函数。例如:
function sayHello(name) {
console.log("Hello, " + name);
}
sayHello("Alice"); // Hello Alice
2.作为对象的方法调用
当函数被赋值给对象的属性时,这个函数就被视为该对象的方法。通过对象名+方法名的方式调用。此时,函数内部的this
关键字会指向该对象。例如:
var person = {
name: "Bob",
greet() { // 简写
console.log("Hello, " + this.name);
},
// greet: function() { // 常规写法
// console.log("Hello, " + this.name);
// }
};
person.greet(); // Hello Bob
3.使用 new 关键字调用
通过new
关键字调用函数,此时函数将作为构造函数,用于创建对象。
function Person(name) {
this.name = name;
this.greet = function() {
console.log("Hello, " + this.name);
};
}
var person1 = new Person("Eve");
person1.greet(); // Hello Eve
构造函数默认返回新创建的对象,但如果构造函数返回一个对象,则返回值为该对象。【没这么玩的,仅了解】
function Person(name) {
this.name = name;
this.greet = function() {
console.log("Hello, " + this.name);
};
return {
name
}
}
var person1 = new Person("Eve");
console.log(person1) // {name: "Eve"}
person1.greet(); // person1.greet is not a function
4.call 或 apply 调用
这两个方法允许你显式地设置函数体内的this
值,并调用该函数。两者的主要区别在于传递参数的方式:call
方法接受一个参数列表,而apply
方法接受一个包含多个参数的数组。例如:
function greet(greeting, name) {
console.log(this); // {country: "USA"}
console.log(greeting + ", " + name);
}
var obj = {
country: "USA",
};
greet.call(obj, "Hello", "Charlie"); // Hello Charlie
greet.apply(obj, ["Hi", "David"]); // Hi David
四、函数的参数
- 形参(形式参数):定义函数时括号内的参数,用于接收调用函数时传递的实参值。
- 实参(实际参数):调用函数时括号内传递的值,用于赋值给函数内的形参。
- 默认参数:ES6中引入的特性,允许为函数的参数设置默认值。
- 剩余参数:使用
...
操作符,可以将多余的参数收集到一个数组中。
function sum(a = 0, b = 0, ...c) { // a和b为形参, 0为默认参数, c为剩余参数
return a + b;
}
sum(1, 2); // 2和3为实参
五、函数的返回值
- 函数可以通过
return
语句返回一个值给调用者。 - 如果函数没有
return
语句或return
后面没有跟任何值,则默认返回undefined
。 return
语句会立即终止函数的执行,并返回后面的值。return
可以返回任意类型数据,包括返回一个函数。
六、作用域和作用域链
1.作用域
作用域规定了变量和函数的作用范围,超出了这个范围,变量和函数遍不能被访问。JavaScript中有几种不同类型的作用域,主要包括全局作用域、函数作用域和块级作用域(ES6之后引入)。
【1】全局作用域
全局作用域是最外层的作用域,在代码的任何地方都能访问到全局作用域中的变量和函数。在浏览器环境中,全局作用域就是window
对象,在Node.js环境中,全局作用域是global
对象。在全局作用域中声明的变量会成为全局对象的属性。
var globalVar = "I am global";
function testGlobalScope() {
console.log(globalVar); // 可以访问全局变量
}
testGlobalScope(); // 输出: I am global
【2】函数作用域
函数作用域是定义在函数内部的变量和函数只能在函数内部访问。这意味着函数内部声明的变量和函数不会影响到函数外部的代码,也不会被函数外部的代码所影响。
function testFunctionScope() {
var localVar = "I am local";
console.log(localVar); // 可以访问局部变量
}
testFunctionScope(); // 输出: I am local
console.log(localVar); // ReferenceError: localVar is not defined
【3】块级作用域(ES6+)
从ES6开始,JavaScript引入了let
和const
关键字,这两个关键字声明的变量具有块级作用域。块级作用域意味着变量只在声明它的代码块(如if
语句、for
循环、{}
块等)内部有效。这有助于避免变量提升(hoisting)带来的问题,并使代码更加清晰和安全。
if (true) {
let blockScopedVar = "I am block scoped";
console.log(blockScopedVar); // 可以访问块级变量
}
// console.log(blockScopedVar); // ReferenceError: blockScopedVar is not defined
for (let i = 0; i < 5; i++) {
// 每次迭代i都是新的绑定
}
// console.log(i); // ReferenceError: i is not defined
2.作用域链
作用域链本质上是一种变量查找机制。变量会优先在当前作用域查找,如果当前作用域找不到,则会向上逐级查找父作用域直至全局作用域。这种层层嵌套的作用域串联起来就是作用域链。子作用域能访问父作用域,但父作用域不能访问子作用域。
作用域链的创建过程大致如下:
- 全局执行上下文:当脚本开始执行时,会创建一个全局执行上下文,并初始化一个全局对象(在浏览器中是
window
对象)。全局执行上下文的作用域链只包含一个元素:全局对象本身。 - 函数执行上下文:当调用一个函数时,会创建一个新的函数执行上下文,并初始化一个名为
[[Scope]]
的内部属性,这个属性包含了父执行上下文的作用域链以及函数对象本身(即函数体内的局部变量和参数)。这个新的作用域链会在函数执行期间被用来查找变量和函数。 - 查找变量:当在函数内部访问一个变量时,JavaScript引擎会首先在当前函数的作用域中查找这个变量。如果没有找到,就会继续向上在父执行上下文的作用域中查找,直到找到为止。如果在全局作用域中也没有找到,就会抛出
ReferenceError
异常。
var x = 'global';
function foo() {
var y = 'local';
function bar() {
console.log(x); // 查找x,首先在当前作用域中查找,未找到,然后沿着作用域链向上,在foo的作用域中查找,还是未找到,最后在全局作用域中找到
console.log(y); // 查找y,在当前作用域中直接找到
}
bar();
}
foo();
七、什么是闭包?
闭包是指一个函数,它引用了其外部作用域中的变量。换句话说,闭包是函数及其创建时词法环境的组合,这个函数记住了它被创建时的环境。
1.闭包的作用
- 数据封装和隐私保护:通过闭包,我们可以创建私有变量,这些变量只能被闭包内部的函数访问,从而实现了封装和隐藏内部状态。
- 模块化代码:闭包可以用于创建模块,每个模块都可以封装自己的状态和行为,从而实现高内聚低耦合的代码设计。
- 回调函数和异步编程:在JavaScript等语言中,闭包常用于设置回调函数,这些回调函数在将来的某个时间点(如异步操作完成时)执行,并能访问到定义它们时的上下文。
- 函数工厂:闭包可以用于创建函数工厂,即返回函数的函数。这些返回的函数可以访问并操作它们被创建时的环境中的变量。
2.创建闭包的步骤
- 定义一个外部函数,该函数至少包含一个内部函数。
- 内部函数引用外部函数的变量。
- 将内部函数作为返回值返回。
function outer() { // 1.声明外层函数
let count = 0 // 2.声明局部变量
function inner() { // 3.声明内层函数
count++ // 4.内层函数访问外层变量
debugger
return count
}
return inner // 5.返回内层函数
}
const counter = outer() // 6.调用外层函数
counter() //1
counter() //2
counter() //3
浏览器在 Closure 中存储了闭包的值,以供下次使用。
八、构造函数
1.什么是构造函数?
构造函数是一种特殊的函数,主要用来初始化对象。
2.构造函数的特点
- 它们只能由 “new” 关键字来执行。
- 它们的命名以大写字母开头【约定】。
- 构造函数中的 this 指向实例化对象。
- 构造函数 的返回值默认为新创建的对象。
- 构造函数创建的实例对象彼此独立,互不影响。
3.构造函数实例化过程
面试常问:new 关键字执行了哪些操作?其实是一个意思。
【1】创建一个新的空对象。
【2】将这个新对象的内部[[Prototype]]
(即__proto__
)链接到构造函数的prototype
对象上。
【3】构造函数中的this
指向这个新对象。
【4】函执行构造函数中的代码,为新对象添加属性。
【5】如果构造函数返回的是一个对象,则返回该对象;否则,返回步骤1创建的对象。
4.内置构造函数
JavaScript提供了多个内置构造函数,如Object
、Array
、String
、Number
、Boolean
、Date
等,用于创建不同类型的对象。
let obj = new Object(); // 创建一个空对象
obj.prop = 'value'; // 给对象添加属性
console.log(obj); // 输出: { prop: 'value' }
5.自定义构造函数
开发同学可以定义自己的构造函数来创建具有特定属性和方法的对象。自定义构造函数通过new
关键字调用时,会按照上述new
操作符的执行流程来创建并初始化对象。
function Person(name, age) {
this.name = name;
this.age = age;
this.greet = function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
}
var person1 = new Person('Alice', 30);
person1.greet(); // 输出: Hello, my name is Alice and I am 30 years old.
var person2 = new Person('Bob', 25);
person2.greet(); // 输出: Hello, my name is Bob and I am 25 years old.
6.实例成员&静态成员
【1】实例成员
通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员(实例属性和实例方法)。它们通过构造函数内的this
关键字来定义和访问。
function Person(name, age) {
this.name = name; // 实例属性
this.age = age;
this.greet = function() { // 实例方法
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
}
var person1 = new Person('Alice', 30);
person1.greet(); // 访问实例方法
console.log(person1.name); // 访问实例属性
【2】静态成员
构造函数自身的属性和方法被称为静态成员(静态属性和静态方法)。特点:
- 静态成员不会被实例继承,也不能通过实例来访问。它们通过构造函数本身来访问。
- 静态方法中的 this 指向构造函数。
function Person(name, age) {
// 实例成员...
}
// 静态属性
Person.species = 'Human';
// 静态方法
Person.greetSpecies = function() {
console.log(`Hello, I am a ${this.species}.`);
};
// 访问静态成员
console.log(Person.species); // 输出: Human
Person.greetSpecies(); // 输出: Hello, I am a Human.
// 注意:静态成员不能通过实例来访问
console.log(person1.species); // undefined,因为species是静态属性
person1.greetSpecies(); // TypeError,因为greetSpecies是静态方法
在ES6及更高版本中,可以使用class
语法来更清晰地定义构造函数及其成员,包括静态成员。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
static species = 'Human';
static greetSpecies() {
console.log(`Hello, I am a ${Person.species}.`);
}
}
var person1 = new Person('Alice', 30);
person1.greet(); // 访问实例方法
console.log(Person.species); // 访问静态属性
Person.greetSpecies(); // 访问静态方法
九、原型和原型链
1.原型
为什么要用原型?
因为构造函数中的方法有个缺陷,存在浪费内存的问题。
<script>
// 方式一
function Person(name) {
this.name = name
this.sing = function(){
console.log('唱歌')
}
}
let ldh = new Person('刘德华')
let zxy = new Person('张学友')
console.log(ldh.sing === zxy.sing) // false
// 方式二
function Person(name) {
this.name = name
}
Person.prototype.sing = function() {
console.log('唱歌')
}
let ldh = new Person('刘德华')
let zxy = new Person('张学友')
console.log(ldh.sing === zxy.sing) // true
</script>
什么是原型?
JS 规定,每个构造函数都有一个prototype
属性,该属性指向一个对象,这个对象被称为原型对象。
- 构造函数定义在原型上的属性和方法是所有实例对象共享的。
- 构造函数和原型对象上的 this 都指向实例化对象。
2.原型链
原型链是什么?
- 原型链是由多个原型对象通过
__proto__
(或内部的[[Prototype]]
)属性连接而成的链式结构。
原型链查找规则:
- 当访问一个对象的属性或方法时,首先查找这个对象自身有没有。
- 如果没有就查找它的原型(也就是__proto__指向的 prototype 原型对象)
- 如果还没有找到,就查找原型对象的原型(Object的原型对象)
- 一直找到 Object 为止(null)
- __proto__对象原型的意义在于为对象成员查找机制提供了一个方向,或者说一条路线。
- 可以使用 instanceof 检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
注意:
- 所有原型链的终点都是 Object 函数的 prototype 属性
- 每一个构造函数都拥有一个 prototype 属性,此属性指向一个对象,也就是原型对象
- 原型对象默认拥有一个 constructor 属性,指向指向它的那个构造函数
- 每个对象都拥有一个隐藏的属性 __ proto __,指向它的原型对象
示例
假设我们有两个构造函数Person
和Student
,其中Student
继承自Person
:
function Person(name) {
this.name = name
}
Person.prototype.sayName = function () {
console.log(this.name)
}
function Student(name, grade) {
Person.call(this, name) // 借用构造函数继承属性
this.grade = grade
}
Student.prototype = Object.create(Person.prototype) // 原型链继承方法
Student.prototype.constructor = Student // 修复constructor指向
Student.prototype.sayGrade = function () {
console.log(this.grade)
}
var student = new Student('Alice', 'A')
student.sayName() // 输出: Alice
student.sayGrade() // 输出: A
在这个例子中,Student
的实例student
有一个原型链:student -> Student.prototype -> Person.prototype -> Object.prototype -> null
。当调用student.sayName()
时,由于student
本身没有sayName
方法,JavaScript引擎会沿着原型链向上查找,最终在Person.prototype
上找到了sayName
方法并执行。
十、箭头函数
JS中的箭头函数是ES6中引入的一种更简洁的函数写法。箭头函数使用 =>
语法,而不是传统的 function
关键字来定义函数。箭头函数有几个关键特性,这些特性使得它们在某些情况下比传统函数更加有用和方便。
1.语法
箭头函数的基本语法如下:
- 省略小括号,当形参有且只有一个的时候。
- 省略花括号,当代码体只有一条语句时。
- 省略return,如果只有一条语句,且有返回值时,可以省略return。如果返回对象,需要小括号包裹。
const double = n => n*2
const sum = (a, b) => a+b
const square = x => x*x
const getObj = (id,name) => ({ id,name })
[1,2,3].map(x => x*x)
[2,1,3].sort((a,b) => a - b)
2.特性
- 箭头函数没有自己的 this 对象。【箭头函数的this就是定义时上层作用域中的this,指向是固定的】
- 不能作为构造函数实例化对象,不能使用new。
- 不能使用 arguments 对象,可使用剩余参数代替。
- 因为箭头函数没有自己的 this,所以不能使用 call/apply/bind 方法改变 this 指向。
- 箭头函数同样没有自己的
super
或new.target
。 - 由于箭头函数不是构造函数,因此它们没有
prototype
属性。
3.示例
// 传统函数
function add(a, b) {
return a + b;
}
// 箭头函数
const add = (a, b) => a + b;
// 捕获外部 `this` 的值
const obj = {
value: 1,
getValue: function() {
// 传统函数
setTimeout(function() {
console.log(this.value); // undefined,因为这里的 `this` 指向了全局对象或 `undefined`(严格模式下)
}, 1000);
// 箭头函数
setTimeout(() => {
console.log(this.value); // 1,因为箭头函数捕获了外层函数的 `this`
}, 1000);
}
};
obj.getValue();
十一、this指向
在JavaScript中,this
的指向依赖于函数的调用方式。
1.this指向规则
下面是一些常见的 this
指向场景:
- 全局上下文中:
- 在全局执行环境中(即在任何函数体外部),
this
指向全局对象。在浏览器环境中,这个全局对象是window
;在Node.js环境中,这个全局对象是global
。
- 在全局执行环境中(即在任何函数体外部),
- 函数上下文中:
- 非严格模式:如果函数作为普通函数调用,
this
指向全局对象(浏览器中是window
,Node.js中是global
)。 - 严格模式(使用
"use strict";
):如果函数在严格模式下作为普通函数调用,this
的值是undefined
。 - 方法调用:如果函数作为对象的方法被调用,
this
指向该对象。 - 构造函数调用:使用
new
关键字调用函数时,函数内的this
指向新创建的对象实例。 - 箭头函数:箭头函数不绑定自己的
this
,它会捕获其所在(即定义时)上下文的this
值作为自己的this
值。
- 非严格模式:如果函数作为普通函数调用,
- 事件处理器中:
- 在事件处理函数中,
this
通常指向触发事件的元素。
- 在事件处理函数中,
- 定时器函数中:
- 在
setTimeout
或setInterval
的回调函数中,this
默认指向全局对象(浏览器中是window
,Node.js中是global
),除非在调用时使用了.bind()
、.call()
或.apply()
方法来显式设置this
的指向。
- 在
- 回调函数和Promise:
- 在回调函数和Promise的
.then()
、.catch()
等方法中,this
的指向可能会丢失或不是你期望的对象。这时,可以使用.bind()
、箭头函数或其他方法来确保this
的正确指向。
- 在回调函数和Promise的
2.示例
function myFunction() {
console.log(this); // 取决于调用方式
}
// 全局调用
myFunction(); // 在浏览器中是 window,在Node.js中是 global
// 作为对象的方法调用
const obj = {
myMethod: myFunction
};
obj.myMethod(); // this 指向 obj
// 使用 new 调用(构造函数)
function MyConstructor() {
this.name = 'Alice'; // this指向实例对象
}
const instance = new MyConstructor();
console.log(instance.name); // Alice
// 箭头函数
const objWithArrow = {
arrowMethod: () => {
console.log(this); // 箭头函数不绑定自己的this,这里的this指向全局对象
}
};
objWithArrow.arrowMethod(); // 在浏览器中是 window,在Node.js中是 global
// 定时器中的this
setTimeout(function() {
console.log(this); // 在浏览器中是 window,在Node.js中是 global
}, 1000);
// 使用箭头函数在定时器中保持this指向
setTimeout(() => {
console.log(this); // 这里的this取决于外层作用域的this
}, 1000);
3.改变 this 指向
改变 this 指向有4种方式,分别是改用箭头函数、call()、apply()和bind()。箭头函数没有自己的 this,它会捕获上下文的 this 的值。而call()、apply()和bind()各有其用,选择哪个取决于你的具体需求。call
和 apply
适合于需要立即调用函数并改变 this
指向的场景,而 bind
适合于需要预先改变 this
指向,并可能稍后调用的场景。
1.call()
使用 call 方法会立即调用函数,同时指定被调用函数中 this 的值。
语法:fn.call(thisArg, arg1, arg2, …) 【传递的参数用逗号分隔】
返回值:就是函数的返回值。
function greet() {
console.log('Hello, ' + this.name + '!');
}
const person = {
name: 'Alice'
};
greet.call(person); // 输出: Hello, Alice!
2.apply()
使用 apply 方法会调用函数,同时指定被调用函数中 this 的值。
语法:fn.call(thisArg, [argsArray]) 【传递的参数必须包含在数组里】
返回值:就是函数的返回值。
apply 主要跟数组有关系,比如使用 Math.max 求数组最大值。
let obj = {
name: 'lili',
age: 10
}
function fn(...arg) {
console.log(this.name, arg, this === obj) // 'lili'、['a','b']、true
}
fn.apply(obj, ['a', 'b'])
// 求数组最大值
let arr = [1, 2, 3]
const max = Math.max.apply(null, arr) // Math.max(...arr)
console.log(max) // 3
3.bind()
bind()方法不会调用函数,但可以指定调用函数中 this 的值。
语法:fn.bind(thisArg, arg1, arg2, …)
返回值为改变 this 指向后的新函数,新函数等于旧函数。
function greet(greeting) {
console.log(greeting + ', ' + this.name);
}
const person = {
name: 'Alice'
};
const boundGreet = greet.bind(person, 'Hello');
boundGreet(); // 输出: Hello, Alice!
十二、立即执行函数表达式(IIFE)
立即执行函数表达式是一种在定义后立即执行的函数表达式。它常用于创建一个独立的作用域,这样可以避免变量污染全局作用域,也可以隐藏私有变量。
1.立即执行表达式语法
(function() {
// 你的代码
console.log("这是一个立即执行函数表达式");
})();
// 或者使用括号包围整个函数体,然后在末尾添加()来立即执行
(function() {
// 你的代码
console.log("这也是一个立即执行函数表达式");
}());
// 传递参数,参数可以是任意数据类型
(function(greeting) {
console.log(greeting + ", world!");
})("Hello"); // 输出: Hello, world!
2.使用场景
【1】模块化代码:创建独立的作用域,防止全局变量污染。
var myModule = (function() {
var privateVariable = "这是一个私有变量";
function privateFunction() {
console.log("这是一个私有函数");
}
return {
publicFunction: function() {
privateFunction(); // 可以在这里调用私有函数
console.log(privateVariable); // 可以访问私有变量
}
};
})();
myModule.publicFunction(); // 依次输出: "这是一个私有函数"、"这是一个私有变量"
// 尝试直接访问 privateVariable 或 privateFunction 会导致 ReferenceError
// console.log(privateVariable); // Uncaught ReferenceError: privateVariable is not defined
// privateFunction(); // Uncaught ReferenceError: privateFunction is not defined
【2】库和框架:定义私有变量和函数,对外提供接口。
以 Vue2 代码为例(代码极简化):
(function(global, factory) {
global = typeof globalThis !== 'undefined' ? globalThis : global || self;
global.Vue = factory();
})(this, (function() { // 参数:1.this在浏览器环境中为
function Vue(options) {
this._init(options);
}
Vue.prototype._init = function() { };
Vue.prototype._render = function() { };
Vue.prototype.$mount = function() { };
// ......
return Vue; // 匿名函数返回构造函数 Vue
}));
在上面的伪代码中可以看到,立即执行表达式传了两个参数,一个是this,在浏览器环境中this为Window;二是一个匿名函数,匿名函数内部又声明了一个构造函数 Vue,在函数的结尾将 Vue 返回。再看代码执行部分,判断 this 是否存在,然后给 this 添加 Vue 属性,属性值为参数中匿名函数执行后的返回结果 Vue。代码执行完,Window 对象上就多了一个 Vue 属性。
不止是 Vue2,还有 Vue3、React等框架,它们打包完都是这么处理的。
【3】闭包:闭包和IIFE经常一起使用来创建私有变量和函数,并对外提供公共接口。
var counter = (function () {
var count = 0; // 私有变量
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
})();
console.log(counter.getCount()); // 输出: 0
console.log(counter.increment()); // 输出: 1
console.log(counter.increment()); // 输出: 2
console.log(counter.decrement()); // 输出: 1
十三、回调函数
回调函数是将一个函数(f1)作为参数传递给另一个函数(f2),并在后者(f2)执行时调用前者(f1)的函数。回调函数在异步编程中非常常见,如Ajax请求、setTimeout等。
1.简单的回调函数示例:
function greet(name, callback) {
console.log("Hello, " + name);
// 调用回调函数
callback();
}
function sayGoodbye() {
console.log("Goodbye!");
}
// 使用回调函数
greet("Alice", sayGoodbye);
2.匿名的回调函数示例:
function greet(name, callback) {
console.log("Hello, " + name);
// 调用回调函数
callback();
}
// 使用匿名函数作为回调函数
greet("Alice", () => {
console.log("Goodbye!");
}); // 依次打印 "Hello, Alice"、"Goodbye!"
3.异步操作的回调函数示例:
function fetchData(url, callback) {
// 假设这里是一个异步的网络请求
setTimeout(() => {
// 模拟异步操作
const data = "这是从服务器获取的数据";
// 调用回调函数,并将数据作为参数传递
callback(data);
}, 1000); // 假设请求耗时1秒
}
function handleData(data) {
console.log(data);
}
// 使用回调函数处理异步获取的数据
fetchData("http://example.com/data", handleData);
在JavaScript中,很多异步操作,如定时器、文件读写、网络请求等,都通过回调函数来处理结果。
十四、递归函数
递归函数是一种自我调用的函数。递归函数必须要有终止条件,否则会导致无限递归,从而引发栈溢出错误。递归函数在排序、数据结构、数学计算等方面应用较多。
1.示例
1.计算一个数的阶乘是递归函数的一个经典示例。阶乘定义为n的阶乘等于n乘以(n-1)的阶乘,直到1的阶乘为1。
function factorial(n) {
// 基本情况
if (n === 1 || n === 0) {
return 1;
}
// 递归步骤
else {
return n * factorial(n - 1);
}
}
console.log(factorial(5)); // 输出 120
2.使用递归函数对树状结构的数据进行操作,你一定有经验。
假如树的结构如下:
const tree = {
value: 1,
children: [
{
value: 2,
children: [
{ value: 4 },
{ value: 5 }
]
},
{
value: 3,
children: [
{
value: 6,
children: [
{ value: 7 },
{ value: 8 }
]
}
]
}
]
};
接下来,定义一个递归函数来遍历这棵树并打印每个节点的值:
function traverseTree(node) {
// 首先,打印当前节点的值
console.log(node.value);
// 然后,检查是否有子节点
if (node.children && node.children.length > 0) {
// 如果有,递归地遍历每个子节点
node.children.forEach(child => {
traverseTree(child);
});
}
}
// 调用递归函数
traverseTree(tree); // 打印结果:12453678
或者,计算树的高度(即从根节点到最远叶子节点的最长路径上的节点数):
function calculateTreeHeight(node) {
// 如果节点为空,则高度为0
if (!node) {
return 0;
}
// 递归地计算每个子树的高度
let maxHeight = 0;
for (let child of node.children || []) {
const height = calculateTreeHeight(child);
if (height > maxHeight) {
maxHeight = height;
}
}
// 当前树的高度是其子树的最大高度加1(当前节点)
return maxHeight + 1;
}
console.log(calculateTreeHeight(tree)); // 输出树的高度
2.注意事项
- 终止条件:没有终止条件的递归函数会导致无限递归,从而引发栈溢出错误。
- 考虑递归的深度:对于深度很大的递归,可能会导致栈溢出。在JavaScript中,调用栈的大小是有限的,因此递归深度也受到限制。
- 性能问题:递归函数可能会比迭代函数更慢,因为它们需要额外的函数调用开销。此外,递归调用会占用调用栈的空间,因此它们可能会受到内存限制的影响。
- 避免不必要的递归:在某些情况下,使用迭代或循环可能更合适,因为它们不需要额外的函数调用开销,也不会占用调用栈的空间。
十五、迭代函数
迭代函数是数学中一种通过重复计算以求解问题的方法。在编程中,迭代通常通过循环结构(如for循环、while循环)来实现。
1.示例
跟递归函数同样的数据结构:
const tree = {
value: 1,
children: [
{
value: 2,
children: [
{ value: 4 },
{ value: 5 }
]
},
{
value: 3,
children: [
{
value: 6,
children: [
{ value: 7 },
{ value: 8 }
]
}
]
}
]
};
遍历树并打印出每个节点的值:
function traverseTreeIteratively(node) {
if (!node) return;
// 使用栈来存储待处理的节点
const stack = [node];
while (stack.length > 0) {
// 弹出栈顶元素
const currentNode = stack.pop();
// 处理当前节点(例如,打印其值)
console.log(currentNode.value);
// 如果当前节点有子节点,则将它们推入栈中(注意:这里假设是后序遍历,所以先右后左)
if (currentNode.children) {
for (let i = currentNode.children.length - 1; i >= 0; i--) {
stack.push(currentNode.children[i]);
}
}
}
}
// 调用迭代遍历函数
traverseTreeIteratively(tree); // 打印结果:12453678
2.与递归函数的区别
- 结构不同:
- 迭代是环结构,从初始状态开始,每次迭代都遍历这个环,并更新状态,多次迭代直到到达结束状态。
- 递归是树结构,可以理解为重复“递推”和“回归”的过程,当“递推”到达底部时就会开始“回归”,其过程相当于树的深度优先遍历。
- 实现方式:
- 迭代通常通过循环语句(如for、while)来实现。
- 递归则通过函数自身的调用来实现,直到满足某个终止条件。
- 时间复杂度与空间复杂度:
- 迭代的时间复杂度通常较低,因为它不涉及额外的函数调用开销。然而,如果迭代次数非常多,也可能导致较高的时间复杂度。
- 递归的时间复杂度可能较高,特别是当递归深度很大时,因为每次函数调用都会占用一定的栈空间,并且存在重复计算的风险。递归的空间复杂度通常较高,因为它需要存储每一层的函数调用信息。
- 错误处理:
- 迭代中,如果迭代器赋值或增量错误,或在终止条件中设置不当,可能导致无限循环,但这通常不会导致系统崩溃。
- 递归中,如果指定终止条件时出现错误,可能导致无限递归调用,从而可能导致系统CPU崩溃或栈溢出错误。
- 应用场景:
- 迭代更适用于那些可以明确知道循环次数或可以通过循环来逼近答案的问题。
- 递归更适用于那些可以将问题分解为与原问题相似的较小问题的场景,如树的遍历、图的搜索、分治算法等。
好了,分享结束,谢谢点赞,下期再见。