JS学习笔记(三)变量、作用域与内存
文章目录
一、原始值和引用值
ES变量包含两种不同类型的数据:原始值和引用值。
- 原始值:最简单的数据,保存原始值的变量按值访问
- 引用值:多个值构成的对象,保存在内存中,JS不允许直接访问内存,所以操作对象时,实际操作的时对象的引用而非对象本身**(JS中字符串不是引用类型)**
1.1 复制值
-
在通过变量把一个原始变量赋值到另一个变量时,原始值会被复制到新变量的位置。而两个变量可以独立使用,互不干扰。
-
在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量的位置。操作完成后。两个便是实际指向同一个对象,会相互影响
1.2 传递参数
ES中所有函数的参数都是按值传递的
1.3 确定类型
-
typeof :针对原始值
-
instanceof :判断一个值什么类型的对象。所有引用值都是Object的实例
二、执行上下文
执行上下文分为:全局上下文,函数上下文和块级上下文
变量或函数的上下文决定了他们可以访问哪些数据,以及行为。每一个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。
全局上下文是最外层的上下文,在浏览器中,全局上下文就是我们常说的window对象。所有通过var定义的全局变量和函数都会成为window对象的属性和方法。使用let和const的顶级声明不会在全局上下文中,但在作用域链解析上效果是一样的。
上下文在其代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出时才会被销毁,如关闭网页或退出浏览器)
每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推倒一个上下文栈上。在函数执行完后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。
上下文中的代码在执行的时候,会创建变量对象的一个作用域链。该作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。正在执行的上下文的变量对象始终位于作用域链的最前端。若上下文是函数,则其活动对象用作变量对象。活动对象起初只有一个定义变量:arguments(全局上下文中没有)。全局上下文的变量对象始终是作用域链的最后一个变量对象。
内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文中的任何东西。每个上下文都可以到上一级上下文中去搜索变量和函数,但任何上下文都不能到下一级上下文中去搜索。
2.1 作用域链增强
执行上下文主要有:全局上下文和函数上下文,但某些语句会导致**在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。**出现这个现象的两种情况:
- try/catch语句的catch块:会创建一个新的变量对象,这个对象包含要抛出的错误对象的声明
- with语句:会向作用域链前端添加指定的对象
2.2 变量声明
1. 使用var的函数作用域声明
-
在使用var声明变量时,变量会被自动添加到最接近的上下文。在函数中,最接近的上下文就是函数的局部上下文,若函数未经声明就被初始化,则它会被自动添加到全局上下文。
-
var声明会被提升到函数或全局作用域的顶部
2. 使用let 的块级作用域声明
块级作用域是由最近的一对包含花括号{}界定的。即if块,while块,function块,甚至单独的块也是let声明变量的作用域。
- let在同一作用域里不能声明两次,否则会报错。
3. 标识符查找
搜索标识符从作用域链前端开始,以给定的名称搜索对应的标识符,局部上下文 -> 全局上下文搜索
三、垃圾回收(重点)
JS使用垃圾回收机制,即执行环境负责在代码执行时管理内存,通过自动内存管理实现内存分配和闲置资源回收。
思路:确定哪个变量不会再使用,然后释放它占用的内存。过程时周期性的,即垃圾回收程序每隔一定时间就会自动运行。标记策略:标记清理和引用计数
3.1 标记清理(最常用)
思路
当变量进入上下文,如再函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而当变量离开上下文时,会被加上离开上下文的标记。
垃圾程序运行时,会标记在内存中存储的所有变量。然后,他会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。之后再有被加上标记的变量就是待删除的,因为任何在上下文中的变量都访问不到他们了
3.2 引用计数
思路
对每个值都记录它被引用的次数
策略
- 声明变量时赋予的引用值为1
- 同一个值被赋给另一个变量,引用数加1
- 若保存对该值引用的变量被其他值覆盖,引用数减1
- 引用数为0时,回收内存
缺陷
在代码中存在循环引用时会出现问题
3.3 内存管理
若内存占用量保持在较小的值可以让页面的性能更好。若数据不再必要,则可把它设置为null,释放其引用。叫做解除引用。最适合全局变量和全局对象的属性
function foo(name) {
let localPerson = new Object();
localPerson.name = name;
return localPerson;
}
let globalPerson = foo('Niki');
// 接触globalPerson对值的引用
globalPerson = null;
注意:解除一个值的引用并不会自动导致相关内存被回收。解除引用的关键在于确保相关的值已经不在上下文中了,因此它在下次垃圾回收时会被回收。
1. 通过const和let声明提升性能
const和let都以块为作用域,相比于使用var,使用这两个新关键字可能会更早的让垃圾回收接入,回收内存。
2. 隐藏类和删除操作
V8引擎在将解释后的JavaScript代码编译为实际的机器码时会利用”隐藏类“。运行期间,V8会将创建的对象与隐藏类关联,以跟踪他们的属性特征。
使用delete关键字会导致生成相同的隐藏片段。
function foo() {
this.title = 'hhhhh';
this.author = 'niki';
}
let a1 = new foo();
let a2 = new foo();
delete a1.author;
代码结束后,即使两个实例使用了同一个构造函数,它们也不再共享一个隐藏类。动态删除属性与动态添加属性导致的后果一样。最好是把不想要的属性设置为null;这样可以保持隐藏类不变和继续共享,同时也能达到删除引用值供垃圾回收程序回收的效果。
3. 内存泄漏
可能造成内存泄露的情况:
- 意外声明全局变量
function setName(){
name = 'niki';
}
解释器会把变量name当作window属性创建,只要window本身不被清理就不会消失。
解决方案:在变量声明前加上var,let或const
- 定时器
let name = 'niki';
setInterval(() => {
console.log(name);
},100);
这个例子定时器的回调通过闭包调用了外部变量,只要定时器一直运行,回调函数引用的name就会一直占用内存
- 使用JavaScript闭包
let outer = function(){
let name = 'Jake';
return function() {
return name;
};
}
调用outer()导致分配给name的内存被泄露。代码执行后创建了一个内部闭包,只要返回的函数存在就不能清理name。因为闭包一直在引用他。
4. 静态分配与对象池
浏览器决定何时运行垃圾回收程序的一个标准就是对象更替速度。
策略:
使用对象池
**在初始化时,创建一个对象池,用来管理一组可回收对象。应用程序可以向这个对象池请求一个对象、设置其属性使用它,然后在操作完成后再把它返回给对象池,用来管理一组可回收的对象池。**由于没发生对象初始化,垃圾回收探测就不会发现有对象更替。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/150439.html