侯策《前端开发核心知识进阶》读书笔记——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 也会被清除。

闭包实现单例模式 

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决:一个全局使用的类频繁地创建与销毁。

何时使用:当您想控制实例数目,节省系统资源的时候。

如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

关键代码:构造函数是私有的。

function Person() {
    this.name = 'lucas'
}

const getSingleInstance = (function(){
    var singleInstance
    return function() {
         if (singleInstance) {
            return singleInstance
         } 
        return singleInstance = new Person()
    }
})()

const instance1 = getSingleInstance()
const instance2 = getSingleInstance()

我们有 instance1 === instance2。因为借助闭包变量 singleInstance,instance1 和 instance2 是同一引用的(singleInstance),这正是单例模式的体现。

参考资料:

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

原文地址:https://www.cnblogs.com/fmyao/p/12787734.html