一眼看穿JS变量,作用域和内存问题

更新日期: 2018-12-10阅读: 1.7k标签: js知识

这篇文章将梳理下环境,作用域链,变量对象和活动对象,以及内存管理问题。  


基本类型和引用类型的值

我们都知道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 
}


没有块级作用域

JS只有函数作用域和全局作用域,没有块级作用域。

声明变量

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来释放其引用,这个做法叫做解除引用。这一做法适用于大多数全局变量和全局对象的属性,局部变量会在它们离开执行环境时自动被解除引用。


来自:https://segmentfault.com/a/1190000017314858


链接: https://www.fly63.com/article/detial/1531

classList的使用,原生js对class的添加,删除,修改等方法的总结,以及兼容操作

classList是一个DOMTokenList的对象,用于在对元素的添加,删除,以及判断是否存在等操作。以及如何兼容操作

js原型链,Javascript重温OOP之原型与原型链

js的原型链,得出了一个看似很简单的结论。对于一个对象上属性的查找是递归的。查找属性会从自身属性(OwnProperty)找起,如果不存在,就查看prototype中的存在不存在。

讲解JavaScript 之arguments的详解,arguments.callee,arguments.caller的使用方法和实例

arguments是什么?在javascript 中有什么样的作用?讲解JavaScript 之arguments的使用总结,包括arguments.callee和arguments.calle属性介绍。

WebSocket的原理及WebSocket API的使用,js中如何运用websocket

WebSocket是HTML5下一种新的协议,为解决客户端与服务端实时通信而产生的技术。其本质是先通过HTTP/HTTPS协议进行握手后创建一个用于交换数据的TCP连接

javascript对dom的操作总汇,js创建,更新,添加,删除DOM的方法

HTML文档在浏览器解析后,会成为一种树形结构,我们需要改变它的结构,就需要通过js来对dom节点进行操作。dom节点(Node)通常对应的是一个标题,文本,或者html属性。

深入理解JS中引用类型和基本类型

javascript中基本类型指的是那些保存在栈内存中的简单数据段,即这种值完全保存在内存中的一个位置。 引用类型指那些保存在堆内存中的对象,意思是变量中保存的实际上只是一个指针,这个指针指向内存中的另一个位置,该位置保存对象。

深入理解Javascript中apply、call、bind方法的总结。

apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;第一个参数都是this要指向的对象,也就是想指定的上下文;都可以利用后续参数传参;bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。

理解js中prototype和__proto__和的区别和作用?

在js中有句话叫一切皆对象,而几乎所有对象都具有__proto__属性,可称为隐式原型,除了Object.prototype这个对象的__proto__值为null。Js的prototype属性的解释是:返回对象类型原型的引用。每个对象同样也具有prototype属性,除了Function.prototype.bind方法构成的对象外。

js中“=”,“==”,“===”的使用和深入理解

Js支持“=”、“==”和“===”的运算符,我们需要理解这些 运算符的区别 ,并在开发中小心使用。它们分别含义是:= 为对象赋值 ,== 表示两个对象toString值相等,=== 表示两个对象类型相同且值相等

JS的变量作用域问题,理解js全局变量和局部变量问题

js的变量分为2种类型:局部变量和全局变量。主要区别在于:局部变量是指只能在变量被声明的函数内部调用,全局变量在整个代码运行过程中都可以调用。值得注意的js中还可以隐式声明变量,而隐式声明的变量千万不能当做全局变量来使用。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!