变量,作用域与内存结构。
记录于 03月29日 · 日曜日11 min read

不忘初心方得始终。

变量,作用域与内存结构。

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:

使用var声明变量时,变量会被添加到最近的上下文。未声明的就被初始化的变量会被自动添加到全局上下文中。

function add(a,b){ sum = a+b; } let result = add(10,20) //30 console.log(sum) // 30
js

注意:这里需要调用add函数 sum才会被初始化。如果不调用打印的话还是得不该对象的。

重点注意:未经声明而初始化变量是非常低级的错误,可能会导致变量被覆盖或内存泄漏等。 🤯

  • let:

es6新增的关键字跟var很相似,但他的作用域是块级的。块级作用域由最近的一对包含花括号界定,比如if,while,function。

let与var不同之处还有是在同一作用域内不可以声明两次,重复的var声明会被忽略,而let将会报错。

所以let的行为非常适合在循环中充当迭代变量。使用var声明会泄露到循环外部,这种情况应该避免。

  • const:

使用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进行定义变量。