闭包从混乱到清晰学习小结

563 阅读5分钟

最近学习了 You-Dont-Know-JS 的文档,对一直在学习但从未理解、学会的闭包有了恍然大悟式的理解,下面是整理的学习小结。

相关概念:

学习闭包前需要理解的相关概念,学会这些后再看闭包就很容易理解了。
YDKJS讲解的 作用域与闭包

作用域

作用域是一组规则,它决定了一个变量(标识符)在哪里和如何被查找。

词法作用域

词法作用域意味着作用域是由编写时函数被声明的位置的决策定义的。JavaScript采用词法作用域(lexical scoping),也就是静态作用域。

JS作用域

全局作用域、函数作用域、ES6新增的块作用域。
函数作用域:functions are executed using the scope chain that was in effect when they were defined 函数的作用域、作用域链在函数声明时已确定,与其运行时所在的作用域无关(词法作用域、静态作用域)。
函数体内声明的变量、函数参数是局部变量,只在函数内有定义。
立即执行函数作用域:

  • 创建命名空间,私有作用域,避免全局变量的污染。
(function IIFE(){
    var a = 3;
    console.log( a ); // 3
})();
console.log( a ); // ReferenceError: a is not defined  
// 在立即执行函数内定义的变量,是函数的内部变量,在外部访问时报引用错误。 
  • 模拟块级作用域。
for (var i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}
// 1010

改造代码使之打印0-9:

for (var i = 0; i < 10; i++) {
  (function (j) {
    setTimeout(() => {
      console.log(j);
    }, 1000);
  })(i);
}
// 0 1 2 3 4 5 6 7 8 9  
// 函数参数作为函数的局部变量,每次循环,立即执行函数都创建各自的局部变量。

效果等同于:

for (let i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}

作用域链

链式作用域结构(scope chain),函数定义时创建。
作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端始终是当前执行的代码所在环境的变量对象,下一个变量对象来自包含环境,再下一个变量对象则来自下一个包含环境,一直延续到全局执行环境。
内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数:

var globalKey = 1;
function outer() {
  var outerKey = globalKey + 1;
  function inner() {
    var innerKey = outerKey + 1;
  }
  console.log(inner.prototype);
}
Inner; //ReferenceError: inner is not defined 

内部属性[[Scopes]]可查看作用域链相关信息 :

微信截图_20210521135719.png

闭包

YDKJS对闭包的定义: 闭包就是函数能够记住并访问它的词法作用域,即使当这个函数在它的词法作用域之外执行时。
无论使用什么方法将内部函数 传送 到它的词法作用域之外,它都将维护一个指向它最开始被声明时的作用域的引用(词法作用域查询规则),而且无论什么时候执行它,这个闭包就会被行使。
计时器、事件处理器、Ajax请求、跨窗口消息、web worker、或者任何其他的异步(或同步!)任务,传入一个 回调函数,就在它周围悬挂了一些闭包。

var scope = "global scope";
function checkscope() {
  var scope = "local scope";
  function f() { return scope; }
  return f;
}
checkscope()() //"local scope"

根据词法作用域,在函数 f 的作用域链上查找变量scope的时候,先找到checkscope函数内的变量scope,所以返回"local scope"

function counter() {
  var n = 0;
  return {
    count: function () {
      return n++;
    },
    reset: function () {
      n = 0;
    },
  };
}
var c = counter(),
  d = counter();
c.count(); //0
d.count(); //0  
c.reset();
c.count(); //0  -->解释1
d.count(); //1  -->解释2

解释1:同一个外部函数内可以定义多个嵌套函数,这些嵌套函数即闭包函数共享私有变量。reset()和count() 共享变量count。
解释2:外层函数每次运行,都会生成一个新的闭包,各个闭包内的变量互不影响。c,d的count互不影响。

闭包与this

this豁然开朗:this 是在函数被调用时建立的一个绑定,它指向 什么 是完全由函数被调用的调用点来决定的。

  1. new绑定:通过 new 调用?使用新构建的对象。
  2. 明确绑定:通过 call 或 apply(或 bind)调用?使用指定的对象。
  3. 隐含绑定:通过持有调用的环境对象调用?使用那个环境对象。
  4. 默认绑定:默认:strict mode 下是 undefined,否则就是全局对象。
function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        console.log(this) //解释1
        console.log(name); //解释2
    }
    return displayName;
}
var myFunc = makeFunc();
myFunc(); 
//Window {window: Window, self: Window, document: document, name: "The Window", location: Location, …} 
//Mozilla   

解释1:displayName 函数在全局环境执行,this指向Window。this此处使用默认绑定。
解释2:name 变量根据函数定义时的词法作用域确定。

var name = "The Window";
var object = {
    name: "My Object",
    getNameFunc: function() {
        console.log('1... ',this); //解释1
        console.log('2... ',this.name); 
        return function() { 
            console.log('3... ',this); //解释2
            console.log('4... ',this.name); 
        };
    }
};
object.getNameFunc()(); 
//1...  {name: "My Object", getNameFunc: ƒ}
//2...  My Object
//3...  Window {window: Window, self: Window, document: document, name: "The Window", location: Location, …}
//4...  The Window

解释1:getNameFunc() 函数执行,this 指向函数调用的环境对象 object。this 此处使用隐含绑定。
解释2:object.getNameFunc() 表达式返回一个函数,函数在全局环境执行,this 指向 Window。this 此处属于隐含绑定-隐含丢失。

闭包的使用

节流、防抖、柯里化、vue事件总线、vue nextTick源码

  • 返回函数,延迟执行。
  • 实现私有变量、私有方法。

还可深入学习的方向:闭包与内存泄漏。
以上,欢迎讨论,如有错漏也欢迎指正,希望对你有帮助😀