前端面试-JS-闭包

2 阅读10分钟
  • 闭包、是什么,
  • 使用场景,写出几个闭包方法实现

在 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: ƒ}

内存泄露怎么引起的

juejin.cn/post/684490…

  • 什么是内存泄漏

申请的内存没有及时回收掉,被泄漏了

  • 为什么会发生内存泄漏?

虽然前端有垃圾回收机制,但当某块无用的内存,却无法被垃圾回收机制认为是垃圾时,也就发生内存泄漏了

而垃圾回收机制通常是使用标志清除策略,简单说,也就是引用从根节点开始是否可达来判定是否是垃圾

上面是发生内存泄漏的根本原因,直接原因则是,当不同生命周期的两个东西相互通信时,一方生命到期该回收了,却被另一方还持有时,也就发生内存泄漏了

  • 什么情况会发生内存泄漏

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);
});