1 回答

TA貢獻1866條經驗 獲得超5個贊
JS中對于函數的聲明語法在標準中就有兩種不同的語法樣式,其一稱為函數聲明
樣式,另一個是函數表達式
樣式,在標準中也寫得很簡略,像下面這樣,上面的是函數聲明
樣式,下面則是函數表達式
,opt
記號代表的是可選的,也就是可有可無的意思:
FunctionDeclaration :function Identifier ( FormalParameterList opt ){ FunctionBody } FunctionExpression :function Identifier opt ( FormalParameterList opt ){ FunctionBody }
所以在函數表達式
這種樣式,函數的識別名是可以不需要有的,但它因為是個表達式
,可以賦給另一個變量當值來看待,所以才會有像var f = function(){ return 23 }
的這種寫法,真正在函數調用時是用前面的變量識別名稱f,而不是額外的那個可有可無的函數識別名稱。如果充分區分出函數聲明
與函數表達式
的兩種不同樣式,在文中最好以函式創建
這字詞來說明函數聲明或函數表達式的這兩種語句較佳。
那要分辨何者是函數聲明
又何者是函式表達式
?最簡單的方式就是看語句的開頭,在單一個語句中以function
作為該語句開頭的函數,應該就是函數聲明
,函式表達式
不會以function開頭。所以下面的幾例都是函式表達式
而不是函式聲明
樣式:
var a = function() { return 3; } var a = function bar() { return 3; } (function sayHello() { alert("hello!"); })();
而為何會有兩種不同的函數語法樣式,主要是這兩種樣式的用處不同,這有很多不同的應用情況。
最明顯的例子是函數聲明
有特殊的提升(hoisting)特性,在同一作用域中的函數聲明
會先被提升到此作用域的最上面,所以函數聲明
可以寫在代碼文檔的后面,但可以在代碼文檔的上面位置使用。此外,函數聲明
只能在函數中塊級或整個應用的全局區中使用,它沒辦法在其他的塊級中使用,例如像if、for等的花括號({})中。而在某些情況下,當需要把函數整體塊級作為一種值,用來當其他函數的傳參或返回時,例如回調函數的語法結構,就是要使用函式表達式
的樣式才可以達到。當然,基本上這兩者看起來好像都是長得一樣,實際上在執行階段的運作并不相同。
帶有名稱的函數表達式,有個專用語稱之為"具名函數表達式"(Named function expressions,NFE),這個函數的識別名,它的作用域到底是在什么地方,答案是在函數的主體(FunctionBody)內部。原因當然它只是個原本就可有可無的"代理"函數名,真正的這函數識別名稱是被賦值的那個變量識別名。
正常情況下,你只能在函數表述式中的主體中使用這個"代理函數名",這也是符合標準的規定,如下面的例子:
var f = function foo(){ return typeof foo; };typeof foo; // "undefined"f(); // "function"
那么又為何要使用這個"代理函數名",不是可有可無的嗎?
因為這個名稱在調試時,可以明確地在呼叫堆疊中看到,如果是不加這名稱,也就是"匿名函數表達式"在調試時是看不準是呼叫什么的。這使得調試時多了一些便利,所以它會被用在這個情況下。
比較特別的情況是在IE8以前的版本中,它里面的JS引擎并不是現在的標準ECMAScript規范,而是JScript 5.8。IE8并沒有設計這個封閉作用域,來界定出函數表達式的作用域,而且,在IE8中認為這種"具名函數表達式",相等于函數聲明
。
以上的資料主要參考Named function expressions demystified與Function Declarations vs. Function Expressions
后面針對題目本身提供一些解答:
上面代碼中test函數的參數foo函數是函數表達式對吧?也算是函數聲明吧?
是函數表達式
而不是函數聲明,這是依照ECMAScript標準的說明。只是它有帶函數名,是上面說的"具名函數表達式"(NFE)。
代碼中的foo函數到底存在于哪個作用域里面呢?
在函數表達式
的函數定義塊級作用域里,也就是函數的主體(FunctionBody)內部,上面有例子。但這有例外情況,在IE8以前的IE瀏覽器,這代碼應該是會如同函數聲明樣式一樣的執行結果。
另一個問題忘了說明,補充一下。
全域中有一個bar=99變量,在test函數中的區塊中也有聲明一個bar=1,那么如果在傳參中是以函數類型傳入test函數,有存取bar如console.log(bar)
,為何答案是99而不是1?
主要是因為傳參的作用域是在全局作用域,并非函數的作用域之中。也就是說像下面的代碼:
test(function foo(){ console.log(bar);});
相等于下面的代碼:
var f = function foo(){ console.log(bar); }test(f);
也就是說在本例子中,test的傳參在傳入后,然后被調用,里面要作什么事,已經可以決定了結果,也就是相當于:
var bar = 99;var f = function foo(){ console.log(99); // bar在全局中已賦值為99} test(f);
上面說的概念雖然會有點怪異,在這個例子你可以把f這個函數,當成只是稍晚些打印到控制臺的一種變量而已。
因為很特別的是,這個f函數,它并沒有傳參,也沒有返回,單純只是打印bar變量,這個bar變量如果要能有值,只能從函數所能存取得到的作用域而來,而且是以函數聲明的主體上下文作用域為主。
例子中的傳參的作用域是位于全局作用域,所以只能存取得到全局的那個var bar = 99
變量。
會讓人誤解的是這個test函數中的寫法:
function test(fn){ var bar = 1; fn(); }
有可能會直覺得認定,應該是相當于下面的寫法:
function test(function foo(){ console.log(bar); }){ var bar = 1; foo(); }
所以它的結果應該是:
console.log(1)
但結果不是上面那樣,實際上這相當于下面的例子,因為f函數不需要傳參,所以相等于直接在test函數中調用而已:
var f = function foo(){ console.log(99); // bar在全局中已賦值為99}function test(){ var bar = 1; f(); }
除非f函數的內容,是定義在test函數的區塊之中時,才有可能得到1的打印結果,像下面這樣:
function test(){ var bar = 1; var f = function foo(){ console.log(bar); // bar相當于1 } f(); }
JS中的作用域,是屬于詞法上的作用域,也就是"靜態"的作用域,作用域中的變量綁定,在執行前就已經決定好了,按定義是在編譯期間。在JavaScript: The Definitive Guide這本書中對作用域的一句說明如下:
函數在調用(執行)時,使用它們被定義(聲明)時被影響的作用域鏈
這與題中所引用的 你不知道的JavaScript 書中的說明是一樣的:
無論函數在哪里被調用,也無論它如何被調用,它的詞法作用域都只由函數被聲明時所處的位置決定。
添加回答
舉報