一、作用域链
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…