闭包

发布时间 2023-07-06 14:44:48作者: nini-

闭包

闭包是一个函数及其捆绑的周边环境状态引用的组合。即闭包可以让开发者从内部函数访问外部函数的作用域。在JavaScript中闭包会随着函数的创建而被同时创建。

一、词法作用域

function init() {
            const name = 'wyl';
            function displayName() {
                alert(name);
            }
            displayName();
        }
init();

init函数创建了一个局部常量name和displayName函数。其中displayName()是定义在init()里面的内部函数,并且仅可以在init()函数体内使用。displayName()没有自己的局部变量,但是因为它可以访问到外部函数的变量,所以displayName()可以使用父函数init()声明的变量。

const makeFun = () => {
            const name = 'wyl';
            const displayName = () => {
                alert(name);
            }
            return displayName
        }
const myFunc = makeFun(); // 由于是displayName是从外部函数返回,所以需要多调用一次才能看到弹出框
myFunc();

在一些编程语言中函数的局部变量仅存在于函数的执行期间,一旦函数执行完毕,其内部变量将不再被访问。但是,在JavaScript中,这段代码仍然得到了与上述init()方法一致的输出效果。原因在于JavaScript的函数形成了闭包。闭包是由函数以及声明该函数的词法环境组合而成的,该环境中包含了在闭包创建时作用域内的所有局部变量/常量。

在本例子中,myFunc是执行makeFun函数时创建的displayName函数实例的引用,其中displayName的实例维持了一个对它词法环境(常量name存储在其中)的引用。因此,当myFunc被调用时,name仍然有效。

const makeAdder = (x) => {
            return function(y) {
                return x + y;
            }
        }
const add1 = makeAdder(5);
const add2 = makeAdder(10);

console.log(add1(2)); // 7
console.log(add2(6)); // 16

从本质上讲,makeAdder是一个函数工厂,它创建了将指定的值和它的参数相加的函数,在上述示例中使用函数工厂创建了两个新的函数,add1和add2。他们都是闭包,共享相同的函数定义,但是保存了不同的词法环境,在add1中x为5,而add2中x为10。

二、闭包的实用性

1.按钮点击更改页面字体像素

通过给按钮添加点击事件来改变页面元素的字体大小

代码如下:

css代码:

body {
        font-family: Arial, Helvetica, sans-serif;
        font-size: 12px;
    }
h1 {
        font-size: 1.5em;
    }
h2 {
        font-size: 1.2em;
    }

这里使用了两种计量单位,px和em,还有一种计量单位是rem。他们三者虽然都是计量单位,但是也有区别,即px是依据显示器的大小来计量单位;而em则是根据父元素的大小来计量,如果父元素是16px,那么1em则为16px;rem是依据根元素<html/>的大小来计量。

js代码:

function makeSizer(size) {
            return function() {
                document.body.style.fontSize=size+'px';
            }
        }
const size1 = makeSizer(12);
const size2 = makeSizer(14);
const size3 = makeSizer(16);

HTML代码:

 <h1>我是h1</h1>
 <h2>我是h2</h2>
 <button onclick="size1()">12</button>
 <button onclick="size2()">14</button>
 <button onclick="size3()">16</button>

将闭包运用到按钮点击事件中,通过不同的词法环境达到不同的字体效果。

2.用闭包模拟私有方法

在java中支持方法声明为私有的,即只能被同一个类中的其他方法调用。而JavaScript中没有这种原生支持,但是可以使用闭包来模拟私有方法,私有方法不仅有利于限制对代码的访问,还提供了管理全局命名空间的能力,避免非核心的方法扰乱代码的公共接口部分。

下面展示了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量,这种方式也称为模块模式:

var Counter = (function() {
            var privateCounter = 0; // 每次调用其中一个方法,通过改变函数类变量的值,会改变这个闭包的词法环境。
            function changeBy(val) {
                privateCounter += val;
            }
            return {
                increment: function() {
                    changeBy(1);
                },
                decrement: function() {
                    changeBy(-1);
                },
                value: function() {
                    return privateCounter;
                }
            }
        })()

console.log(Counter.value()); // 0
Counter.increment();
Counter.increment();
console.log(Counter.value()); // 2
Counter.decrement();
console.log(Counter.value()); // 1

这里创建了一个词法环境,供三个函数所共用,即Counter.increment()和Counter.decrement()、Counter.value()。该共享环境创建于一个立即执行的匿名函数体内,这个环境中包含两个私有项,即privateCounter变量和changeBy函数,这两项都无法在匿名函数外部直接访问,必须通过函数内返回的三个公共函数进行访问。

var mackCounter = function() {
            var privateCounter = 0;
            function changeBy(val) {
                privateCounter += val;
            }
            return {
                increment: function() {
                    changeBy(1);
                },
                decrement: function() {
                    changeBy(-1);
                },
                value: function() {
                    return privateCounter;
                }
            }
        }

var Counter1 = mackCounter();
var Counter2 = mackCounter();
console.log(Counter1.value()); // 0
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); // 2
Counter1.decrement();
console.log(Counter1.value()); // 1
console.log(Counter2.value()); // 0

与上一示例不同,这里的Counter1和Counter2是各自独立的,因为调用闭包内的计数器时会通过改变这个变量的值而改变闭包的词法环境,但是在一个闭包内对变量的修改不会影响到另一个闭包中的变量。

3.循环闭包中的易错点

HTML代码:

<p id="help">鼠标选中输入框</p>
<p>邮箱: <input type="text" id="email" name="email"></p>
<p>用户名: <input type="text" id="name" name="name"></p>
<p>年龄: <input type="text" id="age" name="age"></p>

JavaScript代码:

function showHelp(help) {
        document.getElementById('help').innerHTML=help; // 无论定位哪个输入框,展示的都是年龄的信息
       }
       function setupHelp() {
        var helpText = [
            {'id': 'email', 'help': 'xxxx@qq.com'},
            {'id': 'name', 'help': 'wyl'},
            {'id': 'age', 'help': '18'},
        ]

        for(var i = 0; i < helpText.length; i++) {
            var item = helpText[i];
            document.getElementById(item.id).onfocus = function() {
                showHelp(item.help);
            }
        }
       }
       setupHelp();

运行这段代码会发现无论焦点在哪个输入框上展示的消息都是关于年龄的信息。原因是赋值给onfocus的是闭包,这些闭包是由他们的函数定义和在setupHelp作用域中捕获的环境组成的。这里的三个闭包在循环中被创建,但是他们共用一个词法作用域,在这个作用域中存在一个变量item,该变量是由var声明的,由于变量提升,所以具有函数作用域,当onfocus的回调执行时,item.help的值被决定。由于循环在事件触发前早已执行完毕,因此变量的对象item(被三个闭包所共享)已经指向了helpText的最后一项,因此无论点击哪个输入框所得到的都是年龄的数据。

解决办法:

①使用更多的闭包

function showHelp(help) {
        document.getElementById('help').innerHTML=help;
    }
function makeInfo(item) {
        return function() {
            showHelp(item.help)
        }
    }
function setupHelp() {
        var helpText = [
            {'id': 'email', 'help': 'xxxx@qq.com'},
            {'id': 'name', 'help': 'wyl'},
            {'id': 'age', 'help': '18'},
        ]

        for(var i = 0; i < helpText.length; i++) {
            var item = helpText[i];
            document.getElementById(item.id).onfocus = makeInfo(item)
        }
    }
setupHelp();

这样所有的回调函数不再共享一个词法环境,makeInfo函数为每一个回调创建一个新的词法环境,互不影响。

②使用匿名函数立即执行回调

function showHelp(help) {
        document.getElementById('help').innerHTML=help; 
    }
function setupHelp() {
        var helpText = [
            {'id': 'email', 'help': 'xxxx@qq.com'},
            {'id': 'name', 'help': 'wyl'},
            {'id': 'age', 'help': '18'},
        ]

        for(var i = 0; i < helpText.length; i++) {
            (function() {
                var item = helpText[i];
                document.getElementById(item.id).onfocus = function() {
                    showHelp(item.help);
                }
            })() // 马上把当前循环项的 item 与事件回调相关联起来
        }
    }
setupHelp();

③如果不想过多的执行闭包,还可以用es6的let和const关键字来替代var

function showHelp(help) {
        document.getElementById('help').innerHTML=help; // 无论定位哪个输入框,展示的都是年龄的信息
       }
function setupHelp() {
        const helpText = [
            {'id': 'email', 'help': 'xxxx@qq.com'},
            {'id': 'name', 'help': 'wyl'},
            {'id': 'age', 'help': '18'},
        ]

        for(let i = 0; i < helpText.length; i++) {
            const item = helpText[i];
            document.getElementById(item.id).onfocus = function() {
                showHelp(item.help);
            }
        }
       }
setupHelp();

使用cons让每个闭包都绑定了块作用域的变量。

④使用forEach()来遍历helpText数组,并给每个input框添加一个监听器

function showHelp(help) {
        document.getElementById('help').innerHTML=help; 
       }
function setupHelp() {
        const helpText = [
            {'id': 'email', 'help': 'xxxx@qq.com'},
            {'id': 'name', 'help': 'wyl'},
            {'id': 'age', 'help': '18'},
        ]

        helpText.forEach(function(text) {
            document.getElementById(text.id).onfocus = function() {
                showHelp(text.help);
            }
        })
       }
setupHelp();

三、性能考量

如果不是某些特定任务需要使用闭包(例如防抖节流、循环和模块封装),在其他函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能都有影响。