不忘初心方得始终。
变量,作用域与内存结构。
本JS系列博客仅为我个人在学习JS时的查漏补缺。如果本篇有提到与你相同的疑惑,或没注意到的细节。希望可以帮助到你的JS学习。
如有差错欢迎指正
变量,作用域与内存结构。
-
首先先抛出我的疑问点,基本类型与引用类型在内存中的区别。
好接下来,先了解一下ES中的变量,在ES中变量可以包含两种不同类型的数据:
- 原始值:指最简单的数据,可以是number,string,boolean等基本类型。
- 引用值:其为由多个值组成的对象。
而在把一个值赋给一个变量时,JS引擎必须确定到这个值到底是引用值还是原始值。
因为原始数据类型变量的“变量分配”与“数据分配”是在一起的(都在方法区或栈内存或堆内存)。
而引用数据类型变量的“变量分配”与“数据分配”是不在一起的。
想要搞清楚 JavaScript的变量存储机制 ,首先必须要弄明白 堆 和 栈 ,先来看下 堆 和 栈的概念和特点。
可以把堆认为是一个很大的内存存储空间,你可以在里面存储任何类型数据。 但是这个空间是私有的,操作系统不会管在里面存储了什么,也不会主动的去清理里面的内容。 因此在C语言中需要程序员手动进行内存管理,以免出现内存泄漏,进而影响性能。
但是在一些高级语言 如JAVA会有 垃圾回收(GC) 的概念。 用于协助程序管理内存空间,自动清理堆中不再使用的数据。(JS也有垃圾回收机制✌
在栈中存储不了的数据比如对象就会被存储在堆中,在栈中呢是保留了对象在堆中的地址。 也就是对象的引用。提到了栈那么接下来我们看下什么是栈?
栈是内存中一块用于存储局部变量和函数参数的线性结构,遵循着先进后出的原则。数据只能顺序的入栈,顺序的出栈。
当然,栈只是内存中一片连续区域一种形式化的描述,数据入栈和出栈的操作仅仅是栈指针在内存地址上的上下移动而已。
但需要注意的是:内存中栈区的数据,在函数调用结束后,就会自动的出栈,不需要程序进行操作,操作系统会自动回收,也就是:栈中的变量在函数调用结束后,就会消失。 这也正是栈的特点:无需手动管理、轻量、函数调时创建,调用结束则消失。
所以接下来的这段代码就很好理解了。
let name = "xxxxx"
name.age = 27 // 程序不会报错
console.log(name.age) // undefined
-------------------------------------------
let name1 = new String("xxx")
let name2 = "xxxxxx"
name1.age = 26
name2.age = 27
console.log(name1.age) // 26
console.log(name2.age) // undefined
console.log(typeof name1) // object
console.log(typeof name2) // string
JS
值的复制
原始值的变量复制是完全独立的。复制后的两个变量单独存储在栈内存中,可以独立使用,互不干扰。
let num1 = 5
let num2 = num1
let num1 = 10
console.log(num2)// 5
JS
在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量的所在位置。
区别在于,这里的复制其实是一个指针,它指向存储在堆内存中的对象。操作完后,两个变量其实指向同一个对象。
因此一个对象的变化会在另一个对象上反映出来。 (深浅拷贝问题
let obj1 = new Object()
let obj2 = obj1
obj1.name = "123"
console.log(obj1.name) // 123
JS
总结:
- 局部变量有作用域限制,在作用域的栈内保存,随取随用,栈随作用域周期消亡。
- 成员变量注册初始化后是长期存在的,但是长期存在不一定一直有用 所以放到动态内存的堆内。
作用域
- 执行上下文与作用域:上下文是JS中非常重要的概念,变量或函数的上下文决定了他们可以访问哪些数据,以及他们的行为。每个上下文都有一个关联的变量对象 variable object用来存储这个上下文中定义的所有变量和函数。
- 全局上下文(window对象),因此通过var定义的全局变量和函数都会成为window对象的属性和方法。而使用let和const的顶级声明不会定义在全局上下文中,但在作用域解析中其实效果是一样的。
- 上下文将会在所有代码都执行完毕后被销毁(全局上下文会在关闭网页或退出浏览器时销毁
- 上下文的代码执行时将会创建变量对象的一个作用域链。这个东西决定了各级上下文中的代码在访问变量与函数的顺序。
- 作用域的变量搜索是自内而外的(由下向上 直到全局上下文中没有这个变量->报错
var color = "blue"
function changeColor(){
let anotherColor = "red"
function swapColors(){
let tempColor = anotherColor
anotherColor = color
color = tempColor
//这里可以访问到color anotherColor,tempColor
}
//这里可以访问到color anotherColor,但访问不到tempColor
}
//这里只能访问到color
changeColor()
js
- 注意:函数参数被认为是当前上下文的变量,因此也跟上下文中的其他变量遵循相同的访问规则!
变量声明
使用var声明变量时,变量会被添加到最近的上下文。未声明的就被初始化的变量会被自动添加到全局上下文中。
function add(a,b){
sum = a+b;
}
let result = add(10,20) //30
console.log(sum) // 30
js
注意:这里需要调用add函数 sum才会被初始化。如果不调用打印的话还是得不该对象的。
重点注意:未经声明而初始化变量是非常低级的错误,可能会导致变量被覆盖或内存泄漏等。 🤯
es6新增的关键字跟var很相似,但他的作用域是块级的。块级作用域由最近的一对包含花括号界定,比如if,while,function。
let与var不同之处还有是在同一作用域内不可以声明两次,重复的var声明会被忽略,而let将会报错。
所以let的行为非常适合在循环中充当迭代变量。使用var声明会泄露到循环外部,这种情况应该避免。
使用const声明的变量必须同时初始化为某个值。一经声明,在其生命周期的任何时间都不可以再重新赋予新值。
需要注意的是如果使用const声明对象,只会让本变量无法指向其他的对象。而对象内部的键值是可以更改的。这与内存有关(上面有提到🐷
如果想达到整个对象都无法修改的话,可以使用Object.freeze(),这样再给属性赋值不会报错,但可以使其静默失败。
const o1 = {}
o1 = {} // 报错:给常量赋值
---------------------------
const o2 = {}
o2.name = "xxx"
console.log(o2.name) // xxx
---------------------------
const o3 = Object.freeze({})
o3.name = "xxx"
console.log(o3.name) // undefined
js
提示:在开发中,应该尽可能地多使用const声明,除非确实需要一个将来会重新赋值的变量!这样可以从根本上保证提前发现重新赋值而导致的bug💋
总结:
- 变量声明切记小心未初始化就使用,造成不可估计的后果。
- 多使用const进行定义变量。