作用域链和闭包(clousre)拆解(3)

0 阅读6分钟

一、作用域链

1.1 代码分析:

看看输出啥。

function cat() {
  console.log(myName);
}
function dog() {
  var myName = "王中王";
  function dogking() {
    console.log(myName);
  }
  dogking();
  cat();
}
var myName = "旺中旺";
dog();

1.2 调用栈分析

在这里插入图片描述

全局执行上下文和dog函数执行上下文中都包含myname 变量,dogKing和cat函数中的myname变量会取哪个呢? 其实两个函数输出结果并不相同。 dogKing函数输出为“王中王”,cat函数输出为“旺中旺”。 为什么呢?

1.3 作用域链

首先,变量的查找是根据作用域链的规则来的。

那么作用域链是什么呢?作用域链是js引擎用来解析变量的机制。查找变量时,js引擎会先在当前作用域查找,如果没找到,继续向外层查找,直至全局作用域。这个从内向外的查找链条就是作用域链。

那按照这个概念理解,dogKing函数的输出是按照作用域链查找的,cat函数则不是。因为dog和dogKing函数组成了一个闭包,闭包比较特殊,dogKing的外级作用域就是dog函数。

js调用栈中,每个执行上下文中都包含全局执行上下文的引用,我们把这个引用称为outer。 在这里插入图片描述 cat和dog函数查找变量时,首先在当前的执行上下文中查找,没有找到,会继续查询outer指向的全局执行上下文中进行查找。

那为什么cat的外部引用时全局执行上下文,而不是dog函数执行上下文呢?这是因为在执行过程中,作用域链是根据词法作用域决定的。

1.4 词法作用域

词法作用域是js中作用域的静态结构。在代码编写时确定,与代码执行无关。是由函数的嵌套结构确定,与函数调用无关。 因此在函数定义时,根据词法作用域,dog和cat函数的上级作用域都是全局作用域。

1.5 练习

块级作用域变量查找同理。

function cat() {
  let age1 = 20
  console.log(age);
}
function dog() {
  var myName = "王中王";
  function dogking() {
    console.log(myName);
  }
  let age = 18
  dogking();
  cat();
}
var myName = "旺中旺";
let age = 10
dog();

试着根据调用栈分析下cat函数中输出的值。

在这里插入图片描述 这是这段程序的调用栈。var声明的变量和函数声明在变量环境中,let和const声明在词法环境中。 在这里插入图片描述 变量查找时, ①查找当前作用域的词法环境,从栈顶到栈底 ②查找当前作用域的变量环境 ③查找全局作用域的词法环境,从栈顶到栈底,找到age=10 ④找到,结束。

二、闭包

2.1 上代码

function func() {
    var myName = "李三岁"
    let num1 = 1
    const num2 = 2
    var innerFunc = {
        getName:function(){
            console.log(num1)
            return myName
        },
        setName:function(newName){
            myName = newName
        }
    }
    return innerFunc
}
var tem = func()
tem.setName("李逍遥")
console.log(tem.getName())

首先,我们看当执行到func函数结尾时的调用栈情况: 在这里插入图片描述

上述代码中,innerFunc对象中包含getName和setName两个方法,定义在func函数内部。根据词法作用域,即声明时,getName和setName方法可以顺着作用域链访问到func函数中的变量。因此可以引用myName和num1两个变量。 接着innerFunc返回给tem变量后,虽然func函数执行完毕,但是此时依然引用func函数中的两个变量,因此并不会被回收。此时的调用栈情况为:

在这里插入图片描述 func函数执行完成以后,其执行上下文从栈顶弹出,但myName和num1变量还没getName和setName方法使用,因此还保存在内存中。无论在哪里调用这两个方法,都可以访问这两个变量,其他任何方法都访问不到这两个变量。因此这两个方法和变量就组成了闭包。

2.2 定义

mdn中闭包的定义为:闭包是由捆绑起来的(封闭的)函数和函数周围状态(词法环境)的引用组合而成。 因此,闭包可以让内部函数访问其外部作用域,即使外部函数执行结束,内部函数引用的外部函数的变量依然保存在内存中,内部函数及其引用的变量组成闭包。其实宽泛理解,在js中所有的函数都是一个闭包。

2.3 变量查找

闭包中的函数执行后,变量查找时,js引擎会沿着:setName函数执行上下文>func闭包>全局执行上下文的顺序查找。 在这里插入图片描述 在浏览器中打断点后,我们看开发者工具的信息: 在这里插入图片描述 当给myName赋值时,Scope项体现了作用链为:Local>Closure>Block>Global。因为我在vue3的项目中运行,如果在单独的js文件中运行,Block中的变量会在Global中,没有Block这一环。Local就是当前setName方法的作用域。Closure(func)就是func函数的闭包。Global为全局作用域。

2.4 闭包回收

  • 如果引用闭包的变量是个局部变量,等该作用域销毁后,下次gc执行垃圾回收时,进行是否还在使用的判断和内存回收。
  • 如果引用闭包的变量是个全局变量,那么该闭包会一直存在知道页面关闭。若以后闭包不再使用,会造成内存泄漏。(总的内存大小不变,可用的内存大小变小了)这也是闭包的一大缺点。

2.5 闭包的用途

2.5.1 数据封装和私有化

上代码

function createPerson(age) {
  const privateAge = age;   // 私有变量
  
  return {
    getAge: function() {
      return privateAge;
    },
    setAge: function(newAge) {
      if (typeof newAge === 'number' && newAge > 0) {
        privateAge = newAge;
      }
    }
  };
}

const person = createPerson(30);
console.log(person.getAge());  // 输出: 30
person.setAge(35);
console.log(person.getAge());  // 输出: 35

上述代码中,person变量可以通过闭包访问privateAge变量,但外部代码不能访问。 同时也可以作为缓存,保存在内存中。

因此闭包可以用来创建私有变量和方法,防止外部直接访问和修改。

2.5.2 防抖和节流

防抖:

function shake(){
	let timer = null
	
	function func(){
		if(timer != null) clearTimeout(timer)
		
		timer = setTimeout(()=>{
			// todo want to do
		},200)
	}
}

节流

function throttle(){
	let timer = null
	
	function func(){
		if(timer != null) return
		
		timer = setTimeout(()=>{
			// todo want to do
			timer = null
		},200)
	}
}

防抖和节流都是通过闭包使变量存在于内存中,借助变量实现想要的功能。

三、做个题

var obj = {
    myName:"time",
    printName: function () {
        console.log(myName)
    }    
}
function func() {
    let myName = "李三岁"
    return obj.printName
}
let myName = "刘大哥"
let _printName = func()
_printName()
obj.printName()

分析一下,当执行到func()未return时的调用栈: 在这里插入图片描述 执行完成后弹出栈顶: 在这里插入图片描述 此时_printName被赋值,执行时: 在这里插入图片描述 查找myName变量:_printName函数执行上下文词法环境>_printName函数执行上下文变量环境>全局词法环境,找到,输出“刘大哥”。 执行至obj.printName()时,情况相同,obj.printName函数执行上下文中的词法环境和变量环境中均为空,所以查找到全局执行上下文中。 因此printName函数的myName变量是属于全局作用域下的,此作用域链由词法作用域决定。

总结:此段程序中并没有生成闭包。obj不是一个函数,其中的myName和printName是他的两个属性,彼此并没有联系。若想产生联系,需要加上this关键字。否则printName会通过词法作用域链查找myName

文章参考:time.geekbang.org/column/intr… zhuanlan.zhihu.com/p/683323392 juejin.cn/post/737617…