-
闭包和作用域
- 变量声明
- 变量和函数的声明提升
- 作用域和作用域链
- 执行上下文
- 闭包
- 垃圾回收机制
闭包和作用域
变量声明
var 声明特点
- 在使用
var
声明变量时,变量会被自动添加到最接近的上下文 -
var存在声明提升。
var
声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。 -
可多次重复声明。而重复的
var
声明则会被忽略
let 声明特点
-
let
声明存在块级作用域 -
let
声明(创建过程)存在提升。但由于暂时性死区(temporal dead zone),无法在let
声明之前去使用变量 -
在同一作用域内无法重复声明。重复的
let
声明会抛出SyntaxError
错误
const 声明特点
-
const
声明存在块级作用域 -
const
一旦声明后在其生命周期内都无法重新赋予新值 - 其余与
let
声明一致
变量和函数的声明提升
变量声明与函数声明都存在提升。可以记住以下几个点:
- 变量声明中由
var
定义的变量会提升到其所在作用域的顶部。 - 变量声明中
let
和const
提升效果一致,即由其定义的变量都会在创建过程被提升,但在初始化阶段被暂时性死区所扼杀。 - 函数声明优先于变量声明。而函数表达式则会作为一个变量提升,其提升效果取决于用
let
还是var
定义。
变量和函数的具体声明情况如下:
-
let
的「创建」过程被提升了,但是初始化没有提升。 -
var
的「创建」和「初始化」都被提升了。 -
function
的「创建」「初始化」和「赋值」都被提升了。
来看这样三段代码:
第一段:var 变量声明效果
// 第一段
console.log(a) // 输出:undefined
var a = 10
上面代码运行后的实际情况如下:
var a // y 变量声明提升到其所在作用域的顶部
console.log(a)
a = 10
第二段:let
变量声明效果(const
与其一致)
// 第二段
{
console.log(x) // 产生暂时性死区,无法访问变量。
// 报错内容:Uncaught ReferenceError: Cannot access 'x' before initialization
// 在值初始化之前无法访问 x ,即变量在初始化阶段被暂时性死区所扼杀
let x = 10
}
第三段:函数声明与函数表达式声明效果
// 第三段:
var foo = function () {
console.log('我是函数表达式')
}
function foo() {
console.log('我是函数声明')
}
foo()
// 按照我们常规思维去思考一下,也许会输出'我是函数声明'。
// 但去执行一下,输出:'我是函数表达式'
上面代码运行后的实际情况如下:
function foo() { // foo 作为函数声明被提升了
console.log('我是函数声明')
}
var foo // foo 作为 var 变量声明被提升了
foo = function () {
console.log('我是函数表达式')
}
foo()
// 其中函数声明优先于变量声明,这也就解释了为什么不会输出'我是函数声明'。
作用域和作用域链
作用域
作用域分类:
- 全局作用域
- 函数作用域(
function
函数体内 ) - 块级作用域(
let
和const
声明存在块级作用域)
// 全局作用域
function foo() {
// 函数作用域
}
{
let c = 30
// 块级作用域
}
词法作用域:JavaScript会利用词法分析器分析我们书写的代码,从而依据变量和函数的命名位置来动态生成不同的作用域。即我们在定义变量或函数的时候,就已经决定了它们之间在不同作用域上的关系。
作用域链
作用域链由执行上下文中的变量对象逐级构成。
学习要点:
- 作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。
- 每个环境都可以逐级向上搜索作用域链查询变量和函数名;但任何环境都不能通过向下搜索作用域链。
- 自由变量:未在当前作用域定义的变量。自由变量会按照作用域链的查找机制,逐级向上查找与之对应的变量
执行上下文
执行上下文保存着变量对象,作用域链和this
。
学习要点:
- 所有通过var定义的全局变量会函数都会成为
window
对象的属性和方法。但使用let
和const
的声明的全局变量和函数不会定义在全局上下文中。 - 每个函数被调用时都会产生一个执行上下文。这个执行上下文会被推入栈。在函数执行完毕后,该执行上下文会在栈弹出,将控制权返还给之前的执行上下文。
- 当前作用域链中的第一个变量对象来自上一个上下文,下一个变量对象来自再上一个上下文。以此类推直至全局上下文。
执行上下文分类:
- 全局上下文 (
window
对象) - 函数上下文
-
eval()
上下文
来看这样一段代码:
let a = 10
function sum() {
let b = 20
function add() {
let c = 30
console.log(a + b + c)
}
add()
}
sum()
执行上下文内容如下:
全局执行上下文:
[ 作用域链:[], 变量对象:[ a, sum ], this: window ]
sum 函数执行上下文:
[ 作用域链:[ 全局变量对象:[ a, sum ] ], 变量对象:[ b, add ] , this: window ]
add 函数执行上下文:
[ 作用域链:[ add函数的变量对象: [ b, add ], 全局变量对象:[ a, sum ] ], 变量对象:[c] , this: window ]
入栈过程
-
首先调用
sum
函数将其推入栈,产生了第一个函数执行上下文。 -
紧接着
sum
函数内部又调用add
函数,于是又将其函数推入栈,产生了第二个函数执行上下文。
出栈过程
-
add
函数执行完毕后将其弹出栈,控制权交给sum
函数。 -
sum
函数执行完毕后将其弹出栈,控制权交给全局上下文。 -
当浏览器关闭后,全局上下文会出栈。
闭包
闭包定义:在一个嵌套函数里,内部函数可以访问外部函数的变量。
闭包应用:封装对象的私有属性和方法。即对数据作隐藏和封装,防止污染全局变量
闭包作用:多个闭包可以共享相同的函数定义,但却保存了不同的词法环境。
来看这样三段代码:
// 前置知识:setTimeout在事件循环机制中作为宏任务,for循环属于微任务。
// 宏任务会在微任务之后执行,即我们的for循环会先一步于setTimeout结束。
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i)
}, 3000)
} // 输出结果:输出10次 10 !
// 每循环一次,都共享了相同的词法环境(全局作用域)。
我们给setTimeout套一个立即执行函数,如下:
for (var i = 0; i < 10; i++) {
(function (i) { // 我们的闭包函数,相对于全局环境
setTimeout(function () {
console.log(i) // 内部函数访问了外部函数的变量
}, 3000)
})(i)
} // 输出结果:3秒后输出 0 1 2 3 4 5 6 8 9
// 每循环一次,立即执行函数就创建了不同的词法环境(块级作用域)。
我们换另一种形式去验证一下:
for (var i = 0; i < 10; i++) {
let a = function (i) { // 我们的闭包函数,相对于全局环境
setTimeout(function () {
console.log(i) // 内部函数访问了外部函数的变量
}, 3000)
}
a(i)
} // 输出结果:同样3秒后输出 0 1 2 3 4 5 6 8 9
特别注意:不能滥用闭包,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
垃圾回收机制
作用:垃圾回收程序会跟踪记录需要使用的变量和不需使用的变量,自动进行内存管理实现内存分配和闲置资源回收。
内存的生命周期:
- 分配你所需要的内存
- 使用分配到的内存(读、写)
- 不需要时将其释放
在浏览器的发展史上,主要有两种标记策略:引用计数和标记清理。
引用计数
基本原理:当首次声明变量并赋一个引用类型值时,会将这个值的引用次数设定为1。当这个值被赋给其他变量时,这个值的引用次数会再加1。当这个值被其他值所覆盖时,引用次数会减1。直到引用次数为0时,垃圾回收机制则会“上门回收”这个值
来看这样一段代码:
let a = { name: '小红' } // 首次值赋变量,引用计数为 1
let b = a // 值赋变量,引用计数 +1 为 2
let c = a // 值赋变量,引用计数 +1 为 3
c = null // 值被覆盖,引用计数 -1 为 2
b = null // 值被覆盖,引用计数 -1 为 1
a = null // 值被覆盖,引用计数 -1 为 0 被垃圾回收机制回收
循环引用(引用计数的缺陷问题)
来看这样一段代码
function foo() {
let a = { name: '小红' }
let b = { name: '小明' }
a.name = b // b赋值给a对象中的name,b的引用次数为2
b.name = a // a赋值给b对象中的name,a的引用次数为2
} // 说明:对象属性值作为变量被赋值
foo()
过程解析:函数的变量对象在函数调用完成之后会将每个变量值设为null
,以便垃圾回收机制进行回收。但在引用计数算法的策略中,函数在调用后,循环引用的变量a
和b
依然保留了一次引用次数。也就是说,这两个引用类型的引用次数为1,不会进行回收。
标记清除
基本原理:标志清除算法把“对象不再需要”简化定义为“对象是否可以获得”。垃圾回收器将定期从根(全局对象)开始,找所有从根开始引用的对象,然后找这些对象引用的对象......直到最终垃圾回收器将找到所有可以获取的对象和收集所有不能获取的对象,其中不能获取的对象则会被回收。
参考
MDN-闭包
我用了两个月的时间才理解 let
JavaScript高级程序设计(第4版)