JavaScript闭包的原理与缺陷

    xiaoxiao2026-08-14  9

    闭包的原理

    闭包是指有权访问另一个函数作用域中的变量的函数。根据下面的代码示例来理解什么是闭包,在add函数内部的匿名函数中,访问到外部函数的变量outerArg,在执行add(10)之后外部函数返回了,并且将内部的匿名函数赋值给了变量addTen,此时通过addTen调用函数,依然可以访问到outerArg,也就是10。这个闭包中的变量,只能通过调用addTen函数访问,无法通过其他渠道访问到,下面代码最后一行通过输出属性的方式尝试访问结果是输出undefined。outerArg是属于add函数的作用域中的变量,addTen有权访问add函数作用域中的变量,因此addTen是一个闭包。闭包产生的本质是:在一个函数(外部函数)内部定义的函数(内部函数)会将外部函数作用域中的活动对象添加到自己的作用域链中,下面代码中inner函数将add函数的outerArg添加到自己的作用域链上。在add函数执行完之后,其执行环境会被销毁,但由于inner函数还在引用outerArg,所以outerArg不会被销毁,依然保留在inner函数的作用域链中。直到inner函数(addTen函数)被销毁之后,outerArg才会跟着其作用域链一起被销毁。由于闭包变量是位于作用域链上,因此必须调用闭包函数进入其作用域之后才能访问到闭包变量。

    function add(outerArg) { function inner(innerArg) { return innerArg + outerArg; } return inner; } var addTen = add(10); console.log(addTen(1)); // 输出11 console.log(addTen(2)); // 输出12 console.log(addTen(3)); // 输出13 console.log(addTen.outerArg); // undefined

    闭包的缺陷

    首先,闭包会将外部函数的活动对象都添加到自己的作用域链中,因此相对于普通的函数会更加耗费内存。

    其次,闭包只能获取到外部函数中任何变量的最后一个值。如下面代码所示,在for循环中的匿名函数可以访问到闭包变量i,但是由于闭包所保存的是整个变量对象,因此所有闭包函数中访问到的变量i其实就是同一个变量i(Outer函数的变量i),而Outer函数在执行完毕后,其变量i的值为5,所以5个闭包函数访问到的值都是5。

    function Outer() { var arr = new Array(); for (var i = 0; i < 5; ++i) { arr[i] = function() { return i; }; } return arr; } var arr = Outer(); console.log(arr[0]()); // 输出5 console.log(arr[1]()); // 输出5 console.log(arr[2]()); // 输出5 console.log(arr[3]()); // 输出5 console.log(arr[4]()); // 输出5

    最后,this对象的指向可能与预期的不一致。以下面代码为例,getName的执行结果是输出Window而不是someObj,原因就是getNameFunc函数返回之后,它的执行环境会被销毁,返回的函数赋值给getName,当我执行getName的时候执行环境实在全局环境下,this指向的对象是window,this.name引用到的是全局作用域下的name,也就是’Window’。

    var name = 'Window'; var someObj = { name: 'someObj', getNameFunc: function() { return function() { return this.name; } } }; var getName = someObj.getNameFunc(); console.log(getName()); // 输出'Window'

    闭包缺陷的解决方案

    对于闭包只能获取到外部函数中任何变量的最后一个值问题,可通过定义一个立即执行函数(IIFE)来解决。这种方法的原理其实是在执行立即调用函数时,传入变量i作为参数,而i是按值传递的,相当于复制了一次i的值,所以5次循环调用了5次函数复制了5个不同的i的值创建了5个值不同的变量num,而内部函数(这里指代码中注释inner处的函数)的闭包变量不在是引用i,而是引用了变量num。每个内部函数都有其对应的闭包变量num,这个时候闭包函数的行为就符合我们的预期效果了。

    function Outer() { var arr = new Array(); for (var i = 0; i < 5; ++i) { arr[i] = function(num) { // inner return function() { return num; } }(i); } return arr; } var arr = Outer(); console.log(arr[0]()); // 输出0 console.log(arr[1]()); // 输出1 console.log(arr[2]()); // 输出2 console.log(arr[3]()); // 输出3 console.log(arr[4]()); // 输出4

    对于this对象问题,可以通过避免直接使用this对象的方式来解决。这种方式其实是给this对象起了个别名,用that指向外部函数作用域的this对象,然后在内部函数中引用that,将其加入到闭包中,这样就可以正确地访问到外部函数作用域的this对象了。

    var name = 'Window'; var someObj = { name: 'someObj', getNameFunc: function() { var that = this; // 别名 return function() { return that.name; } } }; var getName = someObj.getNameFunc(); console.log(getName()); // 输出'someObj'

    最后,分享一个实用的闭包内存优化技巧。闭包会引用外部函数的整个活动对象,这种机制可能会导致保存多余的变量而造成内存浪费,以下面代码为例,内部函数仅仅引用了obj对象的id属性,而闭包会把整个obj对象都保存下来,如果该对象上有很多耗费内存的属性,那么这种简单的引用方式会导致产生一个很大的闭包。优化方式如AnotherOuter函数的写法所示,关键在于把需要引用的变量保存下来,然后通过把obj对象设置为null让它丢失引用以便被自动内存回收机制回收处理,这样,闭包函数引用的就只有id这个变量了。

    function Outer() { var obj = {}; obj.id = '12345678'; obj.name = 'aha'; // ... 假设经过很多处理过程,最后obj上带有很多属性 return function() { return obj.id; }; } function AnotherOuter() { var obj = {}; obj.id = '12345678'; obj.name = 'aha'; // ... 假设经过很多处理过程,最后obj上带有很多属性 var id = obj.id; obj = null; return function() { return id; }; }
    转载请注明原文地址: https://ju.6miu.com/read-1311179.html
    最新回复(0)