这篇文章将梳理下环境,作用域链,变量对象和活动对象,以及内存管理问题。
基本类型和引用类型的值
我们都知道JS中的数据类型有两大类,基本数据类型和引用数据类型,下面从三个方面来解剖他们
①保存方式
基本类型的值是指简单的数据段,引用类型的值是指那些可能由多个值构成的对象。
-
基本类型
- 按值访问
- 可以直接操作保存在变量中实际的值
-
引用类型
- 按引用地址访问
- 保存在内存中的对象,而JS不能不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间,所以说在实际操作过程中操作的是对象的引用,而不是实际的对象。
②复制变量值
- 基本类型在复制变量值的时候,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上。也就是说基础类型的值复制给新变量后,会在栈内存中开辟一个新的地址空间去存储值,原值和复制值参与任何操作都互不影响
- 引用类型在复制变量值的时候,同样会在栈内存中开辟一个新的地址空间去存储值,只不过,引用类型复制的是指针,原值和复制值的指针指向同一堆内存中存储的值,也就是说着两个变量实际上将引用同一对象,因此改变其中一个变量,就会影响到另一个变量。
③传递参数
先了解一个基本原则,ECMAScript中所有函数的参数都是按值传递的,千万不能觉得在局部作用域中修改的对象会在全局作用域中反映出来,就说明参数是按引用传递的。
根据这个原则,如果参数值是基本类型的,在函数内部修改值,并不会影响到函数外部的值,但如果是引用类型的,参数依旧是值传递,只不过传递的是栈内存的地址值,因此函数内部的修改会影响到函数外部的值。
下面看一个?
let obj_value = {
a: 1,
b: 2
}
function func(val) {
val.a = 3
val.c = 6
console.log(val) // {a: 3, b: 2, c: 6}
}
console.log(obj_value) // {a: 1, b: 2}
func(obj_value)
console.log(obj_value) // {a: 3, b: 2, c: 6}
下面?能证明引用类型的参数也是按值传递的
function func(obj) {
obj.a = 1
obj = {}
obj.a = 2
}
let test = {}
func(test)
console.log(test.a) // 1
上面的?,按照我们理解应该打印出a=2,但事与愿违,首先,test在函数func中新增了一个a属性并赋值为1,此时,obj中传递的是引用类型在栈内存中存储的地址值,也就是说函数内的obj复制的是test地址,他们两个共同指向一个对象,因此通过obj新增,修改删除操作都会反映到函数外部,接下来再看函数内的第二条语句,obj={},这就不得了了,这是重写,也就是说它会抹去obj原本存储的地址值,这就切断了test和obj共同指向一个对象这个联系,因此第三条语句,obj.a=2就是函数内部的事情了。
所以总结一句话,引用类型的增删改操作与其关联所有对象都会受到波及和影响,重新就会切断自身与其余对象的联系
检测类型
typeof()(只适用于基本类型,不适用于对象)
typeof函数可用于检测string,number,boolean,undefined,function还是symbol,但如果变量的值是引用类型或null,则typeof会返回object。
ECMA-262规定任何在内部实现[[call]]方法的对象都应该在应用typeof操作符时返回"function"
对于正则表达式类型的typeof检测,在IE和Firefox中会返回object,其余的返回function。
let func = function() {}
console.log(typeof ('')) // string
console.log(typeof (1)) // number
console.log(typeof (true)) // boolean
console.log(typeof (undefined)) // undefined
console.log(typeof ({})) // object
console.log(typeof (null)) // object
console.log(typeof (Symbol(''))) //symbol
console.log(typeof (func)) // function
instanceof(只适用于对象,不适用于基本类型)
instanceof操作符用于判断是什么类型的对象
如果变量是给定引用类型的实例,那么instanceof操作符就会返回true
- 根据规定,所有的引用类型的值都是Object的实例,因此在检测一个引用类型值和Object构造函数时,instanceof操作符始终会返回true,
- instanceof操作符检测基本类型的值,该操作符始终会返回false,因为基本类型不是对象。
执行环境及作用域
执行环境也称为环境,它定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
全局执行环境是最外围的一个执行环境,根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样,在web浏览器中,全局执行环境被认为是window对象,因此,在浏览器中,创建的所有全局变量和函数都是作为window对象的属性和方法。
ECMAScript程序中执行流的控制机制
每个函数都有各自的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中,而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。
作用域链
代码在环境中执行,就会创建变量对象的作用域链,作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在的环境的变量对象,如果这个环境是函数,则将其活动对象作为变量对象。什么是活动对象呢?活动对象实际就是变量对象在真正执行时的另一种形式。活动对象一开始只包含一个变量,即arguments对象。作用域中的下一个变量对象来自外部环境,再下一个变量对象来自下一个环境,层层嵌套,一直延续到全局执行环境,全局执行环境的变量对象始终都是作用域链中的最后一个对象
环境的访问是沿着作用域链进行的,作用域链是单向的,即由里到外,内部环境可以访问外部环境,反之不行。
变量对象和活动对象的概念
变量对象(VO)
变量对象是与执行上下文对应的概念,定义执行上下文中的所有变量,函数以及当前执行上下文函数的参数列表,也就是说变量对象定义着一个函数内定义的参数列表、内部变量和内部函数
变量对象的内部顺序是参数列表->内部函数->内部变量
变量对象的创建过程
- 检查当前执行环境的参数列表,建立Arguments对象。
- 检查当前执行环境上的function函数声明,每检查到一个函数声明,就在变量对象中以函数名建立一个属性,属性值则指向函数所在的内存地址。
- 检查当前执行环境上的所有var变量声明,每检查到一个var声明,如果VO(变量对象)中已存在function属性名,则跳过,不存在就在变量对象中以变量名建立一个属性,属性值为undefined。
变量对象是在函数被调用,但是函数尚未执行的时刻被创建的,这个创建变量对象的过程实际就是函数内数据(函数参数,内部变量,内部函数)初始化的过程。
活动对象(AO)
未进入执行阶段之前,变量对象中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。所以活动对象实际就是变量对象在真正执行时的另一种形式。
全局变量对象
我们上面说的都是函数上下文中的变量对象,是根据执行上下文中的数据(参数、变量、函数)确定其内容的,全局上下文中的变量对象则有所不同。以浏览器为例,全局变量对象是window对象,全局上下文在执行前的初始化阶段,全局变量、函数都是被挂载倒window上的。
执行上下文的生命周期
延长作用域链
执行环境的类型就两种——全局和局部(函数)
延长作用域链的意思是在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。
延长方法(以下两个语句都会在作用域链的前端添加一个变量对象):
- try-catch语句的catch块:会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。
- with语句:会指定的对象添加到作用域链中
通过with语句延长作用域链
function addLink() {
let name = 'george'
with(local) {
var url = href + name // 此时通过with语句将local对象添加到addLink环境的头部,因此在addLink中就有权可以访问local对象的属性和方法
}
return url
}
var没有块级作用域
JS ES5以前只有函数作用域和全局作用域,没有块级作用域。ES6后出现块级作用域 let, count定义的变量都只在其代码块中有效
声明变量
var声明的变量会自动被添加到最接近的环境中
在函数内部,最接近的环境就是函数的局部环境,在with语句中,最接近的环境是函数环境,初始化变量若没有通过var声明,该变量会自动被添加到全局环境。
查询表示符
当某个环境中为了读取和写入一个标识符时,必须通过搜索来确定标识符实际代表什么。搜索过程从作用域链的前端开始,沿着作用域链向上查找,一直追溯到全局环境变量对象,找到标识符,搜索过程停止,反之,返回undefined。
垃圾收集
JS具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存
收集原理
找出那些不再继续使用的变量,然后释放其占用的内存,为此垃圾收集器会按照固定的时间间隔(或代码中预定的收集时间),周期性地执行这一操作。
收集方法
- 标记清除
- 引用计数
标记清除(最常用的垃圾收集方式)
原理:垃圾收集器在运行的时候回给存储在内存中的所有变量都加上标记,然后去掉环境中的变量以及被环境中的变量引用的变量的标记,最后删除被标记的变量。
标记清除算法将“不再使用的对象”定义为“无法到达的对象”。即从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,保留。那些从根部出发无法触及到的对象被标记为不再使用,稍后进行回收。
引用计数
原理:通过名字很好理解,引用计数,就是跟踪记录每个值被引用的次数,当引用次数为0时,将其删除。
计数方法:当声明一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1,如果同一个值被赋给另一个变量,则该值的引用次数加1,相反,包含这个值引用的变量又取的另一个值,则这个值的引用次数减1。
引用计数的严重问题——循环引用
循环引用指的是对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。
当出现循环引用的时候,引用次数永远不可能为0,这会导致内存得不到回收。
解决方法:手动断开不需要的引用,即,将引用对象置为null
立即执行垃圾回收函数
IE中:window.CollectGarbage()
Opera7或更高版本:window.opera.collect()
管理内存
- 分配给web浏览器的可用内存数量通常要比分配给桌面应用程序的少
- 对于浏览器而言,确保占用最少的内存可以让页面获得更好的性能。
- 优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据,一旦数据不再有用,最好通过将其值设置为null来释放其引用,这个做法叫做解除引用。这一做法适用于大多数全局变量和全局对象的属性,局部变量会在它们离开执行环境时自动被解除引用。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。