资讯专栏INFORMATION COLUMN

js 闭包的使用技巧

dendoink / 3239人阅读

摘要:闭包的学术定义先来参考下各大权威对闭包的学术定义百科闭包,又称词法闭包或函数闭包,是引用了自由变量的函数。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。

前言

上一章讲解了闭包的底层实现细节,我想大家对闭包的概念应该也有了个大概印象,但是真要用简短的几句话来说清楚,这还真不是件容易的事。这里我们就来总结提炼下闭包的概念,以应付那些非专人士的心血来潮。

闭包的学术定义

先来参考下各大权威对闭包的学术定义:

wiki百科

闭包,又称词法闭包或函数闭包,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

其实这个定义就一句话:“闭包是引用了自由变量的函数”,后面的都是这句话的解释。如果你对上一章中的内部函数作用域链有引用type变量的例子还有印象的话,那么在这里你会感觉好像是这么一会一回事。虽然我们不知道自由变量的明确定义,但我们能感觉到type的值就是这个自由变量。
那究竟什么是自由变量?在一个作用域中使用某个变量,而不声明该变量,那么对这个作用域来说,该变量就是一个自由变量。

JavaScript 权威指南

函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为闭包。

这句话有一个关键词:“变量保存”,这确实是js闭包的一大特性。内部函数通过对自由变量的引用,再将自己的引用返回出去(内部函数),达到内部函数保存变量的效果。

JavaScript 高级程序设计

闭包是指有权访问另一个函数作用域中的变量的函数。

这里没有指明另一个函数就是嵌套函数的内部函数。事实上,在js中,只有内部函数有权访问外部函数作用域的变量。(这是由作用域链的查找机制决定的)

让我再结合上节的例子来看下:

function isType(type){
    return function(obj){    //返回一个匿名函数引用
        return Object.prototype.toString.call(obj) == "[object "+ type + "]";    //匿名函数内部保有对自由变量的type的引用
    }
}


var isFunction = isType("Function"); //匿名函数的引用数 1
var isString = isType("String");    //匿名函数的引用数 2

//测试
var name = "Tom";
isString(name)//true
我对闭包的理解

如果 一个内部函数保有对外部作用域变量的引用 并且 这个内部函数也被引用 时,那么无论在什么执行环境下,这个被引用的变量将和这个函数一同存在。那个这个函数就是闭包。

js 闭包技巧 闭包引用带来的问题

下面我来看一道关于闭包的经典面试题,1秒后打印所有的迭代次数。通常我们可能会写出下面这样的代码:

function timeCount() {
    for (var i = 1; i < 5; i++) {
        setTimeout(function(){
            console.log(i)
        },1000)
    }
}

timeCount();    //5 5 5 5 5

事实上这个例子,并不是关于闭包的技巧,相反它是由闭包特性带来的问题。理解这个问题有助于我们理解闭包。首先我们来看导致这个问题的原因:
1.setTimeout为异步任务;
2.回调函数中的i只有一个引用;

异步任务意味着它并不会马上执行,而是被推到一个异步任务队列中等待执行,直到js线程任务执行完后才会去执行这个队列中的任务。(类似的异步任务还有dom的交互事件绑定)
也就是说,当每次执行循环体的setTimeout方法时,js执行器并没有马上执行而是将其推入异步任务队列中。当5次循环执行完后,js线程再去执行异步队列中的任务(此时的i就是5了)。
解决的方法也很简单,那就是不使用i的引用,直接使用i的副本。那怎么使用i的副本?
《JavaScript高级程序设计》中提到,所有函数的参数都是按值传递的,什么意思?比如有一个函数 function add(num){},当我调用这个函数时 add(i), 在add函数内部变量i 不再是外部函数i的引用,而是一个独立存在的 与i的值相等的变量。这也就达到了复制i的作用。
(function(){})()是匿名函数的自执行写法。

function timeCount() {
    for (var i = 1; i < 5; i++) {
        (function(i){
            setTimeout(function(){
                console.log(i)
            },1000)
        })(i)
    }
}

timeCount();    //1 2 3 4 5

有闭包的bug一般都比较隐匿,这会增加调试的难度。这也就是为什么很多老手都不推荐大量使用闭包的原因之一,还有一个就是不释放变量的内存空间。

模拟私有成员

在JavaScript中是没有私有成员的概念,不能使用private关键字声明,所有的属性都是公有的。所以人们在JavaScript编程通常用两种方法来规定私有成员:
1.私有成员以下划线的方式命名;
2.利用闭包来模拟私有成员;
第一种方法是最简单的,而且效果还可以的方法,它的实现完全靠程序员的自觉性,很难保证不会有人刻意去使用私有成员。第二种方法虽然有点难理解,但它确实有效地实现了私有成员的保护。虽然js没有私有成员的概念,但是函数有私有变量的概念,函数外部不能访问私有变量。所以我们可以利用闭包的特性,创建访问私有变量的公有方法(特权方法)。

function Person(value) {
    var name = value;
    this.setName = function(newName) {
        name = newName;
    };
    this.getName= function() {
        return name;
    };
}

var tom = new Person("Tom");
console.log(tom.getName()); // Tom

利用闭包,我们可以通过特权方法来获取和修改私有变量,从而达到约束和规范代码的作用,这在大型应用开发中尤为重要。但是这种写法还需要改进,我们希望实例能够共享实例方法,而不是通过复制来得到这些方法的使用:

(function() {
    var name;
    Person = function(value){ //不声明变量person,使其可以在全局被访问
        name = value;
    };    
    Person.prototype = {
        setName: function (newName) {
            name = newName;
        },
        getName: function() {
            return name;
        }
    }
})()

var tom = new Person("Tom");
console.log(tom.getName()); // Tom

创建一个匿名自执行函数,是为了得到一个静态私有作用域,在这个静态作用域中创建的name变量,这样既可以保证它的数据安全,也能被实例方法所访问。

函数的柯里化

柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。---来自wiki百科
这里有一点要注意,单一参数并不是指一个参数,而是最初函数的参数(外部函数),可以是多个。
它的理论依据是,如果你固定某些参数,你将得到接受余下参数的一个函数。这很容易理解,如果我们有一个二元一次方程,z = x + y;我固定x的值为3,这个方程就变成了一元一次方程,z = 3 + y;
柯里化的过程是清楚了,但是他的目的是什么呢,我们为什么要固定一个参数返回一个新函数?为什么不直接定义一个新函数呢?
如果直接定义一个新函数,原来的参数变成函数内部固定的私有变量,这样一来虽然特定的功能完成了,但是代码的通用性却降低了。基于这个应用场景创造的新函数,换了一个相似的应用场景(只是参数的改变)却不得不重新定义一个新函数,造成了代码的重复。
通用性的增强必然导致适用性的降低,柯里化就是这么一个过程,将原本接受多个参数的函数(因为多个参数,自然适应的业务场景就多,通用性也就强),转为接受少个参数的新函数(参数少,应用的场景也就更明确,适用性也就强)。 这么一来,通过柯里化,开发者便可掌握代码的通用性和适用性之间的平衡。

这个理解起来可能有点吃力,毕竟柯里化是属于函数式编程里的重要技巧,一般像我们这种习惯面向对象开发的人确实会比较难以领会它的精髓。

单例模式

单例模式的定义是产生一个类的唯一实例,很多js的开发者认为,类似Java那种单例模式的创造方式在JavaScript中没有必要。因为在js中,不需要实例化也可创建对象,只要直接全局作用域创建一个字面量对象,以便整个系统访问。
单例模式在js中的应用场景确实也不算多,主要应用在框架层,而大多数js的开发者是从事应用层的开发,所以接触不多。比如一个遮罩层的创建,为确保一次只有一个遮罩层,使用单例模式是最好的选择。

var singleton = function( fn ){
    var result;
    
    return function(){
        return result || ( result = fn .apply( this, arguments ) );
    }
}

var createMask = singleton(
    function(){
        return document.body.appendChild( document.createElement("div") );
    }
)
函数绑定

这一个技巧放在最后讲,是因为ES5规定了对原生函数绑定方法的实现——Function.prototype.bind。使用闭包来绑定this变量的hack技术已经退出历史舞台,但是老版的IE浏览器依然在使用这种技术来实现函数的绑定。
先来看一个场景

var tip = {
    name: "jack",
    say: function() {
        alert(this.name)
    }
}

btn.onclick = tip.say();    // 输出 "",因为window对象存在name属性,是一个空字符串

在注册事件中的事件处理程序没有绑定执行环境,所以当触发事件处理程序时,this指向正在执行的环境对象,在这里是全局对象window。最常见的解决方法就是绑定他的执行环境对象

btn.onclick = tip.say().bind(tip);    // jack

还有一种方法,就是利用apply+闭包来达到绑定效果,apply将事件处理程序与正确的环境对象绑定,再将绑定后的函数返回赋值给事件处理程序。它常用作不支持原生bind方法的兼容性处理。

if (!Function.prototype.bind) {
  Function.prototype.bind = function (oThis) {
    if (typeof this !== "function") {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var aArgs = Array.prototype.slice.call(arguments, 1), 
        fToBind = this, 
        fNOP = function () {},
        fBound = function () {
          return fToBind.apply(this instanceof fNOP
                                 ? this
                                 : oThis || this,
                               aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();

    return fBound;
  };
}

调用方式与原生bind相同。

总结

闭包的技巧就介绍到这,更多的技巧还需要我们去开发中发现、领会并运用。下一章,我们来聊一聊js中最强大的属性之一——prototype。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/86522.html

相关文章

  • JS闭包是什么?

    摘要:大名鼎鼎的闭包面试必问。闭包的作用是什么。看到闭包在哪了吗闭包到底是什么五年前,我也被这个问题困扰,于是去搜了并总结下来。关于闭包的谣言闭包会造成内存泄露错。闭包里面的变量明明就是我们需要的变量,凭什么说是内存泄露这个谣言是如何来的因为。 本文为饥人谷讲师方方原创文章,首发于 前端学习指南。 大名鼎鼎的闭包!面试必问。请用自己的话简述 什么是「闭包」。 「闭包」的作用是什么。 首先...

    Enlightenment 评论0 收藏0
  • js闭包本质

    摘要:也正因为这个闭包的特性,闭包函数可以让父函数的数据一直驻留在内存中保存,从而这也是后来模块化的基础。只有闭包函数,可以让它的父函数作用域永恒,像全局作用域,一直在内存中存在。的本质就是如此,每个模块文件就是一个大闭包。 为什么会有闭包 js之所以会有闭包,是因为js不同于其他规范的语言,js允许一个函数中再嵌套子函数,正是因为这种允许函数嵌套,导致js出现了所谓闭包。 function...

    qianfeng 评论0 收藏0
  • 详解js闭包

    摘要:但闭包的情况不同嵌套函数的闭包执行后,,然后还在被回收闭包会使变量始终保存在内存中,如果不当使用会增大内存消耗。每个函数,不论多深,都可以认为是全局的子作用域,可以理解为闭包。 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 闭包的特性 闭包有三个特性: 1.函数嵌套函数 2.函数内部可以引用外部的参数和变量 3.参数和变量不会...

    Chiclaim 评论0 收藏0
  • JS脚丫系列】重温闭包

    摘要:内部的称为内部函数或闭包函数。过度使用闭包会导致性能下降。,闭包函数分为定义时,和运行时。循环会先运行完毕,此时,闭包函数并没有运行。闭包只能取得外部函数中的最后一个值。事件绑定种的匿名函数也是闭包函数。而对象中的闭包函数,指向。 闭包概念解释: 闭包(也叫词法闭包或者函数闭包)。 在一个函数parent内声明另一个函数child,形成了嵌套。函数child使用了函数parent的参数...

    MartinDai 评论0 收藏0
  • js闭包理解

    摘要:一般来讲,函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域,但是闭包的情况有所不同理解闭包的前提先理解另外两个内容作用域链垃圾回收作用域链当代码在执行过程中,会创建变量对象的一个作用域链。 闭包是javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包来实现。个人的理解是:函数中嵌套函数。 闭包的定义及其优缺点 闭包是指有权访问另一个函数作用域中的变量的...

    EasonTyler 评论0 收藏0
  • 谈谈我所理解闭包js、php、golang里closure

    摘要:当初看这个解释有点懵逼,理解成闭包就是函数中的函数了。里的闭包最近不满足于只干前端的活,开始用起了。里的闭包最近在学习语言,让我们来看一下语言里的闭包。在中,闭包特指将函数作为值返回的情况,被返回的函数引用了生成它的母函数中的变量。 本人开始接触编程是从js开始的,当时网上很多人说闭包是难点,各种地方对闭包的解释也是千奇百怪。如今开始接触js以外的各种编程语言,发现不光是js,php、...

    betacat 评论0 收藏0

发表评论

0条评论

dendoink

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<