-
Notifications
You must be signed in to change notification settings - Fork 0
/
1-2-2.html
660 lines (636 loc) · 24.4 KB
/
1-2-2.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="./public/favicon.ico" />
<meta http-equiv="cache-control" content="no-cache" />
<title></title>
<link rel="stylesheet" href=" https://necolas.github.io/normalize.css/8.0.1/normalize.css" />
<link rel="stylesheet" href="./hightlight/default.min.css" />
<link rel="stylesheet" href="./css/main.css" />
<link rel="stylesheet" href="./css/copybutton.css" />
<link rel="stylesheet" href="./css/hightlight.css" />
<script src="./hightlight/hightlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.11/clipboard.min.js"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-BEVZJDBC7Z"></script>
<script src="./js/gtag.js"></script>
</head>
<body>
<header>
<nav>
<h1>
<span id="toggle-menu"></span>
<a href="index.html"></a>
</h1>
</nav>
</header>
<main>
<aside>
<nav></nav>
</aside>
<article>
<h2 id="1-2-2">1-2-2 閉包</h2>
<p>
閉包是 JavaScript
中最基本也是最重要的概念之一,涉及作用域、作用域鏈、執行上下文、記憶體管理等多重基礎知識。
</p>
<p>簡易教學影片:</p>
<a href="https://www.youtube.com/watch?v=0rP4YzGdvEU">
youtube: 什麼是閉包(closure)?解說+範例【JavaScript基礎】
</a>
<h3>作用域</h3>
<p>
函數作用域和全域作用域 ,舉例來說,執行以下 foo 函數時,變數 bar 在 函數 foo
的作用域內,因此可以在函數本體內正常存取該變數,並輸出 bar。
</p>
<pre><code class="language-js">
function foo(){
var bar = 3
console.log(bar)
}
foo()
</code></pre>
<p>
執行以上程式會輸出
<code>undefined</code>
,原因是變數 bar 在函數內進行了提升。以上程式與以下程式結果是相等的。
</p>
<pre><code class="language-js">
function foo(){
var bar
console.log(bar)
bar = 3
}
foo ()
</code></pre>
<p>
但是,在使用
<code>let</code>
對 bar 進行宣告時(如下所示)則會顯示出錯
<code>Uncaught ReferenceError: bar is not defined。</code>
</p>
<pre><code class="language-js">
function foo(){
console.log(bar)
let bar = 3 // Uncaught ReferenceError: bar is not defined。
}
foo()
</code></pre>
<p>
我們知道,使用
<code>let</code>
或
<code>const</code>
宣告變數時會針對這個變數形成一個封閉的區塊級作用域,在這個區塊級作用域中,如果在宣告變數前存取該變數,就會出
現
<code>referenceError</code>
錯誤;如果在宣告變數俊存取該變數,則可以正常取得變數值,範例如下。
</p>
<pre><code class="language-js">
function foo(){
let bar = 3
console.log(bar) // 3
}
foo()
</code></pre>
<p>
在對應大括號形成的作用域中存在一個「死區」,起始於函數開頭,終止於相關變數宣告敘述的所在行。在這個範圍
內無法存取使用
<code>let</code>
或
<code>const</code>
宣告的變數。這個「死區」,的專業名稱為 TDE (Temporal Dead Zone),相關語言標準的介紹可參考
ECMA Script@ 2015 Language Specification。
</p>
<pre><code class="language-js">
function foo(){
// TDZ
let bar = 3
console.log(bar) // 3
}
foo()
</code></pre>
<p>以下舉一個特別的例子:</p>
<pre><code class="language-js">
function foo(arg1 = arg2 , arg2){
console.log(`${arg1} ${arg2}`)
}
foo('arg1','arg2') // 'arg1 arg2'
</code></pre>
<p>
在上面的 foo 函數中,如果没有傳入第一個參數,則會使用第二個參數作為第
一個實際參數。呼叫以上程式,傳回內容正常;但是當第一個參數為預設值 undefined 時,執行 arg1
= arg2 會被當作暫時性死區處理,範例如下:
</p>
<pre><code class="language-js">
function foo(arg1 = arg2, arg2){
console.log(`${arg1} ${arg2}`)
}
foo(undefined,'arg2')
// Uncaught ReferenceError : arg2 is not defined
</code></pre>
<p>
以上程式的輸出結果存在問題是因為除了區塊級作用域,函數參數預設值也會受到暫時性死區的影響。
</p>
<pre><code class="language-js">
function foo(arg1 = arg2, arg2){
console.log(`${arg1} ${arg2}`)
}
foo(null,'arg2') // 'null arg2'
</code></pre>
<p>
這就有關 undefined 和 null 的區別了。在執行 foo(null, 'arg2')
時,不會認為「函數第一個參數為預設值」,而會直接接收 null 作為第一個參數的值。
</p>
<pre><code class="language-js">
function foo(arg1){
let arg1
}
foo('arg1') // Uncaught SyntaxError: Identifier 'arg1' has already been defined
</code></pre>
<h3>執行上下文和呼叫堆疊</h3>
<h4>程式執行的兩個階段</h4>
<ul>
<li>程式預先編譯階段</li>
<li>程式執行階段</li>
</ul>
<p>
預先編譯階段是前置階段,這一階段會由編譯器將 JavaScript
程式編譯成可執行的程式。注意,這裡的預先編譯和傳統的編譯不同,傳統的編譯非常複雜,涉及分詞、解析、程式產生
等過程。這裡的預先編譯是 JavaScript 中的獨特概念,雖然 JavaScript
是直譯型語言,編譯一行,執行一行。但是在程式執行前,JavaScript
引擎確實會做一些「預先準備工作」。
</p>
<p>執行時的主要任務是執行程式邏輯,執行上下文在這個階段會全部建立完成。</p>
<p>
在透過語法分析,確認語法無誤之後,便會在預先編譯階段對 JavaScript
程式中變數的記憶體空閒進行分配,我們熟悉的變數提升過程便是在此階段完成的。
</p>
<p>對於預先編譯過程中的一些細節,我們應該注意以下 3 點。</p>
<ul>
<li>在預先編譯階段進行變數宣告。</li>
<li>在預先編譯階段對變數宣告進行提升,但是值為 undefined。</li>
<li>在預先編譯階段對所有非運算式的函數宣告進行提升。</li>
</ul>
<pre><code class="language-js">
function bar(){
console.log('bar1')
}
var bar = function(){
console.log('bar2')
}
bar() // 'bar2'
</code></pre>
<p>會輸出 bar2 ,接著我們調換程式順序,執行以下程式。</p>
<pre><code class="language-js">
var bar = function(){
console.log('bar2')
}
function bar(){
console.log('bar1')
}
bar() // 'bar2'
</code></pre>
<p>
以上輸出結果原因為在預先編譯階段雖然對變數 bar 進行了宣告,但是不會設定值;函數 bar
則被建立並提升。在程式執行時,變數 bar才會(透過運算式)被設定值,設定值的內容是函數本體為
console.log(bar2)的函數,輸出結果為 bar2。
</p>
<pre><code class="language-js">
foo(10)
function foo(num){
console.log(foo)
foo = num;
console.log(foo)
var foo
}
console.log(foo)
foo = 1
console.log(foo)
</code></pre>
<p>執行以上程式,輸出結果如下。</p>
<pre><code class="language-js">
// undefined
// 10
// foo (num){
// console.log(foo)
// foo = num
// console.log(foo)
// var foo
// }
// 1
</code></pre>
<p>
在foo(10)即時執行,會在函數本體內進行變數提升,此時執行函數本體內的第一行會輸出 undefined
,執行函數本體內的第三行會輸出 foo。接著執行 程式,執行到函數本體外的 console.log(foo)
敘述時,會輸出 foo 函數的內容 (因為foo函數內的 foo =num , num 被設定值給函數作用域內的
foo 變數)
</p>
<p>
上題的結論是,作用域在預先編譯階段確定,但是作用域鏈是在執行上下文
的建立階段完全產生的,因為函數在呼叫時才會開始建立對應的執行上下
文。執行上下文包含變數物件、作用域鏈及 this 的指向。
</p>
<p>
程式執行的整個過程說起來就像一條生產管線。第一道工序是在預先編譯階段建立變數物件 (
Variable Object , VO),此時只是建立,而未進行設定值。
到了下一道工序程式執行時,變數物件會轉為啟動物件 (Active Object,AO), 即完成 VO 向 AO
的轉換。此時,作用域鏈也將被確定,它由目前執行環境的
變數物件和所有外層已經完成的啟動物件組成。這道工序確保了變數和函數
的有序存取,即如果未在目前作用域中找到變數,則會繼續向上尋找直到全域作用域。
</p>
<p>這樣的工序在管線上串成一個整體,便是 JavaScript 引擎執行機制最基本的原理。</p>
<h3>呼叫堆疊</h3>
<p>
了解了上面的內容,函數呼叫堆疊便很好了解了。在執行一個函數時,如果
這個函數又呼叫了另外一個函數,而這「另外一個函數」又呼叫了另外一個
函數,這樣便形成了一系列的呼叫堆疊,程式如下。
</p>
<pre><code class="language-js">
function foo1(){
foo2()
}
function foo2() {
foo3 ()
}
function foo3()
foo4(){
}
function foo4(){
console.log('foo4')
}
foo1()
</code></pre>
<p>
不管使用哪種方式,我們都可以從中借助 JavaScript
引擎清晣地看到錯誤堆疊資訊,並由此看出函數呼叫關係。
</p>
<p>
在函數執行完畢並移出堆疊時,函數內的區域變數在下一個垃圾回收(GC)節點會被回收,該函數對應的執行上下文將被銷毀,這也正是我們在外界無法存取函數內定義的變數的原因。也就是說,只有在函數即時執行,相關函數才可以存取該變數,該變數會在預先編譯階段被建立,在執行時被啟動,在函數執行完畢後,其相關上下文會被銷毀。
</p>
<h3>閉包</h3>
<p>先來看一個簡單的範例,程式如下。</p>
<pre><code class="language-js">
function numGenerator(){
let num = 1
num++
return () =>{
console.log(num)
}
}
var getNum = numGenerator()
getNum ()
</code></pre>
<p>
在這個簡單的閉包範例中,numGenerator 建立了一個變數 num ,接著傳回列 印
num值的匿名函數,這個函數參考了變數 num,使得在外部可以透過呼叫 getNum 方法存取變數
num,因此在numGenerator 執行完畢後,即相關呼叫堆
疊移出堆疊後,變數num不會消失,仍然有機會被外界存取。
</p>
<p>執行以上程式,我們能清晰地看到 JavaScript 引擎對執行過程的分析,如圖 下圖所示。</p>
<div>圖1</div>
<img src="./assets/image/1-2-2/1.jpeg" alt="" />
<div>圖2</div>
<img src="./assets/image/1-2-2/2.jpeg" alt="" />
<p>num 值被標記為 Closure,即閉包變數。</p>
<p>
比較前述內容,我們知道在正常情況下外界是無法存取函數內部變數的,函數執行之後,上下文即被銷毀。但是在函數(外層)中,如果我們傳回了另一個函數,且這個傳回的函數使用了函數(外層)內的變數,那麼外界便能夠透過這個傳回的函數取得原函數(外層)內部的變數值。這就是閉包的基本原理。
</p>
<p>
因此,直觀上來看,閉包這個概念為 JavaScript
中存取函數內部變數提供了途徑和便利。這樣做的好處很多,舉例來說,我們可以利用閉包實現
「模組化」;再舉例來說,翻看Redux
原始程式的中介軟體實現機制,會發現其中也大量運用了閉包(函數式理念)。
</p>
<h3>記憶體管理</h3>
<p>
記憶體管理是電腦科學中的概念。不論使用哪種程式語言進行開發,記憶體管理都是指對記憶體生命週期的管理,而記憶體的生命週期無外乎分配記憶體丶讀寫記憶體、釋放記憶體,範例如下。
</p>
<pre><code class="language-js">
var foo = 'bar!' // 分配記憶體
alert(foo) // 讀寫記憶體
foo = null // 釋放記憶體
</code></pre>
<h4>記憶體管理基本概念</h4>
<p>我們知道記憶體空間可以分為堆疊空間和堆積空間,實際如下。</p>
<ul>
<li>
堆疊空間:由作業系統自動分配釋放,儲存函數的參數值、區域變數的值等,其操作方式類似資料結構中的堆疊。
</li>
<li>堆積空間:一般由開發者分配釋放,關於這部分空間要考慮垃圾回收的問題。</li>
</ul>
<p>
在 JavaScript 中,資料類型(未包含 ES Next
新資料類型)包含基底資料型態和參考類型,實際如下。
</p>
<ul>
<li>基底資料型態:如 undefined、null、number、boolean 、string 等。</li>
<li>參考類型:如 object 、array、function 等。</li>
</ul>
<p>
一般情況下,基底資料型態按照值大小儲存在堆疊空間中,佔有固定大小的記憶體空閒;參考類型儲存在堆積空間中,記憶體空間大小並不固定,需按參考情況來進行存取。範例如下。
</p>
<pre><code class="language-js">
var a = 11
var b = 10
var c = [1, 2, 3]
var d = { e: 20 }
</code></pre>
<p>以上範例對應的記憶體分配示意如下表。</p>
<h4>堆疊空間</h4>
<table>
<tr>
<th>變數</th>
<th>值</th>
</tr>
<tr>
<td>a</td>
<td>11</td>
</tr>
<tr>
<td>b</td>
<td>10</td>
</tr>
<tr>
<td>c</td>
<td>0x0012ff76(堆積空間的記憶體空間地址)</td>
</tr>
<tr>
<td>d</td>
<td>0x0012ff7c(堆積空間的記憶體空間地址)</td>
</tr>
</table>
<h4>堆積空間</h4>
<table>
<tr>
<td>[1,2,3]</td>
</tr>
<tr>
<td>{e:20}</td>
</tr>
</table>
<p>
對分配記憶體和讀寫記憶體的行為,所有語言都較為一致,但釋放記憶體的行為在不同語言之間有差異。舉例來說,JavaScript
依賴宿主瀏覽器的垃圾回收機制,一般情況下不用程式設計師操心。但這並不表示在釋放記憶體方面就
OK 了,某些情況下依然會出現記憶體洩漏現象。
</p>
<p>
記憶體洩漏是指記憶體空閒明明已經不再被使用,但由於某種原因並沒有被
釋放的現象。這是一個非常「玄學」的概念,因為記憶體空間是否還在使用
在某種程度上是不可判斷的,或判斷成本很高。記憶體洩漏的危害非常直觀:
它會直接導致程式執行缓慢,甚至當機。
</p>
<h3>記憶體洩漏場景</h3>
<p>案例1</p>
<pre><code class="language-js">
var element = document.getElementById("element")
element.mark = "marked"
// 移除 element 節點
function remove(){
element.parentNode.removeChild(element)
}
</code></pre>
<div>圖3</div>
<img src="./assets/image/1-2-2/3.jpeg" alt="" />
<div>圖4</div>
<img src="./assets/image/1-2-2/4.jpeg" alt="" />
<p>
在上面的程式中,我們只是把 id 為 element 的節點移除了,但是變數
element依然存在,該節點佔有的記憶體無法被釋放,如圖所示。為了解決這一問題,我們需要在
remove方法中增加 element =null,這樣更為穩妥。
</p>
<p>案例2</p>
<pre><code class="language-js">
var element = document.getElementById('element' )
element.innerHTML = '<button id="button">點擊</button>'
var button = document.getElementById('button')
button.addEventListener('click', function(){
// ...
})
element.innerHTML=''
</code></pre>
<p>
執行以上程式,因為存在 element innerHTML = '' 這行敘述, button 元素已經 從 DOM
中移除了,但是由於其事件處理控制碼還在,所以該節點變數依然無法被回收。因此,我們還需要增加
removeEventListener 函數,以防止記憶體 洩漏。
</p>
<p>案例3</p>
<pre><code class="language-js">
function foo(){
var name = 'lucas'
window.setInterval(function(){
console.log(name)
}, 1000)
}
foo ()
</code></pre>
<p>
在這段程式中,由於存在 window.setInterval ,所以 name
記憶體空閒始終無法被釋放,如果不是業務要求的話,一定要記得在合適的時機使用
clearInterval清理。
</p>
<h3>瀏覽器垃圾回收</h3>
<p>
對於瀏覽器垃圾回收,除了開發者主動確保回收外,大部分場景下瀏覽器都會依靠標記清除、參考計數兩種演算法進行回收。
</p>
<h3>記憶體洩漏和垃圾回收注意事項</h3>
<p>
畢竟如今瀏覽器千變萬化且一直在演進中。從以上範例中可以看出,借助閉包來綁定資料變數,可以保護這些資料變數的區塊在閉包存活時,始終不被垃圾回收機制回收。正因為閉包使用不當極有可能引發記憶體洩漏,因此需
要格外注意。
</p>
<pre><code class="language-js">
function foo(){
let value = 123
function bar(){
alert(value)
}
return bar
}
let bar = foo()
</code></pre>
<p>
在以上範例中,變數 value 將被儲存在記憶體中,如果加上 bar = null ,則隨著 bar 不再被參考,
value 也會被清除。
</p>
<p>結合瀏覽器引擎的最佳化情況,對上述程式進行改動,修改後的程式如下。</p>
<pre><code class="language-js">
function foo(){
let value = Math.random()
function bar(){
debugger
}
return bar
}
let bar = foo()
bar()
</code></pre>
<p>
在 Chrome 瀏覽器 V8 最新引擎中執行上述程式,並在函數 bar 中設定中斷 點,會發現 value
沒有被參考,如圖所示。
</p>
<div>圖5</div>
<img src="./assets/image/1-2-2/5.jpeg" alt="" />
<p>下面在 bar 函數中加入對 value 的參考,程式如下。</p>
<pre><code class="language-js">
function foo(){
let value = Math.random()
function bar(){
console.log(value)
debugger
}
return bar
}
let bar = foo()
bar()
</code></pre>
<p>此時 value 就出現了如下圖:</p>
<div>圖6</div>
<img src="./assets/image/1-2-2/6.jpeg" alt="" />
<p>現在看一個書中代碼案例,接著透過 Chrome devtool 來檢測排除洩漏問題。</p>
<pre><code class="language-js">
var array = []
function createNodes(){
let div
let i = 100
let frag = document.createDocumentFragment()
for (; i> 0; i--){
div = document.createElement("div")
div.appendChild(document.createTextNode(i))
frag.appendChild (div)
}
document.body.appendChild(frag)
}
function badCode(){
array.push([...Array(100000).keys()])
createNodes()
setTimeout(badCode, 1000)
}
badCode()
</code></pre>
<p>
以上程式遞迴呼叫了 badCode,這個函數每次向 array 陣列中寫入新的由 100000 項 0~1
數字組成的新陣列,badCode 函數使用全域變數 array 後並沒有
手動釋放記憶體,垃圾回收機制不會處理 array,因此會導致記憶體洩漏;同 時,badCode
函數呼叫了 createNodes 函數,每秒會建立 100 個 div 節點。
</p>
<p>記憶體狀況如下圖所示</p>
<div>圖7</div>
<img src="./assets/image/1-2-2/7.jpeg" alt="" />
<p>
執行20秒快照後可以發現,JS Heap 和 Nodes 線隨著時間線一直在上升,並沒有被
垃圾回收機制回收。因此,可以判斷這段程式存在較大的記憶體洩漏風險。
如果不知道問題程式的位置,要想找出風險點,那就需要在 Chrome Memory 標籤中,對JS Heap
中的每一項,尤其是 Size 較大的前幾項展開調查。
</p>
<div>圖8</div>
<img src="./assets/image/1-2-2/8.jpeg" alt="" />
<p>就拿本網站為例同樣執行20秒快照,沒有記憶體洩漏疑慮的報表會如下所示:</p>
<div>圖9</div>
<img src="./assets/image/1-2-2/9.jpeg" alt="" />
<div>圖10</div>
<img src="./assets/image/1-2-2/10.jpeg" alt="" />
<h3>例題分析</h3>
<h4>例題1</h4>
<pre><code class="language-js">
const foo = (function() {
var v = 0
return()=>{
return v++
}
}())
for(let i = 0; 1 < 10;i++){
foo()
}
console.log(foo()) // 10
</code></pre>
<p>foo 是一個立即執行函數,當我們嘗試列印 foo 時,要執行以下程式。</p>
<pre><code class="language-js">
const foo = (function() {
var v = 0
return ()=>{
return v++
}
}())
console.log(foo)
// 輸出結果如下。
()=>{
return v++
}
</code></pre>
<p>
在循環執行 foo 時,參考自由變數 10 次,v 自動增加10次,最後執行 foo 時,獲得
10。這裡的自由變數是指沒有在相關函數作用域中宣告,但卻被使用了的變數。
</p>
<h4>例題2</h4>
<pre><code class="language-js">
const foo = ()=> {
var arr = []
var i
for (i = 0; i < 10; i++) {
arr[i] = function() {
console.log(i)
}
}
return arr[0]
}
foo(){}
</code></pre>
<p>
答案是10。在本例中,自由變數為 i ,類似實戰例題1 ,執行 foo 傳回的是 arr[0], arr[0]
此時是函數,其中變數 i 的值為 10。
</p>
<h4>例題3</h4>
<pre><code class="language-js">
var fn = null
const foo=()=>{
var a=2
function innerFoo() {
console.log(a) // 2
}
fn = innerFoo
}
const bar =()=>{
fn()
}
foo()
bar()
</code></pre>
<p>
正常來講:根據呼叫堆疊的知識,foo
函數執行完畢後,其執行環境生命員期會結束,所佔用的記憶體會被垃圾收集器釋放,上下文消失。但是透過將
innerFoo 函數設定值給全域變數 fn, foo 的變數物件 a 也會被保留下來。所以,函數 fn 在函數
bar 內部即時執行,依然可以存取這個被保留下來的變數 物件,輸出結果為 2。
</p>
<h4>例題4</h4>
<pre><code class="language-js">
var fn = null
const foo = () => {
var a = 2
function innerFoo(){
console.log(c)
console.log(a)
}
fn = innerFoo
}
const bar = ()=> {
var c = 100
fn()
}
foo ()
bar ()
</code></pre>
<p>上面例子會報錯。</p>
<p>
其實在 bar 中執行 fn 時, fn 已經被複製為 innerFoo,變數 c 並不在其作用域鏈上, c 只是 bar
函數的內部變數,因此會顯示出錯 ReferenceError: c is not defined。
</p>
<div>圖11</div>
<img src="./assets/image/1-2-2/11.jpeg" alt="" />
</article>
</main>
</body>
<script type="module" src="./js/main.js"></script>
</html>