- 闭包、是什么,
- 使用场景,写出几个闭包方法实现
在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。
闭包是一个可以访问外部作用域的内部函数,即使这个外部作用域已经执行结束。
说起闭包,可以直接和纯函数比较
纯函数:可以理解为确定输入和输出,无副作用。方便单元测试
闭包: 当一个函数被创建并传递或从另一个函数返回时,它会携带一个背包。背包中是函数声明时作用域内的所有变量。
官方解释是:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
阮一峰说的闭包也是很简洁的:“我的理解是,闭包就是能够读取其他函数内部变量的函数。”
注意点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便
改变父函数内部变量的值
用途
它的最大用处有两个,
- 一个是前面提到的可以读取函数内部的变量,
- 另一个就是让这些变量的值始终保持在内存中
// 定长参数
function add (a, b, c, d) {
return [
...arguments
].reduce((a, b) => a + b)
}
function currying (fn) {
let len = fn.length
let args = []
return function _c (...newArgs) {
// 合并参数
args = [
...args,
...newArgs
]
// 判断当前参数集合args的长度是否 < 目标函数fn的需求参数长度
if (args.length < len) {
// 继续返回函数
return _c
} else {
// 返回执行结果
return fn.apply(this, args.slice(0, len))
}
}
}
let addCurry = currying(add)
let total = addCurry(1)(2)(3)(4) // 同时支持addCurry(1)(2, 3)(4)该方式调用
function add (...args) {
return args.reduce((a, b) => a + b)
}
function currying (fn) {
let args = []
return function _c (...newArgs) {
if (newArgs.length) {
args = [ ...args, ...newArgs ]
return _c
} else {
return fn.apply(this, args)
}
}
}
let addCurry = currying(add)
// 注意调用方式的变化
console.log(addCurry(1)(2)(3)(4, 5)())
闭包是为了做什么
- 内部函数可以引用外部函数的参数和变量
- 方法执行中的中间值可以存储起来
1.希望变量长期驻扎在内存当中(一般函数执行完毕,变量和参数会被销毁)
2.避免全局变量的污染
js闭包问题
请实现一个sum 支持以下方式求和
- sum(1)(4)(6)()
- sum(1,2,3)(12)(1,2,3)
- sum(1,2,3)
function sum(){
var num=0;
Object.values(arguments).map(i=>{return num+=i})
return function su(){
if(arguments[0]){
Object.values(arguments).map(i=>{return num+=i})
return su
}else{
return num;
}
}
}
console.log(sum(1)(4)(6)())
console.log(sum(1,2,3)(11)())
console.log(sum(1,2,3)(12)(1,2,3)())
console.log(sum(1,2,3)())
闭包的应用
闭包可以使代码组织方式的自由度大大提升,在日常使用中有非常广泛的用途。
简单的有:
- ajax请求的成功回调
- 事件绑定的回调方法
- setTimeout的延时回调
- 函数内部返回另一个匿名函数
我们详细讲下以下几个应用场景:
- 构造函数的私有属性
- 计算缓存
- 函数节流、防抖
实现多个计时器
function createCounter(name) {
var counter = 0;
function increment() {
counter = counter + 1;
console.log(name+ ":Number of events: " + counter);
}
return increment;
}
var counter1 = createCounter('one');
var counter2 = createCounter('two');
counter1(); // one:Number of events: 1
counter1(); // one:Number of events: 2
counter2(); // two:Number of events: 1
counter1(); // two:Number of events: 3
构造函数的私有属性
- 由于javascript中天然没有类的实现,某些不希望被外部修改的私有属性可以通过闭包的方式实现。
function Person(param) {
var name = param.name; // 私有属性
this.age = 18; // 共有属性
this.sayName = function () {
console.log(name);
}
}
const tom = new Person({name: 'tom'});
tom.age += 1; // 共有属性,外部可以更改
tom.sayName(); // tom
tom.name = 'jerry';// 共有属性,外部不可更改
tom.sayName(); // tom
计算缓存
常见获取临时缓存信息(比如公司季报)
// 平方计算
var square = (function () {
var cache = {};
return function(n) {
if (!cache[n]) {
cache[n] = n * n;
}
return cache[n];
}
})();
// 实现 add(1)(2)(3)()=6 add(1,2,3)(4)()=10
function add() {
let nums = [...arguments].reduce((s, v) => s + v)
function temp() {
if (!arguments.length) {
return nums
}
nums += [...arguments].reduce((s, v) => s + v)
return temp
}
return temp
}
函数节流、防抖
// 节流
function throttle(fn, delay) {
var timer = null, firstTime = true;
return function () {
if (timer) { return false;}
var that = this;
var args = arguments;
fn.apply(that, args);
timer = setTimeout(function () {
clearTimeout(timer);
timer = null;
}, delay || 500);
};
}
// 防抖
function debounce(fn, delay) {
var timer = null;
return function () {
var that = this;
var args = arguments;
clearTimeout(timer);// 清除重新计时
timer = setTimeout(function () {
fn.apply(that, args);
}, delay || 500);
};
}
高频面试题
// 1
for ( var i = 0 ; i < 5; i++ ) {
setTimeout(function(){
console.log(i);
}, 0);
}
// 5 5 5 5 5
// 2
for ( var i = 0 ; i < 5; i++ ) {
(function(j){
setTimeout(function(){
console.log(j);
}, 0);
})(i);
// 这样更简洁
// setTimeout(function(j) {
// console.log(j);
// }, 0, i);
}
// 0 1 2 3 4
setTimeout(function(j) {
console.log(j);
}, 0, i);
// 3
for ( let i = 0 ; i < 5; i++ ) {
setTimeout(function(){
console.log(i);
},0);
}
// 0 1 2 3 4
// 4
var scope = 'global scope';
function checkscope(){
var scope = 'local scope';
console.log(scope);
}
// local scope
// 5
var scope = 'global scope';
function checkscope(){
var scope = 'local scope';
return function f(){
console.log(scope);
};
}
var fn = checkscope();
console.log(fn()); // local scope
// 6
var scope = 'global scope';
function checkscope(){
var scope = 'local scope';
return function f(){
console.log(scope);
};
}
checkscope()(); // local scope
var obj = {
name: 'tom',
sayName() {
console.log(this.name);
}
}
obj.sayName(); // tom
var obj = {
name: 'tom',
sayName() {
var name = 'alan';
console.log(this.name);
}
}
obj.sayName();// 'tom'
var name = 'jerry';
var obj = {
name : 'tom',
sayName(){
return function(){
console.log(this.name);
};
}
};
obj.sayName()(); // jerry
// var name = 'jerry';
var obj = {
name: 'tom',
sayName() {
var name = 'alan';
console.log(this.name);
}
};
var sayName = obj.sayName;
sayName(); // '' // jerry
function fun(a,b) {
console.log(b)
return {
fun: function(c) {
return fun(c,a);
}
};
}
var d = fun(0); // undefined
d.fun(1); // 2 0
d.fun(2); // 2 0
d.fun(3); // 2 0
var d1 = fun(0).fun(1).fun(2).fun(3);
// 2 undefined 2 0
// 2 1
// 2 2
var d2 = fun(0).fun(1); d2.fun(2);
// 2 undefined
// 2 0
// 2 1
// 2 1
d2.fun(3); // {fun: ƒ}
内存泄露怎么引起的
- 什么是内存泄漏
申请的内存没有及时回收掉,被泄漏了
- 为什么会发生内存泄漏?
虽然前端有垃圾回收机制,但当某块无用的内存,却无法被垃圾回收机制认为是垃圾时,也就发生内存泄漏了
而垃圾回收机制通常是使用标志清除策略,简单说,也就是引用从根节点开始是否可达来判定是否是垃圾
上面是发生内存泄漏的根本原因,直接原因则是,当不同生命周期的两个东西相互通信时,一方生命到期该回收了,却被另一方还持有时,也就发生内存泄漏了
- 什么情况会发生内存泄漏
1. 意外的全局变量
全局变量的生命周期最长,直到页面关闭前,它都存活着,所以全局变量上的内存一直都不会被回收
当全局变量使用不当,没有及时回收(手动赋值 null),或者拼写错误等将某个变量挂载到全局变量时,也就发生内存泄漏了
2. 遗忘的定时器
setTimeout 和 setInterval 是由浏览器专门线程来维护它的生命周期,所以当在某个页面使用了定时器,当该页面销毁时,没有手动去释放清理这些定时器的话,那么这些定时器还是存活着的
也就是说,定时器的生命周期并不挂靠在页面上,所以当在当前页面的 js 里通过定时器注册了某个回调函数,而该回调函数内又持有当前页面某个变量或某些 DOM 元素时,就会导致即使页面销毁了,由于定时器持有该页面部分引用而造成页面无法正常被回收,从而导致内存泄漏了
如果此时再次打开同个页面,内存中其实是有双份页面数据的,如果多次关闭、打开,那么内存泄漏会越来越严重
而且这种场景很容易出现,因为使用定时器的人很容易遗忘清除
3. 使用不当的闭包
函数本身会持有它定义时所在的词法环境的引用,但通常情况下,使用完函数后,该函数所申请的内存都会被回收了
但当函数内再返回一个函数时,由于返回的函数持有外部函数的词法环境,而返回的函数又被其他生命周期东西所持有,导致外部函数虽然执行完了,但内存却无法被回收
所以,返回的函数,它的生命周期应尽量不宜过长,方便该闭包能够及时被回收
正常来说,闭包并不是内存泄漏,因为这种持有外部函数词法环境本就是闭包的特性,就是为了让这块内存不被回收,因为可能在未来还需要用到,但这无疑会造成内存的消耗,所以,不宜烂用就是了
4. 遗漏的 DOM 元素
DOM 元素的生命周期正常是取决于是否挂载在 DOM 树上,当从 DOM 树上移除时,也就可以被销毁回收了
但如果某个 DOM 元素,在 js 中也持有它的引用时,那么它的生命周期就由 js 和是否在 DOM 树上两者决定了,记得移除时,两个地方都需要去清理才能正常回收它
5. 网络回调
某些场景中,在某个页面发起网络请求,并注册一个回调,且回调函数内持有该页面某些内容,那么,当该页面销毁时,应该注销网络的回调,否则,因为网络持有页面部分内容,也会导致页面部分内容无法被回收
如何监控内存泄漏
内存泄漏是可以分成两类的,一种是比较严重的,泄漏的就一直回收不回来了,另一种严重程度稍微轻点,就是没有及时清理导致的内存泄漏,一段时间后还是可以被清理掉
不管哪一种,利用开发者工具抓到的内存图,应该都会看到一段时间内,内存占用不断的直线式下降,这是因为不断发生 GC,也就是垃圾回收导致的
针对第一种比较严重的,会发现,内存图里即使不断发生 GC 后,所使用的内存总量仍旧在不断增长
另外,内存不足会造成不断 GC,而 GC 时是会阻塞主线程的,所以会影响到页面性能,造成卡顿,所以内存泄漏问题还是需要关注的
我们假设这么一种场景,然后来用开发者工具查看下内存泄漏:
场景一:在某个函数内申请一块内存,然后该函数在短时间内不断被调用
// 点击按钮,就执行一次函数,申请一块内存
startBtn.addEventListener("click", function() {
var a = new Array(100000).fill(1);
var b = new Array(20000).fill(1);
});
一个页面能够使用的内存是有限的,当内存不足时,就会触发垃圾回收机制去回收没用的内存
而在函数内部使用的变量都是局部变量,函数执行完毕,这块内存就没用可以被回收了
所以当我们短时间内不断调用该函数时,可以发现,函数执行时,发现内存不足,垃圾回收机制工作,回收上一个函数申请的内存,因为上个函数已经执行结束了,内存无用可被回收了
所以图中呈现内存使用量的图表就是一条横线过去,中间出现多处竖线,其实就是表示内存清空,再申请,清空再申请,每个竖线的位置就是垃圾回收机制工作以及函数执行又申请的时机
场景二:在某个函数内申请一块内存,然后该函数在短时间内不断被调用,但每次申请的内存,有一部分被外部持有
// 点击按钮,就执行一次函数,申请一块内存
var arr = [];
startBtn.addEventListener("click", function() {
var a = new Array(100000).fill(1);
var b = new Array(20000).fill(1);
arr.push(b);
});