JavaScript 原理

深入理解函数和作用域

通过讲解作用域、作用域链、闭包和回调函数,帮助前端小白全面理解函数相关知识。

🎯 引言

学会这篇文章,你将理解前端中函数和作用域的核心概念,掌握如何利用闭包和回调函数写出高效、灵活的代码。实际开发中,这些知识能帮你避免很多常见错误,也能让你的代码更简洁、易维护。


⚙️ 什么是作用域?

作用域,简单来说,就是变量的“可见范围”。你可以理解成一个变量在哪里能被访问,在哪里不能访问。

在 JavaScript 里,作用域主要有两种:

  • 全局作用域:在代码的任何地方都可以访问的变量,就像你在全局地图上随时能找到一个城市。
  • 局部作用域:定义在函数或者代码块里的变量,只能在那个特定的范围内访问,就像某个房间里的物品,出了房间就看不到了。

举个生活中的例子:

想象你家里的不同房间(作用域),冰箱里的冰淇淋(变量)只能在厨房(局部作用域)里拿,出去客厅(其他作用域)就拿不到了。但家门口的信箱(全局变量)谁都能去开。

var globalVar = '我在全局';
function testScope() {
    var localVar = '我在函数里';
    console.log(globalVar); // 可以访问,输出 "我在全局"
    console.log(localVar); // 可以访问,输出 "我在函数里"
}
testScope();
console.log(globalVar); // 可以访问,输出 "我在全局"
console.log(localVar); // 报错,localVar 在这里不可访问
小提示:变量在定义时所在的作用域决定了它能在哪里被访问。函数内部定义的变量,只能在函数内部用。

🌐 函数内部访问外部变量(作用域链)

当函数想要访问一个变量时,JavaScript 会首先在函数内部找,如果没找到,就会去外层作用域找,再没找到继续往外一直找,直到全局作用域。这个查找的过程就是作用域链

打个比方:

你要找家里的小猫,你可能先在笼子里面找(函数内部作用于),没找到就去客厅找(外层作用于),再没找到就可能要去家门口或楼下找(全局作用域),如果还没找到就是没有了。

var a = 10;
function foo() {
    var b = 20;
    function bar() {
        var c = 30;
        console.log(a); // 10,向上查找变量 a
        console.log(b); // 20,向上查找变量 b
        console.log(c); // 30,当前作用域变量 c
    }
    bar();
}
foo();

在这个例子里,bar 函数访问不到自己作用域外的 ab,就会沿着作用域链向上找,最终找到了。

小提示:作用域链保证了外层变量在内部函数中是可见的,但内部变量对外层函数或全局是不可见的。

🧩 什么是闭包?闭包有什么用?

闭包,是函数和声明该函数的词法环境的组合。换句话说,闭包让函数能够“记住”并访问它定义时的作用域,即使函数在另一个作用域执行。

这个概念可能有点抽象,举个例子更容易懂:

function outer() {
    var count = 0;
    function inner() {
        count++;
        console.log(count);
    }
    return inner;
}

var fn = outer(); // outer 执行后返回 inner 函数
fn(); // 输出 1
fn(); // 输出 2
fn(); // 输出 3

这里,inner 函数形成了一个闭包,保存了 count 变量的状态,即使 outer 函数已经执行完毕,inner 依然可以访问并修改 count

闭包的常见用途

  • 数据封装:用闭包隐藏变量,保护数据不被外部直接访问,只能通过特定函数访问。
  • 实现私有变量:在对象中模拟私有变量,防止外部随意修改。
  • 函数工厂:根据参数返回不同功能的函数。
生活例子:你去咖啡店点了杯咖啡,咖啡师帮你把咖啡装好放在柜台(函数返回),你拿走咖啡后,虽然咖啡师不在了,但咖啡(闭包里的变量)还在,你可以慢慢喝。

🔄 回调函数:什么是“把函数当参数传”?

回调函数,就是把一个函数当作参数传递给另一个函数,等到适当的时候再调用。

比如说,你请朋友帮你买东西,告诉他买完回来告诉你。这个“告诉你”的动作就是回调函数。

function greet(name) {
    console.log('你好,' + name);
}

function processUserInput(callback) {
    var name = '小明';
    callback(name);
}

processUserInput(greet); // 输出 "你好,小明"

这里,greet 被传入 processUserInput,后者在合适的时候调用 greet,完成问候动作。

回调函数的应用场景

  • 处理异步操作,比如网络请求、定时器
  • 事件监听,比如按钮点击
  • 自定义控制流程,比如数组遍历中的回调
注意:不要在回调函数里传入括号,比如写成 processUserInput(greet()),这会直接调用函数,而不是传函数本身。

🧾 小节总结

  • 作用域决定变量的可访问范围,分全局和局部作用域。
  • 函数内部访问外部变量依赖作用域链,逐层向上查找。
  • 闭包让函数可以记住定义时的环境,常用来封装私有变量和实现函数工厂。
  • 回调函数是把函数作为参数传给另一个函数,方便异步和事件处理。

❓ 知识问答(Q&A)

Q:什么是作用域链?
A:作用域链是函数访问变量时,从内部作用域逐层向外查找变量的过程,直到找到或者到达全局作用域。

Q:闭包如何实现数据的私有化?
A:闭包使函数可以访问定义时的变量,这些变量对外部不可见,只能通过闭包函数操作,从而实现数据的封装和保护。

Q:回调函数和普通函数调用有什么区别?
A:回调函数是作为参数传给另一个函数,等那个函数运行时调用,而普通函数调用是直接执行。


🧪 小练习

  1. 编写一个计数器函数 makeCounter,返回一个函数,每次调用都返回累加的数字。
function makeCounter() {
    // 在这里编写代码
}
var counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
  1. 实现一个函数 filterArray,接收一个数组和一个回调函数,返回过滤后的新数组。回调函数决定元素是否保留。
function filterArray(arr, callback) {
    // 在这里编写代码
}

var nums = [1, 2, 3, 4, 5];
var evenNums = filterArray(nums, function (num) {
    // 在这里写判断逻辑,保留偶数
});
console.log(evenNums); // [2, 4]

🎉 恭喜你已经掌握了作用域、作用域链、闭包和回调函数技能啦!这些知识是前端开发的基石,未来学习更高级的内容会更顺畅。继续加油!