"红宝书" 通常指的是《JavaScript 高级程序设计》,这是一本由 Nicholas C. Zakas(尼古拉斯·扎卡斯)编写的 JavaScript 书籍,是一本广受欢迎的经典之作。这本书是一部翔实的工具书,满满的都是 JavaScript 知识和实用技术。
不管你有没有刷过红宝书,如果现在还没掌握好,那就一起来刷红宝书吧,go!go!go!
系列文章:
第一部分:基本知识(重点、反复阅读)
第二部分:进阶内容(重点、反复阅读)
第 10 章 函数
基本概念
- 函数定义: 使用关键字(例如
function
)定义函数。 - 参数: 函数可以接受输入参数,这些参数是在调用函数时传递给函数的值。
- 返回值: 函数可以产生输出,即返回值,通过
return
语句定义。
函数的类型
在不同的上下文中,函数可以分为几种不同的类型,其中包括但不限于:
- 全局函数(Global Functions): 在整个程序中都可访问的函数。
- 局部函数(Local Functions): 在其他函数内部定义的函数,只能在包含它的函数内部访问。
- 匿名函数(Anonymous Functions): 没有名称的函数,通常作为参数传递给其他函数,或者作为立即执行的函数表达式(IIFE)使用。
- 箭头函数(Arrow Functions): ES6 引入的一种简写语法,具有更短的语法和改变
this
行为的特点。 - 高阶函数(Higher-Order Functions): 接受函数作为参数或返回一个函数的函数,常用于函数式编程。
- 构造函数(Constructor Functions): 可以通过
new
关键字调用的函数,用于创建对象的实例。
函数的调用模式
函数的调用方式有四种主要模式:
- 函数调用模式(Function Invocation Pattern): 直接调用函数,例如
func()
。 - 方法调用模式(Method Invocation Pattern): 通过对象调用的函数,例如
object.method()
。 - 构造函数调用模式(Constructor Invocation Pattern): 使用
new
关键字调用的函数,创建对象实例。 - 间接调用模式(Indirect Invocation Pattern): 使用
call
或apply
方法调用函数,可以改变函数的上下文。
箭头函数
箭头函数没有自己的 this
,它继承自外围作用域(即定义时所在的作用域)的 this
。这与传统的函数(函数表达式或函数声明)不同,它们的 this
值会在运行时动态确定。
- 不适用于构造函数:箭头函数不能被用作构造函数,不能使用
new
关键字调用。 - 没有
arguments
对象:箭头函数没有自己的arguments
对象,它继承自外围作用域的arguments
。 - 不能用作 generator 函数:箭头函数不能使用
yield
关键字创建迭代器。 - 没有
this
、arguments
、super
和new.target
绑定。 - 不能通过
call()
、apply()
、bind()
方法修改this
。 - 没有原型 prototype 属性。
函数声明和函数表达式
函数声明
是一种定义函数的方式,使用 function
关键字后跟函数名。函数声明提升至作用域的顶部,因此可以在声明之前调用函数。
js
function greet() {
console.log('Hello, World!');
}
greet(); // 调用函数
函数表达式
是将函数赋值给变量的一种方式,函数表达式的函数名是可选的。这种方式的函数通常是匿名函数,但也可以有名字。
函数表达式的变量不会提升,只有在赋值语句执行时才会创建。
js
// 匿名函数表达式
const greet = function() {
console.log('Hello, World!');
};
// 有名字的函数表达式
const sayHello = function greet() {
console.log('Hello, World!');
};
函数声明 vs 函数表达式:
- 提升(Hoisting): 函数声明会被提升到当前作用域的顶部,可以在声明之前调用。而函数表达式只有在赋值语句执行时才会创建,不会提升。
- 调用时间: 函数声明可以在定义之前和之后调用,而函数表达式只能在定义之后调用。
- 命名: 函数声明必须有名称,而函数表达式可以是匿名的,也可以有名字。
- 赋值给变量: 函数声明不能被赋值给变量,而函数表达式通常是赋值给变量的。
在函数表达式中,可以为函数指定一个名字,这被称为命名函数表达式。命名函数表达式的主要用途是在函数内部引用自身,方便递归调用。
js
const factorial = function self(n) {
if (n === 0 || n === 1) {
return 1;
} else {
return n * self(n - 1);
}
};
console.log(factorial(5)); // 输出: 120
IIFE 是一种立即调用的匿名函数表达式,通常用于创建私有作用域,防止变量污染全局作用域。
js
(function() {
// 这里是一个私有作用域
const localVar = 'IIFE Example';
console.log(localVar);
})();
// localVar 在此处不可访问
call
、apply
和 bind
call
、apply
和 bind
都是 JavaScript 中用于改变函数执行上下文(this
值)的方法,它们在用法和效果上有一些区别。
js
function greet(name) {
console.log(`Hello, ${name}! My age is ${this.age}.`);
}
const person = { age: 25 };
greet.call(person, 'John');
// 输出: Hello, John! My age is 25.
apply
方法与 call
类似,也是在函数执行时立即调用。区别在于它接收一个数组或类数组对象作为参数,其中数组的每个元素都会作为参数传递给函数。
js
function greet(name, city) {
console.log(`Hello, ${name}! I live in ${city}. My age is ${this.age}.`);
}
const person = { age: 25 };
greet.apply(person, ['John', 'New York']);
// 输出: Hello, John! I live in New York. My age is 25.
bind
方法也是用于改变函数执行上下文,但它不会立即调用函数,而是返回一个新的函数。新函数在调用时将具有绑定的 this
值和可选的参数。
js
function greet(name) {
console.log(`Hello, ${name}! My age is ${this.age}.`);
}
const person = { age: 25 };
const boundGreet = greet.bind(person, 'John');
boundGreet();
// 输出: Hello, John! My age is 25.
尾调用优化
尾调用优化(Tail Call Optimization,TCO)是一种编译器优化技术,用于优化尾递归函数的性能
。
尾调用是指函数的最后一个操作是调用另一个函数
。
尾调用优化的目标是减少调用栈的大小
,提高程序的性能和内存利用率。
js
// 尾递归函数,计算阶乘
function factorial(n, result = 1) {
if (n <= 1) {
return result;
}
return factorial(n - 1, n * result);
}
console.log(factorial(5)); // 输出: 120
ES6 尾调用优化的关键:如果函数的逻辑允许基于尾调用将其销毁,则引擎就会那么做。
尾调用优化的条件:
尾调用优化的条件就是确定外部栈帧真的没有必要存在了
:
- 代码在严格模式下执行。
- 外部函数的返回值是对尾调用函数的调用。
- 尾调用函数返回后不需要执行额外的逻辑。
- 尾调用函数不是引用外部函数作用域中自由变量的闭包。
js
"use strict"
// 基础框架
function fib(n){
return fibImp1(0, 1, n)
}
// 执行递归
function fibImp1(a, b, n){
if(n===0){
return a
}
return fibImp1(b, a+b, n-1)
}
闭包
闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的
。
js
function outerFunction() {
let outerVariable = 'I am from outer function';
function innerFunction() {
console.log(outerVariable);
}
return innerFunction;
}
const closure = outerFunction();
closure(); // 输出: I am from outer function
- 访问外部变量: 闭包可以访问包含它的外部函数的变量,即使外部函数已经执行完毕。
- 保持状态: 由于闭包内部可以访问外部变量,可以用闭包来保持一些状态信息,形成类似私有变量的效果。
- 数据隐藏和封装: 通过闭包可以创建私有变量,实现数据的隐藏和封装。
- 实现模块化: 使用闭包可以创建模块,将相关的功能和变量封装在一起。
- 异步编程: 在回调函数和事件处理函数中经常使用闭包,以便访问外部的变量。
- 内存管理: 闭包可能导致内存泄漏,因为它会保持对外部变量的引用,使得这些变量不能被垃圾回收。
- 性能: 过度使用闭包可能导致性能问题,因为每个闭包都会创建一个新的作用域链。
this 对象
如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后就可以按照顺序应用下边的规则来判断 this 的绑定对象。
箭头函数
会继承外层函数调用的 this 绑定。- 由
new
调用?绑定到新创建的对象。 - 由
call 或 apply 或 bind
调用?绑定到指定的对象。 - 由
上下文对象
调用?绑定到那个上下文对象。 - 默认:严格模式下帮点个到
undefined
,否则绑定到全局对象
。
未完待续...
参考资料
《JavaScript 高级程序设计》(第 4 版)