你有没有过这样的经历?对着屏幕上的代码抓耳挠腮:"明明定义了变量,为啥打印出来是 undefined?"、"这函数都执行完了,怎么还能访问里面的变量?"
今天咱们就来扒一扒 JavaScript 里那些像捉迷藏一样的变量行为,保证让你看完直呼 "原来如此"!
一、var:最调皮的变量声明
先看个经典案例:
// 闭包/3.js里的坑
var myname = '男模'
function showName() {
console.log(myname); // 猜猜这里打印啥?
if (0) {
var myname = '鹤' // 永远不会执行的代码
}
console.log(myname);
}
showName()
运行结果居然是两个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被锁在块里了)
}
b = 3和d = 5被取出来之后栈将会被销毁
作用域链
v8在当前作用域中查找一个变量,如果找不到就去上一级作用域查找,再找不到,再去上一级,层层往上。
一个函数被编译时,一定会用一个outer指针来记录该作用域的外层作用域是谁
词法作用域:函数声明的位置
JavaScript 里的变量查找就像走亲戚,先找自己家,没有就去父母家,再没有就去爷爷奶奶家...
三、作用域链:变量的寻亲之路
// 闭包/5.js的亲戚访问
function bar() {
console.log(myName); // 找myName
}
function foo() {
var myName = '小君'
bar() // 执行时会找谁?
}
var myName = '总'
foo() // 最终打印"总"
bar 函数找 myName 时,不会因为在 foo 里被调用就去找 foo 家的,而是按自己的出身(被定义的位置)找 —— 这就是 "静态作用域",只看代码结构不看调用位置。
四、闭包:变量的保鲜盒
- 一个函数执行完毕后,他的执行上下文会被销毁
- 一个函数的内部函数一定有权利访问该外部函数的变量(作用域的规则)
当调用一个外部函数中返回的内部函数后,即使外部函数已经执行结束,但是内部函数依然引用了外部函数中的变量,那么外部函数的执行上下文就不能被完全销毁,而是会保留一个集合,用来装内部函数需要引用的变量,我们把这个集合称为闭包。
- 优点:定义私有变量,封装模块
- 缺点:内存泄漏(可用内存减少)
最神奇的来了!闭包能把变量像放进保鲜盒一样保存起来:
// 闭包/6.js的魔法
function foo() {
var myname = '佳颖'
var age = 18
function bar() {
console.log(myname); // 访问foo里的变量
}
return bar // 把函数带出去
}
var baz = foo()
baz() // 居然还能打印"佳颖"!
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
如果不用闭包,直接 pushfunction(){console.log(i)},最终会全部打印 6,因为 i 最后变成了 6。闭包就像给每个函数拍了张快照,记录下当时的 i 值。
总结:变量管理口诀
-
var 声明作用域乱,函数内部都能窜
-
let/const 守规矩,块级作用跑不了
-
作用域链向上找,静态作用看结构
-
闭包就像保鲜盒,变量常驻不消失
JavaScript 的这些特性看着复杂,其实就像生活中的收纳逻辑:var 是随便扔的杂物,let/const 是带标签的收纳盒,闭包则是带锁的保险柜。理解了这些,下次变量再 "捉迷藏",你就能轻松找到它们啦~
你还遇到过哪些变量相关的奇葩问题?评论区分享一下吧!