对闭包的理解
闭包是函数运行时产生的机制,函数执行会形成一个全新的私有上下文,可以保护里面的私有变量和外界互不干扰(保护机制),但是大家认为的闭包需要当前上下文>不能被出栈释放,这样私有变量及它的值也不会被释放掉(保存机制)。
从性能角度讲,项目中应该减少对闭包的使用(因为闭包会产生不释放的栈内存,过多使用容易导致内存溢出或者降低性能)
问:闭包好不好?
答:大量应用闭包肯定会导致内存的消耗,但是闭包保护和保存作用,在真实开发中还是需要的,所以要合理使用。
问:闭包的作用是什么?
- 保护(私有变量和外界没有必然联系)
- 保存(形成不销毁的栈内存,里面的私有变量等信息保存下来了)
闭包的应用:
- 实战用途
- 高阶编程:柯理化 / 惰性函数 / compose函数
- 源码分析:JQ / LODASH / REACT(REDUX / 高阶组件 / HOOKS)
- 自己封装的插件组件
1、JQuery应用 -> 为了防止全局变量污染,JQ中的方法和变量需要用闭包保护起来。(导入jq后,它里面有大量的方法,如果不把这些方法保护起来,用户编写的方法很容易和jq方法名相同产生冲突,即全局变量污染)
/* ***** JQuery源码 ***** */
(function(global,factory){
//area1
...
//typeof window !== 'undefined'?window:this -> 验证当前所处环境的全局对象是window还是global等
//factory -> function(window,noGlobal){}
factory(global); // -> 执行area2-function
})(typeof window !== 'undefined'?window:this,function(window,noGlobal){
//area2
...
var jQuery = function(){
...
}
//通过给全局对象增加属性,把私有jquery方法暴露到全局作用域下供外面使用(<=>等价于return jQuery)
//外面需要使用函数中的私有内容,可以基于window.xxx和return xxx两种方式实现。
window.jQuery = window.$ = jQuery;
});
/* ***** 使用 ***** */
jQuery(); //相当于window.jQuery()
或
$();
真实项目中,我们一般把自己写的内容放到一个闭包中,这样可以有效防止自己的代码和别人代码产生冲突(全局变量污染:真实项目中是尽可能减少对全局变量的使用的);如果需要把自己的东西给别人用,基于return和window.xxx等方式暴露给别人即可。
2、基于let/const/class等创建变量,会把所在大括号(除对象的大括号之外)当作一个全新的私有块级作用域;
- 函数执行会产生私有的栈内存(作用域/执行上下文)
- let等也会产生私有的栈内存(var不会)
闭包练习题:
var ary = [1,2,3,4];
function fn(ary){
ary[0] = 0;
ary = [0];
ary[0] = 100;
return ary;
}
var res = fn(ary);
console.log(ary); //[0,2,3,4]
console.log(res); //[100]
解析:
/*
第一步:全局作用域-变量提升
ary
fn——AF0(堆内存,存储fn函数中的代码字符串)
res
第二步:代码执行
*/
var ary = [1,2,3,4]; //2.1赋值-引用数据类型-开辟堆内存AF1
//2.2变量提升阶段做处理
function fn(ary){
/*
第三步:fn函数执行 传参ary,即AF1 形成私有作用域fn(AF1)
3.1——形参赋值,变量提升 ——> ary赋值AF1
3.2——代码执行
arr[0]=0; AF1堆中的第一个元素值修改
ary = [0]; 开辟新堆BF0,一个元素为0,修改ary对应的地址为BF0
ary[0] = 100; 修改BF0中元素为100
return ary; -> return BF0;
*/
ary[0] = 0;
ary = [0];
ary[0] = 100;
return ary;
}
var res = fn(ary); //2.3-fn函数传实参,执行函数并将返回值赋给res 3.3-res值为BF0
console.log(ary); //第四步:输出[0,2,3,4]
console.log(res); //第五步:输出[100]
/*
var test = 自执行函数执行的返回结果;
1、自执行函数执行
test = AF0(小函数);
*/
var test = (function(i){
/*
1.1 形参赋值,变量提升 —— i=2
1.2 return AF0(小函数);
*/
return function(){
//传递5无用,没有形参
alert(i*=2); //i=i*2,i找上级=> 2*2=4 => "4"
}
})(2)
test(5);
var a = 1;
var obj = {
name:'tom'
} //obj——AF0
function fn(){
//形参赋值&变量提升 var a2;
var a2 = a; //a2 = 1;
obj2 = obj; //window.obj2 = AF0; 不是私有
a2 = a; //a2=1;
obj2.name = 'jack'; //AF0堆中的name修改为jack
}
fn();
console.log(a) //1
console.log(obj) //{name:'jack'}
先形参赋值后变量提升
var a = 1;
function fn(a){
/*
1、形参赋值 a=1
2、变量提升
var a;(无效,因为已有形参a)
function a... -> 堆BF0; (声明无效,但需要给a赋值为函数)
*/
console.log(a); // 输出: 函数-f a() {}
var a = 2;
function a(){}
console.log(a) //2
}
fn(a); //把全局a的值1当作实参传递给fn
/*
1.全局作用域
1.1变量提升:a b A-AF0
1.2代码执行:a=0 b=0 A(1)执行
2.2执行完之后,A改为BF0
*/
var a=0,b=0;
function A(a){
/*
2.A(1)执行形成私有作用域AAA => AF0(1)
2.1形参赋值&变量提升 形参赋值a=1
2.2代码执行 A=BF0,开辟堆存储代码字符串 A是全局window.A
2.3 alert(a++); 弹出字符串"1"——先弹出a在累加 a=2
3.A(2)执行形成私有作用域BBB => BF0(2)
3.1形参赋值&变量提升 形参赋值b = 2;
3.2代码执行
alert(a+b++); 弹出字符串"4"——先弹出4在累加 b=3
=>a+2 //a是上级作用域AAA中的 b是私有
=>b++
*/
A = function(b){
alert(a+b++);
}
alert(a++);
}
A(1);
A(2);
创建函数
- 开辟一个堆内存(16进制内存地址)
- 声明当前函数作用域(函数在哪创建,那么它执行时候所需要查找的上级作用域就是谁)
- 把函数体中的代码当做字符串存储进去
- 把堆内存的地址赋值给函数名/变量名
函数执行
- 形成一个全新的私有作用域、执行上下文、私有栈内存(执行一次形成一个,多个之间也不会产生影响)
- 形参赋值 & 变量提升
- 代码执行(把所属堆内存中的代码字符串拿出来一行行执行)
- 遇到一个变量,首先看它是否为私有变量(形参和在私有作用域中声明的变量是私有变量),是私有的就操作自己的变量即可,不是私有的则向上级作用域中查找...一直找到全局作用域为止 =>作用域链查找机制
- 私有变量和外界的变量没有必然关系,可以理解为被私有栈内存保护起来了,这种机制其实就是闭包的保护机制
闭包应用
//在结构上存储元素的索引
<button index="0"></button>
<button index="1"></button>
<button index="2"></button>
//事件委托
var arr = ['a','b','c'];
document.body.onclick = function(ev){
let target = ev.target,
targetTag = target.tagName;
if(targetTag == 'BUTTON'){
var index = target.getAttribute('index');
document.body.innerHTML = arr[index];
}
}
闭包作用域练习:
function fun(n, o) {
console.log(o);
return {
fun: function (m) {
return fun(m, n);
}
};
}
var c = fun(0).fun(1);
c.fun(2);
c.fun(3);
//输出结果:undefined 0 1 1
关于内存释放
堆内存释放
创建一个引用类型值,就会产生一个堆内存。
如果当前创建的堆内存不被其他东西占用了,浏览器会在空闲时查找每一个内存的引用情况,不被占用的都会被回收释放掉。
let obj = {
age:18
}
let oop = obj;
此时obj和oop都占用了对象的堆内存,想要释放堆内存,需要手动解除变量盒值的关联(null——空对象指针)
obj = null;
oop = null;
浏览器的垃圾回收机制:
-
引用计数(以IE为主):在某些情况下会导致计数混乱,这样会造成内存不能被释放(内存泄漏)
-
检测引用(占用)(以谷歌为主):浏览器在空闲时会依次检测所有的堆内存,把没有被任何事物占用的内存释放掉,以此优化内存。
手动释放内存,其实就是解除占用,手动赋值为null即可。
栈内存释放
- 打开浏览器形成的全局作用域是栈内存;
- 手动执行函数形成的私有作用域是栈内存;
- 基于ES6中的let/const形成的块作用域也是栈内存;
全局栈内存:关掉页面的时候才会销毁;
私有栈内存:
-
一般情况下,函数执行完成,形成的私有栈内存就会被销毁释放掉(排除出现无限递归、死循环模式);
-
但是一旦栈内存中的某个东西(一般都是堆地址)被私有作用域以外的事物给占用了,则当前栈内存不能被立即释放销毁(特点:私有作用域中的私有变量等信息也保留下来了);
市面上认为的闭包:函数执行形成不能被释放的私有栈内存才是闭包;
//情况1
function fn(){
...
}
fn(); //函数执行形成栈内存,执行完成栈内存销毁
//情况2
function x(){
return function(){
...
}
}
let f = x(); //f占用了x执行形成的栈内存中的一个堆地址,则x执行形成的栈内存不能被释放