最详细的JavaScript高级教程(十九)函数表达式和闭包

这一章我们介绍函数表达式,在开始的时候我们会复习到很多之前学过的知识。

定义

我们之前学过函数提升相关的知识,定义的函数会提升,定义的函数作为变量则不会提升,我们还举例说过下面的代码才能达到效果

var condition = false;
var sayHi = null;
if(condition){
    sayHi = function(){
        alert('true');
    }
}else{
    sayHi = function(){
        alert('false');
    }
}

现在,我们给这种函数定义之后赋值给变量的写法叫函数表达式。

var func = function(arg1, arg2){
    return arg1 + arg2;
}

这种写法是函数表达式最常见的写法,但不是唯一的写法,我们之后还会介绍别的函数表达式的写法。

递归

我们知道递归就是自己调用自己,我们也学习了下面这种写法不好,最好使用arguements.callee()方法以防止函数名称的改变:

function A(){
    A(); 
}
var B = A;
A = null;
B(); // 报错,这种方法不好,在函数中调用了调用了函数名造成了耦合
function A(){
    arguements.callee(); 
}
var B = A;
A = null;
B(); // 可以运行,但是严格模式不允许使用callee

我们发现这两种写法一种不好,好的方法严格模式还不让用,这怎么办呢?我们使用下面的办法:

var func = (function A(){
    A();
}); // 这里的括号写不写都可以,但是写上更好
var B = func;
func = null;
B(); 

闭包的概念

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

闭包的调用原理

我们之前在函数的学习中,学过函数的调用原理,如果不记得了可以复习一下,我们这里看看闭包的调用原理

function createComparisonFunction(propertyName){
    return function(obj1, obj2){
        var value1 = obj1[propertyName];
        var value2 = obj2[propertyName];
        if(value1 < value2){
            return -1;
        }else if(value1 > value2){
            return 1;
        }else{
            return 0;
        }
    };
}
var compare = createComparisonFunction('name');
var result = compare({name : 'Nic'}, {name : 'Bob'});
alert(result);
compare = null; 

我们看到实现的效果是createComparisonFunction执行完毕之后,还可以调用compare,compare会记住之前初始化的name值。我们说,一般在函数执行完毕之后,其活动对象就会被销毁,propertyName只是一个局部变量,为什么一直存在了呢?这就是闭包跟普通函数调用的不同。

下面我们来描述一下上面代码中函数的调用:

  1. 定义了compare,调用了createComparisonFunction,创建了createComparisonFunction的执行环境,以及作用域链,创建了createComparisonFunction函数的活动对象,作用域链上有两个对象,一个createComparisonFunction的活动对象,一个全局活动对象。createComparisonFunction的活动对象中propertyName的值为name
  2. 在createComparisonFunction函数调用完成之后,其执行环境,作用域链都被销毁了,但是由于其返回了一个函数,这个函数的作用域链中引用了createComparisonFunction的活动对象,所以其活动对象无法被销毁。
  3. 当调用compare方法的时候,使用了之前遗留的活动对象,所以name生效了
  4. 在使用完成之后,调用compare = null释放compare对象,才能彻底释放遗留的活动对象

其具体的指向如下图:
在这里插入图片描述

注意:

书中写的是作用域链是在函数调用的时候生成的,按照上面的实验,createComparisonFunction只是返回了函数,其活动对象就无法被销毁了,这是为什么呢?

因为其定义使用的是函数表达式而非函数定义,在使用函数表达式创建函数的时候,因为变量名必须有所指向,所以作用域链,活动对象就都创建出来了。

闭包中的变量问题

我们看下面的代码返回什么:

function createFunctions(){
    var result = new Array();
    for(var i = 0;i < 10; i++){
        result[i] = function(){
            return i;
        }
    }
    return result;
}
for(var i = 0;i < 10; i++){
       alert(createFunctions()[i]()); // 全是10
}

分析一下:

  1. createFunctions返回的是一个函数数组,返回的是一堆函数,但是这一堆函数依赖着同一个createFunctions的活动对象,而i存在于createFunctions的活动对象中,并不存在于每一个返回的函数的活动对象中
  2. 当调用createFunctions()的时候,result数组就被填充完了,填充的过程中,i不断地变大,直到10。所以不管createFunctions()i中的i是几,其返回的值都是10

那么如何返回正确的值呢。其关键在如何让函数返回的i的值存在于各自的活动对象中而不是createFunctions的活动对象中。我们可以使用下面的方法:

function createFunctions(){
    var result = new Array();
    for(var i = 0;i < 10; i++){
        result[i] = function(num){
            return num;
        }(i);
    }
    return result;
}
for(var i = 0;i < 10; i++){
       alert(createFunctions()[i]); // 返回0-10
}

结合上面说的问题,这种方法从根本上解决问题,根本问题是变量在哪个活动对象的问题,通过函数的赋值,我们将i作为变量传给了内部函数的活动对象,由于i是基本类型,传值,所以i被传进了内部函数的活动对象中。

闭包中的this对象

由于活动对象中的this指向的是调用的环境对象,所以调用闭包的函数,this的指向问题很大。我们分析下面的例子:

var name = 'The Window';
var object = {
    name: 'My Object',

    getNameFunc: function(){
        return function(){
            return this.name;
        }
    }
};
alert(object.getNameFunc()()); // The Window

分析:
this返回的是调用的对象,因为这里object.getNameFunc()获取了内部方法,再加一个(),等于在全局对象上调用了方法,所以指向了全局对象window

而实际上,我们认为内部函数是在object中被调用的,我们希望this指向object,为了实现这种情况,我们可以使用that

var name = 'The Window';
var object = {
    name: 'My Object',

    getNameFunc: function(){
        var that = this;
        return function(){
            return that.name;
        }
    }
};
alert(object.getNameFunc()()); // My Object

分析一下:

  1. 在闭包中使用that,其活动对象中没有that,所以到getNameFunc的活动对象中找这个值,找到了之后使用getNameFunc的this属性作为that
  2. 如果想在闭包中访问getNameFunc的arguements对象,也需要这样操作

闭包的内存泄漏问题

IE9之前如果闭包中存着一个HTML元素,那么这个元素不能被销毁。在IE9之后和其他浏览器中没有问题,我们在这里不展开说

闭包的用处:模仿块作用域

在实际的开发中,所有的程序员都在同一个全局作用域下编程,很容易带来潜在的命名冲突,我们应该尽量少的在全局作用域中添加变量,再者,在全局作用域下添加的闭包如果不即时释放,会消耗额外的资源。

我们知道其他语言有块作用域而js没有,其他的语言在for循环的时候定义了i,for循环结束,i就失效了,而js不会。我们还知道,函数是js中唯一可以实现块作用域的方法,一个函数中的作用域才是自己的。这样我们就有了下面的方法来解决问题,模拟块作用域。

(function() {
    // 这是一个模拟的块作用域
    // 这是一个闭包
})();

经过这么一写,包起来的就是自己的作用域,在调用完成之后立即释放。那么这个是不是一个闭包呢?我们学过,闭包是有权访问另一个函数作用域变量的函数,我们看,把这段代码放到任何函数中去,都可以访问这个函数的作用域变量,所以这是一个闭包

我们来看几个应用

  function outputNumber(count) {
    (function() {
      for (var i = 0; i < count; i++) {
        alert(i);
      }
    })();
    alert(i); //报错 i is not defined
  }
  outputNumber(3);

使用这种方法可以:

  1. 限制像全局作用域添加过多的变量和函数,减少命名冲突
  2. 创建私有作用域,开发人员可以使用自己的变量不担心搞乱全局作用域

在ES6的let出现之后我们不需要这么模拟了,可以使用真正的块变量:

{
    let a = 1;
}

闭包的用处:私有变量

  function MyObj(name) {
    this.getName = function() {
      return name;
    };
    this.setName = function(value) {
      name = value;
    };
  }
  var person = new MyObj('wf');
  alert(person.getName()); //wf
  person.setName('s');
  alert(person.getName()); //s

通过对于构造函数中添加get set方法,实现了对于name属性的封装。这个实践实现了私有变量但是这个模式基于构造函数模式,我们之前论证过构造函数模式并不是很好的实现对象的模式,每个实例都会重建get set方法,有额外的资源消耗。所以这个方式并不是很好的方式。

闭包的用处:静态私有变量

我们之前说了不想每次实例化都重建方法,所以我们很自然的想到之前的办法,将set get方法写到构造函数的prototype中,写道构造函数之外。

我们学了闭包,将这一系列操作写入一个闭包中只暴露构造函数不失为一个好办法。

  (function() {
    var name = ''; //这是一个私有变量
    Person = function(value) {
      name = value;
    };
    Person.prototype.getName = function() {
      return name;
    };
    Person.prototype.setName = function(value) {
      name = value;
    };
  })();
  var person1 = new Person('Nic');
  alert(person1.getName()); //Nic

闭包的用处:模块模式

模块模式就是一种为单例创建私有变量和特权方法。这种方法用于对全局单例对象定义私有变量,并且定义方法操作时候使用

var application = function() {
    var component = new Array(); //定义
    component.push(new BaseComponent()); // 初始化
    return {
      //返回单例对象
      getComponent: function() {
        return component.length;
      },
      registerComponent: function(com) {
        if (typeof com == 'object') {
          component.push(com);
        }
      }
    };
}();

闭包的用处:增强模块模式

我们看到之前我们讲的模块模式返回的是一个字面量标识的对象,这个对象使用typeof判断就是个Object,如果我们要求,返回的单例必须是某类型的,这时候我们可以把字面量改为特定类型的实例,也不影响功能,看下面的的代码:

var application = (function() {
    var component = new Array(); //定义
    var app = new BaseComponent(); // 要求返回的必须是BaseComponent类型
    app.getComponent = function() {
      return component.length;
    };
    app.registerComponent = function(com) {
      if (typeof com == 'object') {
        component.push(com);
      }
      return app;
    };
})();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值