闭包

mdn-闭包
mdn-微任务与javascript运行时环境

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

前言

说来十分惭愧,工作两年多还是对闭包以及执行上下文一知半解,之前一直认为函数嵌套就是闭包。
在看了winter老师的课,察觉到自己认知错误,但对课程还有些不明白和不确定,去查阅了mdn文档得出一些心得。

闭包

1. 什么是闭包

从mdn的说明来看,简单地说绑定了词法环境的函数就是闭包。闭包是由函数以及声明该函数的词法环境组合而成的。而当一段javascript代码运行时,它是在运行执行上下文,词法环境是执行上下文的一部分,也就是说函数就是闭包
(这里我之前理解有偏差,不应该将执行上下文和闭包划为关联,但这句话说的是没问题的。)

词法作用域
词法(lexical)一词指的是,词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。

2. 闭包的优缺点

  • 优点:模拟私有方法
  • 缺点:影响性能

通常创建一个对象或者类的时候,方法应该挂在原型上,而不是定义在对象的内部函数中(构造器)。因为每次内部函数被调用时,方法都会被重新赋值一遍,也就是说,对于每个对象的创建,方法都会被重新赋值。(个人理解是会一个新的执行上下文被创建和销毁,消耗内存)(这里理解错误,即使是同一个函数,每次执行的时候也会创建一个上下文,故而这里的内存消耗就是指方法被重新赋值)

mdn的示例很好简单明了

// 闭包处理
function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}
var a = new MyObject('chen', 22)
a.getName()
// 原型处理
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;
};

复制代码

用闭包的方式处理时,每次调用a.getName()都去重新定义这个函数;
而用原型处理时,原型可以为所有对象共享,不必在每一次创建对象时定义方法。

执行上下文

当一段 JavaScript 代码在运行的时候,它实际上是运行在执行上下文中。

每一个执行上下文都是一种作用域层级。每个代码段开始执行的时候都会创建一个新的上下文来运行它,并且在退出的时候销毁掉。

有三种类型代码会创建一个新的执行上下文:

  • 全局上下文:为运行代码主体而创建的执行上下文,它是为那些除了javascript函数之外的代码而创建的(我理解的是准确的来说是为除了函数体代码外创建的,定义函数的时候是用的全局上下文,而函数体是用的本地上下文)。
  • 本地上下文:每个函数会在执行的时候创建自己的上下文。
  • 使用eval()函数也会创建一个新的执行上下文。
let outputElem = document.getElementById("output");

let userLanguages = {
  "Mike": "en",
  "Teresa": "es"
};

function greetUser(user) {
  function localGreeting(user) {
    let greeting;
    let language = userLanguages[user];

    switch(language) {
      case "es":
        greeting = `¡Hola, ${user}!`;
        break;
      case "en":
      default:
        greeting = `Hello, ${user}!`;
        break;
    }
    return greeting;
  }
  outputElem.innerHTML += localGreeting(user) + "<br>\r";
}

greetUser("Mike");
greetUser("Teresa");
greetUser("Veronica");
复制代码

这段代码包含3个执行上下文,其中有些会在程序运行的过程中多次创建和销毁。每个上下文创建的时候会被推入执行上下文栈。当退出的时候,它会出上下文栈中移除。

  • 程序开始运行时,全局上下文就会被创建好。
    • 当执行到greetUser(‘Mike’)的时候,会为greetUser()函数创建一个它的执行上下文。这个执行上下文会被推入执行上下文栈中。
      • 当greetUser()调用localGreeting()的时候,会为该方法创建一个执行上下文(我理解的是为localGreeting()执行函数创建一个上下文给它的函数体使用),并且在localGreeting()退出的时候它的执行上下文会从执行栈中弹出并销毁。程序会从栈中获取下一个执行上下文并恢复执行,也就是从greetUser()剩下的部分开始执行。
      • 当greetUser()执行完毕并退出,其上下文也会从执行栈中弹出并销毁。
    • 当执行到greetUser(‘Teresea’)时,程序会为它创建一个上下文并推入栈顶。
      • 当greetUser()调用localGreeting()的时候,创建另一个上下文并运行该函数,当localGreeting()退出的时候,它的上下文也从栈中弹出并销毁。
        greetUser()得到恢复并继续执行剩下的部分。
      • greetUser()执行完成并退出,它的上下文从栈中弹出并销毁。
    • 当执行到greetUser(‘Veronica’)时,程序为它创建一个上下文并推入栈顶。
      • 当执行到localGreeting()时,为其创建一个上下文,localGreeting()执行完成将其上下文从栈中弹出并销毁,(程序从栈中获取下一个上下文继续执行,即greetUser()继续执行)。
      • greetUser()执行完毕退出,其上下文从栈中弹出并销毁。
  • 主程序退出,全局执行上下文从栈中弹出。此时所有上下文都已弹出,程序执行完毕。

关于递归:
每一次调用自身都会创建一个新的上下文。这使js运行时能够追踪递归的层级以及从递归中得到返回的值,但每次递归都会消耗内存来创建新的上下文。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享