JavaScript 函数核心知识体系:从基础到精通
目录
- [函数的本质与对象特性]
- [this绑定的四种模式]
- [arguments对象与参数处理]
- [闭包机制深度解析]
- [递归算法详解]
- [回调函数与异步编程]
1. 函数的本质与对象特性
1.1 函数就是对象
在JavaScript中,函数是一种特殊的对象。它不仅包含可执行的代码,还可以拥有属性和方法。
// 函数作为对象的证据
function sayHi() {
console.log("Hello!");
}
// 可以像普通对象一样添加属性
sayHi.name = "greeting";
sayHi.version = "1.0";
console.log(sayHi.name); // "greeting"
console.log(sayHi.version); // "1.0"
1.2 函数的原型链结构
所有函数都连接到 Function.prototype,而 Function.prototype 又连接到 Object.prototype。
function fn() {}
└── [[Prototype]] → Function.prototype
└── [[Prototype]] → Object.prototype
└── [[Prototype]] → null
这意味着函数不仅能调用 call()、apply() 等方法(来自 Function.prototype),还能调用 toString() 等方法(来自 Object.prototype)。
1.3 函数的两个隐藏属性
每个函数在创建时都会附带两个隐藏属性:
- 函数的上下文:决定
this的绑定规则 - 实现函数行为的代码:函数体中的逻辑
1.4 prototype属性详解
每个函数对象在创建时都会自动获得一个 prototype 属性。
function Person(name) {
this.name = name;
}
console.log(Person.prototype);
// 输出: { constructor: Person }
// 默认constructor指向函数本身
console.log(Person.prototype.constructor === Person); // true
关键区分:
| 名称 | 是什么? | 作用 |
|---|---|---|
fn.__proto__ | 函数对象的 [[Prototype]] | 指向 Function.prototype,用于继承函数方法(如 .call()) |
fn.prototype | 函数的 prototype 属性 | 是一个普通对象,用于作为 new fn() 实例的原型 |
一句话总结:
__proto__是"我爸爸是谁"(功能来源);
prototype是"我的孩子将来长什么样"(模板)。
2. this绑定的四种模式
2.1 方法调用模式
当一个函数被作为一个对象的方法调用时,this 指向该对象。
var p = {
name: "Alice",
sayName: function() {
console.log(this.name);
}
};
p.sayName(); // 输出: Alice → this === p
2.2 函数调用模式
独立调用函数时,this 的指向取决于是否在严格模式下:
'use strict';
var fn = p.sayName;
fn(); // TypeError: Cannot read property 'name' of undefined
// 因为 this 是 undefined
// 非严格模式下,this 指向全局对象(window)
2.3 构造器调用模式
使用 new 关键字调用函数时,会创建一个新的对象,this 指向这个新对象。
function Person(name) {
this.name = name; // this 指向 new 创建的新对象
}
var p = new Person("Bob");
console.log(p.name); // Bob
2.4 apply/call调用模式
可以显式地指定 this 的值。
function greet() {
console.log("Hello, I'm " + this.name);
}
var obj = { name: "Charlie" };
greet.apply(obj); // 输出: Hello, I'm Charlie → this === obj
2.5 this绑定口诀
"this 不是‘我’,而是‘谁调用我’。"
- 方法调用 → this 是调用者
- 独立调用 → this 是全局/undefined
- new 调用 → this 是新对象
- apply 调用 → this 是你指定的
3. arguments对象与参数处理
3.1 arguments的本质
每当函数被调用时,会自动获得一个名为 arguments 的"免费"参数,它包含了所有传入的实际参数。
function greet(name, age) {
console.log("Name:", name);
console.log("Age:", age);
console.log("Other args:", arguments[2], arguments[3]);
}
greet("Alice", 25, "student", "developer");
// Other args: student developer
3.2 arguments的设计缺陷
尽管被称为"数组",但 arguments 并不是真正的数组,而是一个"类数组对象"。
function test() {
console.log(arguments.length); // 3
console.log(arguments[0]); // 'a'
console.log(arguments.slice()); // ❌ TypeError!
}
test('a', 'b', 'c');
3.3 arguments转换为真数组
有三种常用方法将 arguments 转换为真正的数组:
function convertArgs() {
// 方法1:Array.from()
var args1 = Array.from(arguments);
// 方法2:扩展运算符(ES6)
var args2 = [...arguments];
// 方法3:call + slice
var args3 = [].slice.call(arguments);
return args1;
}
3.4 现代替代方案
推荐使用 ES6 的 rest 参数来替代 arguments:
const sum = (...nums) => {
return nums.reduce((total, num) => total + num, 0);
};
sum(1, 2, 3); // 6
sum(4, 8, 15, 16, 23, 42); // 108
💡 金句:"arguments 是 JS 的历史遗产,它让函数变得灵活,但也留下了‘非数组’的遗憾。"
4. 闭包机制深度解析
4.1 什么是闭包?
闭包 = 一个函数 + 它被创建时所处的词法环境的引用
当一个内部函数即使在其外部函数执行完毕后仍能访问其变量时,就形成了闭包。
4.2 形成闭包的三个必要条件
| 条件 | 说明 | 示例 |
|---|---|---|
| ① 内部函数 | 必须有一个函数定义在另一个函数内部 | function outer() { function inner() {} } |
| ② 访问外部变量 | 内部函数必须引用外层函数的局部变量 | inner() { console.log(x); } |
| ③ 外部函数执行完毕后,内部函数仍可被调用 | 内部函数被返回或赋值给全局变量 | return inner; 或 window.fn = inner; |
4.3 经典示例
示例1:最简闭包
function makeAdder(x) {
return function(y) {
return x + y; // 访问外部变量 x
};
}
const add5 = makeAdder(5);
console.log(add5(3)); // 8
示例2:私有变量封装
function createCounter() {
let count = 0;
return {
increment: () => ++count,
decrement: () => --count,
value: () => count
};
}
const counter = createCounter();
counter.increment();
console.log(counter.value()); // 1
// count 对外界完全隐藏 → 实现了「数据封装」
示例3:事件监听器中的闭包
for (let i = 0; i < 3; i++) {
document.getElementById(`btn${i}`).addEventListener('click', () => {
console.log(`Button ${i} clicked!`);
});
}
4.4 let/const在闭包中的行为
| 场景 | var 行为 | let/const 行为 | 原因 |
|---|---|---|---|
| 在循环中定义函数 | 所有函数共享同一个变量 | 每次迭代都有独立的绑定 | let/const 有块级作用域 |
// ❌ var 陷阱
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出: 3, 3, 3
}
// ✅ let 正确
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出: 0, 1, 2
}
4.5 闭包的用途
| 用途 | 说明 | 示例 |
|---|---|---|
| 数据封装 | 隐藏实现细节 | createCounter() 中的 count |
| 函数工厂 | 动态生成函数 | makeAdder(5) |
| 模块化开发 | 避免全局污染 | IIFE 模块 |
| 异步编程 | 保持上下文 | setTimeout(() => console.log(x), 100) |
4.6 闭包风险与优化
| 风险 | 如何避免 |
|---|---|
| 内存占用增加 | 避免引用大型 DOM 对象或数据结构 |
| 意外保留作用域 | 不要在循环中无意捕获大对象 |
| 调试困难 | 使用 Chrome DevTools 查看 Scope |
🔑 终极口诀:"一嵌套、二访问、三脱离——三者俱全才是闭包。"
5. 递归算法详解
5.1 什么是递归?
递归是指函数直接或间接地调用自身。必须包含:
- 基线条件(Base Case):停止递归的条件
- 递推关系(Recursive Case):问题规模变小的自我调用
5.2 汉诺塔问题详解
问题描述
将 n 个圆盘从源柱移动到目标柱,遵循以下规则:
- 每次只能移动一个圆盘
- 不能将较大的圆盘放在较小的圆盘上面
- 可以使用辅助柱作为过渡
递归解法
var hanoi = function(disc, src, aux, dst) {
if (disc > 0) {
hanoi(disc - 1, src, dst, aux); // 步骤1:把上面n-1个移到辅助柱
console.log('Move disc ' + disc + ' from ' + src + ' to ' + dst); // 步骤2:移动最大盘
hanoi(disc - 1, aux, src, dst); // 步骤3:把n-1个从辅助柱移到目标柱
}
};
hanoi(3, 'Src', 'Aux', 'Dst');
完整执行流程(n=3)
| 步骤 | 输出 | 物理状态 |
|---|---|---|
| ① | Move disc 1 from Src to Dst | [3,2] / [] / [1] |
| ② | Move disc 2 from Src to Aux | [3] / [2] / [1] |
| ③ | Move disc 1 from Dst to Aux | [3] / [2,1] / [] |
| ④ | Move disc 3 from Src to Dst | [] / [2,1] / [3] |
| ⑤ | Move disc 1 from Aux to Src | [1] / [2] / [3] |
| ⑥ | Move disc 2 from Aux to Dst | [1] / [] / [3,2] |
| ⑦ | Move disc 1 from Src to Dst | [] / [] / [3,2,1] |
数学规律
移动 n 个盘所需的最少步数为:T(n) = 2ⁿ - 1
T(1) = 1
T(2) = 3
T(3) = 7
T(4) = 15
...
🧠 核心思想:"要把 n 个盘从 A→C,必须先搬走上面 n−1 个盘到 B,再把最大的盘移到 C,最后把 n−1 个盘从 B 移到 C。"
6. 回调函数与异步编程
6.1 同步 vs 异步
同步方式(阻塞式)
request = prepare_the_request();
response = send_request_synchronously(request); // 卡住等待
display(response);
❌ 问题:网络请求会导致客户端"假死"状态。
异步方式(非阻塞式)
request = prepare_the_request();
send_request_asynchronously(request, function(response) {
display(response);
});
✅ 优点:立即返回,不阻塞UI,响应到达时自动调用回调函数。
6.2 回调函数本质
- 函数作为参数传递给另一个函数
- 延迟执行,在特定事件触发时执行
- 实现事件驱动编程模型
- 达到代码解耦的效果
6.3 实际应用示例
function simulateAsyncRequest(callback) {
console.log("开始请求...");
setTimeout(() => {
const response = "Hello from server!";
console.log("响应到达!");
callback(response);
}, 1000);
}
function display(response) {
console.log("显示结果:" + response);
}
simulateAsyncRequest(display);
"回调 = 把‘要做什么’告诉别人,等事情做完后再通知你。"
6.4 发展演进
虽然回调是异步编程的基础,但存在"回调地狱"问题,因此发展出了:
- Promise:解决回调嵌套过深的问题
- async/await:让异步代码看起来像同步代码
总结与展望
核心知识点回顾
- 函数是对象:具有属性和方法,通过原型链继承
- this绑定:由调用方式决定,掌握四种模式
- arguments:类数组对象,现代开发推荐使用 rest 参数
- 闭包:记住作用域的能力,是许多高级特性的基础
- 递归:分治思想的体现,适用于树形结构等问题
- 回调:异步编程的基石,理解事件循环的关键