跟着月影学JS | 青训营笔记

54 阅读5分钟

这是我参与「第五届青训营」笔记创作活动的第4天。JavaScript语言变化多端,内容丰富

一、本堂课重点内容:

JS闭包、JS组件封装

二、详细知识点介绍:

JS闭包

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

词法作用域

请看下面的代码:

function init() {
  var name = "Mozilla"; // name 是一个被 init 创建的局部变量
  function displayName() { // displayName() 是内部函数,一个闭包
      alert(name); // 使用了父函数中声明的变量
  }
  displayName();
}
init();

init() 创建了一个局部变量 name 和一个名为 displayName() 的函数。displayName() 是定义在 init() 里的内部函数,并且仅在 init() 函数体内可用。请注意,displayName() 没有自己的局部变量。然而,因为它可以访问到外部函数的变量,所以 displayName() 可以使用父函数 init() 中声明的变量 name

运行该代码后发现, displayName() 函数内的 alert() 语句成功显示出了变量 name 的值(该变量在其父函数中声明)。这个 词法作用域 的例子描述了分析器如何在函数嵌套的情况下解析变量名。词法(lexical)一词指的是,词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。

闭包

现在来考虑以下例子:

function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name);
    }
    return displayName;
}
​
var myFunc = makeFunc();
myFunc();

运行这段代码的效果和之前 init() 函数的示例完全一样。其中不同的地方(也是有意思的地方)在于内部函数 displayName() 在执行前,从外部函数返回。

第一眼看上去,也许不能直观地看出这段代码能够正常运行。在一些编程语言中,一个函数中的局部变量仅存在于此函数的执行期间。一旦 makeFunc() 执行完毕,你可能会认为 name 变量将不能再被访问。然而,因为代码仍按预期运行,所以在 JavaScript 中情况显然与此不同。

原因在于,JavaScript 中的函数会形成了闭包。 闭包 是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。在本例子中,myFunc 是执行 makeFunc 时创建的 displayName 函数实例的引用。displayName 的实例 维持了一个对它的词法环境(变量 name 存在于其中)的引用。因此,当 myFunc 被调用时,变量 name 仍然可用,其值 Mozilla 就被传递到alert中。

下面是一个更有意思的示例 — 一个 makeAdder 函数:

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

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

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

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

在我们的授课中的一个例子

使得某个函数只运行一次的 once 函数.

function once(fn) {
  return function(...args) {
    if(fn) {
      const ret = fn.apply(this, args);
      fn = null;
      return ret;
    }
  }
}
​
const foo = once(() => {
  console.log('bar');
});
​
foo();
foo();
foo();

根据上面的解释,这里就是利用了函数的闭包。

foo 是执行 once 时,once 函数创建并返回的内层函数实例的引用。该返回的函数的实例 维持了一个对它的词法环境(变量 fn 存在于其中)的引用。因此,当 foo 第一次被调用时,变量 fn 仍然可用,其值为即 once 传入的参数 fn(类型是方法),而在执行过一次后,词法环境中的变量 fn 被赋值为 null,之后就不会再次触发函数主体。

而如果给 foo 传入了参数,那么就会在利用不定长参数展开 ...arg 和内层的 fn.apply(this, args) 来实现参数的利用。

buttons.forEach((button) => {
    button.addEventListener('click', once((evt) => {
        const target = evt.target;
        target.parentNode.className = 'completed';
        setTimeout(() => {
            list.removeChild(target.parentNode);
        }, 2000);
    }));
});

三、实践练习例子:

我们在react中也经常遇到这样的例子。react不同于vue,当绑定的函数需要传递参数时,vue中是

<Component event=handleEvent(arg) />

而react必须写成高阶函数的形式。

<Component event={()=>handleEvent(arg)} />

我也是最近开始学习react,所以会比较不太适应。也许vue封装了jsx的这个要求也说不定。另外redux中的 connect()() 函数其内部逻辑显然也是高阶函数。

四、课后个人总结:

本章的知识点需要大量的实例和参考资料来辅助理解,JS便是如此,最近学习的JSX用到了很多之前在vue中很少使用或者不理解的JS用法。

五、引用参考:

我参考了MDN作为本次闭包解释的笔记。