手记

「闭包」攻略

$ 闭包是什么?

闭包是函数和声明该函数的词法环境的组合,是指有权访问另一个函数作用域中变量的函数。可能听了会蒙圈,继续如下解释:

  JS的闭包其实是对JS函数作用域的一种利用。在函数作用域中定义的变量,由于只存在函数自身的内存栈中,同样,在浏览器垃圾回收机制作用下,这部分变量不能被函数外界的所访问引用,而在同一个作用域中定义的函数却可以访问相同作用域下定义的变量。

  因此,创建闭包的关键之处就在于,我们可以在函数作用域内部定义一个可以访问该作用域下的所有变量的子函数,最终将这个子函数return出来。利用子函数对这些函数内部变量的调用,以达到外界访问函数内部变量的效果。而这个子函数和其所在的词法环境环境,就叫做闭包

$ 闭包有什么特性?

  其实上方也提及到了,主要有以下三个特性:

  1. 在函数内再嵌套函数,这也是函数最直接的展示

  2. 内部函数可以引用外层函数作用域内的参数和变量

  3. 被引用的参数和变量不会被垃圾回收机制回收

$ 为什么要有闭包?

  要解释这个问题时,你可能会把什么是闭包中的内容赘述一遍,答得不算错,但并不准确,缺少的是对闭包形成机制部分内容的解释,为理解需要先拓展一下知识:

  JS引擎在工作时有两个阶段:语法检查阶段运行阶段;而运行阶段又分为预解析阶段和执行阶段。当语法检查阶段执行错误时,浏览器会放弃运行阶段直接报错,也就是我们在coding的时候常见的一些报红。

在预解析阶段,先创建执行上下文,执行上下文包括变量对象,作用域链 和 this 值。来理解一下这三个对象。

  1. 变量对象Variable object
      JS中,使用var声明变量,使用function声明函数、及当前函数的形参(形参是不需要声明但要依附于函数存在的特殊变量)

  2. 作用域链
      当前变量对象 + 所有父级作用域[[scope]]。它其实就是一个变量对象的链,Active Object即AO,函数创建后就有静态的[[scope]]属性(可以打印在控制台中查看当前状态AO中有哪些属性),直到该函数中被销毁。在部分文章中也提及叫内存栈。理解不一,意思相同。

  3. this
      进入执行上下文后,将不再改变。

创建执行上下文后,会对变量对象AO的属性进行首次填充。所谓的属性,就是var, function 及函数形参名。而他们的值,变量的值为undefined, 函数的值为函数定义,形参的值为实参,还没有传入实参的则为undefined

  与解析阶段完成之后,进入执行代码阶段。此时,执行上下文有个Scope属性(区别于函数的[[scope]]属性)。

Scope = 当前AO.concat([[scope]])

  JS解析器逐行读取并执行代码,变量对象中的属性值可能因赋值语句而改变。当我们查询外部作用域的变量时,其实就是沿着作用域链,依次在这些变量对象里遍历标志符,直到最后的全局变量对象。

  好了,我们再回到闭包问题,先看看闭包的代码表现:

function outer() {    var  a = 5;    return function inner () {        return a;
    }
}var getInnerData = outer();console.log(getInnerData);  // function inner() {return a;}var innerData = getInnerData(); 
console.log(innerData);  // 5

  以上代码中,getInnerData函数就是一个闭包。函数执行时,其上下文有个Scope属性,该属性作为一个作用域链包含有该函数被定义时所有外层的变量对象的引用,所以定义了闭包的函数虽然销毁了,但是其变量对象依然被绑定在函数 inner 上,保留在内存中。

  事实上,只要代码保持对getInnerDate 函数的引用,函数自身的[[scope]]属性就绑定着闭包的活动对象。


  但要留意的是,基于js的垃圾回收机制,outer 的变量对象里,只有仍被引用的变量会继续保存在内存中:




第二日补充】以上内容获取并不能满足你对闭包完全了解的需求,接着往下看:

$ 实用的闭包

  再来回顾一下闭包的代码表现

function makeFunc() {    var name = "Mozilla";    function displayName() {        return name;
    }    return displayName;
}var myFunc = makeFunc();var myName = myFunc();console.log(myName );  // Mozilla

  通过执行外部函数myFunc()调用到了makeFunc()的函数内部变量name,这就是闭包中所形容的:有权访问另一个函数作用域中变量的函数。

  吃瓜群众的我们会认为,函数中的局部变量仅在函数的执行期间可用。一旦 makeFunc() 执行完毕,我们会认为 name 变量将不能被访问。然而,这里的name为什么还能访问呢?

  原因是,JavaScript中的函数会形成闭包。 闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量。在我们的例子中,myFunc 是执行 makeFunc 时创建的 displayName 函数实例的引用,而 displayName实例仍可访问其词法作用域中的变量,即可以访问到name 。由此,当 myFunc 被调用时,name 仍可被访问,其值 Mozilla 就被传递到console中。

  还有个更有意思的例子:

function makeAdder(x) {  return function(y) {    return x + y;
  };
}var add5 = makeAdder(5);var add10 = makeAdder(10);console.log(add5(2));  // 7console.log(add10(2)); // 12

  在这个示例中,我们定义了 makeAdder(x) 函数,它接受一个参数 x ,并返回一个新的函数。返回的函数接受一个参数 y,并返回x+y的值。

  从本质上讲,makeAdder 是一个函数工厂 — 他创建了将指定的值和它的参数相加求和的函数。在上面的示例中,我们使用函数工厂创建了两个新函数 — 一个将其参数和 5 求和,另一个和 10 求和。

add5add10 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 add5 的环境中,x5。而在 add10 中,x 则为 10

  以上只是闭包的基本应用与代码表现,那闭包到底实用在哪儿呢?

闭包允许将函数与其所操作的数据(环境)关联起来。这显然类似于面向对象编程,在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或多个方法相关联

  因此,通常你使用只有一个方法的对象的地方,都可以使用闭包。

  假设我们有这么一个需求,在界面上我们有三个按钮来调整页面字体大小,通常我们会这么做

body {  font-family: Helvetica, Arial, sans-serif;  font-size: 12px;
}h1 {  font-size: 1.5em;
}h2 {  font-size: 1.2em;
}

  设置一个根元素的字体大小,其他部分则根据根元素的字体大小来计算出相对值。通过DOM的绑定来实现对根元素大小的调整。

function makeSize(size) {    return function() {        document.body.style.fontSize = size + 'px';
    }
}var size12 = makeSize(12);var size14 = makeSize(14);var size18 = makeSize(18);

  此时定义好了三个调整字体大小的闭包,我们将其绑定到页面的点击事件上,这样就实现了我们的需求。

document.getElementById('size-12'). = size12;document.getElementById('size-14'). = size14;document.getElementById('size-18'). = size18;

$ 用闭包模拟私有方法

  在java中,是支持将方法声明为私有的,即,他们只能被同一个类中的其他方法所调用。

  而javascript中,没有这种原生支持,但我们可以使用闭包来模拟私有方法。私有方法不仅仅有利于限制对代码的访问,还提供了管理全局命名空间的强大能力,避免了非核心的方法弄乱了代码的公共接口部分。

  下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量.
这个方式也称为 【模块模式】

var Counter = (function() {    var privateCounter = 0;    function changeBy(val) {
        privateCounter += val;
    }    return  {        increment: function() {
            changeBy(1);
        },        decrement: function() {
            changeBy(-1);
        },        value: function() {            return privateCounter;
        }
    }
})();console.log(Counter.value());   // 0Counter.increment();
Counter.increment();console.log{Counter.value()};  // 2Counter.decrement();console.log(Counter.value());  // 1

  仔细观察,在之前的示例中,每个闭包都有它自己的词法环境;而这次我们只创建了一个词法环境,为三个函数所共享:Counter.incrementCounter.decrementCounter.value

  该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。

这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。

你应该注意到我们定义了一个匿名函数,用于创建一个计数器。我们立即执行了这个匿名函数,并将他的值赋给了变量counter。我们可以把这个函数储存在另外一个变量makeCounter中,并用他来创建多个计数器。

var makeCounter = function() {    var privateCounter = 0;    function changeBy(val) {
        privateCounter += val;
    }    return {        increment: function() {
            changeBy(1);
        },        decrement: function() {
            changeBy(-1);
        },        value: function() {            return privateCounter;
        }
     }  
};var Counter1 = makeCounter();var Counter2 = makeCounter();console.log(Counter1.value()); /* logs 0 */Counter1.increment();
Counter1.increment();console.log(Counter1.value()); /* logs 2 */Counter1.decrement();console.log(Counter1.value()); /* logs 1 */console.log(Counter2.value()); /* logs 0 */

  请注意两个计数器 counter1counter2 是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 privateCounter

  也就是说, 每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。

以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。

$ 在循环中创建闭包:一个常见错误

  在ECMAScript 2015(ES6) 引入let关键字之前,在循环中有一个常见的闭包创建问题。参见以下示例:

<p id="help">Helpful notes will appear here</p><p>E-mail: <input type="text" id="email" name="email"></p><p>Name: <input type="text" id="name" name="name"></p><p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {    document.getElementById('help').innerHtml = help;
}function setupHelp() {    var helpText = [
        {'id': 'email', 'help': 'Your e-mail address'},
        {'id': 'name', 'help': 'Your full name'},
        {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ]
}for (var i =0; i< helpText.length; i++) {    var item = helpText[i];    document.getElementById(item.id).onfocus = function() {
        showHelp(item.help)
    }
}

setupHelp();

  该例试图通过helpText的定义及for循环遍历来实现当点击某个控件时显示提示信息

  运行这段代码后,发现它没有达到想要的效果。无论焦点在哪个input上,显示的都是关于年龄(最后一项)的信息。

  原因是,赋值给onfocus的是闭包,这些闭包是由他们的函数和定义在setupHelp作用域中捕获的环境所组成的,这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item,当onfocus的回调执行时,item.help的值被决定。由于循环在事件触发之前早已执行完毕变量对象item(被三个闭包所共享)已经指向了helpText的最后一项。

  解决办法是使用更多的闭包,一种办法就是使用之前提到的工厂函数

function showHelp(help) {  document.getElementById('help').innerHTML = help;
}function makeHelpCallback(help) {  return function() {
    showHelp(help);
  };
}function setupHelp() {  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];  for (var i = 0; i < helpText.length; i++) {    var item = helpText[i];    document.getElementById(item.id).onfocus =
       makeHelpCallback(item.help);
  }
}

setupHelp();

  另一种办法是使用匿名闭包

function showHelp(help) {  document.getElementById('help').innerHTML = help;
}function makeHelpCallback(help) {  return function() {
    showHelp(help);
  };
}function setupHelp() {  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];  for (var i = 0; i < helpText.length; i++) {    var item = helpText[i];    document.getElementById(item.id).onfocus =
       makeHelpCallback(item.help);
  }
}

setupHelp();

  为了避免过多的使用闭包,也可以使用let来解决这个问题

for (var i = 0; i < helpText.length; i++) {    let item = helpText[i];    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }

  这个例子使用let而不是var,因此每个闭包都绑定了块作用域的变量,这意味着不再需要额外的闭包。

$ 对性能的考量

  如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。

   例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是,每个对象的创建)。

  考虑以下例子:

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

  这个例子中,并没有使用到闭包,劣势是外部函数无法访问到MyObject内部的方法getNamegetMessage。为此我们可以改成如下形式:

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;
};

  在前面的两个示例中,继承的原型可以为所有对象共享,不必在每一次创建对象时定义方法



作者:果汁凉茶丶
链接:https://www.jianshu.com/p/465e5155caec


0人推荐
随时随地看视频
慕课网APP