浅谈Javascript闭包的一些理解

写这篇文章的原因很简单,临近毕业,不得不考虑找工作的问题,面临着就业的压力,开始在前辈的教育下,开始刷题之旅,从leetcode到牛客,从编程到客观题。无奈自己实在是真的渣,牛客上的Javascript客观题的准确率一度掉到了53%,很多一些经典的知识,如闭包,继承等都完全不知所云。于是从头开始系统学习一些不了解的知识,并在总结别人博客的基础上谈一下自己的理解,如有错误,还请在评论区留言纠正我。
这里感谢一下阮一峰大神,本文很大程度都借鉴了阮老师的学习Javascript闭包(Closure),查看原文请点击这里。

1 变量的作用域

阮老师解释到,变量的作用域分为两种,全局变量和局部变量。这两个概念理解起来很容易,Javascript函数内部可以直接读取到全局变量,如下例所示。

var name = "John";//name定义在函数外部 此时的name是一个全局变量
function print(){
    console.log(name);//此时控制台输出John
}

Javascript函数外部不能读取函数内部的局部变量,如下例所示。

function print(){
    var name = "Jenny";//name定义在函数内部 此时的name是一个局部变量
}
console.log(name);//此时控制台输出undefined 因为无法在全局中找到name变量 

2 闭包简介

一般的,不能通过外部函数读取局部变量。但是,在实际开发中,我们有时候需要得到函数内的局部变量。解决这个问题,可以两种方法,方法一,在函数a的内部再定义一个函数b,来获取函数a的局部变量。

function print(){//此时print相当于函数a
    var name = "Jay";
    function print2(){//此时print2相当于函数b
        console.log(name);
    }
}

在上面的代码中,函数print2就被包括在函数print内部,这是print内部的所有局部变量,对print2都是可见的,但是反过来就不行,print2内部的局部变量,对print就是不可见的。这就是Javascript语言特有的“链式作用域”结构(chain scope)。子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
——阮一峰

方法二,既然print2可以读取print中的局部变量,我们只需返回print2的值,就可以在print外部读取它的内部变量。

function print(){
    var name = "Jay";
    function print2(){
        console.log(name);
    }
    return print2;
}
var result = print();
result();//此时控制台输出Jay

这里的print2函数就是一个闭包。阮老师对闭包的理解是,闭包就是能够读取其他函数内部变量的函数。闭包的本质,就是将函数内部和函数外部连接起来供我们使用。

3 闭包的用途

闭包的最大用处有两个,一个是前面提到的可以读取函数 ,另一个就是让这些变量的值始终保持在内存中。

function print(){
    var num = 4399;
    add = function(){//这里add要写成全局函数,否则在调用的时候会说没有定义
        num += 1;
    }
    function print2(){
        console.log(num);
    }
    return print2;
}
var result = print();//result实际是闭包print2函数
result();//控制台输出4399
add();
result();//控制台输出4400,说明num一直保存在内存中,调用add函数后执行加1运算

注意:
1、闭包会是的函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2、闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要小心,不要随便改变父函数内部变量的值。

4 几个例子

首先我们看下阮老师文章里面留下的两个思考题。

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        return function(){
            //console.log(this);//控制台输出Window {...}
            return this.name;
        }
    }
}
console.log(object.getNameFunc()());//控制台输出The Window

个人理解,object.getNameFunc()()指的是return function(){return this.name;}这个函数,可以把object.getNameFunc()整体看成一个函数A,即A()返回this.name,函数里面的this找不到就往上一层找,因此,this指的是Window对象。

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        var that = this;
        //console.log(this);//控制台输出Object{...}
        //console.log(that);//控制台输出Object{...}
        return function(){
            //console.log(this);//控制台输出Window{...}
            //console.log(that);//控制台输出Object{...}
            //return this.name;//控制台输出The Window
            return that.name;
        }
    }
}
console.log(object.getNameFunc()());//控制台输出My Object

这里一开始this赋值给了一个对象中的局部变量,此时这里的this和that指的都是Object对象。后面return函数里面this没有对其赋值,找不到this,所以只有在方法调用的时候,才能确定其指向(换言之,继续往上一层找),而that指的是Object对象,所以that.name输出"My Object"。
接下来看一个更让你头大的。下面这个例子是Zing大神给我耐心解答的时候举出的,觉得理解起来更有帮助。这里也要感谢下Zing大神。

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        return function(){
            //console.log(this);//控制台输出Object{name : "I have a headache"}
            return this.name;
        }
    }
}
var test = {"name" : "I have a headache", b2 : object.getNameFunc()}
console.log(test.b2());//输出I have a headache

这里调用的时候test调用自己的b2方法,b2又调用了object的getNameFunc()方法。getNameFunc返回的this.name在这个函数中找不到this,因此需要在上一层找,于是就找到了test对象,这里this.name即test.name。

function f1(){
    var arr = new Array();
    for (var i = 0;i < 3;i++){
        arr[i] = function(){
            return i;
        }
    }
    return arr;
}
var test = f1();
console.log(test[0]());//控制台输出3
console.log(test[1]());//控制台输出3
console.log(test[2]());//控制台输出3

这道题就是我遇到的一个很经典的错题,当时我认为应该返回0,1,2,万万没想到,/(ㄒoㄒ)/~~,一个都没猜对。这里要注意,var test = f1();只是定义函数,真正执行方法的是test[0]()test[1]()test[2](),当这些方法执行的时候,i已经变成了3,因此控制台都会输出3。那么怎么修改一下才能让它输出0,1,2呢?这里给出三种方法。
方法一

function f1(){
    var arr = new Array();
    for (var i = 0;i < 3;i++){
        arr[i] = function(num){
            return function(){
                return num;
            }
        }(i);
    }
    return arr;
}
var test = f1();
console.log(test[0]());//控制台输出0
console.log(test[1]());//控制台输出1
console.log(test[2]());//控制台输出2

方法一给出的这种解法是我个人认为最难理解的一种,这里没有直接返回i,而是通过一个匿名函数,返回一个num值。在调用匿名函数的时候,传入一个变量i。由于函数是按值传递的,所以就会将变量i的值复制给参数num,而在匿名函数内部,又返回一个访问num的闭包。这样一来,就可以返回各自不同的数值。
下面看方法二,方法二是方法一的改进版,不过方法二比方法一理解起来要好一点。
方法二

function f1(){
    var arr = new Array();
    function test(num){
        return function(){
            return num;
        }
    }
    for (var i = 0;i < 3;i++){
        arr[i] = test(i);
    }
    return arr;
}
var test = f1();
console.log(test[0]());//控制台输出0
console.log(test[1]());//控制台输出1
console.log(test[2]());//控制台输出2

这里将test函数单独抽出来,个人认为比方法一看上去好理解一些,这里不再详细解释,请读者自行理解。
下面看方法三,方法三是前些日子看ES6时无意间看到的。

ES6新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。
——阮一峰《ECMAScript 6 入门》

方法三

function f1(){
    var arr = new Array();
    for (let i = 0;i < 3;i++){
        arr[i] =function(){
            return i;
        }
    }
    return arr;
}
var test = f1();
console.log(test[0]());//控制台输出0
console.log(test[1]());//控制台输出1
console.log(test[2]());//控制台输出2

let声明的变量只在本轮循环有效,所以每次循环的i都是一个新的变量。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为Javascript引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

5 总结

以上是我个人对Javascript闭包的一些理解,希望对各位有所帮助。写的不对的地方还请各位大神提出意见,我会加以改正。码字不易,请尊重作者版权,转载注明出处。
By BeLLESS 2017.4.7 9:26

你可能感兴趣的:(浅谈Javascript闭包的一些理解)