学习笔记6—作用域与内存

1 原始值与引用值

原始值就是最简单的数据,引用值则是由多个值构成的对象。
在把一个值赋给变量时,JavaScript引擎必须确定这个值是原始值还是引用值。保存原始值的变量是按值访问的,因为我们操作的就是存储在变量中的实际值。引用值是保存在内存中的对象。与其他语言不同,JavaScript不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是该对象的引用,而非实际对象本身。为此,保存引用值的变量是按引用访问的。

2 复制值

除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。

let num1 = 5;
let num2 = num1;

num1包含数值5,当把num2初始化为num1时,num2也会得到数值5,这个值跟存储在num1中的5是完全独立的。这两个变量可以独立使用,互不干扰。
在把引用值从一个变量赋值给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的实际上是一个指针,指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此在一个对象上面的变化会在另一个对象上反映出来:

let obj1 = new Object();
let obj2 = obj1;
obj1.name = "Nicholas";
console.log(obj2.name); //"Nicholas"

变量obj保存了一个新对象的实例,然后这个值被复制到obj2,此时两个变量都指向了同一个对象。

3 传递参数

ECMAScript中所有函数的参数都是按值传递的,意味着函数外的值会被复制到函数内部的参数中,就像从一个变量复制到另一个变量一样。如果是原始值,那么就跟原始值变量的复制一样,如果是引用值,那么就跟引用值的变量复制一样。

function setName(obj){
    
    
	obj.name = "Nicholas";
}
let person = new Object();
setName(person);
console.log(person.name); //"Nicholas"

4 确定类型

为了解决是什么类型对象的问题,ECMAScript提供了instanceof操作符:

console.log(person instanceof Object);//变量person是Object么
console,log(colors instanceof Array); //变量colors是Array么
console.log(pattern instanceof RegExp); //变量pattern是RegExp么

如果变量给定引用类型的实例,则instanceof操作符返回true

5 执行上下文与作用域

关于执行上下文的理解:编程时,我们一般也是先给程序定义一些前提(环境变量、描述环境变化的全局变量等),这些“前提”就是上文,然后再编写各功能模块的代码,这是下文。
变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台处理数据会用到它。
执行上下文有且只有三类,全局执行上下文,函数上下文,与eval上下文;由于eval一般不会使用,这里不做讨论。
全局上下文是最外层的上下文。在浏览器中,全局上下文就是我们常说的window对象,因此所有通过var定义的全局变量和函数都会成为window对象的属性和方法。使用let和const的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。
每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上,在函数执行完后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。ECMAScript程序的执行流就是通过这个上下文栈进行控制的。
作用域:指的是您有权访问的变量集合。
上下文中的代码在执行过程中,会创建变量对象的一个作用域链。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端。如果上下文是函数,则其活动对象用作变量对象。活动对象最初只有一个定义变量:arguments。作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文,全局上下文的变量对象始终是作用域链的最后一个变量对象。
代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符。

var color = "blue";
function changeColor(){
    
    
	if(color === "blue"){
    
    
		color = "red";
	}else{
    
    
		color = "blue";
	}
}
changeColor();

对于这个例子,changeColor()的作用域链包含两个对象:一个是他自己的变量对象(就是定义arguments对象的那个),另一个是全局上下文的变量对象。这个函数内部之所以能够访问变量color,就是因为可以在作用域链中找到它。
此外,局部作用域中定义的变量可用于在局部上下文中替换全局变量:

        var color = "blue";

        function changeColor() {
    
    
            let anotherColor = "red";

            function swapColors() {
    
    
                let tempColor = anotherColor;
                anotherColor = color;
                color = tempColor;
            }
            swapColors();
        }
        changeColor();
        console.log(color);

以上代码涉及3个上下文:全局上下文、changeColor()的局部上下文和swapColors()的局部上下文。全局上下文中有一个变量color和一个函数changeColor()。changeColor()的局部上下文中有一个变量anotherColor和一个函数swapColors(),但在这里可以访问全局上下文中的变量color。swapColors()的局部上下文中有一个变量tempColor,只能在这个上下文中访问到。全局上下文和changeColor()的局部上下文都无法访问到tempColor。而在swapColors()中则可以访问另外两个上下文中的变量,因为它们都是父上下文。

6 垃圾回收

JavaScript是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。基本思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一段时间就会自动运行。垃圾回收程序必须跟踪记录哪个变量还会使用,以及哪个变量不会再使用,以便回收内存。如何标记未使用的变量也许有不同的实现方式。但在浏览器的发展史上,用到过两种主要的标记策略:标记清理和引用计数。
(1)标记清理
JavaScript最常用的垃圾回收策略是标记清理,当变量进入上下文时,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而在上下文中的变量,逻辑上讲,永远不应该释放它们的内存,因为只要上下文中的代码在运行,就有可能用到它们。当变量离开上下文时,也会被加上离开上下文的标记。
给变量加标记的方式有很多种,比如当变量进入上下文时,反转某一位,或者可以维护“在上下文中”和“不在上下文中”两个变量列表,可以把变量从一个列表转移到另一个列表。标记过程的实现并不重要,关键是策略。
垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量标记去掉。在此之后再被加上标记的变量就是待删除的了,因为任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并回收它们的内存。
(2)引用计数
另一种没那么常用的垃圾回收策略是引用计数,其思路是对每个值都记录它被引用的次数。声明变量并给他赋一个引用值是,这个值的引用次数为1.如果同一个值又被赋给另一个变量,那么引用次数加1.类似地,如果保存对该引用的变量被其他值给覆盖了,那么引用次数减一。当一个值的引用次数为0时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行时就会释放引用次数为0值的内存。
引用计数最早由Netscape Navigator 3.0采用,但很快就遇到了严重的问题:循环引用。所谓循环引用就是对象A有一个指针指向对象B,而对象B也引用对象A,如:

function problem(){
    
    
	let objectA = new Object();
	let objectB = new Object();
	objectA.someOtherObject = objectB;
	objectB.anotherObject = objectA;
}

在这个例子中,object和objectB通过各自的属性相互引用,意味着它们的引用计数都为2,在标记清理策略下,这不是问题,因为在函数结束后,这两个对象都不在作用域中。而在引用计数策略下objectA和objectB在函数结束后还会存在,因为它们的引用数永远不会变成0。
(3)静态分配与矢量池
为了提高JavaScript的性能,最后要考虑的一点往往就是压榨浏览器了。此时,一个关键问题就是如何减少浏览器执行垃圾回收的次数。开发者无法直接控制什么时候开始收集垃圾,但可以间接控制触发垃圾回收的条件。理论上,如果能合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因释放内存而损失的性能。
浏览器决定何时运行垃圾回收程序的一个标准就是对象更替的速度。如果有很多对象被初始化。然后一下子又都超出了作用域,那么浏览器就会采用更激进的方式调度垃圾回收程序运行,这样当然会影响性能:

function addVector(a,b){
    
    
	let resultant = new Vector();
	resultant.x = a.x + b.x;
	resultant.y = a.y + b.y;
	return resultant;
}

调用这个函数时,会在堆上创建一个新对象,然后修改它,最后再把它返回给调用者。如果这个矢量对象的生命周期很短,那么它会很快失去所有对它的引用,成为可以被回收的值。加入这个矢量加法函数频繁被调用,那么垃圾回收调度程序会发现这里对象更替速度很快,从而会更频繁地安排垃圾回收。该问题的解决方案是不要动态创建矢量对象,比如可以修改上面的函数,让它使用一个已有的矢量对象:

function addVector(a,b,resultant){
    
    
	resultant.x = a.x + b.x;
	resultant.y = a.y + b.y;
}

当然,这需要在其他地方实例化矢量参数resultant,但这个函数的行为没有变。那么在哪里创建矢量可以不让垃圾回收调度程序盯上呢?
一个策略是使用对象池,在初始化的某一时刻,可以创建一个对象池,用来管理一组可回收的对象。应用程序可以向这个对象池请求一个对象、设置其属性,使用它,然后在操作完成后再把它还给对象池,由于没发生对象初始化,垃圾回收探测就不会发现有对象更替,因此垃圾回收程序就不会那么频繁地运行。P100

猜你喜欢

转载自blog.csdn.net/qq_43599049/article/details/113002876