JavaScript 闭包

本文详细解析了JavaScript闭包的概念,包括其优点如实现公有变量、缓存、封装及防止全局变量污染,同时也讨论了闭包的缺点,如可能导致的内存泄漏。文章通过多个示例说明了闭包如何工作,以及在不同场景下this指向的变化,特别是闭包在Node.js和严格模式下的特殊行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 闭包简介

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures

一个函数内部包含嵌套函数并且嵌套函数被返回出来,那么内部函数被保存到了外部,就会生成闭包,此时边有一个外部的引用指向这个嵌套函数。闭包是由函数以及声明该函数的词法环境组合而成的。

【例】如下,b函数被保存到了外部,输出结果为101、102

   function a () {
      var num = 100;

      function b () {
         num++;
         console.log(num);
      }
      return b;
   }
   var glob = a();
   glob();//101
   glob();//102

2. 闭包缺点

闭包会导致原有作用域链不能释放,造成内存泄漏。

3. 闭包作用

闭包将函数与其所操作的某些数据(环境)关联起来。类似于面向对象编程。
可以利用闭包来实现类似Java中的私有修饰符的功能。

3.1. 实现公有变量

【例】函数累加器

   function add () {
      var count = 0;
      function sum () {
         count++;
         console.log(count + "\n");
      }
      return sum;
   }
   var counter = add();
   counter();
   counter();

3.2. 可以做缓存

【例1】简易存储结构,此时的 obj 中的方法被返回出来,可以临时存储变量 food

   function eater () {
      var food = ""; // honey缓存在food中
      var obj = {
         eat: function () {
               console.log("I am eating " + food);
               food = "";
         },
         push: function (myfood) {
               food = myfood;
         }
      }
      return obj;
   }
   var eater1 = eater();
   eater1.push("honey");
   eater1.eat();

3.3. 可以实现封装、属性私有化

【例】此时的 privateCounter 就是一个私有属性,可以通过函数接口调用,但是无法直接访问。

   var Counter = (function () {
      var privateCounter = 0; // 私有属性,无法直接访问
      function changeBy (val) {
         return privateCounter += val;
      }
      return {
         incre: function (val) {
               console.log(changeBy(val));
         },
         decre: function (val) {
               console.log(changeBy(-val));
         },
      }
   })();

   Counter.incre(3);
   Counter.decre(5);

3.4. 模块化开发,防止污染全局变量

闭包既能重复使用局部变量,又不污染全局!

4. 闭包的this指向

【例2】题外话,代码注释中提了个问题,为什么那里的this指向window?

因为函数最终是在window(node中是global)上完成调用的。

简单的作用域问题是笔试面试常考的题目,在JavaScript中这很关键。

   function memory (f) {
      var cache = {};
      return function () {
         // 传入类数组 3, 4, 5
         var key = arguments.length + ":" + Array.prototype.join.call(arguments, ',');
         console.log(typeof (key) + ":" + key);
         if (key in cache) { // 如果值在缓存,直接读取返回
               console.log("值在缓存,直接读取返回:" + cache[key]);
               return cache[key];
         } else { // 否则执行计算,并把结果放到缓存,此处的 f 指阶乘运算
               // 此处的this指向window,这是为什么?
               console.log(this);
               cache[key] = f.apply(this, arguments);
               // 此处的arguments是一个类数组,但是阶乘运算只会取第一位进行阶乘运算
               console.log(cache[key]);
               return cache[key];
         }
      }
   }
   var factorial = function (n) { // 阶乘
      return (n <= 1) ? 1 : n * factorial(n - 1);
   }
   var factorialWithMemory = memory(factorial);
   factorialWithMemory(3, 4, 5); //3:3,4,5

【例3】闭包指向附加示例

   let num = 1,
      obj = {
         num: 2,
         getNum: function () {
               return (function () {
                  return this.num;
               })();
         }
      }
   console.log(obj.getNum());
   // 在浏览器输出1,在ndoe输出undefined

【例3】里面的自执行匿名函数不属于任何对象,他不是一个对象的方法(无法使用点运算符调用)。在非严格模式中,无指向的、函数内部的this,指向window。this的值取决于调用上下文,如果一个函数不是作为某个对象的方法被调用,那么this就是global或则window。否则就是该对象。对象实例化之后的this指的是本身,未实例化则this指的是调用者的对象。

匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window。new Function 中的this指全局对象,eval中的this指调用上下文中的this,要是想要改变这情况,可以这么写:

   var num = 1,
      obj = {
         num: 2,
         getNum: function () {
               return (function (self) {
                  return self.num;
               })(this);
         }
      }
   console.log(obj.getNum()); // 2
   function m (method) {
      var a = {};
      a.m = method;
      method();//window
      a.m();//对象a
   }
   m(function () { console.log(this) });
   let m = () => {
      console.log(this)
   };
   let a = { name: "Hui" };
   m(); //打印结果为window(node中是一个空对象)
   m.call(a); //打印结果为window(node中是一个空对象)
   // 箭头函数中的this是固定的,不可变
   // 所以call、apply、bind 的第一个参数会被忽略
   var m = () => {
      console.log(this)
      // 定义了一个箭头函数为m,里面的this将会一直是创建时的this,是啥呢?
      // 此时全局中的this是window(node中是{})
   };
   var obj = {
      a: m,
      b: function () {
         m();
      },
      c: function () {
         var m2 = () => {
               console.log(this)
         }; //每次调用都会新定义这个箭头函数
         m2();
      }
   }
   obj.a(); // Window (node中是一个空对象)
   obj.b(); // Window (node中是一个空对象)
   // 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this
   // 是b方法,b方法的this指向全局对象
   obj.c(); // {a: ƒ, b: ƒ, c: ƒ}

   var obj2 = {};
   obj2.c = obj.c;
   obj2.c(); // {c: ƒ}

参考

总结就是函数中的this指向调用它的对象,但在匿名执行函数中明显没有人调用它,或者说默认是全局对象调用了他。

之所以指向 Window 和《ECMAScript® 2015 Language Specification》有关,它指出了,如果thisArgumentsundefined,则thisValue就是 [[globalThis]]

引入this的初衷就是想在原型继承的情况下,拿到函数的调用者,如果函数没有指明调用者呢,那就让this指向全局对象。更多的情况在注视中写了。

4.1. 特殊情况

在node,以及在严格模式中,各种this的指向可能和浏览器中不一样。

比如在node中输出this是{},而不是 Object [global] ,当然在匿名执行函数中shuchuthis指向的是 Object [global]

   function as (params) {
      'use strict';
      console.log(this); // undefined
      console.log(this === global); // false
   }
   as()

   function asas (params) {
      console.log(this); // Object [global]
      console.log(this === global); // true
   }
   asas()
// 这是一个表达式,输出的结果和在Chrome中不一样
var m = () => { console.log(this); }
m(); // {}

console.log(globalThis); // Object [global]

// 这是一个匿名函数
(function () {
    console.log(this); // Object [global]
})()

// 这是一个函数声明,这代表了global对象的方法,所以指向global
function as () {
    console.log(this);
}
as(); // Object [global]

5. 使用闭包注意点

  1. this指向问题
  2. 内存消耗问题(建议在退出函数前将不使用的局部变量删除)
  3. 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
  4. 外部变量的值是否改变
   function test () {
      var array = []
      for (var count = 0; count < 5; count++) {
         array[count] = function (value) { // 立即执行函数
               return function () {
                  console.log(value)
               }
         }(count)//此处若不是用立即执行函数则输出的结果都是5
      }
      return array
   }

   var result = test()
   result[0](); // 0
   result[1](); // 1
   result[2](); // 2
   result[3](); // 3
   result[4](); // 4

参考:

  1. ruanyf 学习Javascript闭包(Closure)
  2. JavaScript学习笔记(十一) 闭包

6. 立即执行函数

针对初始化功能的函数(只执行一次,执行后再也不需要,不希望它继续占内存)

为什么这样做就相当于立即执行函数?
因为正常执行函数都是函数的引用或者说名称加上执行符号(),例如test();这里所说的函数的引用或者说名称实际上代表了函数的表达式,在函数表达式后面加上执行符号就代表执行函数,此处

   (function () {}());// W3C建议
   (function () {})();

【例】1

   //可以执行,因为var 变量名 = function(){}相当于函数表达式
   //执行之后再访问test1为undefined,
   //因为function作为立即执行函数执行后放弃了表达式的名称test1
   var test1 = function () {
      console.log('doing'); // doing
   }();
   console.log(test1); // undefined

【例】2

   //会报错,不能执行,因为此处 function 是函数声明,而不是表达式
   function test2 () {
      document.write("doing");
   } ();
   // 这么写本身就存在语法错误了!

【例】3

   //不执行,也不报错,对比test2由于立即执行符号()传了参数
   //所以系统会把函数后面的立即执行符号看作单独的一行表达式
   // function test3 (a, b, c, d) {
   // document.write(a + b + c + d);}
   // (1, 2, 3, 4);
   // 此处与直接写 1, 2, 3, 4 其实一样
   function test3 (a, b, c, d) {
      document.write(a + b + c + d + "doing");
   } (1, 2, 3, 4);

【例】4

   //能执行,因为+把函数声明转换为表达式,同时! - || &&都可以
   + function test4 () {
      console.log('doing');
   }();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值