侯策《前端开发核心知识进阶》读书笔记——Javascript中的Closure(一)

块级作用域和暂时性死区

变量提升现象:

function foo() {
    console.log(bar)
    var bar = 3
}
foo()
//undefined
function foo() {
    console.log(bar)
    let bar = 3
}
foo()
//Uncaught ReferenceError: bar is not defined

暂时性死区(TDZ——Temporal Dead Zone):

函数默认值受TDZ的影响

function foo(arg1 = arg2, arg2) {
    console.log(`${arg1} ${arg2}`)
}

foo(undefined, 'arg2')

// Uncaught ReferenceError: arg2 is not defined

执行上下文和调用栈

JavaScript 执行主要分为两个阶段:

  • 代码预编译阶段
  • 代码执行阶段

预编译阶段是前置阶段,这个时候由编译器将 JavaScript 代码编译成可执行的代码。 

执行阶段主要任务是执行代码,执行上下文在这个阶段全部创建完成。

预编译过程做的事情:

  • 预编译阶段进行变量声明;
  • 预编译阶段变量声明进行提升,但是值为 undefined;
  • 预编译阶段所有非表达式的函数声明进行提升。
function bar() {
    console.log('bar1')
}

var bar = function () {
    console.log('bar2')
}

bar()
//bar2

var bar = function () {
    console.log('bar2')
}

function bar() {
    console.log('bar1')
}

bar()
//bar2

思考题:

foo(10)
function foo (num) {
    console.log(foo)
    foo = num;       
    console.log(foo)
    var foo
} 
console.log(foo)
foo = 1
console.log(foo)

输出:

undefined
10
ƒ foo (num) {
    console.log(foo)
    foo = num     
    console.log(foo)
    var foo
}
1

作用域在预编译阶段确定,但是作用域链是在执行上下文的创建阶段完全生成的。因为函数在调用时,才会开始创建对应的执行上下文。执行上下文包括了:变量对象、作用域链以及 this 的指向。

我们在执行一个函数时,如果这个函数又调用了另外一个函数,而这个“另外一个函数”也调用了“另外一个函数”,便形成了一系列的调用栈。

调用关系:foo1 → foo2 → foo3 → foo4。这个过程是 foo1 先入栈,紧接着 foo1 调用 foo2foo2入栈,以此类推,foo3foo4,直到 foo4 执行完 —— foo4 先出栈,foo3 再出栈,接着是 foo2 出栈,最后是 foo1 出栈。这个过程“先进后出”(“后进先出”),因此称为调用栈。

注意:正常来讲,在函数执行完毕并出栈时,函数内局部变量在下一个垃圾回收节点会被回收,该函数对应的执行上下文将会被销毁,这也正是我们在外界无法访问函数内定义的变量的原因。也就是说,只有在函数执行时,相关函数可以访问该变量,该变量在预编译阶段进行创建,在执行阶段进行激活,在函数执行完毕后,相关上下文被销毁。

闭包

函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数在全局环境下可访问,就形成了闭包。

function numGenerator() {
    let num = 1
    num++
    return () => {
        console.log(num)
    } 
}

var getNum = numGenerator()
getNum()

内存管理

内存的生命周期:

  • 分配内存空间
  • 读写内存
  • 释放内存空间
var foo = 'bar' // 在堆内存中给变量分配空间
alert(foo)  // 使用内存
foo = null // 释放内存空间

js中基本数据类型和引用数据类型的存储方式可以参看之前的博客

基本数据类型和引用数据类型的区别

内存泄漏场景

只是把 id 为 element 的节点移除,但是变量 element 依然存在,该节点占有的内存无法被释放。

var element = document.getElementById("element")
element.mark = "marked"

// 移除 element 节点
function remove() {
    element.parentNode.removeChild(element)
}

需要在 remove 方法中添加:element = null,这样更为稳妥。

var element = document.getElementById('element')
element.innerHTML = '<button id="button">点击</button>'

var button = document.getElementById('button')
button.addEventListener('click', function() {
    // ...
})

element.innerHTML = ''

 element.innerHTML = '',button 元素已经从 DOM 中移除了,但是由于其事件处理句柄还在,所以依然无法被垃圾回收。我们还需要增加 removeEventListener,防止内存泄漏。

浏览器垃圾回收

大部分的场景浏览器都会依靠以下两种算法来进行垃圾回收:

  • 标记清除
  • 引用计数

具体实现方式可以参看之前的文章:JS垃圾回收机制 

闭包带来的内存泄漏

借助闭包来绑定数据变量,可以保护这些数据变量的内存块在闭包存活时,始终不被垃圾回收机制回收。因此,闭包使用不当,极可能引发内存泄漏,需要格外注意。

function foo() {
    let value = 123

    function bar() { alert(value) }

    return bar
}

let bar = foo()

可以看出,变量 value 将会保存在内存中,如果加上:

bar = null

随着 bar 不再被引用,value 也会被清除。

参考资料:

侯策 前端开发核心知识进阶

猜你喜欢

转载自www.cnblogs.com/fmyao/p/12787734.html