千字长文详解闭包

闭包

1.闭包入门

闭包有许多种理解方式,尽管我的理解不能算是绝对正确,但是我希望它能加深你的理解。你可以随意copy我给出的示例,

观察它们的工作方式。最终你会有深入的理解闭包。

C语言及其他诸多语言中,函数作为栈上自动内存管理的一部分,当函数调用结束时,为该函数分配的内存会被清除。

而在 JavaScript 中,即使调用完函数,该函数内部定义的变量和方法任然会保留在内存中。在函数执行完后保留函数内部

定义的变量和方法的链接,这就是闭包工作方式的一部分。

JavaScript是一门不断进化的语言。在闭包出现的时候, JavaScript还没有类或私有变量的概念。可以说在ES6之前,

闭包可以用来大致模拟类似于对象的私有方法的内容。闭包是JavaScript传统编程风格的一部分。这是面试中最常见的问题之一。

2.什么是闭包

在函数退出了之后,闭包还能保留对所有局部函数变量的引用

如果对JavaScript中的作用域规则和执行语境委托控制流一无所知,那么闭包理解起来会很难,但是如果从简单的示例入手,这会变的简单一些。

要理解闭包,至少需要理解如下结构。这主要是因为JavaScript允许在一个函数中定义另一个函数。从技术角度来讲这就是闭包。

    //函数global定义在与window一起创建的全局作用域中 global的this指向window
function global(){
    //  在调用global时,将为该函数创建一个新的执行语境。在声明绑定示例化的过程中,
    //  在JavaScript解释器的内部,inner将作为一个新的局部对象被创建,其作用域指
    //  向global的执行语境的变量环境
    function inner(){
        console.log("inner")
    }
    inner();    //调用inner
   
}
global(); //输出"inner"
复制代码

在下面的代码中,全局函数sendEmail 定义了一个匿名函数,并将其赋值给变量 send。该变量仅对 sendEmail 函数的作用域可见,

而对全局作用域不可见。

function sendEmail(){
    let msg = `"${name}" 》 "${words}"`;
    let send = function(){ console.log(msg) };
    send()
}
​
sendEmail('小动脉','你好');
复制代码

当我们调用 sendEmail 时,它会创建并调用 send 方法。全局作用域无法直接调用 send 方法。

控制台输出:小动脉 > 你好

我们可以通过调用从函数返回的私有方法(内部函数),来公开对变量,方法等等的引用。在下面代码中第 004 行并不是调用send 方法,而是返回对它的引用。除此之外,下面的代码块和上一个代码块完全相同

function sendEmail(){
    let msg = `"${name}" 》 "${words}"`;
    let send = function(){ console.log(msg) };
    return send;
}
​
//创建对sendEmail的引用
let ref = sendEmail('小动脉','你好');
​
//用变量名进行调用
ref();
复制代码

现在,我们就可以在全局作用域中通过引用来调用 send 方法。即使在调用完 sendEmail 函数之后,变量 msgsend 仍会保留在内存中。在 C语言中,它们会从栈内存中自动清除,我们将无法再次访问它们。但在JavaScript中可以。

我们再来看一个示例。在下面的示例中,首先定义全局变量 get , set , add , 和 decreased

let get, set, add, decreased;
​
function manager(){
    console.log("manager()");
    let number = 15;
    get = function(){ console.log(number) };
    set = function(val){ number = val };
    add = function(){ number++ };
    cut = function(){ number-- }
}
复制代码

为了将匿名函数赋值给全局函数,我们至少需要运行一次 manager 。如下所示

manager();  //初始化manager
add();  //15
for (let i = 0; i < 200; i++)( add() );
get();  //215
cut();
get();  //214
set(755);
get()   //755
let old_get = get;  //保存对get的引用
​
manager();  //再次初始化manager
get();  //15
old_get();  //755
复制代码

详解

在第一次调用manager之后,执行的函数和所有的全局引用都链接到了它们各自的匿名函数。这样就创建了我们的第一个闭包。

我们调用 add , cutset , 来修改manager 函数中定义的变量 number 的值。每一步都会用 get 来打印该值以确保它已经改变了。

3.漂亮的闭包

函数式编程使用闭包,其原因与面对对象编程使用私有方法类似。闭包以函数的形式为对象提供 api 方法。

如果根据这个思路进一步创建一个看起来很漂亮的闭包,返回多个方法,而不是只有一个方法,那会怎么样呢?请看以下示例

let get = null; //全局getter函数的变量
​
function closure(){
    
   this.inc = 0;
   get = () => this.inc; //getter
   function add(){ this.inc++; }
   function cut(){ this.inc--; }
   function set(v){ this.inc = v; }
   function reset(){ this.inc = 0 ;}
   function del(){
       delete this.inc; //变为undefined 即未赋值
       this.inc = null; //将其赋值为null
       console.log("this.inc已被删除");
   }
   function readd(){
       //如果为null或undefined
       if(!this.inc){
           this.inc = "added"
       }
   }
    //同时返回多个方法
    return [add, cut, set, reset, del, readd];
}
复制代码

del方法会从对象中彻底删除 inc 属性,而 readd 会重新添加该属性。为简单起见,这里并没有执行防错处理。如果在 inc 属性已被删除后去访问这些方法时,将会发生引用错误。

//初始化闭包
let f = closure();
//变量f现在指向一组公开的方法,把方法赋值给函数名,它们就进入了全局作用域了
//为各个方法赋上唯一的函数名
let add = f[0];
let cut = f[1];
let set = f[2];
let res = f[3];
let del = f[4];
let readd = f[5];
​
//注意要用get()方法来获取this.inc的值,其余方法只是对this.inc进行操作并没有返回this.inc, 方法后的注释是this.inc的值
add(); //1
add(); //2
add(); //3
cut(); //2
get(); //2
set(7); //7
get(); //7
readd(0); //0
get(); //0
​
//最后可以用 del 方法删除该属性本身
//删除属性
del(); //null
get(); //null
​
//这时调用其他方法将会报错,我们需要将inc属性重新添加到对象中
readd();
get(); //"added"
​
//将 inc 属性的值重置为0,递增1
res(); //0
add(); //1
get(); //1
复制代码

4.闭包小结

如果在一个函数中创建另一个函数,就创建了闭包。

当调用的函数包含另一个函数时,就会新建执行语境,它持有所有局部变量的全新副本。通过链接到全局作用域中定义的变量名,或在外层函数中使用 return 关键字返回闭包,就可以在全局作用域中创建它们的引用。

闭包可以使你持有所有局部函数变量的引用,在函数执行退出后仍然可使用。

注意:new Function 构造函数不会创建闭包。这是应为构造函数创建的函数只能在全局作用域中运行

5.柯里化

柯里化(currying) 指的是将一个多参数的函数拆分成一系列函数,每个拆分后的函数都只接受一个参数。通过定义时立即返回所有内部函数来链接闭包,就可以创建柯里化函数。

let add = function(a){
    return function(b){
        return a+b;
    }
}
​
let f = add('aa');
console.log(f('bb')); //aabb
console.log(f('cc')); //aacc
console.log(f('dd')); //aadd
​
//函数 add 会返回一个匿名函数,因此当函数 add 接收了参数 1 赋值给 f 的时候,它会根据第二给参数再次进行调用。
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享