第4章 变量、作用域与内存
4.1 原始值和引用值
- 原始值 primitive value 保存原始值的变量是按值 by value访问的,操作的是存储在变量中的实际值,例如下面的6中原始值
Undefined Null Boolean Number String Symbol - 引用值 reference value 引用值是保存在内存中的对象,JavaScript不允许直接访问内存位置,不能直接操作对象所在的内存空间,操作对象时,实际上操作的是该对象的引用 reference
注意 在很多语言中,字符串是使用对象表示,因此被认为是引用类型,但是ECMAScript 打破了这个惯例
4.1.1 动态属性
动态属性的意思是对于引用值而言,可以随时添加、修改、删除其属性和方法
let person = new Object();
person.name = "冬篱的川";
console.log(person.name);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3cYEaPhr-1626868188152)(https://i.loli.net/2021/07/21/Toxk9Amn7wb6D3Y.png)]
但是如果给原始值添加属性或者方法,虽然不会报错,但是会报undefined
let name = "冬篱的川";
name.age = 23;
console.log(name.age);

4.1.2 复制值
- 原始值在复制变量到另一个变量时,原始值会被复制到新变量的位置,实际上的内存区域不同
- 引用值从一个变量赋值到另一个变量时,复制的值实际上是一个指针,同时指向堆内存中的同一个对象
4.1.3 传递参数
可以这样理解:ECMAScript 中所有函数的参数都是按值传递的,即引用值按照引用值复制值得方式,传递的是变量的引用;而原始值就是原始值的变量复制一样
4.1.4 确定类型
typeof 操作符最适合判断一个变量是否为原始类型,是判断一个变量是否为字符串、数值、布尔值或undefined的最好方式,但是如果值是对象或null,那么typeof 则返回object
let s = "冬篱的川";
let b = true;
let i = 22;
let u;
let n = null;
let o = new Object();
console.log(typeof s);
console.log(typeof b);
console.log(typeof i);
console.log(typeof u);
console.log(typeof n);
console.log(typeof o);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-weUCQoQ3-1626868188157)(https://i.loli.net/2021/07/21/SFrp9uwE1Gnmsdx.png)]
4.2 执行上下文与作用域
-
全局上下文是最外层的上下文 -
浏览器中的全局上下文是windows 对象,所有通过var 指定的全局变量和函数都会成为window 对象的属性和方法 -
上下文中的代码在执行的时候,会创建变量对象的一个作用链域 scope chain -
代码执行时的标识符解析是通过作用链域逐级搜索标识符名称完成的。搜索的过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符。 -
局部作用域中定义的变量可以用于在局部上下文替换全局变量。
var color = "blue";
function changeColor() {
let anotherColor = "red";
function swapColors() {
let tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColor();
}
changeColor();
4.2.1 作用域增强
这两种情况都会在作用域链前端添加一个变量对象。
- 对于
with 语句来讲,会向作用链域前端添加指定的对象
function buildUrl() {
let qs = "?debug=true";
with(location) {
let url = href + qs;
}
return url;
}
这里,with 语句将location 对象作为上下文,因此location 对象会被添加到作用链域最前端。这里的href 变量实际上是引用的location.href 变量
4.2.2 变量声明
- 使用
var 的函数作用域声明
使用var 声明变量时,变量会自动添加到最接近的上下文,函数中最接近的上下文是函数的上下文,在with 语句中,最接近的上下文也是函数上下文。如果变量未经声明就被初始化了,那么它将自动的被添加到全局上下文。
var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前,这个现象就做提升 hoisting。提升让同一作用域的代码不必考虑变量是否已经声明就可以直接使用。
function test() {
{
var name = "冬篱的川";
}
console.log(name);
}
test();
function test2() {
{
let name = "冬篱的川";
}
console.log(name);
}
test()2;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-umEe19fS-1626868188160)(https://i.loli.net/2021/07/21/YHEpZWtcK829seM.png)]
- 使用
let 的块级作用域声明
- 块级作用域是由最近的一对包含花括号的{}界定,即
if while function 块,甚至单独的块也是let 声明变量的作用域 let 在同一作用域不能声明两次,重复声明会跑出SyntaxError ,重复的var 声明会被忽略
let 上面的行为就非常适合在循环中声明迭代变量,因为使用var 的迭代变量会泄漏到循环外部
- 使用
const 的常量声明
- 使用
const 声明的变量必须同时初始化为某个值,一经声明,在其生命周期的任何时候都不能在重新赋予新值
const a;
const b = 3;
console.log(b);
b = 4;

const 声明值应用到顶级原语或对象,赋值为对象的cosnt 变量不能被重新赋值为其它的引用值,但是对象的键不受限制
const o = {};
o.name = "冬篱的川";
console.log(o.name);

但是如果想让整个对象都不能修改,可以使用Object.freeze() 方法,这样再给属性赋值时虽然不会报错,当会静默失败:
const o2 = Object.freeze({});
o2.name = "冬篱的川";
console.log(o2.name);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0aIR0mK8-1626868188166)(https://i.loli.net/2021/07/21/WTZabR4cxMP9OC3.png)]
由于 const 声明暗示变量的值是单一类型且不可修改, JavaScript 运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查找
注意 开发实践表明,如果开发流程并不会因此而受很大影响,就应该尽可能地多使用const 声明,除非确实需要一个将来会重新赋值的变量。这样可以从根本上保证提前发现重新赋值导致的 bug
- 标识符查找
标识符查找开始与作用域链前端,以给定的名称搜索对应的标识符,如果在局部上下文中找到该标识符,则搜索停止,变量确定;如果没有,则继续沿作用链域搜索,这个过程一直持续到搜索至全局上下文的变量对象,如果仍未找到,则说明其未声明
4.3 垃圾回收
- JavaScript是使用垃圾回收的语言,就是说执行环境负责代码执行时管理内存
- JavaScript通过自动内存管理实现内存分配和闲置资源回收,主要是靠周期性的确定那个变量不再使用,然后释放内存
4.3.1 标记清理 mark-and-sweep
垃圾回收程序运行的时候,会标记内存中存储的所有变量(记住,标记方法有很多种)。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存
即是把引用的变量的标记去掉,未引用的标记,然后释放
4.3.2 引用计数 reference counting
其思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为 0 的值的内存
这里会遇到循环引用的问题,即A引用B,B也引用A
function problem() {
let a = {};
let b = {};
a.b = b;
b.a = a;
}
4.3.3 性能
垃圾回收程序会周期性运行,如果内存中分配了很多变量,则可能造成性能损失,因此垃圾回收的时间调度很重要。尤其是在内存有限的移动设备上,垃圾回收有可能会明显拖慢渲染的速度和帧速率。开发者不知道什么时候运行时会收集垃圾,因此最好的办法是在写代码时就要做到:无论什么时候开始收集垃圾,都能让它尽快结束工作
4.3.4 内存管理
将内存占用量保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据。如果数据不再必要,那么把它设置为 null ,从而释放其引用。这也可以叫作解除引用。
- 通过
const 和 let 声明提升性能, 块级作用域能够更早地让垃圾回收程序介入 - 隐藏类和删除操作
- 内存泄漏
- 静态分配与对象池
|