js核心系列(五)—— 你从不理解闭包,直到你要去面试

闭包的概念

MDN中说到 闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。感觉这个解释让很多人感觉不好理解,个人觉得闭包没有那么复杂,本质就是上级作用域内变量的生命周期,因为被下级作用域内引用,而没有被释放。就导致上级作用域内的变量,等到下级作用域执行完以后才正常得到释放。

也可以简单的理解为 闭包让开发者可以从内部函数访问外部函数的作用域

当函数内部返回一个函数且子函数没在父级作用域内完成整个生命周期的话,父级函数是没办法完成一整个生命周期的,闭包正是利用这一点卡住了父级函数的作用域。让我们可以在外部,访问到函数内部的变量。

在 JavaScript 中,闭包会随着函数的创建而被同时创建。

理解闭包

闭包(closure)是 JavaScript 语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

理解闭包,首先必须理解变量作用域。前面提到,JavaScript 有两种作用域:全局作用域和函数作用域。函数内部可以直接读取全局变量。

var name = 'jimmy';

function f1() {
  console.log(n);
}
f1() // jimmy

上面代码中,函数f1可以读取全局变量name

但是,正常情况下,函数外部无法读取函数内部声明的变量。

function f1() {
  var name = 'jimmy';
}

console.log(name)
// Uncaught ReferenceError: name is not defined

上面代码中,函数f1内部声明的变量name,函数外是无法读取的。

如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。

function f1() {
  var name = 'jimmy';
  function f2() {
  console.log(name); // jimmy
  }
}

上面代码中,函数f2就在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是 JavaScript 语言特有的”作用域链”(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

既然f2可以读取f1的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

function f1() {
  var name = 'jimmy';
  function f2() {
    console.log(name);
  }
  return f2;
}

var result = f1();
result(); // jimmy

上面代码中,函数f1的返回值就是函数f2,由于f2可以读取f1的内部变量,所以就可以在外部获得f1的内部变量了。

闭包就是函数f2,即能够读取其他函数内部变量的函数。由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

从函数的创建和执行看闭包

前面我们学习了堆栈,以及执行上下文的概念,那我们从这个角度来理解下闭包

  function makeAdder(a) {
    const adder = (b) => {
      return a + b;
    };
    return adder;
  }
  const addTen = makeAdder(10);
  const result = addTen(5);
  console.log(result);

执行此代码时,会发生下列情况。

  1. 在添加到堆栈的全局执行环境中,我们声明了一个 makedAdder 函数和一个变量 addTen。
  2. 当我们使用参数 a = 10调用 makeAdder 时,addTen 的值将是返回的结果。
  3. 这些变量(makAdder 和 addTen)使用默认值进行初始化,并在创建阶段在全局执行环境中引用安全内存。然后在执行阶段,我们遇到了 makAdder 的定义和调用。
  4. 创建了 makAdder 的执行环境(函数执行上下文),函数开始执行参数 a = 10,并被推入堆栈。
  5. 在 makAdder 的执行过程中,将一个变量adder添加到它的环境中,并创建adder函数以及它自己的执行环境。在这个环境范围中,保存一个父 makAdder的参数作为引用,并返回创建的 Adder 函数。
  6. adder返回的函数值被保存到全局环境中的变量 addTen,以及在其函数定义期间创建的相关执行环境。
  7. MakAdder 的执行环境从堆栈中弹出并标记为 GarbageCollection
  8. 然后我们使用参数 b = 5调用 addTen。变量 a 是从早期的 makAdder 词法环境创建的 addTen 的捕获闭包范围中解析出来的。

闭包的作用

形成块级作用域

比如我们可以使用闭包能使下面的代码按照我们预期的进行执行(每隔1s打印 0,1,2,3,4)。

  for (var i = 0; i < 5; i++) {
    (function (j) {
      setTimeout(() => {
        console.log(j);
      }, j * 1000);
    })(i);
  }

我们应该尽量避免往全局作用域中添加变量和函数。通过闭包模拟的块级作用域

读取外层函数内部的变量,让这些变量始终保持在内存中

闭包的最大用处有两个,一个是可以读取外层函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。

function createIncrementor(start) {
  return function () {
    return start++;
  };
}

var inc = createIncrementor(5);

inc() // 5
inc() // 6
inc() // 7

上面代码中,start是函数createIncrementor的内部变量。通过闭包,start的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc使得函数createIncrementor的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。

为什么闭包能够返回外层函数的内部变量?原因是闭包(上例的inc)用到了外层变量(start),导致外层函数(createIncrementor)不能从内存释放。只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取。

装对象的私有属性和私有方法。

function Person(name) {
  var _age;
  function setAge(n) {
    _age = n;
  }
  function getAge() {
    return _age;
  }

  return {
    name: name,
    getAge: getAge,
    setAge: setAge
  };
}

var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25

上面代码中,函数Person的内部变量_age,通过闭包getAgesetAge,变成了返回对象p1的私有变量。

注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。

性能考量

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

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

考虑以下示例:

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;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

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

原文链接:https://juejin.cn/post/7225407341633863741 作者:jimmy_fx

(1)
上一篇 2023年4月23日 上午11:12
下一篇 2023年4月24日 上午10:05

相关推荐

发表回复

登录后才能评论