作用域链 × 闭包:三段代码,看懂 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 秒背完收工

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

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

相关推荐
San30.1 小时前
深入理解 JavaScript 异步编程:从 Ajax 到 Promise
开发语言·javascript·ajax·promise
风止何安啊1 小时前
收到字节的短信:Trae SOLO上线了?尝尝鲜,浅浅做个音乐播放器
前端·html·trae
抱琴_1 小时前
大屏性能优化终极方案:请求合并+智能缓存双剑合璧
前端·javascript
用户463989754321 小时前
Harmony os——长时任务(Continuous Task,ArkTS)
前端
fruge1 小时前
低版本浏览器兼容方案:IE11 适配 ES6 语法与 CSS 新特性
前端·css·es6
颜酱1 小时前
开发工具链-构建、测试、代码质量校验常用包的比较
前端·javascript·node.js
mCell2 小时前
[NOTE] JavaScript 中的稀疏数组、空槽和访问
javascript·面试·v8
柒儿吖2 小时前
Electron for 鸿蒙PC - Native模块Mock与降级策略
javascript·electron·harmonyos
颜酱2 小时前
package.json 配置指南
前端·javascript·node.js