# JavaScript 函数核心知识体系:从基础到精

6 阅读7分钟

JavaScript 函数核心知识体系:从基础到精通

目录

  1. [函数的本质与对象特性]
  2. [this绑定的四种模式]
  3. [arguments对象与参数处理]
  4. [闭包机制深度解析]
  5. [递归算法详解]
  6. [回调函数与异步编程]

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 什么是递归?

递归是指函数直接或间接地调用自身。必须包含:

  1. 基线条件(Base Case):停止递归的条件
  2. 递推关系(Recursive Case):问题规模变小的自我调用

5.2 汉诺塔问题详解

问题描述

将 n 个圆盘从源柱移动到目标柱,遵循以下规则:

  1. 每次只能移动一个圆盘
  2. 不能将较大的圆盘放在较小的圆盘上面
  3. 可以使用辅助柱作为过渡
递归解法
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:让异步代码看起来像同步代码

总结与展望

核心知识点回顾

  1. 函数是对象:具有属性和方法,通过原型链继承
  2. this绑定:由调用方式决定,掌握四种模式
  3. arguments:类数组对象,现代开发推荐使用 rest 参数
  4. 闭包:记住作用域的能力,是许多高级特性的基础
  5. 递归:分治思想的体现,适用于树形结构等问题
  6. 回调:异步编程的基石,理解事件循环的关键