温故而知新,这些JS基础知识你都知道吗?
今天和大家分享的是 JavaScript 中有关变量的知识,希望这篇文章能让你对JS中的变量有新的认识.
目录:
- 变量的执行环境(执行上下文)
- 执行上下文的生命周期
- 创建变量对象
- 变量的数据存储
- 变量的内存空间
- 变量的垃圾回收
- let/const/var的区别
执行环境(执行上下文)
javascript的运行环境主要包括以下三种:
- 全局环境:代码运行起来后会首先进入全局环境.
- 函数环境:当函数被调用时,会进入当前函数中执行代码.
- eval环境:不建议使用,这里不做介绍.
js运行环境也叫做执行上下文,因此在一个JavaScript程序中,必定会出现多个执行上下文.
JS引擎会以栈(遵循后进先出的数据存储方式)的方式来处理执行上下文,也就是我们通常所说的函数调用栈。栈底永远是全局上下文,栈顶则是当前正在执行的上下文. 处于栈顶的执行上下文执行完毕后,会自动出栈.
看个例子可能会更形象些:
function declare() {var a = 1;function update() {a = 2;}update();
}
declare();
下面是上方用例中的执行上下文对应的进出栈流程示意图:
假如上方的 declare 函数处于全局环境中,那么代码运行时会经历以下几步:
- 首先第一步就是全局上下文入栈.
- 全局上下文入栈后,遇到的第一个可执行代码就是 declare() 函数的调用,此函数一旦调用,就会创建自己的执行上下文,此时declare EC入栈.
- 在新开辟的declare EC执行上下文中,执行内部的可执行代码,直到遇到 update() 函数调用时,又会创建一个新的执行上下文,此时update EC入栈.
- 当update EC中的可执行代码执行完毕之后,发现不再有其他执行上下文生成的情况,此上下文会自动从栈中弹出.
- update EC执行上下文弹出后,会继续执行 declare EC执行上下文中的可执行代码,直到顺利执行完毕,且没有遇到其他执行上下文,则自动从栈中弹出.
- 最后执行栈中只剩下全局上下文,若浏览器不关闭,全局上下文会一直存在,直到浏览器窗口关闭,全局上下文才会最终出栈.
执行上下文的生命周期
说完执行上下文的入栈、出栈情况,下面说说执行上下文的生命周期.
当一个函数调用时,一个新的执行上下文就会被创建,一个执行上下文的生命周期可分为两个阶段:
- 创建阶段 --- 此阶段执行上下文会分别创建变量对象、确认作用域链、以及确定this指向.
- 执行阶段 --- 执行代码,这个时候会完成变量赋值、函数引用、以及执行其他可执行代码等工作.
画个图看起来会更直观:
这篇文章接下来主要介绍变量的生命周期,所以我们主要针对执行上下文中 创建变量对象、内存空间、变量赋值阶段 展开讲解。
创建变量对象
JS中声明的所有变量都保存在变量对象中, 变量对象的创建依次经历以下步骤:
1. 首先获得函数的参数变量及其值.
2. 依次获取当前上下文中所有的函数声明. 在变量对象中会以函数名建立一个属性,属性值指向该函数所在的内存地址.
3. 依次获取当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性。 如果是var声明,则属性值会初始化为undefined.
变量的数据存储
变量对象创建完成后,接下来就是对数据的存储,基础数据类型往往会保存在栈内存中(特殊情况除外),而引用数据类型的值是保存在堆内存中的对象,在js语言中,不允许直接访问堆内存空间中的数据. 当我们操作对象时实际上是在操作对象的引用,而不是实际的对象. 因此,引用数据类型都是按引用访问的,这里的引用可以理解为保存在变量对象中的一个地址,该地址与堆内存中的对象相关联.
下边通过一个列子,来看下变量对象的存储:
function fun(){var a = 1;var b = 'hello world';var c = {x: 100};var d = {y: [1,2]};
}
当fun()函数调用时,会创建一个执行上下文,在当前上下文中创建变量对象,变量对象存放格式如下:
如果是基本数据类型,在栈中存储数据本身; 如果是引用数据类型,在栈中存储的是堆中对象的引用;
变量的内存空间
说完变量对象的创建与存储之后,接下来再说说变量的内存空间的使用过程. 内存空间的使用同样也具有自己的生命周期,包含:
- 分配内存阶段
- 使用分配到的内存
- 不需要时释放内存
再来看个例子:
var a = 20; // 分配内存
console.log(a + 1); // 使用内存
a = null; // 释放内存
上边的三行代码分别对应着分配内存、使用分配到的内存、以及释放内存三个过程。其中,分配和使用应该很好理解,最终要的是释放的过程,涉及到垃圾回收机制的实现原理。
变量的垃圾回收
Javascript中具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。所以,在日常开发中,开发人员很少再关心内存使用的问题.
垃圾回收机制的原理就是:找出那些不再继续使用的变量,垃圾收集器会按照固定的时间间隔,周期性的释放其占用的内存.
最常用的垃圾收集方式是标记清除算法. 主要依靠 "引用" 的概念,当一块内存空间中的数据能够被访问时,垃圾回收器就会认为 "该数据能够被获得",不能够被获得的数据,就会被打上标记,并回收内存空间,这种方式叫做 标记 --- 清除算法.
注: 在局部作用域中,当函数执行完毕后,垃圾收集器会很容易做出判断并做局部变量做回收操作。但在全局作用域中,变量什么时候需要自动释放内存空间则很难判断,所以,我们在做开发时,应尽量避免全局变量的使用。如果使用了全局变量,建议通过 a = null 这样的方式释放引用,以确保能够及时的回收内存空间.
介绍完了执行环境、变量对象的创建、内存、垃圾回收后。我们再来看一个最常见的问题:let/const/var 关键字声明变量的方式,来熟悉下变量的生命周期以及它们之间存在的区别.
----------------- 我是分割线 ----------------
let/const/var的区别
关于var、let、const的各自特性,想必大家都已经很清楚了,这里简单罗列:
使用以上三个关键词定义的变量,之所以会有不同的使用特性,这跟他们的生命周期有直接的关系。 JS引擎下,变量的生命周期包含四个阶段:
- - 声明阶段
- - 初始化阶段
- - 赋值阶段
- - 释放阶段
下边来分别分析下它们生命周期中存在的区别:
- var 声明变量的生命周期
当使用var声明一个变量时:
// 第一个特性
function fun() {// 变量提升 -- 不存在暂时性死区console.log(a); // undefinedvar a = 1;
}// 第二个特性 --- 不支持块级作用域
console.log(a); // undefined
{var a = 1;
}
console.log(a); // 1
以函数作用域为例,在函数作用域中遇到 var a = 1
这段代码,编译器会首先查找当前作用域下是否已经声明过该变量,若已经存在,则直接忽略这次声明;若不存在,则在当前作用域中声明一个名字为 a
的变量,并进行初始化操作(变量 a
的值初始化为 undefined
),这也是为什么使用 var
可以进行重复声明的原因. 这里的声明和初始化均被提升至作用域的最顶端.
当函数调用执行时,JS引擎会在当前的函数作用域为该变量进行赋值操作,在 var a = 1
这条语句之前的任何位置访问变量 a
,它的值将会是 undefined
. 因为初始化操作也被进行了提升.
当代码执行至 var a = 1
时,引擎也会从当前作用域下查找是否存在变量 a
, 若存在,直接进行赋值;若不存在会强制在当前作用域声明一个变量 a
, 并进行赋值操作.
注: 不要随意在代码块中使用var声明变量,因为它会直接挂到全局变量window对象上,污染全局环境,且不能及时的进行垃圾回收释放内存.
----------------- 我是分割线 ----------------
2. let 声明变量的生命周期
使用let进行变量声明:
// 第一个特性 --- 块级作用域
{let a = 1;
}
console.log(a); // a is not defined// 第二个特性 --- 暂时性死区
console.log(a); // a is not defined
let a = 1;// 第三个特性 --- 不可重复声明
let a;
let a = 1; // Uncaught SyntaxError: Identifier 'a' has already been declared
let
的生命周期与 var
的主要区别在于,声明和初始化阶段没有同时进行提升,只有声明阶段被提升至当前作用域的最上方,所以在声明和初始化阶段之间就出现了暂时性死区现象,此阶段不可访问该变量,否则会抛出异常.
let 不可重复声明,遇到let声明的变量时,编译器同样会先查找当前作用域下是否已经声明过该变量,若已经存在,引擎就会抛出异常(Uncaught SyntaxError: Identifier 'a' has already been declared);若不存在,则在当前作用域中声明一个名字为 a 的变量,并把声明操作提升至作用域最上方.
最重要的是 let
具备了块级作用域,它很好的规避了 var
身上的缺点,在代码块内声明变量,避免污染全局变量.
----------------- 我是分割线 ----------------
3. const 声明变量的生命周期
使用const声明变量:
// 第一个特性 --- 一旦声明不可修改
const a = 1;
a = 2; // Uncaught TypeError: Assignment to constant variable.// 第二个特性 --- 存在暂时性死区
console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization (初始化前无法访问a)
const a = 1;
与 let
最大的区别是 const
是用来定义常量的,它创建一个值得只读引用,一旦定义不可修改. const
也存在暂时性死区, 如果我们在声明之前访问该变量的值,则会抛出异常(初始化前无法访问该变量),这也很好的证明了,const
的生命周期同样存在声明提升,并且它的初始化和赋值操作必须一起完成.
注: 使用 const
声明变量时,实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。
一个常量不能和它所在作用域内的其他变量或函数拥有相同的名称. 更多const示例描述,可参考:
constdeveloper.mozilla.org不管是var、let、还是const声明的变量,在变量使用完毕后,最终都会随着执行上下文的出栈、浏览器的关闭、或者手动释放内存的环节进行垃圾回收. 由于JS中自动垃圾回收机制的存在,使我们往往在开发时忽略了内存使用的问题,但这个是所有变量都要经历的过程.
以上就是今天的所有分享啦~ 感谢您认真读完,共勉!
参考文献:
《Javascript高级程序设计》
《Javascript核心技术开发解密》