JS笔记「闭包」V03

312 阅读6分钟

<<你不知道的JavaScript>>上卷

  • 定义:当函数可以记住并访问所在词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行
  • 小栗子:
  function foo() {
    var a = 2;
    function bar() {
      console.log(a) // 2
    }
    bar()
  }
  foo()

是不是有点像嵌套作用域,函数 bar() 可以访问外部作用域中的的变量a。

  • 这个是闭包吗?
function foo() {
  var a = 2;
  function bar() {
    console.log(a)
  }
  return bar;
}
var baz = foo()
baz() // 2 -- 这就是闭包的效果

JS引擎有垃圾回收器来释放不在使用的内存空间,通常情况下 foo() 会被回收,然而以上例子显然不能进行回收,因为有闭包的存在,导致它会阻止这件事情的发生,使得它的内部作用域依然存在,故没有被回收。

  • bar() 依然持有对该作用域的引用,而这个引用就叫做闭包,所以最后 baz是可以访问变量 a 的。
  function foo() {
   var a = 2
   function baz() {
     console.log(a) // 2
   }
   bar(baz)
  }
  function bar(fn) {
    fn() // 这不就是闭包
  }
  foo()

把 baz 传递给 bar ,当我们调用这个内部函数时(fn),访问了 a

var fn
function foo() {
  var a = 2
  function baz() {
    console.log(a)
  }
  fn = baz 
}
function bar() {
  fn() // 闭包
}
foo()
bar() // 2

将传递内部函数传递到所在的词法作用域外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

  • 那是不是说?
function wait(message) {
  setTimeout(function timer() {
    console.log(message)
  },1000)
}
wait("Hellow,closure!")

timer将值传递给setTimeout(...),timer具有涵盖wait(...)作用域的闭包,因此还保留对变量message的引用,wait(...)执行1000毫秒后,它的内部作用域不会消失,timer函数依然保持有wait(...)作用域的闭包。

function setupBot(name, selector) {
  $(selector).click(function activator() {
    console.log("Activating:" + name)
  })
}
setupBot("Closure Bot 1", "#bot_1")
setupBot("Closure Bot 2", "#bot_2")

定时器、事件监听器、Ajax请求、跨窗口通信、Wed Workers 或者其他的异步(或者同步)任务中,只要使用回调函数,实际上就是在使用闭包!

  • 它是闭包吗?
var a = 2
(function IIFE() {
  console.log(a)
})()

从严格意义来说它并不是闭包,因为 IIFE 并不是在它本身的词法作用域以外执行的。

那到底什么是闭包?

在构造函数体内定义另外的函数作为目标对象的方法函数,而这个对象的方法函数反过来引用外层函数体中的临时变量。这使得只要目标对象在生存期内始终能保持其方法,就能间接保持原构造函数体当时用到的临时变量值。尽管最开始的构造函数调用已经结束,临时变量的名称也都消失了,但在目标对象的方法内却始终能引用到该变量的值,而且该值只能通这种方法来访问。即使再次调用相同的构造函数,但只会生成新对象和方法,新的临时变量只是对应新的值,和上次那次调用的是各自独立的。

  • 作用:能够在函数定义的作用域外,使用函数定义作用域内的局部变量,并且不会污染全局。

  • 原理:基于词法作用域链和垃圾回收机制,通过维持函数作用域的引用,让函数作用域可以在当前作用域外被访问到,也就是说闭包就是能够读取其他函数内部变量的函数。

作用域

  • 作用域:用于确定在何处以及如何查找变量(标识符)的一套规则。
  • 词法作用域:词法作用域是定义在词法阶段的作用域。词法作用域是由写代码时将代码和块作用域写在哪里来决定的,因此当词法作用域处理代码是会保持作用域不变(大部分情况)。
  • 块作用域:指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常用{}包裹)。常见的块级作用域有 with,try/catch,let,const 等。
  • 函数作用域:属于这个函数的全部变量都可以在整个函数范围内使用及复用(包括嵌套作用域)。
  • 作用域链:查找变量时,先从当前作用域开始查找,如果没有找到,就会到父级(词法层面上的父级)作用域中查找,一直找到全局作用域。作用域链正是包含这些作用域的列表。

闭包的用途

  1. 可以读取函数内部的变量,并且保护函数内的变量安全,无法通过其他途径访问到,因此保护了变量的安全性。
  2. 在内存中维持一个变量,不会被垃圾回收。
function f1(){
    var n=520;
    function f2(){
      console.log(n); // 520
    }
  }

f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

  1. 通过保护变量的安全实现JS私有属性和私有方法(不能被外部访问)推荐阅读:javascript.crockford.com/private.htm… 私有属性和方法在Constructor外是无法被访问的
function Constructor(...) {
    var that = this;
    var membername = value;
    function membername(...) {...}
}

闭包的缺点

  • 不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包把变量存在内存,导致处理速度和内存消耗方面对脚本性能具有负面影响,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
  • 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。 例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。
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;
  }
};

但我们不建议重新定义原型。可改成如下例子:

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

参考文献:

  1. MDN
  2. 你不知道的 JavaScript(上卷)
  3. 维基百科
  4. web.archive.org/web/2010082…
  5. www.cnblogs.com/cxying93/p/…