JS高级程序设计学习之——变量、作用域与内存

一、原始值与引用值

ES 变量可以包含两种不同类型的数据:原始值和引用值
原始值就是最简单的数据,保存原始值的变量是按值访问的。引用值则是由多个值构成的对象,引用值是保存在内存中的对象,JS不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用而非实际的对象本身。所以保存引用值的变量是按引用访问的。

1、动态属性

原始值和引用值的定义方式很类似,都是创建一个变量,然后赋值。但是变量在保存了这个值之后,可以对这个值做什么,则不相同。
引用值: 可以随时添加、修改和删除其属性和方法。
原始值: 不能有属性,尽管尝试给原始值添加属性不会报错。打印出现undefined
只有引用值可以动态添加后面可以使用的属性

特殊情况:
原始类型的初始化可以只使用原始字面量的形式,如果使用的是new关键字,则JS会创建一个Object类型的实例。

let name1 = "Nick"
let name2 = new String("Amy")
name1.age = 21;
name2.age = 22; 
console.log(name1.age) //undefined  尽管尝试给原始值添加属性也不会报错
console.log(name2.age) // 22
console.log(typeof name1) // string
console.log(typeof name2) // object

2、复制值

除了存储方式不同,原始值和引用值在通过变量复制时也有所不同。
原始值:复制时,会产生一个副本,等同于被复制的值。两个变量独立使用,互不干扰。
引用值:复制时,复制的是指向引用对象的指针,两个变量指向同一个对象,一个对象上面的变化会影响另一个对象。

//原始值:复制时
let num1 = 5; 
let num2 = num1;
num1 = 10;
console.log(num1,num2) //10 5
//引用值:复制时
let person1 = {name:"xx",age:21}
let person2 = person1;
person2.age = 22;
console.log(person1,person2) //{name: 'xx', age: 22} {name: 'xx', age: 22}

3、传递参数⭐⭐

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

//参数被赋值为原始值
function addTen(num) { 
 	num += 10; 
 	return num; 
} 
let count = 20;
let result = addTen(count); 
console.log(count); // 20,没有变化
console.log(result); // 30
//参数被赋值为引用类型
function setName(obj) {
 	obj.name = "Nicholas"; 
} 
let person = new Object(); 
setName(person); 
console.log(person.name); // "Nicholas"

注意:函数的参数虽然为对象,但是依旧是按值传递的,我们进一步理解

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

当person传入setName()时,其name属性被设置为“wwy”.然后变量obj被设置为一个新对象且name属性被设置为“xx”。如果向参数赋值是按照引用赋值,那么person的name应该变为“xx”, 因为假设形参obj拿到的是person的引用,而不是person引用的值,那么当函数内部生成新对象,并对obj 进行重新指向时,形参obj的指向改变,外部的person的指向也因该改变,但是结果证明console.log(person.name) 输出的为"wwy",所以即使函数参数是引用类型,也是按值传递 当 obj 在函数内部被重写时,它变成了一个指向本地对象的指针。而那个本地对象在函数执行结束时就被销毁了。

二、执行上下文与作用域

执行上下文(以下简称“上下文”)的概念在 JavaScript 中是颇为重要的。变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上。
全局上下文是最外层的上下文。根据 ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一样。在浏览器中,全局上下文就是我们常说的 window 对象,因此所有通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法。使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。
上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。

我们来看一个例子:

var color = "blue";
function changeColor(){
    let anotherColor = "red";
    function swapColor(){
        let tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
        //这里能访问到tempColor、anotherColor、color
    }
    //这里能访问到anotherColor和color
    swapColor();
}
//这里只能访问到color
changeColor();

swapColors()局部上下文的作用域链中有 3 个对象:swapColors()的变量对象、changeColor()的变量对象和全局
变量对象
。swapColors()的局部上下文首先从自己的变量对象开始搜索变量和函数,搜不到就去搜索上一级变量对象。changeColor()上下文的作用域链中只有 2 个对象:它自己的变量对象和全局变量对象。因此,它不能访问 swapColors()的上下文。
用图来展示作用域之间的关系:
![在这里插入图片描述](https://img-blog.csdnimg.cn/36cb1c863474448a8b5eed56c6476aa2.png
在这里插入图片描述
不同颜色的矩形代表不同的上下文。内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问到内部上下文中的任何东西。

1、作用域链增强

with try…catch(这里不详细展开)

2、变量声明

var、let、const
var 局部变量,全局变量;变量提升;
let 块作用域;不能变量提升;同一块中不允许冗余声明,会报错;暂时性死区;
const 块作用域;不能变量提升;同一块中不允许冗余声明,会报错;暂时性死区;const声明的时候必须对其初始化。

三、垃圾回收

JS是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。JS通过自动内存管理实现内存分配和闲置资源回收。思路:确定哪个变量不会再使用,然后释放它占用的内存,这个过程是周期性的,即垃圾回收程序每隔一定时间就会自动运行。垃圾回收过程是一个近似且不完美的方案。
我们以函数中局部变量的正常生命周期为例。函数中的局部变量会在函数执行时存在。此时,栈(或堆)内存会分配空间以保存相应的值。函数在内部使用了变量,然后退出。此时,就不再需要那个局部变量了,它占用的内存可以释放,供后面使用。这种情况下显然不再需要局部变量了,但并不是所有时候都会这么明显。垃圾回收程序必须跟踪记录哪个变量还会使用,以及哪个变量不会再使用,以便回收内存。
垃圾回收如何标记未使用的变量的方式有:标记清理 引用计数

1、标记清理(最常用)

垃圾回收程序运行的时候,会标记内存中存储的所有变量(记住,标记方法有很多种)。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。

2、引用计数

是对每个值都记录它被引用的次数。
存在循环引用问题,引用数永远不会变为0,如果多次调用会导致大量内存永远不会被释放。

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

3、 内存管理

将内存占用量保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据,如果数据不再必要,那么把它设置为null,从而释放其引用。这也可以叫做解除引用。这个最适合全局对象的属性。局部变量在超出作用域后会被自动解除引用。

function createPerson(name){ 
	 let localPerson = new Object(); 
	 localPerson.name = name; 
	 return localPerson; 
} 
let globalPerson = createPerson("Nicholas"); 

// 解除 globalPerson 对值的引用
globalPerson = null;

在上面代码中,变量globalPerson保存着createPerson()函数调用的返回值。在createPerson()内部,localPerson创建了一个对象,并添加了一个name属性,最后localPerson作为函数的返回值被返回,然后被赋值给了globalPerson。localPerson这个局部变量在函数执行完之后就会被解除引用,垃圾回收。但是globalPerson是一个全局变量,则不会被处理,需要显示的去处理,应该在不再需要时手动解除其引用。比如最后一行使 globalPerson = null;
注意:解除对一个值的引用并不会自动导致相关内存被回收,解除引用的关键在于确保相关的值不在上下文里了,因此在下次垃圾回收的时候会被回收。

a、通过const和let声明提升性能

const和let有助于改善代码风格,而且同样有助于改进垃圾回收的过程。因为const和let都以块(而非函数)为作用域,所以相比于使用var,使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收该回收的内存。在块作用域比函数作用域更早终止的情况下,这就有可能发生。

b、隐藏类和删除操作

根据 JavaScript 所在的运行环境,有时候需要根据浏览器使用的 JavaScript 引擎来采取不同的性能优化策略。截至 2017 年,Chrome 是最流行的浏览器,使用 V8 JavaScript 引擎。V8 在将解释后的 JavaScript代码编译为实际的机器码时会利用“隐藏类”。如果你的代码非常注重性能,那么这一点可能对你很重要。运行期间,V8 会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类的对象性能会更好,V8 会针对这种情况进行优化,但不一定总能够做到。
(不详细展开)

c、内存泄漏

意外声明全局变量是最常见但也最容易修复的内存泄漏问题。下面的代码没有使用任何关键字声明变量:

function setName() { 
 	name = 'Jake'; 
}

此时,解释器会把变量 name 当作 window 的属性来创建(相当于 window.name = ‘Jake’)。可想而知,在 window 对象上创建的属性,只要 window 本身不被清理就不会消失。这个问题很容易解决,只要在变量声明前头加上 var、let 或 const 关键字即可,这样变量就会在函数执行完毕后离开作用域。
使用 JavaScript 闭包很容易在不知不觉间造成内存泄漏。请看下面的例子:

let outer = function() { 
	let name = 'Jake'; 
 	return function() { 
 		return name; 
 	}; 
 }; 

调用 outer()会导致分配给 name 的内存被泄漏。以上代码执行后创建了一个内部闭包,只要返回
的函数存在就不能清理 name,因为闭包一直在引用着它。假如 name 的内容很大(不止是一个小字符
串),那可能就是个大问题了。

4、静态分配与对象池(不展开讨论)

总结:

  • 原始值可能是这6中原始数据类型之一:undefined、null、string、boolean、number、symbol
  • 原始值大小固定,因此保存在栈内存
  • 从一个变量到另一个变量复制原始值创建该值得第二个副本
  • 引用值是对象,存储在堆内存
  • 包含引用值的变量实际上只包含指向相应对象的一个指针,而不是对象本身
  • 从一个变量到另一个变量的复制只会复制指针,因此结果是两个变量都指向同一个对象
  • typeof 操作符可以确定值的原始类型,而 instanceof 操作符用于确保值的引用类型
  • 任何变量都存在于某个执行上下文中(也称为作用域)。这个上下文作用域决定了变量的生命周期,以及它们可以访问代码的哪些部分。
  • 执行上下文分全局上下文函数上下文块级上下文
  • 代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数
  • 函数或块的局部上下文不仅可以访问自己作用域内的变量,而且也可以访问包含上下文乃至全局上下文中的变量。
  • 全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据
  • 变量的执行上下文用于确定什么时候释放内存
  • JS是使用垃圾回收的编程语言,开发者不需要操心内存分配和回收。
  • 离开作用域的值会被自动标记为可回收,然后在垃圾回收期间被删除。
  • 主流的垃圾回收算法是标记清理,即先给当前不使用的值加上标记,再回来回收它们的内存。
  • 引用计数是另一种垃圾回收策略,需要记录值被引用了多少次。JavaScript 引擎不再使用这种算法,但某些旧版本的 IE 仍然会受这种算法的影响,原因是 JavaScript 会访问非原生 JavaScript 对象(如 DOM 元素)。
  • 引用计数在代码中存在循环引用时会出现问题。
  • 解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对象、全局对象的属性和循环引用都应该在不需要时解除引用
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值