从 "变量捉迷藏" 到 "闭包魔法":JavaScript 里那些让人头秃又上瘾的坑

233 阅读4分钟

你有没有过这样的经历?对着屏幕上的代码抓耳挠腮:"明明定义了变量,为啥打印出来是 undefined?"、"这函数都执行完了,怎么还能访问里面的变量?"

今天咱们就来扒一扒 JavaScript 里那些像捉迷藏一样的变量行为,保证让你看完直呼 "原来如此"!

一、var:最调皮的变量声明

先看个经典案例:

// 闭包/3.js里的坑

 var myname = '男模'

function showName() {

 console.log(myname); // 猜猜这里打印啥?

 if (0) {

   var myname = '鹤' // 永远不会执行的代码

 }

 console.log(myname);

}

showName()

9DFD826C-39BD-4F46-87B9-A3C4FD7BD212.png

运行结果居然是两个undefined!这就像你在家找钥匙,明明记得放玄关了,结果翻遍全家都找不到 —— 因为var有 "变量提升" 的超能力,会把声明提到作用域顶部,但赋值还在原地。

换成 C 语言就规矩多了(闭包 / 2.c):

char* myname = '男模';

void showName () {

 printf("%s \n", myname); // 这里肯定打印"男模"

 if (0) {

   char* myname = '鹤';

 }

}

二、let/const:给变量装了防盗门

ES6 的 let 和 const 就懂事多了,它们有严格的块级作用域:

// 闭包/1.js的灵魂拷问

if (1) {

 let a = 1

}

console.log(a); // 报错:a is not defined

这就像你把零食锁进抽屉,出了这个房间就别想偷吃了。而 var 呢,就像把零食扔在客厅,哪个房间都能拿到(甚至 if/for 块里定义的 var,外面也能访问)。

再看个嵌套场景(闭包 / 4.js):

function foo() {

 var a = 1

 let b = 2

 {

   let b = 3 // 这个b和外面的b是两个独立变量

   var c = 4 // var无视块级作用域

   let d = 5

   console.log(a); // 1(能访问外层变量)

   console.log(b); // 3(当前块的b)

 }

 console.log(b); // 2(外层的b)

 console.log(c); // 4(var声明的变量能访问)

 console.log(d); // 报错(d被锁在块里了)

}

2.png b = 3和d = 5被取出来之后栈将会被销毁

作用域链

v8在当前作用域中查找一个变量,如果找不到就去上一级作用域查找,再找不到,再去上一级,层层往上。

一个函数被编译时,一定会用一个outer指针来记录该作用域的外层作用域是谁

词法作用域:函数声明的位置

JavaScript 里的变量查找就像走亲戚,先找自己家,没有就去父母家,再没有就去爷爷奶奶家...

三、作用域链:变量的寻亲之路

// 闭包/5.js的亲戚访问

function bar() {

 console.log(myName); // 找myName

}

function foo() {

 var myName = '小君'

 bar() // 执行时会找谁?

}

var myName = '总'

foo() // 最终打印"总"

3.png

bar 函数找 myName 时,不会因为在 foo 里被调用就去找 foo 家的,而是按自己的出身(被定义的位置)找 —— 这就是 "静态作用域",只看代码结构不看调用位置。

四、闭包:变量的保鲜盒

  1. 一个函数执行完毕后,他的执行上下文会被销毁
  2. 一个函数的内部函数一定有权利访问该外部函数的变量(作用域的规则)

当调用一个外部函数中返回的内部函数后,即使外部函数已经执行结束,但是内部函数依然引用了外部函数中的变量,那么外部函数的执行上下文就不能被完全销毁,而是会保留一个集合,用来装内部函数需要引用的变量,我们把这个集合称为闭包。

  • 优点:定义私有变量,封装模块
  • 缺点:内存泄漏(可用内存减少)

最神奇的来了!闭包能把变量像放进保鲜盒一样保存起来:

// 闭包/6.js的魔法

function foo() {

 var myname = '佳颖'

 var age = 18

 function bar() {

   console.log(myname); // 访问foo里的变量

 }

 return bar // 把函数带出去

}

var baz = foo()

baz() // 居然还能打印"佳颖"!

4.png

foo 函数都执行完了,它里面的变量本该被回收,却因为 bar 函数还惦记着(引用着),所以一直保存在内存里。这就像你出差了,但家里钥匙被朋友拿着,你的家就不会被拆迁~

五、实战:用闭包解决经典问题

最常见的就是 for 循环绑定事件的坑,看看闭包怎么救场(闭包 / 7.js):

var arr = []

// 用闭包保存每次的i值

for (var i = 1; i <= 5; i++) {

 function foo(j) {

   arr.push(function() {

     console.log(j); // 这里的j是每次传入的i

   })

 }

 foo(i)

}

// 执行后会依次打印1,2,3,4,5

5.png

如果不用闭包,直接 pushfunction(){console.log(i)},最终会全部打印 6,因为 i 最后变成了 6。闭包就像给每个函数拍了张快照,记录下当时的 i 值。

总结:变量管理口诀

  1. var 声明作用域乱,函数内部都能窜

  2. let/const 守规矩,块级作用跑不了

  3. 作用域链向上找,静态作用看结构

  4. 闭包就像保鲜盒,变量常驻不消失

JavaScript 的这些特性看着复杂,其实就像生活中的收纳逻辑:var 是随便扔的杂物,let/const 是带标签的收纳盒,闭包则是带锁的保险柜。理解了这些,下次变量再 "捉迷藏",你就能轻松找到它们啦~

你还遇到过哪些变量相关的奇葩问题?评论区分享一下吧!