2

前言

闭包这个概念几乎成了JavaScript面试者必问的话题之一,可以毫不客气地说对闭包的理解和运用体现了一名js工程师的功底。那么闭包到底是什么,它又能带来什么特别的作用?网上有很多文章和资料都讲述了这个东西,但是大多解释得比较含糊,涉及闭包底层过程却一笔带过,对于初学者的理解十分不友好。在这里,我想讲述清楚闭包的来龙去脉,加深大家对此理解,如有讲述不合理的地方,欢迎指出并交流。

例子

假设我们有这样一个需求,判断某个对象是否为指定类型,比如判断是否为函数:

function isFunction(obj){
    return (typeof obj === 'function');
}

如果业务功能只需要这一种类型判断,这么写当然没有问题,但是如果业务逻辑还需要有是否为字符串类型、是否为数组类型等判断时该怎么办?使用switch来对传参进行判断?

function isType(obj,type) {
    switch (type) {
        case 'string': 
            return (typeof obj === 'string')
        case 'array':
            return (typeof obj === 'array')
         case 'function':
            return (typeof obj === 'function')
        default:
            break;
    }
}

这样写似乎也还不错,但是如果用闭包特性来写,整体的代码就会优雅很多:

function isType(type){
    return function(obj){
        return Object.prototype.toString.call(obj) == '[object '+ type + ']'
    }
}

//定义一个判断是否为函数类型的函数
var isFunction = isType('Function'); 
var isString = isType('String');

//测试
var name = 'Tom';    
isString(name)//true

先把Object.prototype.toString与typeof的问题放一边,这种书写方式是否比上一个switch的方式更为清楚且易扩展?(观众老爷:清楚个毛啊,明明更复杂了好吧!)稍安勿躁,下面我就解释:
1、Object.prototype.toString与typeof都可以对变量进行类型判断,不同之处在于后者对引用类型的变量判断都会返回'object',因此很难确定返回的值是不是函数。 而前者更为严谨,在任何值上调用Object.toStrng()会返回一个[object NativeConstructorName]格式的字符串。
2、再来说说这里的闭包特性,isType函数的作用是返回一个用于定制类型判断的匿名函数。当我们调用isType('String')时,得到的是一个这样的函数:

var isString = isType('String');
//等价于
var isString = function (obj) {
    return Object.prototype.toString.call(obj) == '[object String]';
}

这种模式是不是有点似曾相识?是否有点像工厂模式?确实挺像的,只不过工厂模式是用来定制对象的,而这个是用来定制函数的。事实上这是一个闭包在js里的经典技巧,它有一个很装逼的名字函数柯里化。

为什么会这样?

之所以能实现这种效果,是因为闭包的特性使得返回的匿名函数的作用域链一直保存着对type变量的引用。
什么意思呢,这里我想从另一个方面来解释,假设js不存在闭包这个特性,那上面的代码执行效果又会变成什么样?
按照一般的理解来说,在调用并执行完isType('String')方法后,isType函数内部变量都应该被回收清除,变量type会被清空;也就是说当我再调用isString(obj)时,它得到的应该是一个type变量为undefined的函数:

var isString = function (obj) {
    return Object.prototype.toString.call(obj) == '[object undefined]';
}

undefined?什么鬼?为什么不是返回指定type='String'的函数?
事实上,return function(){} 形式返回的并不是一个函数,而是一个函数的引用。什么是引用,简单来说就是一个指向这个函数在内存中的地址。也就是说这个返回来的匿名函数并没有“定型”成真正的

var isString = function (obj) {
    return Object.prototype.toString.call(obj) == '[object String]';
}

它实际上还是这个函数:

var isString = function (obj) {
    return Object.prototype.toString.call(obj) == '[object '+ type +']';
}

既然如此,为什么我们可以成功的得到我们想要的函数?就是由于闭包特性导致isType()在执行完后,垃圾回收器并没有清空内部变量type。没有清空的原因是,内部函数(返回的匿名函数)的作用域链仍然保有对 外部函数(isType)的变量type的引用。JavaScript的垃圾回收器对于这种 保有引用的变量是不会清除的。
关于什么是作用域链以及作用域链和垃圾回收之间的具体关系,才是真正涉及闭包来龙去脉的真正原因,但是我要放到下一段讲。这里我要再举一个例子,以验证我前面所说的。

再来一个例子

  function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999
  nAdd();
  result(); // 1000

这里盗用阮大侠的例子,相信很多朋友都会看过他这篇关于对闭包概念解释的文章。这里算是做一个补充吧。
nAdd=function(){n+=1},js语法的书籍都讲过,不以var 声明的变量都会被默认创建并提至全局变量中。虽然不推荐这种做法,容易造成全局变量污染和难以调试等问题,但是写个小代码测试就没什么问题了。
f2被返回,并将f2的引用赋值给了result。由于f2函数的作用域链保有对n的引用,所以在执行完f1()之后,n并没有被回收清除。 这时再调用nAdd(),因为nAdd函数的作用域链也对n保有引用,所以在执行n+1的操作后,所有引用这个n的地方都会+1。

作用域链和执行环境

对于非计算机科班出身的朋友看到这两个名词,心中会不会有一丝不安?其实他们并不难懂。
当某个函数第一次被调用时,会创建一个执行环境及相应的作用域链,并把作用域链赋值给一个特殊的内部属性,scope。然后使用this.arguments和其他命名参数的值来初始化函数的活动对象。在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,......直至作为作用域链终点的全局执行环境。

function isType(type){
    return function(obj){
        return Object.prototype.toString.call(obj) == '[object '+ type + ']'
    }
}

var isString = isType('String');

var name = 'Tom';    
isString(name)//true

当我第一次调用isString(name)时,执行环境会去创建一个包含this、arguments和obj的活动对象。而外部函数的变量对象(this和type)在isString()执行环境的作用域链中则处于第二位。全局的变量对象window则在isString()的作用域链中排第三位。作用域链上的变量对象的排列顺序也就决定了执行时变量查找的顺序。这也解释了,为什么当外部有多个相同变量名的变量时,解析器会取离它最近的那一个外部变量。

这里也说明了一个常用的开发技巧————缓存。
在函数内部,缓存一个变量可以减少执行器查找变量的次数,提升执行性能,因为它总是位于这个执行环境的作用域链上的第一位活动对象中。

当调用isType('String')之后,内部函数执行环境的作用域链就有了包含type变量的活动对象,垃圾回收的机制之一就是 判断一个对象是否存在被引用,如果是则不清除。而此时内部函数被isString变量引用,所以在执行完isString(name)后,内部变量type依然存在。

闭包的滥用会导致一些副作用,比如内存溢出、调试困难等。所以要慎用,清除闭包的方法就是消除引用。在该例中,令isString = null 即可清除引用。

总结

闭包在js编程中有很多实用的技巧,这里由于本人精力不济,所以留着下次再说。88


24号来看你
32 声望3 粉丝

我是一名前端开发工程师,我喜欢看恐怖电影~!