有趣的闭包

在javascipt的学习和编码过程中,闭包是一个特别重要的知识点,学好和弄懂了闭包对于后续的开发过程非常有用。

定义

闭包是指这样的作用域,它包含有一个函数,这个函数可以调用被这个作用域所封闭的变量、函数或者闭包等内容。通常我们通过闭包所对应的函数来获得对闭包的访问。
为了便于理解,我们先来看看下面的demo

function foo() {
  var a = 2;
  function bar() {
    console.log(a);
  }
  return bar;
}
var baz = foo();
baz(); 

函数foo执行后,通常会期待foo的整个内部作用域被销毁,因为引擎有垃圾回收机制来释放不再使用的内存,看上去foo的内容不再被使用,所以会自然的考虑对其进行回收。闭包的神奇之处在于他可以阻止这件事情的发生,事实上执行foo后,其内部作用域依然存在,因为外部的baz保留着对内部作用域中bar函数的引用,并且bar能够访问foo作用域内的变量,函数等。

接下来,我们再看看下面的例子

function foo() {
  var a = 2;
  function bar() {
    console.log(a);
  }
  bar(); //2
}

上面的定义算闭包么,从技术上来看,是闭包,但是考虑到闭包的实际用途,不算闭包。当函数可以记住并访问所在的词法作用域时, 就产生了闭包, 即使函数是在当前词法作用域之外执行。重点是记住作用域,并在词法作用域之外访问词法作用域内的变量和函数。上面的例子,内部bar的执行在当前词法作用域内就执行了,没有体现出闭包的价值。

继续往下走,你觉得立即执行的函数算闭包么。

(function () {
  var a = 1;
  function run() {
     console.log(a); 
  }
  run();
})();

根据上面的解释,从技术上来说,是闭包,但是考虑到实际的用途,不算闭包,如果做如下更改,那不管怎么说,他都是闭包。

(function () {
  var a = 1;
  setTimeout(function run() {
    console.log(a);
  }, 0);
})();

深入到引擎的内部原理中,内置的工具函数 setTimeout(..) 持有对一个参数的引用,这个参数也许叫作 fn 或者 func,或者其他类似的名字。引擎会调用这个函数, 在例子中就是内部的run函数,而词法作用域在这个过程中保持完整。

实际上闭包在我们的代码中随处可见,比如在定时器,事件监听器,ajax请求中都可以找到闭包的影子。

循环和闭包

为了说明闭包,不得不谈谈for循环

for(var i = 0; i <= 5; i++) {
    setTimeout(function timer() {
        console.log(i);
    }, 0);
}

正常情况下,我们期待着能输出0-5,但是事与愿违,我们得到的是6个6。事实上延迟函数中的回调会放在另一个任务队里中,待当前的js任务执行结束之后,才会去执行,换句话说,timer回调的执行时异步的,会在for循环结束以后才执行,那么问题就来了,此时的i是多少呢?没错,就是6,所以我们得到了6个6。

我们会假设循环中的每个迭代在执行时会捕获一个i的副本,但是根据作用域的工作原理,尽管循环中的每个函数都是在迭代中分别定义的,但是他们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。

这时候聪明的你,或许已经想到了,我们可以通过立即执行的函数来创建一个私有所有用。

for (var i= 1; i<= 5; i++) { 
  (function(j) { 
    setTimeout(function timer() { 
        console.log(j); 
    }, 0);
  })(i);
}

通过上面的修改,我们就可以输入6个6了。每次迭代,立即执行函数都会生成一个新的作用域,这个作用域就是一个闭包,通过闭包内的timer函数,我们可以访问到作用域中的保存的变量i.

仔细分析我们的解决方案,每次迭代都创建了一个新的作用域,换句话说,每次迭代我们都需要一个块作用域。ES6中的let申明,正好可以完成这项任务。

for (var i= 1; i<= 5; i++) { 
  let j = i;
  setTimeout(function timer() { 
        console.log(j); 
    }, 0);
}

当然了,我们还可以这样写

for (let i= 1; i<= 5; i++) { 
  setTimeout(function timer() { 
        console.log(i); 
    }, 0);
}

不过,后一种写法,有一个特殊的行为。for循环头部的let声明,每次迭代都会声明,以后的每次迭代都会使用上一次迭代结束时的值来初始化这个变量。

模块

模块化可以算得上是闭包最好的应用了

function model() {
    var name = 'jason';
    function getName() {
        return name;
    }
    function setName(newName) {
      name = newName;
    }
    //public api
    return { 
        getName,
        setname
    }
}

稍微改造一下,就可以变成单例模式

var model = (function () {
    var name = 'jason';
    function getName() {
        return name;
    }
    function setName(newName) {
      name = newName;
    }
    //public api
    return { 
        getName,
        setname
    }
})();

闭包中的this问题

或许你会从各种书籍或者博客中看到,闭包中的this指向window,真的是这样么,真的指向window么?

function foo() {
        function() {
        console.log(this);
    }   
}
var bar = foo();
bar() // 输出window对象
function foo() {
        var o = { a: 1};
        function bar() {
        console.log(this);
    }
        return bar.bind(o); 
}
var baz = foo();
baz() // 输出o  {a: 1}

this是一个有趣的问题,我们不能简单的认为闭包中的this指向window,除了上面的例子,还有DOM事件回调中的this,也不指向window,关于this的问题,以后会单独讨论,这次就不赘述了。

性能问题

如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用,方法都会被重新赋值一次,也就是说,为每一个对象的创建。

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

应该做如下更改

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};

贪玩儿的小灰灰感谢您的耐心阅读,如果有问题记得留言哦 =_=

公众号 | 贪玩儿的小灰灰


  • 每周都会推送原创、有用、有趣的旅行图文和攻略。

你可能感兴趣的:(有趣的闭包)