第九章 作用域与闭包
面经
作用域
对象无块级作用域 函数有块级作用域
闭包
闭包的作用
自执行函数是闭包吗
概念:闭包又称为词法闭包 (lexical closure ), 一个函数和对其周围词法环境(Lexical Environment)的引用捆绑在一起,这样的组合就是闭包
在V8的词法解析时,闭包就已经被确定。闭包只跟定义有关,跟调用无关,与this的绑定规则相反
核心功能:内层函数中访问到外层函数的作用域及其变量
核心机制:作用域链,是作用域链的一种功能性实现,常用的就是块级作用域,函数作用域;全局作用域一般不直接使用
作用
基于原有功能丰富params,拓展额外功能
实现类似于private的功能,对外隐藏引用
变量传递 && 状态保持
在js中
每当创建一个函数,闭包就会在函数创建是同时被创建出来
其是一个对象,存储两个索引,用于引用,相当于指针
- 函数
- 关联的环境
在js中
广义上来讲,函数都是闭包
狭义上来讲(更严格的角度上来讲),函数访问了外部变量(有访问),它是一个闭包
*自执行函数本身不是闭包,内部封装的函数是
为什么闭包会导致内存泄露?
- 闭包本身比普通函数更加消耗内存。 闭包拥有其作用域链上的上级的活动对象;包含其函数作用域
- 执行完毕后不会自动消除——垃圾回收机制 && 标记清除 && 引用计数。
this
只跟调用位置有关
类别
默认绑定什么都不加,默认从上下文中找——global && Arrow Function
new绑定
对象绑定(隐式
call、bind、apply(显
优先级
new > 显式 > 隐式 > 默认
显式绑定中: bind && apply > call
基础
环境存在的价值是被需要 ,并且不同环境有其作用范围
关键:
1 是否被使用
2 作用的范围
特性:
1 js的全局的环境不会被回收。是真正的全局,渗透到各个作用域
2 除全局变量外,作用域与作用域不靠接口的话不相关联
js
<script></script> / 全局环境 1级
function(){} 函数环境 2级
块作用域 2级
function在每次调用的时候都会重新配置一回环境,相当于玩游戏重新开了一局,局与局之间无关。
如:
function show(){console.log('a')};
show();show();show();show();
执行多次会开辟相应次数的内存地址:
执行一次之后,继续执行第二次,第二次开辟新的内存地址并访问第二次的内存地址,所以第一次的内存永远不会被再次访问,触发垃圾回收。
★生命周期
总结:
如果环境定义的数据被使用,不删。
js
内存未被接收的情况:
1:定义完成函数,不开辟内存(因为没有调用),处于计划阶段。
function plan(){
let n = 1;
function sum(){
console.log(++n);
}
}
2:调用函数,动工,开辟出内存空间
plan() 开辟出来的空间包括了n和sum()函数,但是sum()的环境空间没有被创建,因为sum()没有被调用
js
内存被接收(延长生命周期):
1 定义函数,并设置接收
function plan(){
let n = 1;
return function sum (){
console.log(++n);
}
}
let variable = plan();
2 相同变量接收函数
variable();
variable();
variable();
计算机中只要是被用到的程序都不会被清空。
在此基础上,因为已经定义了变量variable接收plan()函数,所以
variable指向唯一的plan()。
此时调用variable(),改变唯一plan();
再次调用的variable()则是在上次调用的基础上再次调用plan()(因为指向的环境是唯一的)
如果函数体为:
function plan(){
return function sum (){
let n = 1;
function show(){
console.log(++n);
}
}
}
那么情况就会不一样,无论同此调用了多少回,因为show()函数没有返回,所以会一直再重复创建sum()函数(而没有++n的返回值)。
修改的话,将show()返回即可。
let variable = plan();
3 不同变量接收函数
let variable2 = plan();
let variable3 = plan();
let variable4 = plan();
此时就是不同变量接收,那么就是开辟新的内存地址
*构造函数与此类似
块作用域
js
块级作用域只适用于 let 和 const
var没有块级作用域
{
let a = 1;
}
{
let a = 1;
}
js
var let const 在for循环的差别
for(var i = 1;i <= 3; i++){
console.log(i);
}
console.log(i);
//最后的结果是i = 4。
而var 没有块作用域这个性质,所以i就定义到了全局里。
而let 定义就只限于for循环体内部。
function time_between() {
for (var i = 1; i <= 5; i++) {
setInterval(() => {
console.log(i)
}, 1000)
}
}
time_between();
var定义的i会显示到6
而let定义的i会显示到5
一般来说用let。
var伪块作用域
js
var虽然没有块作用域,但是有函数作用域。
如果想让通过var声明的变量在块里使用,需要进行转换。
嵌套区域
js
let arr = [];
for(let i = 1;i <= 4;i++){
arr.push(function(){
return i;
});
}
结合前边的生命周期,此时每次循环都会想arr里传递一个函数,所以匿名函数得以被保留。
因为实际上是一个函数,所以这种写法:
console.log(arr[0])返回的是函数体
而想得到值,需要:
console.log(arr[0]())
但是如果使用var来定义i的时候,var因为没有块作用域,所以最后无论指向arr的哪个索引,返回结果都是最后的值。
解决这个问题需要看透原因:
因为返回的是i的值,没有i本身,所以会依次向上查找。所以解决这个问题,只需要在push函数体的上方套上一层函数。且这个函数需要传递i的参数。所以要嵌套一个传参为i的立即执行函数且包裹一个function
let arr = [];
for(var i = 1;i <= 4;i++){
(function(i){
return arr.push(function(){
return i;
});
})(i)
}
★闭包
子函数可以访问到其它函数作用域的数据。
js
通过闭包优化filter函数,获取价格区间
let arr = [1,23,43,54,534,12,43,32,55,34]
function select(a, b) {
return function (v) {
return v >= a && v <= b;
}
}
console.log(arr.filter(select(1,100)))
js
闭包+sort 自定义排序模块
let tb = [{ name: "car", price: 800 }, { name: "food", price: 20 }, { name: "gun", price: 300 }, { name: "computer", price: 1000 }, { name: "paper", price: 3 }];
function st(filed,type="desc") {
return function(a, b){
if(type == "desc"){
return a[filed] > b[filed] ? 1 : -1;
}else{
return a[filed] > b[filed] ? -1 : 1;
}
}
}
let res = tb.sort(st("price",""))
console.table(res)
通过闭包访问上级作用域的数据。
js
内存泄漏-闭包的问题:
释放内存: 将元素设置为null
解决:设置null 既可以对元素进行使用,又释放内存
let divs = document.querySelectorAll("div");
divs.forEach(function(element){
let item = element.getAttribute("desc");
element.addEventListener("click",function(){
console.log(item);
console.log(element)
})
element = null;
})
js
闭包在this里的使用
函数闭包中包含的this是特殊指针。
普通函数this指向window
箭头函数this按照闭包特性访问:即父级this。
窗口抖动
我们大多时候只关注了闭包,而缺少了对环境的重复考虑。
js
以下代码会产生抖动:
let buttons = document.querySelectorAll("button");
buttons.forEach(function(item){
item.addEventListener('click',function(){
let left = 1;
setInterval(function(){
item.style.left = left++ + "px";
}, 1);
})
})
解决方案:
原先基础视频课程学到的是设置结束计时器来达到这个效果。
而通过环境分析,实际上每次点击都会创造出来一个环境,核心是
left=1 这条语句会在不同环境被引用。
所以只需要让let left = 1 放在计时器之外。
但是会出现新的问题:多个setinterval共同作用,速度不断变快。
加上条件判断:
let buttons = document.querySelectorAll("button");
buttons.forEach(function (item) {
let left = 1;
let status = false;
item.addEventListener('click', function () {
if (!status) {
status = true;
setInterval(function () {
item.style.left = left++ + "px";
}, 1);
}
})
})