超级吃瓜群众 發表於 2025-6-17 10:13:00

Web前端入门第 66 问:JavaScript 作用域应用场景(闭包)

<h2 id="什么是作用域">什么是作用域?</h2>
<p>就像孙悟空给唐僧画个圈圈一样,这个圈圈就可以称之为作用域,这个比喻可能不太形象。</p>
<p>作用域和孙悟空的圈圈还是有点区别,作用域内部可以获得作用域外部的变量,而内部的变量无法逃逸到作用域外面,如果逃逸出去了,那就造成<strong>内存泄漏</strong>了,程序将会出现崩溃!</p>
<h3 id="全局作用域">全局作用域</h3>
<p>可以理解为就是放在 JS 最外层的那部分内容,比如:变量、函数、对象等等。凡是定义在最外层的内容,都是属于全局作用域,在全局作用域下的任意函数都可访问到这部分内容。</p>
<pre><code class="language-js">var wechat = '前端路引';

(function () {
function test1 () {
    console.log(wechat);
}
test1(); // 输出 '前端路引'
})()
</code></pre>
<p>以上代码用到了自执行函数 <code>(function () {})()</code>,作用就是为了创建一个局部作用域,避免变量污染全局作用域,在很多优秀的插件中都能看到它的影子。</p>
<p>上面代码中的 <code>wechat</code> 变量,就是全局作用域下的变量,<code>test1</code> 函数定义在全局作用域内部,所以对于 <code>test1</code> 函数来说,全局作用域中的变量它都是可以访问的。</p>
<h3 id="函数作用域">函数作用域</h3>
<p>也可称之为 <code>局部作用域</code>,生效范围在函数内部,在函数外面无法访问。</p>
<pre><code class="language-js">function test2 () {
var wechat = '前端路引';
console.log(wechat);
}
test2();
console.log(wechat); // 报错:wechat is not defined
</code></pre>
<p><code>wechat</code> 变量定义在函数内部,便是函数作用域,在函数外面无法访问,这就是局部作用域的特性。</p>
<h3 id="块级作用域">块级作用域</h3>
<p>ES6 新增的玩法,一对花括号圈出来的区域,就称之为块级作用域。需注意 <code>var</code> 声明的变量是不存在块级作用域的,只有 <code>let</code> 和 <code>const</code> 才存在块级作用域。</p>
<pre><code class="language-js">{
var wechat1 = '前端路引';
let wechat2 = '前端路引';
const wechat3 = '前端路引';
}
console.log(wechat1); // 输出:前端路引
console.log(wechat2); // 报错:wechat2 is not defined
console.log(wechat3); // 报错:wechat3 is not defined
</code></pre>
<p>或者是像 if 条件判断的花括号一样也存在块级作用域:</p>
<pre><code class="language-js">if (true) {
var wechat1 = '前端路引';
let wechat2 = '前端路引';
const wechat3 = '前端路引';
}
console.log(wechat1); // 输出:前端路引
console.log(wechat2); // 报错:wechat2 is not defined
console.log(wechat3); // 报错:wechat3 is not defined
</code></pre>
<p>当然其他 while、for、do 等循环语句也存在块级作用域。</p>
<h3 id="作用域链">作用域链</h3>
<p>作用域链总是从内部开始,一圈一圈往外部查找,比如:</p>
<pre><code class="language-js">let globalVal = '全局';
function outer() {
let outerVal = '外部';
function inner() {
    let innerVal = '内部';
    console.log(innerVal);    // '内部'(当前作用域)
    console.log(outerVal);    // '外部'(外层作用域)
    console.log(globalVal);   // '全局'(全局作用域)
    console.log(wechat); // 报错:ReferenceError: wechat is not defined
}
inner();
}
outer();
</code></pre>
<p>当内部找不到的时候,就往外一层查找,外层找不到就在全局作用域找,如果全局作用域也找不到,就会报错 <code>ReferenceError</code>。</p>
<h2 id="闭包使用">闭包使用</h2>
<p>基于作用域的特性,就有前辈们发现了 <code>闭包</code> 的用法,闭包这个东东,用得好呢可以说是一把利剑,用得不好那就要反噬主人了。</p>
<p><code>闭包</code> 的用处就是搭建函数内部和外部的桥梁,使函数外部可以访问到函数内部的变量。</p>
<h3 id="闭包的基本样子">闭包的基本样子</h3>
<pre><code class="language-js">function test1 () {
const wechat = '前端路引';
function test2 () {
    console.log(wechat);
}
return test2;
}
test1()(); // 输出:前端路引
</code></pre>
<p>上面代码中 wechat 定义在函数内部,属于函数作用域,test2 也定义在函数内部,使用 test2 访问 wechat 变量的这种方法,就称之为 <code>闭包</code>。</p>
<p>为什么需要调用 test1 需要 <code>()()</code> ?这个只是一种简写,其完整写法应该是这样的:</p>
<pre><code class="language-js">const temp = test1(); // 获得 test1 返回的函数
temp(); // 执行返回函数输出:'前端路引'
</code></pre>
<h3 id="解决循环中的陷阱">解决循环中的陷阱</h3>
<p>在 ES6 出现之前,var 没有块级作用域这个特性,所以循环语句中常常会出现一些坑,比如:</p>
<pre><code class="language-js">for (var i = 0; i &lt; 3; i++) {
setTimeout(function () {
    console.log(i); // 输出:3 3 3
}, 100)
}
</code></pre>
<p>上面代码会输出三次 <code>3</code>,原因是 <code>var</code> 没有块级作用域,setTimeout 函数执行时候,获得的是 for 循环之后的 i 值,所以最终输出都是 3。</p>
<p><strong>使用 let 优化:</strong></p>
<pre><code class="language-js">for (let i = 0; i &lt; 3; i++) {
setTimeout(function () {
    console.log(i); // 输出:0 1 2
}, 100)
}
</code></pre>
<p>let 的块级作用域可以完美保存每次 i 的值,所以最终输出是 0 1 2,这也相当于一种闭包的用法。</p>
<p><strong>使用闭包优化:</strong></p>
<pre><code class="language-js">for (var i = 0; i &lt; 3; i++) {
(function (j) {
    setTimeout(function () {
      console.log(j); // 输出:0 1 2
    }, 100)
})(i)
}
</code></pre>
<p>将 i 以函数参数的形式传入,这样每次循环后,函数内部获得的 <code>j</code> 都是当时的 i 值,所以最终输出是 0 1 2。</p>
<p>上面代码可能难以理解,那么换一种写法看看:</p>
<pre><code class="language-js">function temp (j) {
setTimeout(function () {
    console.log(j); // 输出:0 1 2
}, 100)
}
for (var i = 0; i &lt; 3; i++) {
temp(i)
}
</code></pre>
<p>这样写是否一眼就懂了?</p>
<p><code>(function (j) {})(i)</code> 这种写法就相当于一个自执行函数,这个函数有一个参数 j,每次执行的时候传入 i 值而已。</p>
<p>为什么要一个小括号把 <code>function (j) {}</code> 包起来呢?</p>
<p>如果直接写成 <code>function (j) {}(i)</code>,JS 解析器没办法识别这是一个函数调用,所以需要用小括号括起来。也可以写成 <code>!function (j){}(i)</code> ,也是自执行函数的一种方式。其他的一元运算符都可以用来这么玩,比如:</p>
<pre><code class="language-js">+function (j) {}(i)
-function (j) {}(i)
~function (j) {}(i)
</code></pre>
<p>个人觉得还是小括号比较容易理解。</p>
<h3 id="私有变量">私有变量</h3>
<p>模块化开发的时候,可以使用闭包封装内部的私有变量,这样外部就无法直接访问,以保证私有变量安全,比如:</p>
<pre><code class="language-js">const counter = (function() {
let count = 0; // 私有变量
return {
    increment: () =&gt; count++,
    getCount: () =&gt; count,
};
})();

counter.increment();
console.log(counter.getCount()); // 1
console.log(counter.count);      // undefined(无法直接访问)
</code></pre>
<h3 id="函数柯里化">函数柯里化</h3>
<p>闭包的又一种使用形式,<code>柯里化</code>就是把接受多个参数的函数变换成接受一个单一参数的函数。如下:</p>
<pre><code class="language-js">function add(a) {
return function(b) {
    return a + b;
};
}

const add5 = add(5); // 返回一个闭包,记住 a=5
console.log(add5(3)); // 8
</code></pre>
<h3 id="内存泄漏">内存泄漏</h3>
<p>由于闭包中的变量会常驻内存,如果不及时释放闭包,那么就会造成内存泄漏,比如:</p>
<pre><code class="language-js">function createHeavyObj() {
const bigData = new Array(1000000).fill('*'); // 生成一个大对象
return () =&gt; bigData; // 闭包引用 bigData
}

let fn = createHeavyObj();
// 即使不再需要 bigData,它仍被闭包引用,无法被回收
// 解决方法:手动解除引用
fn = null; // 解除闭包对 bigData 的引用
</code></pre>
<p>如果没有 <code>fn = null</code> 这句代码,那么 <code>bigData</code> 会一直存在(直到页面刷新或者被垃圾回收机制回收),如果 <code>createHeavyObj</code> 有多个地方调用,那么就可能导致内存泄漏。</p>
<h2 id="写在最后">写在最后</h2>
<p>JS 的代码,闭包概念随处可见,在使用时也需特别小心,不放心的时候,就将变量释放 <code>xx = null</code>!</p>


</div>
<div id="MySignature" role="contentinfo">
    <p>&nbsp;</p>
<p style="font-size: 18px;font-weight: bold;">文章首发于微信公众号【<span style="color:rgb(255, 71, 87)">前端路引</span>】,欢迎 <span style="color:#4ec259">微信扫一扫</span> 查看更多文章。</p>
<p>
<img style="max-width: 320px;" src="https://images.cnblogs.com/cnblogs_com/linx/2447020/o_250228035031_%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BA%8C%E7%BB%B4%E7%A0%81.png"/>
</p>
<p>本文来自博客园,作者:前端路引,转载请注明原文链接:https://www.cnblogs.com/linx/p/18932449</p>
<p>&nbsp;</p><br><br>
来源:https://www.cnblogs.com/linx/p/18932449
頁: [1]
查看完整版本: Web前端入门第 66 问:JavaScript 作用域应用场景(闭包)