js基础拾遗——中

作用域与内存

原始值与引用值

ECMAScript包括两种类型的值,引用类型的值和原始类型的值

原始值包括 Undefined、Null、Boolean、Number、String 、Synbol 这六种。原始值是按值访问的,操作的就是存储在变量中的实际值。
引用值是保存在内存中的对象。JavaScript 不允许直接访问内存,所以,操作对象时,操作的是对该对象的引用指针而非实际的对象。因此,引用值的变量是按引用访问的。

复制值

原始类型的值存储在栈中(对象引用指针也在),引用类型存在堆中

原始类型和引用类型的复制是将值复制一份分配到新的地方,原始类型的两个就没有关系了,但对象不行

var a = 12,b = a;
b = 13;
console.log(a)//12

var obj = {prop : 123},obj1 = {prop : 124};
console.log(obj)//{prop : 124}

参数传递

ECMAScript变量的访问分按值访问和按引用访问,但函数是按值传递的,包括引用类型的值
函数调用时传递的参数都会被复制到形参(局部变量)中
原始数据类型:

var num1 = 123;
function fn1 (num){
	num = 124;
	console.log(num1)//123
}
fn1(num)

引用数据类型:

var object = {msg : 123};
function asd(obj){
	obj.msg = "asd";
	console.log(object.msg)//asd
	obj = {msg : "提示信息"}
	console.log(obj)//{msg : "提示信息"}
	console.log(object)//{msg : "asd"}
}

asd(object)

类型判断

ECMAScript 中判断类型的方法分别是:

  • typeof 判断原始类型(除去null)非常好用,返回对应类型的字符串
  • instanceof 判断实例是否属于某个构造器(arr instanceof Array)跨框架则不能得出正确结果
  • Array.isArray() 方法用于判断指定变量是否为数组,可以解决夸框架的问题,但是不支持ES5的浏览器不兼容
  • Object.prototype.toString.call() 方法,能够准确判断出类型 适用于ES5以下

执行上下文和作用域

执行上下文简称上下文,决定变量和函数可以访问哪些数据,以及它们的行为,每个上下文都有一个关联的变量对象
执行上下文可以理解为当前代码的运行环境。

上下文分全局上下文和函数、函数上下文
在一个执行上下文中,最重要的三个属性分别是变量对象(Variable Object)、作用域链(Scope Chain)和 this 指向。

实现ECMAScript的宿主环境不同,全局上下文对象可能不同,比如在浏览器就是window,在node.js中是 global,所有用var声明的变量和声明(除了函数内部)的函数都会成为window对象的属性,可以使用window.xxx来访问,你说既然都成为全局对象的属性了用delete关键字还删不掉。
使用let和const声明的变量不会成为全局对象的属性

一个执行上下文的生命周期分为创建和执行阶段。创建阶段主要工作是生成变量对象、建立作用域链和确定 this 指向。而执行阶段主要工作是变量赋值以及执行其它代码等。

变量对象(Variable Object)

我们已经知道,在执行上下文的创建阶段会生成变量对象,生成变量对象主要有以下三个过程:

  • 检索当前上下文中的参数。该过程生成 Arguments 对象,并建立以形参变量名为属性名,形参变量值为属性值的属性;
  • 检索当前上下文中的函数声明。该过程建立以函数名为属性名,函数所在内存地址引用为属性值的属性;
  • 检索当前上下文中的变量声明。该过程建立以变量名为属性名,undefined 为属性值的属性(如果变量名跟已声明的形参变量名或函数名相同,则该变量声明不会干扰已经存在的这类属性)。

当执行上下文进入执行阶段后,变量对象会变为活动对象(Active Object)。此时原先声明的变量会被赋值。
变量对象和活动对象都是指同一个对象,只是处于执行上下文的不同阶段。

对于全局上下文来说,由于其不会有参数传递,所以在生成变量对象的过程中只有检索当前上下文中的函数声明和检索当前上下文中的变量声明两个步骤。

上下文会在其左右代码执行结束后销毁,全局上下文会在关闭网页或者浏览器。
全局上下文最先进栈,函数调用时会进入相应的上下文
函数都有自己的上下文,在执行的时候被推入上下文栈里执行完后会弹出,将控制权返还给之前的上下文
上下文栈是后进先出,遇到递归的时候会立刻停止当前的函数,实际上是最里层递归先执行最后才是最外层递归

function fn2() {
  console.log('fn2')
}
function fn1() {
  console.log('fn1')
  fn2();
}
fn1();

在这里插入图片描述

上下文的代码在执行的时候,会创建变量对象的一个 作用域链。全局上下文的变量对象始终位于作用域链的最后端,当前正在执行的上下文的变量对象位于作用域链的最前端。代码执行时的标识符解析是通过沿作用域链从最前端开始逐级向后搜索来完成的(作用域链中的对象也有原型对象,因此搜索可能涉及每个对象的原型链)

作用域链

我们已经知道,执行上下文分为创建和执行两个阶段,在执行上下文的执行阶段,当需要查找某个变量或函数时,会在当前上下文的变量对象(活动对象)中进行查找,若是没有找到,则会沿着上层上下文的变量对象进行查找,直到全局上下文中的变量对象(全局对象)。

那么当前上下文是如何有序地去查找它所需要的变量或函数的呢?

依靠作用域链,其包含了当前上下文和上层上下文中的变量对象,以便其一层一层地去查找其所需要的变量和函数。

执行上下文中的作用域链又是怎么建立的呢?

我们都知道,JavaScript 中主要包含了全局作用域和函数作用域,而函数作用域是在函数被声明的时候确定的。

每一个函数都会包含一个 [[scope]] 内部属性,在函数被声明的时候,该函数的 [[scope]] 属性会保存其上层上下文的变量对象,形成包含上层上下文变量对象的层级链。[[scope]] 属性的值是在函数被声明的时候确定的。

所以,作用域链是由当前上下文变量对象及上层上下文变量对象组成的

垃圾回收

在 C 和 C++ 等语言中,内存的管理需要由开发者自己来完成。JavaScript 是使用垃圾回收的语言,通过自动内存管理实现内存的分配和闲置资源回收。思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。这是一个周期性的过程,垃圾回收程序每隔一段时间(或者说代码执行过程中某个预定的收集时间)就会自动运行。

标记清理

垃圾回收程序运行的时候,会标记内存中的所有变量(标记方法有很多中,比如维护两个在和不在上下文的变量列表、变量进入上下文时反转某一位等)。然后,它将所有在上下文中的变量,以及被上下文中变量引用的变量的标记去掉。在此之后还存在标记的变量就可以删除了,原因是上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有变量并回收它们的内存。
目前,基本上所有浏览器都在自己的 JavaScript 实现中采用了标记清理(或其变体),只是运行垃圾回收程序的频率有所差异。

引用计数

目前已经没有浏览器使用这种标记策略了。它的思路是记录每个值被引用的次数(声明一个变量并给它赋一个值,这个值的引用数就为 1)。当一个值的引用数为 0 时,就说明这个值已经没用了,可以回收其内存。垃圾回收程序下次运行时就会释放引用数为 0 的值的内存。
引用计数有一个严重的问题:循环引用。所谓循环引用,就是对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A。比如:(深拷贝的时候也会遇到这问题)

var obj1 = {}
var obj2 = {}

obj1.key = obj2;
obj2.key = obj1;

性能优化

垃圾回收程序会周期性的执行,但开发者不知道垃圾回收程序什么时候会被执行,所以尽可能的在变量使用完以后手动释放(置为 null)。

在有限的内存情况下,在执行的代中只保存必要的数据,可以给页面带来更好的性能。如果数据不再使用,就手动将其设置为 null,从而解除引用。这个操作很适合全局变量和全局对象以及它的属性(因为全局上下文只有页面关闭后才会销毁)。局部变量在超出作用域后就会被自动解除引用。


版权声明:本文为qq_45046157原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。