第 4 章 变量、作用域与内存
4.1 原始值与引用值
JavaScript 不允许直接访问内存位置。
原始值大小固定,因此保存在栈内存(变量、参数)上。引用值是对象,存储在堆内存(对象)上。
包含引用值的变量实际上只包含指向相应对象的一个指针,而不是对象本身。
从一个变量到另一个变量复制引用值只会复制指针,因此结果是两个变量都指向同一个对象。
原始值和引用值的终极面试题
var foo = { bar: 1 }
var arr1 = [1, 2, foo]
var arr2 = arr1.slice(1)
arr2[0]++
arr2[1].bar++
foo.bar++
arr1[2].bar++
console.log(arr1[1] === arr2[0])
console.log(arr1[2] === arr2[1])
console.log(foo.bar)4.1.1 动态属性
给原始值添加属性不会报错
let name = 'LBJhui'
name.age = 28
console.log(name.age) // undefined4.1.2 复制值
通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。在把引用值从一个变量赋给另一个变量时,存储在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针,它指向存储在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来
4.1.3 传递参数
ECMAScript 中所有函数的参数都是按值传递的。
4.1.4 确定类型
typeof 操作符最适合用来判断一个变量是否为原始类型。
result = variable instanceof constructor
4.2 执行上下文与作用域
通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法。使用 let 和 const 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。
❑ 执行上下文分全局上下文、函数上下文和块级上下文。
❑ 代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数。
❑ 函数或块的局部上下文不仅可以访问自己作用域内的变量,而且也可以访问任何包含上下文乃至全局上下文中的变量。
❑ 全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据。
❑ 变量的执行上下文用于确定什么时候释放内存。
4.2.1 作用域链增强
❑ try/catch 语句的 catch 块
❑ with 语句
会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。
4.2.2 变量声明
1.使用 var 的函数作用域声明
变量提升
2.使用 let 的块级作用域声明
作用域是块级的。块级作用域由最近的一对包含花括号{}界定。
let 与 var 的另一个不同之处是在同一作用域内不能声明两次。重复的 var 声明会被忽略,而重复的 let 声明会抛出 SyntaxError。
暂时性死区 (temporal dead zone)
3.使用 const 的常量声明
使用 const 声明的变量必须同时初始化为某个值。一经声明,在其生命周期的任何时候都不能再重新赋予新值。
const 声明只应用到顶级原语或者对象。换句话说,赋值为对象的 const 变量不能再被重新赋值为其他引用值,但对象的键则不受限制。
如果想让整个对象都不能修改,可以使用 Object.freeze(),这样再给属性赋值时虽然不会报错,但会静默失败:
const o3 = Object.freeze({})
o3.name = 'Jake'
console.log(o3.name) // undefined:::detail Object.freeze() 和 const
Object.freeze() 返回的是一个不可变的对象。这就意味着我们不能添加,删除或更改对象的任何属性
const 和 Object.freeze() 并不同,const 是防止变量重新分配,而 Object.freeze() 是使对象具有不可变性
Object.freeze() 只能冻结当前对象
Object.freeze() 仅能冻结对象的当前层级属性,换而言之,如果对象的某个属性本身也是一个对象,那么这个内部对象并不会被 Object.freeze() 冻结
使用冻结对象提升效率 Object.freeze() 冻结对象在 vue 中不会变为响应式
let person = {
a: '1',
b: '2',
c: {
a: 1
}
}
Object.freeze(person)
person.a = '3'
console.log(person) // { a: '1', b: '2', c: { a: 1 } }
person.c.a = '2'
console.log(person) // { a: '1', b: '2', c: { a: '2' } }
person = {
c: '3'
}
console.log(person) // { c: '3' }
// 深冻结
function deepFreeze(obj) {
var propNames = Object.getOwnPropertyNames(obj)
propNames.forEach((name) => {
var prop = obj[name]
if (typeof prop == 'object' && prop !== null) deepFreeze(prop)
})
return Object.freeze(obj)
}:::
4.标识符查找
4.3 垃圾回收
❑ 离开作用域的值会被自动标记为可回收,然后在垃圾回收期间被删除。
❑ 主流的垃圾回收算法是标记清理,即先给当前不使用的值加上标记,再回来回收它们的内存。
❑ 引用计数是另一种垃圾回收策略,需要记录值被引用了多少次。JavaScript 引擎不再使用这种算法,但某些旧版本的 IE 仍然会受这种算法的影响,原因是 JavaScript 会访问非原生 JavaScript 对象(如 DOM 元素)。
❑ 引用计数在代码中存在循环引用时会出现问题。
❑ 解除变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对象、全局对象的属性和循环引用都应该在不需要时解除引用。
4.3.1 标记清理
4.3.2 引用计数
循环引用时无法回收内存
4.3.3 性能
在 IE 中,window.CollectGarbage()方法会立即触发垃圾回收。在 Opera 7 及更高版本中,调用 window. opera.collect()也会启动垃圾回收程序。
4.3.4 内存管理
及时解除引用