东哥东神

V1

2022/02/06阅读:86主题:默认主题

js中变量存哪儿,很多文章都讲错了!

摘要:

  • ①. 问题
  • ②. js中全局变量到底存哪儿
  • ③. 都是因为祖师爷偷懒
  • ④. let声明的全局变量存哪儿
  • ⑤. var声明的变量和用window.a添加的变量差别
  • ⑥. 相关高频笔试题/知识点总结

问题:

书上和网上都说js中的变量是保存在栈内存里的,而对象,其实是保存在堆内存里的。真的对吗?

js中,执行以下程序,查看输出结果:

var a=10;
console.log(a);//10
console.log(window.a); //10
console.log(window["a"]); //10
console.log(window);
//window{
//   a: 10,
//   ...
//}

问题来了!var a=10,是一个原始类型的变量,按书上和网上说,应该保存在栈内存里。

但是,我们竟然在一个名为window的对象中看到了a这个成员属性!可是,window作为一个对象,理应保存在堆里呀?

那么,a=10这个变量,到底保存在栈里?还是堆里呢?(自相矛盾)

js中全局变量到底存哪儿

"事到如今,有些事儿,你也该知道了,其实... ..."

听10年前端东神给你力证js中变量实际的存储位置。

其实,js内存中,保存变量根本不是用栈和堆的。js底层存储任何变量和数据,用的都是对象。而js中对象底层其实都是一种叫关联数组的数据结构。

对象或关联数组的存储结构内部都是由多组"属性名:属性值"这种名值对儿组成的。(扫描文章结尾二维码看东哥详细讲解对象和关联数组)

其实,js中,连作用域也都是一个个的对象。其中,担当全局作用域的对象其实名为window。window对象在你刚一打开网页,就自动创建了。

我们用var在全局声明的变量,其实是保存在全局作用域对象window中的一个成员(属性)——耳听为虚,眼见为实,尽信书不如无书。

js中一切变量,其实都是保存在各种各样作用域对象中的属性而已。全局变量是保存在全局作用域对象window中的属性。局部变量是保存在函数作用域对象(Actived Object,AO)中的属性而已。(关注东哥后续其他文章...)

为什么js的内存中只用关联数组来包打天下?

嗨,还不是因为当年咱祖师爷偷懒,仅用10天时间草率的从五种语言中生拼硬凑出了js语言。当然怎么简单,怎么灵活就怎么实现呗。

"与其说我爱Javascript,不如说我恨它。它是C语言和Self语言一夜情的产物"。这是祖师爷的原话,可见JavaScript语言多么草率

闲言少絮,书接上文。可是,同样是在全局声明变量,改成用let声明,却在全局window对象中找!不!到!

let a=10;
console.log(a); //10
console.log(window.a); //undefined
console.log(window["a"]); //undefined
//window{
//  ... : ...
//}

那么,用let声明的全局变量a,到底存哪儿了?

想要查看let声明的变量的实际存储位置,可分三步:

第一步,新建网页,在全局声明let a=10,并在let a=10;后增加一句debugger。

debugger是让程序暂停在这句话,可以查看程序运行到这句话时,内存的存储情况。但是,debugger关键字只有在按F12打开浏览器开发者工具的情况下,才能暂停。如果没打开F12,即使写上debugger也无法暂停的。

let a=10;
debugger;//让程序暂停在此行代码,只有在打开F12开发者工具时,才能暂停
console.log(a); //10
console.log(window.a); //undefined
console.log(window["a"]); //undefined
//window{
//  ... : ...
//}

第二步,用浏览器打开网页,按F12打开浏览器开发者工具。

第三步,刷新页面,程序暂停在debugger语句上。此时,我们查看右侧作用域位置,就可看到let创建的变量a,实际上是保存在window对象之外的一个叫脚本作用域的对象中。

这个所谓的"脚本作用域"对象是怎么产生的?

其实,js中只有调用函数时才能动态生成作用域对象。而let底层,其实就会被js引擎自动翻译为匿名函数自调。所以,用let声明的变量时,等同于匿名函数自调,也就会动态创建一个类似于函数作用域对象的脚本作用域对象。

我们可以尝试用匿名函数自调代替let,实现相同的效果。就是即不产生全局变量,还能让整个程序运行正常:

所以,let a即使声明在全局,也不是真正的全局变量,而是保存在类似函数作用域的脚本作用域对象中,类似于局部变量

但是,let翻译为匿名函数后,所包裹的代码范围有多大呢?后续代码能不能正常使用let声明的变量呢?

不用担心。 在程序自动将let翻译为匿名函数自调用时,会将与该变量没有冲突的其他后续平级代码一同纳入进这个匿名函数自调中。所以,和let平级的后续代码,依然可以顺利访问该变量。(扫描文章结尾二维码收看东哥let的详细讲解)

其实,即使var a和window.a都可创建全局变量,但是他们还隐藏着一个深层的秘密。一道高频笔试题——var a和window.a有什么差别。

简单说,var a声明的变量,不能用delete window.a方式删除。会长久驻留在全局作用域对象window中。而window.a添加的全局变量,可被delete window.a删除掉。所以,很多大师级的人物写代码时,即使创建全局变量,也会用window.a方式。而不是用var a方式。目的就是为了在不用这个变量时,可以删掉它。内存,寸土寸金,还是要节约使用。

比如:

var a=10;
delete window.a; 
// false
//window{
//  a: 10 没删掉
window.a=10;
delete window.a
// true
//window{
//  ... : ... a已经欸删掉了

为什么会有这种差异?其实也是js引擎底层瞒着我们悄悄做了处理: var a=10声明的变量,底层会设置a属性的configurable特性为false,意为禁止用delete删除。

window.a添加的全局变量,底层不会自动设置a的configurable特性为false,默认为true,意为可用随意删除

使用Object.getOwnPropertyDescriptor(window,"a"),可查看window对象中a的configurable特性,来确定a是否可用delete删除。

比如: 用var创建的变量a:

再比如: 用window.a创建的变量a

(扫描文章结尾二维码收看东哥ES5_getOwnPropertyDescriptor的详细讲解)

其实,ES6中,很多花里胡哨的新语法,都是表面上时髦,而底层并没有改变旧js原有的底层原理。只有明白原理,才不会惧怕新知识,才能打通任督二脉,一通百通。想了解更多ES6新语法的底层原理,可扫描文章结尾二维码,移步东哥b站空间。

这里附赠另一道高频笔试题var, let, const对比表格:

想看js中对象与关联数组详细讲解:

(长按识别二维码就可打开b站视频链接)

想看let特性详细讲解:

(长按识别二维码就可打开b站视频链接)

想看ES5中getOwnPropertyDescriptor的详细讲解:

(长按识别二维码就可打开b站视频链接)

如果你还对let底层相当于匿名函数自调有怀疑,可以看一下东哥b站的另一个高频笔试题视频:let_setTimeout。你会发现let竟然可以形成闭包的效果。而闭包其实是函数和函数作用域才能形成的特殊对象

分类:

前端

标签:

JavaScript

作者介绍

东哥东神
V1