一、闭包
1、闭包的基础概念
PS:以下文字不建议一开始看,或者简单看一下就行,但是肯定 理解不了的。
1)、官方解释:
一个函数和对其周围状态(变量)(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。
闭包让你可以在一个内部函数中访问到其外部函数的作用域。
2)、通俗的来说:JavaScript中所有的function都是一个闭包。不过一般来说,嵌套的function(定义)所产生的闭包更为强大,也是大部分时候我们所谓的“闭包”。即:闭包就是嵌套函数定义。
PS1:通过以上文字描述,你是根本不会理解闭包的,特别是初学者。只有通过后面代码的阅读和测试,才能明白,请静下心来。继续阅读。 PS2:对知识的理解,必须多次阅读概念,而且是使用后多次阅读概念,不是简单的多次阅读概念。
2、闭包的语法和优缺点
1)、通过以下代码,来浅层次理解闭包。
//1、定义闭包
function fn1(){
var n = 250;
function fn2(){//fn2可以读取函数fn1的局部变量; fn2就是个闭包(这个理解有点肤浅)
console.log(n);//250;
}
return fn2;//返回fn2的目的是:外部就可以调用fn2函数。
}
//2、调用闭包
let foo = fn1();//调用fn1。返回fn2,赋给foo。
foo();//相当于在调用fn2.
PS:通过以上代码,你只是直观的认识到了闭包的代码。而不能彻底理解闭包,别着急,静下心来。继续!
2)、基本调用过程及其示意图
function fn01(){
//1、定义变量
let count = 100;
//2、定义函数fn02,注意是定义函数,而不是执行函数
function fn02(){
count++;
console.log("count",count);
}
//3、返回函数fn02
return fn02;
}
let foo = fn01(); //这句话是在调用函数fn01,注意,调用函数fn01时,并不执行fn02的代码
foo(); //这句话是在调用fn02,并不会重新定义count,打印101
foo(); //这句话是在调用fn02,并不会重新定义count 打印102
foo(); //这句话是在调用fn02,并不会重新定义count 打印103
PS:以上代码的阅读,必须仔细。如果以上解释看不明白,看看下图的示意。 let foo = fn01();//只执行了蓝色框的代码,但不是不执行红色框的代码 foo() 只执行红色框的代码,并不执行红色框的代码,即:不会重新定义count变量。 所以,每次调用foo时,呈现了累加的效果
PS:以上只是知道了闭包的执行过程,你如果要进一步理解,请继续!
3)、继续写代码(重点来了)
下面代码,才能让你对闭包有一丢丢的理解,建议把上面的代码和下面的代码一定要运行以下。
let bar = fn01(); //bar的感觉:把fn02和count绑在了一起,这就是闭包
bar();//101
bar();//102
foo();//104
foo();//105
bar();//103
foo();//106
PS: 通过以上代码的执行结果。亲,你是否有一丢丢的感觉--------好像 fn02和count绑在了一起。如果,这个感觉没有出来,请看下图。 每调用一次fn01函数,就有个fn02和count绑在一起。这就是闭包的感觉。
PS:如果以读懂以上代码了,那么,你应该对闭包有一定的感觉了。大蓝框和大红框才是真正的闭包。还记得的文章一开头对闭包的解释吗-----------一个函数和对其周围状态(变量)(lexical environment,词法环境)的引用捆绑在一起 加油,后面更精彩!!!
4)、闭包优缺点
1)、可以读取(其它)函数内部变量(局部变量) 【优点】 2)、让这些变量(外部函数的局部变量)的值始终保持在内存中【优点,也是缺点,那就需要在合适的时机使用】
扩展:通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。但是,在创建了一个闭包以后,这个函数的作用域就会一直保存到闭包不存在为止。
function fn01(){
//1、定义变量
let count = 100;
//2、定义函数fn02,注意是定义函数,而不是执行函数
function fn02(){
count++;
console.log("count",count);
}
//3、返回函数fn02
return fn02;
}
let foo = fn01(); //foo是全局变量(会一直在内存中),引用着局部函数fn02及其对应count。所以,count永远不会被销毁
3)、内存泄漏
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露(有些内存不再使用了,但是,没有被释放)。
3、闭包的面试题讲解【请问你对闭包如何理解】
1)、闭包是 函数的嵌套定义,当调用外部函数时,会产生一个闭包。这个闭包就是外部函数定义的局部变量和内部函数。即:一个函数和对其周围状态(变量)的引用捆绑在一起,这样的组合就是闭包(closure)。
2)、闭包的作用:
内部函数可以使用外部函数的局部变量
让外部函数的局部变量一直存在于内存中(局部变量的生命周期变长了)。
3)、缺点:
因为,闭包会让局部变量一直保持在内存中,所以,不要滥用闭包,以防止内存泄漏。
4)、应用场景(能干啥):
-
让数据私有化(同时使用setter和getter函数,可以保证数据的合法性)
-
防抖
-
节流
-
单例模式
-
柯里化(谨慎的说这一点) …………………… 其实用的挺多。
4、闭包进阶 - 沙箱模式( getter 和 setter 语法 )
1)、沙箱模式[用闭包实现私有属性]
ps:如果某些数据,只希望某些函数操作使用,用全局变量显然不行。
function person(){
let age = 20;//变量age只允许 setAge函数和getAge函数使用。
// 修改age
function setAge(n){
//限定年龄的范围在0-150之间。
if(n<0 || n>150){
return;
}
age = n;
}
// 获取age
function getAge(){
return age;
}
//此处只返回来函数setAge和getAge,并没有返回变量age,所以,外部不能使用变量age
return {
setAge,
getAge
}
}
function foo(){
//这个函数里不能使用变量age
}
let p1 = person();
p1.setAge(100);
console.log(p1.getAge());//100
p1.setAge(250);
console.log(p1.getAge());//100
PS:懂其它面向对象语言的人,应该想到了,这个age变量有点像面向对象中的私有属性。对,你说的太对了。js最早就是用这种方式模拟私有属性的。 难道你没有看出来,age就是一个在函数person里的“全局”变量吗?哈哈。 面向对象中类的属性之于它的成员方法就是“全局”变量 精彩继续!!!
2)、沙箱模式的语法糖(简写)
function person() {
let age = 1
let name = '张三疯'
const obj = {
get age () {
return age
},
set age (val) {
age = val
},
get name () {
return name
},
set name (val) {
name = val
}
}
return obj
}
let p = person();
p.name = "张四疯" //调用 set name函数。是语法糖
p.age = 20;
console.log("p.name",p.name);
console.log("p.age",p.age);
5、闭包的应用:
1)、防抖:
防抖:减少无效的代码执行。
思路:启动一个定时器(setTimeout),把要执行的代码写在定时器里。在定时器时间未到时,下次会清除上次的定时器(上次的代码不会执行)。如果时间到了,那么,会执行代码。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text" id="search">
<input type="text" id="search02">
</body>
</html>
<script>
let f = antiShake();
document.querySelector("#search").onkeydown = f;
document.querySelector("#search02").onkeydown = antiShake();
// 封装的防抖
function antiShake() {
let myTimer = null;
function fn() {
if (myTimer != null) {
clearTimeout(myTimer);
}
myTimer = setTimeout(function () {
// 请求:问后端要数据
console.log("请求");
}, 200)
}
return fn;
}
</script>
2)、节流:
节流:在多次频繁触发的情况下,只希望执行更少的次数。既就是:减少执行次数。
思路:启动定时器,前一次的定时器没有到时间(代码还没有执行),则不再启动定时器。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body style="height: 2000px;">
</body>
</html>
<script>
// 节流:在多次频繁触发的情况下,只希望执行更少的次数。既就是:减少执行次数。
window.onscroll = throttle();
// 封装的节流
function throttle() {
let myTimer = null;
return function(){
console.log("窗口的内容滚动");
if (myTimer != null) {
return;
}
myTimer = setTimeout(function () {
console.log("执行代码");
clearTimeout(myTimer);
myTimer = null;
}, 100)
}
}
</script>
6、闭包的应用进阶 - 柯理化函数
英文:currying
柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
柯里化也可以理解为:提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。柯里化,是一个逐步接收参数的过程。
如:柯里化应该将 sum(a, b, c) 转换为 sum(a)(b)(c)。
解答一:
function sum(a){
return function(b){
return function(c){
return function(d){
return a+b+c+d;
}
}
}
}
let s = sum(1)(2)(3)(4);
console.log(s);
解答二:
如:柯里化应该将 sum(a, b, c) 转换为 sum(a)(b)(c)、sum(a,b)(c)、sum(a,b,c)都可以调用。
function sum(a,b,c,d){
return a+b+c+d;
}
function curry(cb){
let arrs = [];//[1,2,3,4]
function fn(...args){
arrs = [...arrs,...args];//[1,2,3,4]
if(arrs.length<cb.length){//判断实参个数是否和原函数的形参个数的关系
return fn;
}else{
return cb(...arrs);
}
}
return fn;
}
let sfn = curry(sum);
let s = sfn(1)(2)(3)(4);
let s = sfn(1,2)(3)(4);
let s = sfn(1,2)(3,4);
let s = sfn(1,2,3)(4)
// let s = sfn(1,2,3,4);
console.log(s);
解答三:
function sum(a,b,c,d){
return a+b+c+d;
}
// 把sum(a,b,c,d)转成 sum(a)(b)(c)(d);
function curry(cb,arr=[]){
return function(...args){
let arrs = [...arr,...args];
if(arrs.length<cb.length){//判断实参个数是否和原函数的形参个数的关系
return curry.call(this,cb,arrs)//调用闭包时,传入arrs,把前面调用的状态进行保持。
}else{
return cb(...arrs);
}
}
}
let sfn = curry(sum);
// let s = sfn(a)(b)(c)(d);
// let s = sfn(1)(2)(3)(4);
// let s = sfn(1,2)(3)(4);
let s = sfn(1,2,3,4);
console.log(s);
代码欣赏:(未来再看)
//柯里化代码:
const curry = (fn, arr = []) => (...args) => (
arg => arg.length === fn.length ? fn(...arg) : curry(fn, arg)
)([...arr, ...args])
curry()函数实际上就是完成了参数的收集和整理,最终,还是调用原函数本身。
柯里化的使用场景:
1、参数复用,减少重复参数
代码:可以使用解答二和解答三两个函数分别进行测试。
//如:在做项目时,数据可能来自好几个服务器,那么,意味着,请求地址的前缀不一样。
function url(protocol, hostname, baseURL, path) {
return protocol + hostname + "/" + baseURL + "/" + path;
}
function curry(fn,arr=[]){
return function(...args){
let arrs = [...arr,...args];
if(arrs.length<fn.length){//判断实参个数是否和原函数的形参个数的关系
return curry.call(this,fn,arrs)
}else{
return fn(...arrs);
}
}
}
let urlCurry = curry(url)
let douBanApi = urlCurry("https://", "www.douban.com", "doubanapi");//可以尝试是用
console.log(douBanApi("login"));
console.log(douBanApi("regSave"));
console.log(douBanApi("checkUser"));
面试题: 1、封装一个函数,完成功能:multi(2)(3)(4)() 的结果是24, 2、封装一个函数,完成功能:add(2).multi(9).div(3) 的结果是6 console.log(myCalculator(121).add(1).minus(2).multi(3).div(4).getValue()); // 输出90