作用域链 × 闭包:三段代码,看懂 JavaScript 的套娃人生

一句话结论:

JS 的变量查找永远沿「作用域链」往上爬;

闭包则把"本该销毁"的作用域偷偷塞进函数口袋,让它长生不死。

下面用三段递进式代码,从"最简 demo"到"闭包 full feature",逐行剖给你看。


第一段:最简作用域链------「静态词法」碾压「动态调用」

javascript 复制代码
var myName = '极客世界';

function bar() {
  console.log(myName);   // ① 打印什么?
}

function foo() {
  var myName = '极客邦';
  bar();                 // ② 在 foo 里调用 bar
}

foo();

运行结果

复制代码
极客世界

为什么不是"极客帮"?

在我们的脑海里看到这幅代码应该很快就能想到下面这幅图

对啊如果是这幅图按调用栈的顺序没找到应该往下找啊,但是这其实是一个坑。 先来讲讲作用域链吧。 作用域链也叫词法作用域链,静态的,只和函数声明的位置有关,在编译阶段就决定好了,和调用没有关系。 真正的查找应该是按作用域链的规则查找,也就是按下面这幅图的规则

  • 作用域链在定义时(词法阶段)就写好,跟在哪调用没半毛钱关系;
  • bar 的外部词法环境是 全局
  • 变量查找路径:bar → 全局;永远找不到 foomyName

记住口诀:"调用位置只影响栈,变量查找只看出生证明。"


第二段:套娃加深------"同名变量"在作用域链上打伏击

javascript 复制代码
let myAge = 10;
let test = 1;               // 全局 test
var myName = '极客时间';     // 全局 myName

function bar() {
  var myName = '极客世界';   // ② bar 函数环境
  let test1 = 100;
  if (1) {
    let myName = 'Chrome 浏览器'; // ③ 块级环境
    console.log(test);     // 打印哪个 test ?
  }
}

function foo() {
  var myName = '极客邦';    // ① foo 函数环境
  let test = 2;             // 遮蔽全局 test
  {
    let test = 3;           // 块级 test,完全互不影响
    bar();                  // 在块里调用 bar
  }
}

foo();

运行结果

复制代码
1

作用域链图解

  • 块级 test=3test=2 都住在更外层,对 bar 不可见
  • 再次验证:词法作用域只看出身,不看调用栈。

第三段:闭包登场------把外层环境"偷渡"出栈

javascript 复制代码
function foo() {
  var myName = '极客时间';
  let test1 = 1;
  let test2 = 2;

  // 1. 对象里两个函数都「引用」外部变量
  var innerBar = {
    getName: function () {
      console.log(test1); // 引用 test1
      return myName;      // 引用 myName
    },
    setName: function (newName) {
      myName = newName;   // 修改外部 myName
    }
  };
  return innerBar;        // 2. 把内部对象抛到外部
}

var bar = foo();          // foo 执行上下文出栈
                          // 但 myName/test1/test2 因为被引用**不会**被 GC

bar.setName('极客邦');    // 修改的是原来 foo 环境下的 myName
console.log(bar.getName()); // 1. 先打印 test1 → 1
                            // 2. 再打印返回值 → 极客邦

闭包形成条件(背)

  1. 函数嵌套函数;
  2. 内部函数引用外部变量;
  3. 内部函数被返回到外部并存活。

V8 底层怎么做到的?

按理说在var bar=foo() ;这一步的结束的时候要执行出栈操作 bar 里面的变量要回收吧? 但是实际情况并没有这是为什么呢?其实是因为foo 函数执行完后,其执行上下文从栈顶弹出了,但是由于返回的setName,getName 使用了foo 函数内部的变量myName和tsst1,这两个变量依然在内存中,有点像getName,setName 方法背的一个专属背包。 这个背包我们就叫做闭包,这个闭包里面的变量叫自由变量 看图理解吧

因为你在 foo() 内部定义了 getNamesetName 这两个函数,并且它们引用了 foo 内部的变量 (比如 myNametest1),那么:

  • 这两个函数就形成了闭包
  • 它们会"记住"自己被创建时所处的词法环境 (也就是 foo 的作用域)。
  • 即使 foo() 执行完毕、调用栈弹出,只要这两个函数还被外部(比如 bar)引用着,JavaScript 引擎就不会销毁 foo 中被引用的那些变量。

你可以形象地理解为:
每个函数都背着一个小背包(闭包),里面装着它创建时能访问到的外部变量。

  • 于是 test1 还能读到 1,myName 还能被改。

口诀:"栈帧可死,闭包长存。"


日常开发 3 句忠告

  1. 少用闭包随便拉数据------内存泄漏就是这么来的;
  2. 循环里返回函数先 let 再包,别用 var
  3. 调试打开 DevTools → Scope 面板,Local/Closure/Global 一眼看清变量到底从哪来。

30 秒背完收工

变量查找看出生,调用位置是浮云;

内部函数拎外部,闭包环境永长存。

相关推荐
少年姜太公2 小时前
什么?还不知道git cherry pick?
前端·javascript·git
白兰地空瓶4 小时前
🏒 前端 AI 应用实战:用 Vue3 + Coze,把宠物一键变成冰球运动员!
前端·vue.js·coze
Liu.7745 小时前
vue3使用vue3-print-nb打印
前端·javascript·vue.js
松涛和鸣6 小时前
Linux Makefile : From Basic Syntax to Multi-File Project Compilation
linux·运维·服务器·前端·windows·哈希算法
dly_blog6 小时前
Vue 逻辑复用的多种方案对比!
前端·javascript·vue.js
万少6 小时前
HarmonyOS6 接入分享,原来也是三分钟的事情
前端·harmonyos
烛阴6 小时前
C# 正则表达式:量词与锚点——从“.*”到精确匹配
前端·正则表达式·c#
wyzqhhhh7 小时前
京东啊啊啊啊啊
开发语言·前端·javascript
JIngJaneIL7 小时前
基于java+ vue助农电商系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
想学后端的前端工程师7 小时前
【Java集合框架深度解析:从入门到精通-后端技术栈】
前端·javascript·vue.js