我是靠谱客的博主 眼睛大煎蛋,最近开发中收集的这篇文章主要介绍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中声明的所有变量都保存在变量对象中, 变量对象的创建依次经历以下步骤:

  • 首先获得函数的参数变量及其值.

  • 依次获取当前上下文中所有的函数声明. 在变量对象中会以函数名建立一个属性,属性值指向该函数所在的内存地址.

  • 依次获取当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性。 如果是 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引擎下,变量的生命周期包含四个阶段:

  • 声明阶段
  • 初始化阶段
  • 赋值阶段
  • 释放阶段

下边来分别分析下它们生命周期中存在的区别:

1.var 声明变量的生命周期

在这里插入图片描述

当使用var声明一个变量时:

function fun() {
	// 变量提升 -- 不存在暂时性死区
	console.log(a); // undefined
	var 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示例描述,可参考:const-javascript | MDN

不管是var、let、还是const声明的变量,在变量使用完毕后,最终都会随着执行上下文的出栈、浏览器的关闭、或者手动释放内存的环节进行垃圾回收. 由于JS中自动垃圾回收机制的存在,使我们往往在开发时忽略了内存使用的问题,但这个是所有变量都要经历的过程.

最后

以上就是眼睛大煎蛋为你收集整理的JS变量的执行环境和生命周期的全部内容,希望文章能够帮你解决JS变量的执行环境和生命周期所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(42)

评论列表共有 0 条评论

立即
投稿
返回
顶部