作用域和闭包
环境和作用域
环境存在的价值是被需要,环境有其服务的范围,这个范围就是作用域
环境存在的前提是有人需要他,当不需要的时候,这个环境一定会被回收,被破坏掉,每个环境有其的作用域
在js
中,环境当中会有很多资源,有变量,有函数等等
let name = "jlc"; // 创建了一个全局的环境,在全局中定义的所有数据,都会被保留起来的
全局的环境是不会被回收的(因为全局环境会被很多地方依赖),除非将标签关闭和浏览器关闭,是人为的回收,否则全局环境是不会被回收的
全局的作用域不是在当前的平面中有效,在函数中也是有效的(波及范围更广)
函数的环境和作用域
当函数只是定义声明时,如果没有被调用,就不会开辟出内存空间(只是一个计划)
如果函数调用多次,函数会重新创建多个内存地址,每一次都是全新的内存地址
函数执行(调用)之后,会创建一个新的环境(给他一个新的内存地址,来存放函数定义的数据,这个数据默认情况下只在函数体内可以使用(后续有方法可以让它在函数体外面使用)),当函数执行完之后,在函数体内定义的变量就会被清理掉,后续再调用,会重新给你一个函数环境
function hd(){
let n = 1;
function sum(){
console.log(++n);
};
sum();
}
// 如果函数调用多次,函数会重新创建多个内存地址,每一次都是全新的内存地址
hd(); // 2
hd(); // 2
如何将函数中的数据给外界一直使用?可以将函数中的内容(只能是引用类型,如函数)进行return
返回出去,在用一个变量进行接收,数据只要有一个被使用(return
出去的数据进行赋值表示被用),其函数环境就不会被清除
function hd(){
let n = 1;
return function sum(){
console.log(++n);
};
}
// 函数执行一次后,就会创建一个新的空间
let a = hd(); // 将return出来的内容赋值给a(只能返回引用类型),但是其函数环境不会被清除
a(); // 2
a(); // 3
let b = hd();
b(); // 2
function hd(){
let n = 1;
return function sum(){
let m = 1;
function show(){
console.log(++m);
}
show()
};
}
let a = hd();
// 只是引用了函数中的sum(),但是sum()中的内存空间会被重新创建的,sum()里面的空间没有被外部引用
a(); // 2
a(); // 2
function hd(){
let n = 1;
return function sum(){
let m = 1;
return function show(){
console.log(++m);
};
};
}
let a = hd()();
a(); // 2
a(); // 3
构造函数中作用域的使用形态
在构造函数时环境的体现形式
function hd(){
let n = 1;
this.sum = function(){
console.log(++n);
};
}
// 上述函数可以理解为:
function hd(){
let n = 1;
function sum(){
console.log(++n);
};
return {
sum: sum
};
}
let a = new hd(); // 实例化出一个对象
a.sum(); // 2 // 函数被使用了,同作用域下的数据就被保留
a.sum(); // 3
块作用域
在js
中新增了块级作用域,使用{}
进行包裹,在全局中开辟一个块级的作用域
{
let a = 1; // const也有块级作用域
// 在同个块级作用域可以使用a
}
// 在外界不能使用a
可以在不同的块作用域中用相同的名称命名,他们是不影响的,他们两个是不同的数据
注意:var
声明变量是没有块级作用域的
{
var a = 1; // var没有块级作用域的概念,在{}中声明直接会放到全局中
// 在同个块级作用域可以使用a
}
// 在外界也能使用a
let
,const
和var
在for
循环中的差异:
for
循环是有块级作用域的特性的
for(var i = 1; i <= 3; i++){
console.log(i);
}
console.log(i);
// 结果显示
1
2
3
4 // 最后一次i打印4
for(let i = 1; i <= 3; i++){
console.log(i);
}
console.log(i); // 访问不到,报错
// 结果显示
1
2
3
在绝大多数的情况下,使用for
循环是不会进行改变全局的,所以一般都是使用let
的情况
var
虽然没有块级作用域,但是它是有函数作用域的
let arr = [];
for(let i = 1; i <= 3; i++){
// console.log(i); // 执行完之后,i是不进行保留的
arr.push(function(){ // 将函数都放到了数组当中,那么该环境的数据都要进行保留
return i;
})
}
console.log(arr.length); // 3
console.log(arr[0]); // 返回的是一个函数
console.log(arr[0]()); // 执行函数,返回的是1
闭包
一个函数可以访问到其他函数作用域当中的数据,这个现象就是闭包,如子函数可以访问父级(上级)作用域的数据,js
中存在闭包的特性
let arr = [1, 6, 11, 22, 8, 7];
function between(a, b){
return function(v){ // 子函数可以访问父级的变量,会一级一级的往上找
return v >= a && v <= b;
};
}
console.log(arr.filter(between(3, 9)));
使用闭包的特性完成小动画,点击按钮使按钮水平向右移动:
<body>
<style>
button {
position: absolute; //设置按钮绝对定位
}
</style>
<button>name</button>
<button>age</button>
</body>
<script>
let btns = document.querySelectorAll("button"); // 找到所有的按钮
btns.forEach(function(item){
item.addEventListener("click", function(){ // 给元素加上点击事件
let left = 1;
setInterval(function(){ // 涉及到闭包,向父级找item
item.style.left = left++ + "px";
},100);
});
});
</script>
上述情况在时间设置为100ms时,点击按钮后,再次点击该按钮,会出现抖动问题
因为执行该函数时,会产生一个环境,由于还有一个定时器,在该环境的内部还会产生一个函数环境,当我们再点击一次按钮时,会在产生一个新的环境,新环境中包括定时器函数环境,每次点击的时候left都变成了1,所以又从1这个位置向右偏移,这就形成了抖动
为了解决这种情况,可以将left变量放到外层函数的作用域中
<script>
let btns = document.querySelectorAll("button"); // 找到所有的按钮
btns.forEach(function(item){
let left = 1; // 放到外层的作用域
item.addEventListener("click", function(){ // 给元素加上点击事件
setInterval(function(){ // 涉及到闭包,向父级找item
item.style.left = left++ + "px";
},100);
});
});
</script>
上述改进可以使按钮动画不进行抖动了,但是会使按钮加速运动(之前抖动时该情况也存在)
因为当我们点击一次后,会产生一个新的环境,每个环境中包含了定时器的环境,点击十次后,定时器也会执行十次,这样原先的100ms执行一次,变成了10ms执行一次
<script>
let btns = document.querySelectorAll("button"); // 找到所有的按钮
btns.forEach(function(item){
let left = 1;
let interval = false;
item.addEventListener("click", function(){ // 给元素加上点击事件
if(!interval){ // 只让定时器执行一次
interval = true;
setInterval(function(){ // 涉及到闭包,向父级找item
item.style.left = left++ + "px";
},100);
}
});
});
</script>
闭包的内存泄露
闭包可以给我们带来很多的便利,但是也会带来相应的问题,如内容泄露
点击<div>
,弹出其属性值:
<body>
<div desc="jlc">name</div>
<div desc="24">age</div>
</body>
<script>
let divs = document.querySelectorAll("div");
divs.forEach(function(item){
item.addEventListener("click", function(){
console.log(item.getAttribute("desc")); // item表示所有的div对象
});
});
</script>
我们的目的仅仅是想获取div中的属性desc
值,上述代码为了这个功能要将所有的div
对象都保存在内存当中,会导致大量内存的浪费,可以通过以下的方式进行改进:
let divs = document.querySelectorAll("div");
divs.forEach(function(item){
let desc = item.getAttribute("desc");
item.addEventListener("click", function(){
console.log(decs); // 属性值能正常打印
console.log(item); // 对象的元素值就打印不了了,优化了内存
});
item = null;
});
this
在闭包中的历史遗留问题
let hd = {
name: "jlc",
get: function(){
// return this.name; // 指向函数的对象hd
return function(){ // 将结果用一个函数返回出去
return this.user; // 这个时候this指向的是window
// 按理说,这是一个闭包,this应该可以访问父级的作用域,但是this特殊
};
}
};
// this是指当前调用函数的对象
let a = hd.get(); // 在全局中调用
console.log(a()); // 显示undefined
解决这个问题,我们可以通过两种方法:
// 1.this赋值
let hd = {
name: "jlc",
get: function(){
let This = this;
return function(){
return This.user;
};
}
};
// 2.使用箭头函数
let hd = {
name: "jlc",
get: function(){
return ()=>{
return this.user;
};
}
};
let a = hd.get();
console.log(a()); // jlc