这是我参与「第四届青训营 」笔记创作活动的的第3天
在昨天讨论高阶函数的时候,发现很多地方都用了闭包,在使用的时候感觉对这块知识点有点不清楚,于是又整理了一下相关的知识点。
环境和作用域
想要谈及闭包,我们先要知道环境和作用域的概念。
什么是执行环境
执行环境是在javascript代码马上执行之前创建,是动态的,javascript中有三种执行环境。
第一种为:全局执行环境,这是最外围的执行环境,一旦代码被载入,引擎最先进入的就是这个环境。在浏览器中,全局环境就 是window对象,所以所有全局属性和函数都是作为window对象的属性和方法创建。全局执行环境直到应用程序退出时才会被销毁。每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入到一个环境栈中,执行完后,栈将其弹出,把控制权交给之前的执行环境。
let a = 100;
//比如,我们想这样直接定义一个变量,那么这个变量全局生效,直到程序结束,这个变量才被销毁。
第二种为:函数执行环境,当执行流执行一个函数时,JavaScript会创建一个新的函数执行环境,函数执行环境中的代码执行完后,该环境被销毁,保存在其中的所有变量和函数定义也随之被销毁。总结:函数执行环境在代码执行之前创建,在调用完成之后释放
function show(){
let name = 'student';
function showGender(){
let gender = 'nan';
}
console.log(name)
}
show();
在这个函数里面,gender的执行环境是showGender这个函数,在外面打印gender,会得到undefined的结果。同理,name这个变量可以在show这个函数中打印(包括showGender)。
值得注意的是:在多次调用show这个函数的时候,分配的变量的地址是不同的,在每次调用结束后,程序会自动的销毁这些函数内部的变量。
活动对象在最开始只有一个变量,就是 arguments 对象,这个对象在全局环境中是不存在,只有在函数中。
第三种为:Eval执行环境。
用eval执行的语句应该和普通语句没有区别,其作用域就是当前的作用域。
js堆栈内存释放
-
堆内存:存储引用类型值,对象类型就是键值对,函数就是代码字符串。
-
堆内存释放:将引用类型的空间地址变量赋值成
null,或没有变量占用堆内存了浏览器就会释放掉这个地址 -
栈内存:提供代码执行的环境和存储基本类型值。
-
栈内存释放:一般当函数执行完后函数的私有作用域就会被释放掉。
但栈内存的释放也有特殊情况:① 函数执行完,但是函数的私有作用域内有内容被栈外的变量还在使用的,栈内存就不能释放里面的基本值也就不会被释放。② 全局下的栈内存只有页面被关闭的时候才会被释放
函数的生命周期
为了更好的理解函数中变量的创建和销毁的过程,我们举几个例子来解释。
示例1:
let n = 0;
function add(){
console.log(++n);
}
add();//1
add();//2
add();//3
示例2:
function add(){
let n=1;
function sum(){
console.log(++n);
}
sum();
}
add();//2
add();//2
add();//2
示例3:
function add(){
let n=1;
return function sum(){
console.log(++n);
}
}
let a=add();
a();//2
a();//3
let b=add();
b();//2
b();//3
我们先来分析一下上面的三个例子
示例2每一次调用这个add这个函数,都重新分配内存,每一次的调用都是一个单独的操作,所以输出全为2.
示例3相当于给a赋值了一个名为sum的函数,而这个sum函数依赖于n这个变量,所以在掉用a这个函数时,原来的n并没有被销毁。至于函数b,则是重新找了一块内存,里面放着sum这个函数和n这个变量,相当于新的一次变量。
那么为什么举示例1这样的一个例子呢,其实,示例1中的add函数,也是依赖于上面那个全局变量,所以不断累加。或许有人想问,如果没有任何函数调用n这个变量,n是不是会销毁?其实不是,上面我们讲过,全局变量在环境结束的时候才会删除,至于为什么不会像示例3一样,我猜想是以为有全局函数和控制台也使用了n。
理解了上面的例子,我们在将难度升级一下。
示例4:
function add(){
let n=1;
return function sum(){
//console.log(++n);
let m=1;
function show(){
console.log(++m);
}
}
}
let a=add();
a();//2
a();//2
示例4中函数a相当于sum函数,sum里面没有调用sum外面需要的值,每一次调用a都分配了新的空间存m和show函数,所以和示例2相似,只不过示例2清除的是add,而事例4清除的是sum.
示例5:
function add(){
let n=1;
return function sum(){
//console.log(++n);
let m=1;
return function show(){
console.log(++m);
}
}
}
let a=add();
a();//2
a();//3
这里m就是show函数所需要的值,因此不会重新创建.
示例6:
function add(){
let n=1;
return function sum(){
console.log(++n);
let m=1;
return function show(){
console.log(++m);
}
}
}
let a=add();
a();//2,2
a();//3,3
通过上面的示例,这个例子显得格外简单。
构造函数的作用域
function add() {
let n = 1;
this.sum = function () {
console.log(++n);
}
}
let a = new add();
a.sum();//2
a.sum();//3
上面的构造函数的本质其实是:
function add() {
let n = 1;
sum = function () {
console.log(++n);
}
return {
sum:sum
}
}
let a = new add();
a.sum();//2
a.sum();//3
这样看,其实与上面的函数的生命周期是一样的道理。
块级作用域
let 和 const 有块级作用域,但是 var 没有。举几个例子。
示例2:
for(var i=0;i<3;i++){
console.log(i);
}
console.log(i);
//0 1 2 3
示例2:
for(let i=0;i<3;i++){
console.log(i);
}
console.log(i);
//0 1 2 undefine
示例3:
for(var i=0;i<3;i++){
setTimeout(()=>{
console.log(i);
},1000)
}
//3 3 3
示例4:
for(let i=0;i<3;i++){
setTimeout(()=>{
console.log(i);
},1000)
}
//0 1 2
闭包
什么是闭包
在 <<JavaScript高级程序>> 中说,闭包是指有权访问另外一个函数作用域中的变量的函数。
形成闭包的原因
内部的函数存在外部作用域的引用就会导致闭包。
var a = 0
function foo(){
var b =14
function fo(){
console.log(a, b)
}
fo()
}
foo()
这里的子函数
fo内存就存在外部作用域的引用a, b,所以这就会产生闭包
闭包使用的场景
下面这篇文章讲述了闭包的七种使用场景,我就不再赘述。 juejin.cn/post/693746…
这里举一个实际开的例子吧
let arr = [1,23,4,5,6,7,8,9,21,10];
function between(a,b){
return function(v){
return v>= a&& v<=b;
};
}
console.log(arr.filter(between(3,9)));
参考文章
第九章 这次把JS闭包给你讲得明明白白_哔哩哔哩_bilibili
结语
文章如果有不正确的地方,欢迎指正,共同学习,共同进步。
若有侵权,请联系作者删除。