函式作用域(Function Scope)
函式作用域內的變數、函式僅能在這個函式(Function)裡面使用。以下範例發現在全域環境作用域中讀取不到函式作用域中的phoneNumber
、birthDay
、age
,但可以存取到myFunc()
。
function myFunc(){
var phoneNumber = "091234567";
var birthDay = "10/31";
var age = 18;
console.log(phoneNumber);
console.log(birthDay);
console.log(age);
}
var name = "Teagan";
console.log(phoneNumber); //ReferenceError
console.log(birthDay); //ReferenceError
console.log(age); //ReferenceError
console.log(name); //Teagan
myFunc()
//091234567
//10/31
//18
函式宣告(Function Declaration)
傳參數
function yourName(name){
var name = "Teagan";
console.log(name);
}
yourName();
//Teagan
函式運算式(Function Expression)
用變數指定給函式
var yourName = function bar(name) {
var name = "Teagan";
console.log(name);
}
yourName();
//Teagan
IIFE(立即呼叫函式表達式)
IIFE
可以幫助我們避免污染到全域變數,也可以讓函數在被宣告的那刻就立即執行。以下範例(還未賦值):
(function foo(num){
console.log(`This is ${num}`);}
)();
//This is undefined
如果我們要賦值的話可在()
中加入值,如以下範例:
(function foo(num){
console.log(`This is ${num}`);}
)(100);
//This is 100
IIFE
可以幫助我們避免污染到全域變數,在global scope
的firstName
就無法干擾到函式裡的firstName
:
//global scope
var firstName = "Wu";
//function scope
(function yourName(name) {
var firstName = "Hsu";
console.log(`Hello! ${name} ${firstName}`)
})("Teagan");
//Hello! Teagan Hsu
反之,如果要在IIFE
中傳入全域變數,只要傳入(window)
即可:
var dinner = 'beef';
(function food(global){
var dinner = 'vegetable';
console.log(`My dinner is ${dinner}`);
console.log(`My dinner is ${global.dinner}`);
})(window);
//My dinner is vegetable
//My dinner is beef
名稱相同造成的衝突與錯誤
函式宣告與函式運算式可能會因為不當命名,造成衝突與錯誤,像是以下的例子:
function i(a) {
b = j(a * 3);
return (b);
};
function j(a) { return a - 3; };
function j(a) { return a - 1; };
var b;
i(100);
//299
解決方法
①function裡再包入一個function
function i(a) {
b = j(a * 3);
return b;
function j(a) { return a - 3; }
};
function j(a) { return a - 1; };
var b;
i(100);
//297
②使用IIFE
(function i(a) {
b = j(a * 3);
return b;
})(function j(a) { return a - 3; });
//傳值function j(a) { return a - 3; }
function j(a) { return a - 1; }
var b;
i(100);
//297
經典題目
使用setTimeout打印出0、1、2、3、4*
setTimeout
函數的回調(callback)
不會被觸發直到for loop
執行完成。也就是說for loop
執行完成後,才會執行回調函數。for loop
完成後i
的值為5
,當執行回調(callback)
函式時,函式使用的i
值會是最後跑出的5
,這也就是為什麼5
會被執行5次。
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
//5, 5, 5, 5, 5
解決方法
那我們要如何順利使用setTimeout打印出0、1、2、3、4呢?
①使用let
let
可幫助我們解決這個問題,使用let
時每次迭代都會創建一個單獨的作用域,可以打印連續的變量值(variable value)
,範例如下:
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
//0, 1, 2, 3, 4
②把for loop與setTimeout分開
一開始就不要把setTimeout
包在for loop
裡,而是分開寫程式以免污染全域作用域。
for (let i = 0; i < 5; i++) {
timeoutID(i);
};
function timeoutID(i) {
setTimeout(function() {
console.log(i);
}, 1000);
};
//0, 1, 2, 3, 4
③使用IIFE
同理,使用IIFE
也可解決這個問題。
for (var i = 0; i < 5; i++) {
(function(x) {
setTimeout(function() {
console.log(x);
}, 1000);
})(i)
}
//0, 1, 2, 3, 4
區塊作用域(Block Scope(ES6))
ES6 以後可以使用新關鍵字const
和let
宣告區塊作用域(Block Scope)
的變數。
const
const
的特性是宣告時就必須賦值,而且賦值後不可修改其值,否則會報錯。通常使用在不再做更改的變數,像是:
const pi = 3.14
let
let
與var
的差別在於,var
作用於function scope
中,而let
作用於block scope
中。以下例子可以發現var name= "Amy"
取代了原本的Teagan
,因此console.log
的結果會是//Amy
。而使用let
則不會有此問題,因為let
只作用在大括弧中的區塊{…}
以var為例:
function varName () {
var name="Teagan";
if (true) {
var name= "Amy";
}
console.log(name);
}
varName() //Amy
以let為例:
function letName () {
let name = "Teagan";
if (true) {
let name = "Amy";
}
console.log(name);
}
letName() //Teagan
-
參考資料:
- Understanding setTimeout Inside For Loop In JavaScript - Code Handbook
- Watch Out When Using SetTimeout() in For Loop #JS
- 函式與作用域 - 從ES6開始的JavaScript學習生活
- 你懂 JavaScript 嗎?#12 函式範疇與區塊範疇(Function vs Block Scope)- Summer。桑莫。夏天
- 重新認識 JavaScript: Day 18 Callback Function 與 IIFE
- JavaScript: var, let, const 差異
- 鐵人賽:ES6 開始的新生活 let, const