关闭

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

时间: 2018-12-10阅读: 737标签: 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


站长推荐

1.云服务推荐: 国内主流云服务商,各类云产品的最新活动,优惠券领取。地址:阿里云腾讯云华为云

2.广告联盟: 整理了目前主流的广告联盟平台,如果你有流量,可以作为参考选择适合你的平台点击进入

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

关闭

Js中constructor指向问题

首先用一个例子指出来constructor存在形式。由上面的代码我们总结出结论1:上面的代码在控制台可以看出constructor是指向构造器Fruit的引用。这个地方就有点奇怪了。这个constructor到底指向的是那个实例的构造器?

数字在JavaScript中是如何编译的

JavaScript中的所有数字都是浮点数。这篇博客文章解释了这些浮点数如何在64位二进制内部表示。浮点数并不一定等于小数,定点数也并不一定就是整数。所谓浮点数就是小数点在逻辑上是不固定的,而定点数只能表示小数点固定的数值

js记录用户行为浏览记录和停留时间

原来是想使用 cookie 来记录,但是考虑到 cookie 所能记录的数据最大为 4k ,可能不够用。于是使用了 HTML5 的 localStorage (最大数据 5M )来存储( IE8 以上浏览器支持)。这里使用到了 jquery.cookie 的插件,所以页面要引入 jquery 和 jquery.cookie 。

window.location.href的用法(动态输出跳转)

javascript中的location.href有很多种用法,window.location.href 语句可以实现一个框架的页面在执行服务器端代码后刷新另一个框架的页面

Js常用基础算法

冒泡排序;插入排序 过程就像你拿到一副扑克牌然后对它排序一样;快速排序;回文字符串;翻转字符串;字符串中出现最多次数的字符;数组去重;二分查找

原生js判断当前页面是否为激活状态【判断用户是否在浏览当前页面】

但浏览器打开多个网页时候,如何判断我这个页面是否正在被用户浏览呢?我们可以通过document.hidden属性判断当前页面是否是激活状态。

js中return关键词的作用_return的用法详解

return从字面意思来看就是返回,官方定义return语句将终止当前函数并可以返回当前函数的值; 也就是说return后面可以跟一个value,也就是说可以跟javascript中的任何数据类型

(...)这三个点在JavaScript中意味着什么?

解释JavaScript中三个点的作用:数组/对象扩展运算符、rest运算符(使用函数的参数时,无论是完全替换参数还是与函数的参数一起替换参数,这三个点也称为rest运算符)

JavaScript中的循环和作用域

JavaScript有一个特点,也许会让开发者头痛, 是与循环和作用域相关的.const。最简单的方案是用 let 声明、另外一个非常普遍的解决这个问题是使用pre-ES6代码, 同时它被称作即时调用函数表达式(IIFE)

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

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

点击更多...

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