第二章: 函数与作用域:代码组织与执行上下文

第二章: 函数与作用域:代码组织与执行上下文

摘要: 在第一章,我们掌握了 JavaScript 的基本构件。现在,我们将进入一个更核心的领域:函数与作用域。函数是组织可复用代码的基石,而作用域则是控制这些代码中变量访问权限的规则体系。本章将深入探讨函数声明与表达式的区别、闭包的强大威力、this 关键字的精髓,以及如何通过 call, apply, bind 精准控制函数的执行上下文。理解这些概念,是您从“会写代码”迈向“写好代码”的关键一步。

在本章中,我们将层层递进,揭开函数与作用域的神秘面纱:

  1. 首先,我们将从 函数基础 出发,辨析两种核心的函数定义方式及其差异。
  2. 接着,我们将深入 作用域与闭包,理解 JavaScript 是如何管理变量以及函数为何能“记住”其创建时的环境。
  3. 然后,我们将攻克 JS 中最重要也最易混淆的概念之一 —— this 关键字,掌握其指向的四大核心规则。
  4. 紧接着,我们将学习 call, apply, bind 这三个强大的工具,学会如何随心所欲地改变 this 的指向。
  5. 最后,我们将探讨几种 特殊的函数形式,尤其是改变了 this 规则的箭头函数。

2.1. 函数基础:代码复用的核心

您已经了解函数的基本概念,但 JavaScript 中定义函数的方式存在一个不易察觉的但至关重要的区别:函数声明与函数表达式。

定义方式
使用 function 关键字开头,后跟函数名。

1
2
3
4
5
console.log(add(5, 10)); // 15

function add(a, b) {
return a + b;
}

核心特性

  • 提升:整个函数定义在代码执行前被提升到作用域顶部,可在声明前调用。
  • 适用场景:全局/模块级工具函数、无需动态生成时首选。

定义方式
创建匿名函数并赋值给变量(常用 const/let)。

1
2
3
4
5
6
7
8
9
10
11
try {
console.log(subtract(10, 5)); // 报错!
} catch (e) {
console.error(e.message); // Cannot access 'subtract' before initialization
}

const subtract = function (a, b) {
return a - b;
};

console.log(subtract(10, 5)); // 5

核心特性

  • 变量声明提升,赋值不提升:调用发生在赋值前会触发暂时性死区 (TDZ)。
  • 适用场景:回调、按需动态定义、需要闭包或箭头函数时更灵活。

函数的参数

JavaScript 在函数参数处理上非常灵活,ES6 更是引入了便捷的默认值和 Rest 参数。

  • 参数默认值: 为参数提供默认值,当调用函数时未传递该参数或传递了 undefined 时,该默认值会被使用。

    1
    2
    3
    4
    5
    function greet(name = "Guest", message = "Welcome") {
    console.log(`${message}, ${name}!`);
    }
    greet("Prorise"); // Welcome, Prorise!
    greet(); // Welcome, Guest!
  • Rest 参数 (Rest Parameters): 使用 ... 语法,可以将一个不定数量的参数表示为一个数组。这在需要处理可变数量参数的场景中非常有用。

    1
    2
    3
    4
    5
    6
    function sum(...numbers) {
    // 'numbers' 是一个包含了所有传入参数的真实数组
    return numbers.reduce((total, current) => total + current, 0);
    }
    console.log(sum(1, 2, 3)); // 6
    console.log(sum(10, 20, 30, 40)); // 100

    Rest 参数必须是函数参数列表的最后一个参数。


2.2. 作用域与闭包:理解变量的生命周期与“记忆”

承上启下: 我们已经知道如何创建函数,但函数内部的变量是如何被访问和管理的?这就引出了 JavaScript 中两个最核心的概念:作用域和闭包。

作用域 (Scope)

作用域 是指在程序中定义变量的区域,它决定了变量的可见性和生命周期。在现代 JavaScript 中,主要有三种作用域:
  1. 全局作用域 (Global Scope): 在所有函数和代码块之外定义的变量,拥有全局作用域,在代码的任何地方都可以访问。
  2. 函数作用域 (Function Scope): 在函数内部定义的变量,只能在该函数内部访问。
  3. 块级作用域 (Block Scope): (ES6 新增) 在 {} 代码块(如 if, for, while 语句)内由 letconst 声明的变量,只在该代码块内部有效。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. 全局作用域 (Global Scope)
const globalVar = "我在全局作用域";

console.log(globalVar); // -> "我在全局作用域"

function scopeTest() {
// 2. 函数作用域 (Function Scope)
var functionVar = "我仅在函数作用域内可见";

console.log(globalVar); // -> 可以访问全局变量
console.log(functionVar); // -> "我仅在函数作用域内可见"

if (true) {
// 3. 块级作用域 (Block Scope)
let blockVar = "我仅在这个 if 块内可见";
console.log(blockVar); // -> "我仅在这个 if 块内可见"
}
}

闭包 (Closure)

痛点背景: 按照作用域规则,一个函数执行完毕后,其内部的局部变量应该被销毁和回收。但如果我们希望一个函数能“记住”它创建时所在的环境,即使它在其他地方被执行,应该怎么办?

1
2
3
4
5
6
7
function createGreeter() {
const name = "Prorise";
// 此处应该返回一个打招呼的动作,但如果直接返回字符串,name 变量就丢失了
return `Hello, ${name}`; // 这样无法实现“稍后”打招呼
}
const greeting = createGreeter();
console.log(greeting); // 只能立即得到结果

解决方案: 闭包 就是解决这个问题的答案。闭包能使函数执行完后,其内部局部变量因被引用而不被销毁,从而记住创建时的环境。比如在一个函数内部返回另一个函数,内部函数就形成了闭包,可访问外部函数的局部变量。在 JavaScript 中,当一个函数返回另一个函数时,就创建了一个闭包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createGreeter() {
const name = "Prorise";
// 返回一个函数,这个函数“记住”了它被创建时的环境,包括变量 name
return function () {
console.log(`Hello, ${name}`);
};
}

// greeter 变量现在持有了内部返回的那个函数
const greeter = createGreeter();

// 即使 createGreeter 已经执行完毕,greeter 函数依然可以访问到 name 变量
greeter();
greeter();

闭包的核心价值:

  1. 数据封装与私有变量: 闭包可以创建出只能通过特定函数访问的“私有”变量,是实现模块化和封装的基础。
  2. 状态保持: 让函数能够跨多次调用保持状态,例如计数器、缓存等。

2.3. this 关键字:解密上下文的指向

this 是 JavaScript 中最令人困惑的概念之一,但也是最重要的。与许多其他语言不同,JavaScript 中 this 的值并不取决于它在哪里被定义,而是取决于它在何处、以及如何被调用this 指向的是函数的 执行上下文

掌握 this 的关键在于理解以下四种绑定规则:

1. 默认绑定

当函数作为独立函数直接调用时(没有通过对象调用),this 会被绑定到全局对象。在浏览器中是 window,在严格模式 ('use strict') 下是 undefined

1
2
3
4
5
function showThis() {
console.log(this);
}

showThis(); // 在非严格模式下,输出 window 对象

2. 隐式绑定

当函数作为对象的一个方法被调用时,this 会被绑定到调用该方法的那个对象。

1
2
3
4
5
6
7
8
9
const user = {
name: "Prorise",
greet: function() {
// this 指向调用 greet 方法的 user 对象
console.log(`Hello, my name is ${this.name}`);
}
};

user.greet(); // greet 是通过 user 对象调用的

陷阱: 如果将方法赋给另一个变量再调用,隐式绑定会丢失,退化为默认绑定。

1
2
3
4
5
6
7
8
const user = {
name: "Prorise",
greet: function () {
console.log(this.name);
}
};
const standaloneGreet = user.greet;
standaloneGreet(); // 输出 undefined (非严格模式下 this.name 是 window.name)

3. new 绑定

当函数通过 new 关键字调用(作为构造函数)时,this 会被绑定到一个新创建的空对象上。

1
2
3
4
5
6
7
8
function User(name) {
// this 被绑定到一个新对象 {}
this.name = name;
// 构造函数隐式返回 this
}

const userInstance = new User("Prorise");
console.log(userInstance.name);

4. 显式绑定

我们可以通过 call(), apply(), 或 bind() 方法,强制指定函数执行时的 this 值。我们将在下一节详细探讨。


2.4. 核心原理:callapplybind 的应用

上一节我们提到,隐式绑定可能会丢失,导致 this 指向非预期的对象。为了解决这个问题,JavaScript 提供了三种方法来显式地、强制地设置函数的 this 上下文。

作用
立即调用函数,并把 this 绑定到指定对象;其余参数 按逗号逐个传递

记忆法
callC omma(逗号分隔参数)

1
2
3
4
5
6
7
const person = { name: "Prorise" };

function introduce(city, country) {
console.log(`I am ${this.name}, from ${city}, ${country}.`);
}

introduce.call(person, "Guangzhou", "China"); // I am Prorise, from Guangzhou, China.

作用
call 相同,但参数以 数组(或类数组) 一次性传入。

记忆法
applyA rray(数组传参)

1
2
3
4
5
6
7
8
const person = { name: "Prorise" };
const location = ["Shenzhen", "China"];

function introduce(city, country) {
console.log(`I am ${this.name}, from ${city}, ${country}.`);
}

introduce.apply(person, location); // I am Prorise, from Shenzhen, China.

作用
不立即调用,而是返回一个 永久绑定 this 的新函数;可预设部分参数(柯里化)。

记忆法
bindB ind and return(绑定并返回新函数)

1
2
3
4
5
6
7
8
9
10
const person = { name: "Prorise" };

function introduce(city, country) {
console.log(`I am ${this.name}, from ${city}, ${country}.`);
}

const boundIntroduce = introduce.bind(person, "Beijing");

// 稍后调用
boundIntroduce("China"); // I am Prorise, from Beijing, China.

2.5. 特殊函数:IIFE, 箭头函数

除了常规函数,JavaScript 还有一些特殊的函数形式,它们在特定场景下非常有用。

IIFE (立即执行函数表达式)

IIFE (Immediately Invoked Function Expression) 是一个在定义时就立即执行的函数表达式。

痛点背景: 在 ES6 出现块级作用域之前,为了避免在 for 循环等场景中创建的变量污染全局作用域,开发者们发明了 IIFE 来创建一个临时的函数作用域。

1
2
3
4
(function() {
var message = "函数内部作用域";
console.log(message);
})();

虽然 letconst 的块级作用域让 IIFE 用于创建作用域的需求大大降低,但它在一些需要初始化且只执行一次的模块化代码中仍有应用。

箭头函数

ES6 引入的箭头函数提供了一种更简洁的函数写法,但它最重要的特性是 它没有自己的 this 绑定

核心特性: 箭头函数会捕获其定义时所在上下文(作用域)的 this 值,并将其作为自己的 this。这彻底解决了传统函数在回调中 this 指向丢失的问题。

痛点背景: 看一个传统的回调 this 丢失问题。

1
2
3
4
5
6
7
8
9
10
11
const team = {
name: "Prorise Devs",
members: ["Alice", "Bob"],
showMembers: function() {
this.members.forEach(function(member) {
// 这里的 this 不再指向 team 对象,而是 window (默认绑定)
console.log(`${member} is on team ${this.name}`);
});
}
};
team.showMembers();

解决方案: 使用箭头函数,this 会被自动绑定到 showMembers 方法的 this,也就是 team 对象。

1
2
3
4
5
6
7
8
9
10
11
const team = {
name: "Prorise Devs",
members: ["Alice", "Bob"],
showMembers: function() {
this.members.forEach(member => {
// 箭头函数没有自己的 this,它会“继承”外层 showMembers 的 this
console.log(`${member} is on team ${this.name}`);
});
}
};
team.showMembers();

2.6. 本章核心速查

核心速查总结

分类关键项核心描述
函数定义函数声明function name(){},存在函数提升,可在声明前调用。
函数表达式const name = function(){},不提升,更灵活。
核心概念闭包 (Closure)函数能“记住”并访问其定义时的作用域,用于封装和状态保持。
this 绑定隐式绑定obj.method()this 指向 obj
显式绑定使用 call, apply, bind 强制指定 this
箭头函数没有自己的 this,继承外层作用域的 this
this 控制call() / apply()立即执行 函数,区别在于参数是 逗号分隔 还是 数组
bind()返回一个新函数,其 this 被永久绑定。