博客
zyzcos.gitee.io
第四章:变量、作用域与内存
4.1 原始值与引用值
ECMAScript变量包含
两种
不同类型的数据:原始值
和引用值
;
分类 | 组成 | 访问方式 |
原始值 | Undefined、Null、Boolean、Number、String和Symbol六种简单数据 | 按值访问【操作的就是存储的`实际值`】 |
引用值 | 由多个值构成的对象 | 按引用访问【操作的是该对象的引用,通过引用操作对象】 |
4.1.1 动态属性
对于
原始值
而言:不能拥有属性,虽然不会报错,但是无效。
let person = 'zyzc';
person.age = 20;
console.log(person.age); // undefined;
对于
引用值
而言:可以随时添加、修改和删除属性
let person = new Object();
person.age = 18;
原始值
使用new
来定义,可以拥有引用值的行为。
let person = new String('zyzc');
person.age = 18;
console.log(person.age); // 18
console.log(typeof person); // Object
4.1.2 复制值
对于
原始值
而言:会被复制到新变量的位置
。
![](https://i-blog.csdnimg.cn/blog_migrate/877350085393b552ada7b35fe2b866e5.png)
对于
引用值
而言:会将引用值从一个变量赋值给另一个变量,然后通过引用共享同一个对象。
let obj1 = new Object();
obj1.name = 'zyzc';
let obj2 = obj1; // 进行复制
console.log(obj2.name); //'zyzc'
![](https://i-blog.csdnimg.cn/blog_migrate/80be172547f45b435ff104a57fe64e54.png)
4.1.3 传递参数
在ECMAScript中,所有函数的传参都是
按值传递
的。【个人理解为复制副本】
对于原始值而言:
let count = 20;
function add(num){
num +=10;
return num;
}
let result = add(count);
console.log(result); // 30
console.log(count); // 20;
![](https://i-blog.csdnimg.cn/blog_migrate/5878eaf5e8fe8a00fd8896e085c295e2.png)
对于引用值而言:
let personA = new Object();
function setName(obj){
obj.name = 'zyzc';
}
console.log(personA.name); // zyzc
![](https://i-blog.csdnimg.cn/blog_migrate/fbe743612b969b16f0c1ef46149cae66.png)
难道这个和C++那样,传的是指针吗?
let personA = new Object();
function setName(obj){
obj.name = 'zyzc';
obj = new Object();
obj.name = 'hi';
}
console.log(personA.name); // zyzc
显然,如果是
传指针
的话,personA的值也会跟着改变为’hi’;
4.1.4 确定类型
原始值
:使用typeof
可以确定类型
引用值
:使用instanceof
可以确定是什么类型的对象
4.2 执行上下文与作用域
4.2.1 什么是上下文
上下文
决定了可以访问那些数据
,可以表现什么行为
;而每个上下文都关联着一个变量对象
,该变量对象
包括了上下文
中定义的所有变量
和函数
;【最外层的上下文是全局上下文
,一般指的是window对象
】
4.2.2 上下文怎么工作?
每个函数,都有自己的上下文;当代码流入函数的时候,函数的
上下文
压到上下文栈
中,当函数执行完毕之后,函数的上下文
会弹出上下文栈
。
4.2.3 什么是作用域链?
当
上下文
代码执行的时候,会创建变量对象
的作用域链
;该作用域链
决定各级上下文访问变量、函数的顺序。当前作用的上下文
的变量对象
总会在作用域链
的最前端
。而全局上下文
总会在作用域链
的最后端
4.2.4 举个例子说明上下文与作用域链
/*
这里的上下文:全局上下文
这里的作用域链:全局变量对象
这里可以访问的变量:color
这里可以访问的函数:changeColor
这里不可以访问的变量:anotherColor、tempColor、anotherColor。因为作用域链上找不到。
*/
var color = "blue";
function changeColor() {
let anotherColor = "red";
/*
这里的上下文:anotherColor上下文
这里的作用域链:changeColor变量对象 ——> 全局变量对象
这里可以访问的变量:color
这里可以访问的函数:swapColors
这里不可以访问的变量:tempColor、anotherColor。因为作用域链上找不到。
*/
function swapColors(){
/*
这里的上下文:swapColors上下文
这里的作用域链:swapColors变量对象 ——> changeColor变量对象 ——> 全局变量对象
这里可以访问的变量:color、tempColor、anotherColor
*/
let tempColor = anotherColor; // 当使用到anotherColor变量时,在swapColors变量对象中寻找。若找不到,就顺着作用域链,向后找。直到找到或者未找到为止。
anotherColor = color;
color = tempColor;
}
swapColors();
}
changeColor();
寻找变量
是顺着作用域链
由最前端【自身作用域的变量对象】
到最后端【全局作用域的变量对象】
、由内层到外层地寻找的。
4.2.5 作用域链增强
某些语句会在作用域链前端
临时添加
一个上下文.
- try/catch中的catch: 会创建一个新的变量对象,其中包含
要抛出的错误对象的声明
- with:会创建一个包含
关联对象
的变量对象
4.3 垃圾回收
JavaScript是使用
垃圾回收
的语言,即由执行环境
负责管理内存、内存分配和闲置资源回收.
4.3.1 垃圾回收基本思路
- 确定那个变量不会使用
- 释放其占用的内存
- 周而复始的执行
显然,
确定那个变量不会使用
是一个不可判定
的问题,所以不能使用算法来解决.只能每隔一段时间,执行一次垃圾回收。
4.3.2 垃圾回收的策略
-
标记清理:当进行垃圾回收的时候,通过变量的标记,来判定是否需要销毁并回收内存.标记的方法有很多:
- 当变量进入上下文的时候,反转某一位;然后离开上下文的时候,再进行反转.从而达到标记的效果。
- 维护两个表:在上下文表、不在上下文表;
-
引用计数:声明变量并赋值的时候,值的引用数为1;如果同一个值又被赋给另外一个变量,则
引用数+1
;若保存对该值的引用的变量被其他值覆盖,则引用数-1
;当运行垃圾回收程序的时候,会将引用数为0的值清理
。
现在的浏览器采用的大多是
标记清理
,因为引用计数存在循环引用
问题:
function bugTest(){
let objectA = new Object();
let objectB = new Object();
objectA.tail = objectB;
objectB.head = objectA;
}
虽然说两者相互引用确实不应该被清除,但是一旦代码
不在bugTest的上下文中
后,这两个对象还是会一直存在,不被清理。
4.4 性能与内存管理
4.4.1 什么会影响性能?
- 如果内存中分配了很多变量,则可能会造成性能的损失。
- 如果垃圾回收程序的执行时间太长,也可能会造成性能的损失。
4.4.2 内存管理
在JavaScript中,因为具有垃圾回收机制,所以开发者通常无需关心内存管理。
-
如果需要优化内存占用,最佳的手段就是:保证在执行代码的时候,只保存
有必要的数据
。如果数据不再必要,可以通过将其设置为null
,从而释放引用
,这也叫做解除引用
。 -
通过const和let声明提升性能:这两个关键字声明的变量,可能让垃圾回收程序尽早介入,并尽早释放并回收内存。
-
隐藏类和删除操作:V8引擎,为了追踪对象的属性特征,会创建
隐藏类
并与对象
关联起来。其中,隐藏类会占用空间
;function Animal(){ this.name = 'dog'; } // dog1 和 dog2 共享一个隐藏类 let dog1 = new Animal(); let dog2 = new Animal(); // 但是进行属性添加后, dog2.sex = 'male'; // dog1 和 dog2 关联的隐藏类就不一样了。从而增加了内存占用
为了避免以上
先创建再补充
而导致的问题,应当在构造函数
中一次性声明
所需属性;function Animal(sex){ this.name = 'dog'; this.sex = sex; } // dog1 和 dog2 共享一个隐藏类 let dog1 = new Animal(); let dog2 = new Animal("male"); // 当不需要该对象的时候,应该这样操作 dog2 = null; // 解除引用
-
对象池与静态分配
为了提升JavaScript的性能,最后只能
压榨
浏览器了。此中的关键就是:减少浏览器执行垃圾回收的次数
,从而保住因释放内存而损失的性能。决定浏览器什么时候运行垃圾回收程序的一个标准就是:
对象的更替速度
,如果很多对象被初始化
、又一下子超出作用域
,则会调度垃圾回收程序。优化前:每次调用这个函数,都会创建一个新的Vector,若多次调用,则会导致对象的更替速度变快。
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; return resultant; } // 创建一个对象池,用来管理可回收的对象。 const vectorPool = { v1:new Object(), v2:new Object(), v3:new Object() }; vectorPool.v1.x = 10; vectorPool.v1.y = 5; vectorPool.v2.x = 10; vectorPool.v2.y = 5; addVector(vectorPool.v1,vectorPool.v2,vectorPool.v3); console.log(vectorPool.v3.x + '' + vectorPool.v3.y); // 20 10 // 解除引用 v1 = null; v2 = null; v3 = null;
除了使用对象池,也可以使用
数组
进行静态分配。
但是数组的大小是
动态可变
的,为了避免数组大小变化的时候,引来垃圾回收;所以要一开始就预定好够用的数组
;
4.5 总结:
原始值
和引用值
有如下特点:
- 原始值,大小固定,存储在
栈内存
上。 - 引用值是对象,存储在
堆内存
上。 - 包含引用值得变量,实际是一个
指向对象的指针
。 - 引用值的复制,就是
指针的复制
。 typeof
确定原始类型;instanceof
确定引用类型。
- 关于
上下文(作用域)
- 上下文
决定
了变量的生命周期
- 执行上下文分为:
全局上下文
、函数上下文
和块级上下文
- 代码流入一个
新上下文
,都会创建一个作用域链,用于搜索变量和函数 - 作用域的搜索,是自内层到外层的,所以全局作用域只能访问全局上下文的变量和函数。
- 变量的执行上下文,用于确定什么时候释放内存。
- 关于垃圾回收程序
- 离开作用域的值,会被标记为
可回收
,等待垃圾回收程序执行期间被删除。 - 主流的垃圾回收是
标记清理
- 某些旧版本IE仍会使用
引用计数
解除变量
是一个很好的习惯。既可以消除循环引用
,也可以帮助垃圾回收。