JavaScript中的闭包(Closure)


在上一篇介绍JavaScript中的this关键字的文章中我们提到了闭包这个概念。闭包是指有权访问另一个函数作用域中的变量的函数。从函数对象中能够对外部变量进行访问(引用、更新),是构成闭包的条件之一。创建闭包的常见方式,就是在一个函数内部创建另一个函数。为了理解闭包,先来看一下什么是变量的生命周期。

变量的声明周期,就是变量的寿命,相对于表示程序中变量可见范围的作用域来说,生命周期这个概念指的是一个变量可以在多长的周期范围内存在并能够被访问。看下面一个例子:

function extent() {
    var n = 0;
    return function() {
        n++;
        console.log("n=" + n);
    };
}
var returnFun = extent();
returnFun();  //n=1
returnFun();  //n=2

局部变量n是在extent函数中声明的,这个从属于外部作用域中的局部变量,被函数对象给封闭在里面了。被封闭起来的变量的寿命,与封闭它的函数对象的生命周期相同,即闭包延长了局部变量的声明周期。 闭包的概念比较抽象,来来回回反反复复就那么几句话,并不好理解,下面主要通过几个例子来看一下如何使用闭包。

1. 在循环中使用闭包

在循环中使用闭包时,经常会出意想不到的代码执行结果。下面给出五个代码片段,目的都是想要返回一个函数数组,该数组中的每个函数的返回结果都是其在函数数组中的下标:

<!-- 代码片段一 Begin -->
function createFunction() {
    var result = [];
    for(var i = 0; i < 10; i++) {
        result[i] = function() {
            return i;  //闭包,该函数保有外部变量i的访问权
        };
    }
    return result;
}
//调用
var result = createFunction();
result[0]();  //10
result[1]();  //10
... ... ...
result[9]();  //10
<!-- 代码片段一 End -->

<!-- 代码片段二 Begin -->
function createFunction() {
    var result = [];
    for(var i = 0; i < 10; i++) {
        (function(num) {
            result[i] = function() {  //这里的result[i]可以替换为result[num]
                return num;
            };
        })(i);  //和代码片段三与代码片段四的区别一样,这里的 })(i); 可以替换为 }(i));
    }
    return result;
}
//调用
var result = createFunction();
result[0]();  //0
result[1]();  //1
... ... ...
result[9]();  //9
<!-- 代码片段二 End -->

<!-- 代码片段三 Begin -->
function createFunction() {
    var result = [];
    for(var i = 0; i < 10; i++) {
        result[i] = (function(num) {  //这里的result[i]可以替换为result[num]
            return function() {
                return num;
            };
        })(i);
    }
    return result;
}
//调用
var result = createFunction();
result[0]();  //0
result[1]();  //1
... ... ...
result[9]();  //9
<!-- 代码片段三 End -->

<!-- 代码片段四 Begin -->
function createFunction() {
    var result = [];
    for(var i = 0; i < 10; i++) {
        result[i] = (function(num) {  //这里的result[i]可以替换为result[num]
            return function() {
                return num;
            };
        }(i));  //与代码片段三的区别是,(i)所在的位置。两种方式效果相同。
    }
    return result;
}
//调用
var result = createFunction();
result[0]();  //0
result[1]();  //1
... ... ...
result[9]();  //9
<!-- 代码片段四 End -->

<!-- 代码片段五 Begin -->
function createFunction() {
    var result = [];
    for(var i = 0; i < 10; i++) {
        result[i] = function(num) {  //这里的result[i]可以替换为result[num]
            return function() {
                return num;
            };
        }(i);  //与代码片段三和代码片段四的区别是,function(num)没有被()包裹
    }
    return result;
}
//调用
var result = createFunction();
result[0]();  //0
result[1]();  //1
... ... ...
result[9]();  //9
<!-- 代码片段五 End -->

通过代码及其执行结果可以看到,代码片段一与我们的目标不符,生成的函数数组中的每一个函数的返回值都为10。这是因为for循环在结束后,i的值为10。因为闭包的存在,i的声明周期被延长,函数数组中的每一个函数都可以访问createFunction函数中定义的的变量i,这些函数在运行时会返回i的值,因为现在i = 10,因而出现了这种错误结果。

可以使用代码片段二 ~ 代码片段五实现需求,所有方法的原理其实相同,就是通过函数的立即执行将循环中的i保留下来。具体使用那种方式可以根据个人习惯来定,笔者更习惯代码片段二和代码片段三这种方式。

2. 结合例一再次理解一下闭包

<!-- HTML Begin-->
<pid="help">Helpful notes will appear here</p>
<p>E-mail:  <input type="text" id="email" name="email"></p>
<p>Name:    <input type="text" id="name" name="name"></p>
<p>Age:     <input type="text" id="age" name="age"></p>
<!-- HTML End-->

<!-- JavaScript通用代码段 Begin -->
function showHelp(info) {
    document.getElementById('help').innerHTML = info;
}
//为了之后调用
function closureFunc(info) {
    return function() {
        showHelp(info);
    };
}
function setupHelp() {
    var helpText = [
        {id: "email", info: "Please Input YourEmail Address"},
        {id: "name", info: "Please Input Your Name"},
        {id: "age", info: "Please Input Your Age"}
    ];
<!-- JavaScript通用代码段 End -->
 
<!-- 代码片段一 Begin -->
    for(var i = 0; i < helpText.length; i++) {
        var item = helpText[i];
        document.getElementById(item.id).onfocus = function() {
            showHelp(item.info);
        }
    }
<!-- 代码片段一 End -->
 
<!-- 代码片段二 Begin -->
    for(var i = 0; i < helpText.length; i++) {
        var item = helpText[i];
        document.getElementById(item.id).onfocus = closureFunc(item.info);
    }
<!-- 代码片段二 End -->

<!-- 代码片段三 Begin -->
    for(var i = 0; i < helpText.length; i++) {
        (function(num) {
            var item = helpText[num];
            document.getElementById(item.id).onfocus = function() {
                showHelp(item.info);
            };
        })(i);
    }
<!-- 代码片段三 End -->

<!-- 代码片段四 Begin -->
    for(var i = 0; i < helpText.length; i++) {
        var item = helpText[i];
        document.getElementById(item.id).onfocus = (function(info) {
            return function() {
                showHelp(info);
            };
        })(item.info);
    }
<!-- 代码片段四 End -->

<!-- 代码片段五 Begin -->
    for(var i = 0; i < helpText.lenght; i++) {
        var item = helpText[i];
        document.getElementById(item.id).onfocus = (function(info) {
            return function() {
                showHelp(info);
            };
        }(item.info));
    }
<!-- 代码片段五 End -->

<!-- 代码片段六 Begin -->
    for(var i = 0; i < helpText.length; i++) {
        var item = helpText[i];
        document.getElementById(item.id).onfocus = function(info) {
            return function() {
                showHelp(info);
            };
        }(item.info);
    }
<!-- 代码片段六 End -->

}
setupHelp();

通过代码可以看到,代码片段一与我们的目标不符,其余代码片段均可成功,原理与第一个例子完全相同,这里不再赘述,有兴趣的读者可以尝试一下代码片段一与其他代码在实现效果上的差别。

这里注意一点,代码片段六也能成功,但建议最好使用括号将其括起来,即推荐使用代码片段四或代码片段五的形式。如果直接在JavaScript中这样写:

function(info) {
    return function() {
        showHelp(info);
    };
}(item.info);

会导致错误,这是因为JavaScript把function关键字当作一个函数声明的开始,函数声明后面不能跟圆括号直接执行,然而函数表达式后面可以跟圆括号直接执行。要想将函数声明转换为函数表达式,只要像下面这样给它加上一对圆括号:

(function(info){
    return function() {
        showHelp(info);
    };
})(item.info);

如果对函数声明及函数表达式的概念不是很清楚,请参考这里

3. 创建公有方法访问私有变量

可以通过使用闭包模拟私有变量,实现和JavaBean类似的功能:

var Person = function(name, sex, age) {
    var getName = function() {
        return name;
    },
    setName = function(newName) {
        name = newName;
    },
    getSex = function() {
        return sex;
    }
    getAge = function() {
        return age;
    };
    return {
        getName: getName,
        setName: setName,
        getSex: getSex,
        getAge: getAge
    };
};

var person1 = Person("Lucy", "female", 22),
    person2 = Person("Tom", "male", 24);
person1.name;  //undefined  不能直接取私有变量的值
person1.getName();  //Lucy  通过公有方法取私有变量的值
person1.setName("Green");
person1.getName();  //Green  通过公有方法改变私有变量的值
person2.getName();  //Tom  通过公有方法取私有变量的值

上述Person方法有三个私有变量,name sex age,其中,name提供了取值和设值的方法,sexage只提供了取值方法,可以提高数据的安全性,这在之前的一篇博客中提到过,通常被称为稳妥构造函数模式

4. 模拟块级作用域

Java C等许多编程语言中都有块级作用域的概念,以Java为例:

for(int i = 0; i< 10; i++) {
    //do something
}
System.out.println(i);  //i cannot be resolved to a variable

而 JavaScript中没有块级作用域的概念:

for(var i = 0; i< 10; i++) {
    //do something
}
var i;
console.log(i);  //10
var i = 5;
console.log(i);  //5

没有块级作用域的一个弊端是,一个项目的不同开发者之间定义的变量很有可能会重名,甚至同一个人也可能会有命名冲突,全局命名空间很有可能被污染。很多情况下,为了不污染全局命名空间,可以使用闭包来模拟块级作用域:

(function() {
    var just_a_number = 100;
    console.log(just_a_number); //100
})();
console.log(just_a_number);  //Uncaught ReferenceError: just_a_number is not defined

通过模拟块级作用域,可以在很大程度上避免全局作用域被污染,建议在多人维护的JavaScript项目中使用模拟的块级作用域。

关于闭包就说这么多,相信读者如果能够把四个例子搞清楚,对闭包的理解就已经比较深了。闭包并非JavaScript独有,Python、Ruby等许多语言都有闭包的概念,擅长其他语言的读者可以通过自己的擅长语言来理解闭包,毕竟闭包的原理都是相通的。

完。


参考/扩展资料:

《JavaScript高级程序设计(第3版)》 作者:Nicholas C.Zakas 译者:李松峰 曹力

JavaScript 里的闭包是什么?应用场景有哪些? -- 知乎

JavaScript 闭包究竟是什么 -- 博客园

技术文档:闭包 -- 火狐开发者

闭包和引用 -- JavaScript秘密花园

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章