函数作用域链和闭包详解

闭包和函数作用域链紧密相关,所以我打算把他们两个放在一起写

函数的作用域链

  • 我们先看下面例子
let value1 = 5
let value2 = 10

function compare(a,b){
// 需要查看compare的原型对象才能看到[[scopes]]属性
// 因为compare.prototype.constructor和foo指向同一个函数,所以点开constructor选项。
    console.log(compare.prototype)
    if (a>b){
        return 1
    }else {
        return 0
    }
}

let result = compare(value1,value2)
复制代码

image.png

  • 解析
  1. 定义compare()函数时,就会为其创建作用域链,预装载全局变量对象,保存在 [[Scope]] 中,也就是图中的Global和Script。 也就是说,即使不调用compare函数,上图中的红圈部分依旧存在(只是少了一个result)
  2. 调用compare函数:首先创造执行上下文(execution context),然后复制函数的 [[Scope]] 创建其作用域链,最后创建函数的活动对象(用作变量对象)并将其推入作用域链的前端。 (这部分过程在上图[[Scope]]中不可见)
  3. 函数执行完毕之后,活动对象就会被销毁,只留下定义函数时保存在 [[Scope]] 全局变量对象,也就是图中的Global和Script。

  • 作用域链实际上就是一个包含指针的列表,每个指针分别指向一个变量对象

image.png

  • 注释

内部属性 [[Scope]] 包含了一个函数被创建的作用域中对象的集合

如果你不了解 执行上下文、变量对象、活动对象的区别。请移步于此 简书: JS 执行环境(EC),变量对象(VO),活动对象(AO),作用域链(scope chain)


闭包的原理

接下来我们就可以谈 闭包 了

我们先来看《JS高级程序设计》给闭包的定义:引用了 另一个函数作用域中变量函数

  • 为了解释上面这个定义,一起来看下面出于《JS高级程序设计》的例子
function createCompareFun(propertyName){
        return function closure (object1,object2){
            console.log(closure.prototype)
            let name1 = object1[propertyName]
            let name2 = object2[propertyName]

            return name1>name2?1:0
        }
    }

    let compare = createCompareFun(name)
    let result = compare({name:'Nicholas'},{name: 'Matt'})
复制代码

打开控制台查看 closure.prototype

image.png

在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域链中。

  1. 在 createCompareFun 返回 closure 函数后,
  2. closure 函数先复制 createCompareFun 函数的活动对象和全局变量对象到自己身上,因此 createCompareFun 函数的活动对象就是图中的 Closure
  3. closure 函数创建自己的活动对象并将其推入作用域链的前端。(这部分过程在上图中的[[Scope]]不可见)

image.png

闭包的优缺点

  • 优点

说实话,个人觉得闭包的优点是有限的,不过你依旧可以在一些语法技巧上经常见到闭包的出现。

除了 “把变量存到独立的作用域,作为私有成员存在,避免变量污染全局。” 这个优点以外,我确实想不到其他优点,欢迎大佬在评论区补充。

  • 缺点

在上述的第二个例子中,当 createCompareFun 执行完毕以后,其活动对象并没有销毁且一直留在内存中,因为 closure 依旧保留着其引用,直到 closure 被销毁以后才得以销毁

我们可以对第二个例子进行优化

let compare = createCompareFun(name)
let result = compare({name:'Nicholas'},{name: 'Matt'})
compare = null
复制代码

解除对函数的引用后,垃圾回收机制会将其占用内存释放,作用域链也会被销毁(除了全局作用域以外)

闭包中的this

我们先来看两个例子

const name = "The Window"
const object = {
    name:'My Object',
    getNameFun: function (){
        return function (){
            return this.name
        }
    }
}
let a = object.getNameFun()()
console.log(a) // The Window
复制代码
const name = "The Window";
const object = {
    name: 'My Object',
    getNameFun: function () {
        let that = this  // 不同之处
        return function () {
            return that.name
        }
    }
};
let a = object.getNameFun()()
console.log(a) // My Object
复制代码

第一个例子的getNameFun的this指向的是window对象;第二个例子的getNameFun的this指向的是object实例对象

实际上,内部函数不能直接访问到外部函数的this。需要访问包含作用域中的this,则同样需要将其引用先保存到闭包能访问的另一个变量中。

闭包的应用

其实你经常可以在高阶函数中见到闭包的存在

  • 防抖函数
function debounce(fn,wait){
    let timer;
    return function (){
        let context = this
        let arg = [...arguments]

        if (timer){
            clearTimeout(timer)
        } 

        timer = setTimeout(()=>{
            fn.apply(context,arg)
        },wait)
    }
}
复制代码

  • 单例设计模式
class SocketService {
    ws = null // 和服务端连接的socket对象
    connected = false // 标识是否连接成功
    connect = () => {
    } // 定义连接服务器的方法
    send = () => {
    } // 定义发送数据的方法
}

function getInstance() {
    let instance = null;
    return function () {
        if (instance) {
            instance = new SocketService()
        }
        return instance
    }
}
var getSocket = getInstance()
const socket = getSocket()
复制代码

  • 使用闭包遍历取索引值(古老的问题)
for (var i = 0; i < 10; i++) {
    setTimeout(function(){console.log(i)},0) //10个10
}
 
for (var i = 0; i < 10; i++) {
    (function(j){
        setTimeout(function(){console.log(j)},0) // 0 - 9
    })(i)
}
复制代码

  • 用函数定义模块,我们将操作函数暴露给外部,而细节隐藏在模块内部(个人觉得用类实现比较好)
function module() {
    const arr = []

    function pushNum(val) {
        if (typeof val == 'number') {
            arr.push(val)
        }
    }

    function get(index) {
        return index < arr.length ? arr[index] : null
    }
    return {
        pushNumExport: pushNum,
        getExport: get
    }
}

let module1 = module()
module1.pushNumExport(15)
console.log(module1.getExport(0)) // 15
复制代码

  • 使用闭包作为特权方法访问私有变量

JS没有私有成员的概念,所有对象属性都公有的。但是有私有变量,任何定义在函数或块中的变量都可以认为是私有的,私有变量包括函数参数、局部变量、以及函数内部定义的其他函数。

特权方法——能够访问私有变量的公有方法。 把所有私有变量和私有函数都定义在构造函数中,再创建一个能够访问这些私有成员的特权方法,因为定义在构造函数中的特权方法其实是一个闭包。通过构造函数创建实例后,只能通过特权方法来访问了,否则没有办法直接访问私有变量和函数。

function MyObject(){
    //私有变量和私有函数
    let privateVariable = 10;
    function privateFunction(){
        return false;
    }
    
    //闭包为特权方法
    this.publicMethod = function(){
        privateVariable++;
        return privateFunction();
    }
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享