最近在學習JavaScript的函數,函數是JavaScript的一等對象,想要學好JavaScript,就必須深刻理解函數。本人把思路整理成文章,一是為了加深自己函數的理解,二是給讀者提供學習的途徑,避免走彎路。內容有些多,但都是筆者對於函數的總結。
1.函數的定義
1.1:函數聲明
1.2:函數表達式
1.3:命名函數的函數表達式
1.4:函數的重復聲明
1.5:不能在條件語句中聲明函數
2.函數的部分屬性和方法
2.1:name屬性
2.2:length屬性
2.3:toString()方法
3.函數作用域
3.1:全局作用域和局部作用域
3.2:函數內部的變量提升
3.3:函數自身的作用域
1.函數的定義
1.1:函數聲明
函數就是一段可以反復調用的代碼塊。函數聲明由三部分組成:函數名,函數參數,函數體。整體的構造是function命令後面是函數名,函數名後面是一對圓括號,裡面是傳入函數的參數。函數體放在大括號裡面。當函數體沒有使用return關鍵字返回函數時,函數調用時返回默認的undefined;如果有使用return語句,則返回指定內容。函數最後不用加上冒號。
function keith() {}
console.log(keith()) // 'undefined'
function rascal(){
return 'rascal';
}
console.log(rascal()) // 'rascal'
函數聲明是在預執行期執行的,也就是說函數聲明是在浏覽器准備解析並執行腳本代碼的時候執行的。所以,當去調用一個函數聲明時,可以在其前面調用並且不會報錯。
1 console.log(rascal()) // 'rascal'
2 function rascal(){
3 return 'rascal';
4 }
其實這段代碼沒有報錯的原因還有一個,就是與變量聲明提升一樣,函數名也會發生提升。函數名提升會在下面小節談到。
1.2:函數表達式
函數表達式是把一個匿名函數賦給一個全局變量。這個匿名函數又稱為函數表達式,因為賦值語句的等號右側只能放表達式。函數表達式末尾需要加上分號,表示語句結束。
1 var keith = function() {
2 //函數體
3 };
函數表達式與函數聲明不同的是,函數表達式是浏覽器解析並執行到那一行才會有定義。也就是說,不能在函數定義之前調用函數。函數表達式並不像函數聲明一樣有函數名的提升。如果采用賦值語句定義函數並且在聲明函數前調用函數,JavaScript就會報錯。
1 keith();
2 var keith = function() {};
3 // TypeError: keith is not a function
上面的代碼等同於下面的形式。
1 var keith;
2 console.log(keith()); // TypeError: keith is not a function
3 keith = function() {};
上面代碼第二行,調用keith的時候,keith只是被聲明了,還沒有被賦值,等於undefined,所以會報錯。
1.3:命名函數的函數表達式
采用函數表達式聲明函數時,function命令後面不帶有函數名。如果加上函數名,該函數名只在函數體內部有效,在函數體外部無效。
1 var keith = function boy(){
2 console.log(typeof boy);
3 };
4
5 console.log(boy);
6 // ReferenceError: boy is not defined
7
8 keith();
9 // function
上面代碼在函數表達式中,加入了函數名boy。這個boy只在函數體內部可用,指代函數表達式本身,其他地方都不可用。這種寫法的用處有兩個,一是可以在函數體內部調用自身,二是方便除錯(除錯工具顯示函數調用棧時,將顯示函數名,而不再顯示這裡是一個匿名函數)。
1.4:函數的重復聲明
如果同一個函數被多次聲明,後面的聲明就會覆蓋前面的聲明。
1 function keith() {
2 console.log(1);
3 }
4 keith(); //2
5 function keith() {
6 console.log(2);
7 }
8 keith(); //2
上面代碼中,後一次的函數聲明覆蓋了前面一次。而且,由於函數名的提升,前一次聲明在任何時候都是無效的。JavaScript引擎將函數名視同變量名,所以采用函數聲明的方式聲明函數時,整個函數會像變量聲明一樣,被提升到代碼頭部。表面上,上面代碼好像在聲明之前就調用了函數keith。但是實際上,由於“變量提升”,函數keith被提升到了代碼頭部,也就是在調用之前已經聲明了。再看一個典型的例子。
1 if (true) {
2 function foo() {
3 return 1;
4 }
5 } else {
6 function foo() {
7 return 2;
8 }
9 }
10
11 console.log(foo()) //2
這個例子十分典型,調用foo函數之後返回的是2,而不是1。在條件語句中聲明函數會在下面說到。
1.5:不能在條件語句中聲明函數
參考這篇文章,原文有那麼一句話(本人翻譯):在條件語句中聲明函數是非標准結構的特征。也就是說,在if代碼塊聲明了函數,按照語言規范,這是不合法的。但是,實際情況是各家浏覽器往往並不報錯,能夠運行。
由於存在函數名的提升,所以在條件語句中聲明函數,可能是無效的。
1 if (false) {
2 function f() {}
3 }
4 console.log(f()); //undefined
上面代碼的原始意圖是不聲明函數f,但是由於f的提升,導致if語句無效,所以上面的代碼不會報錯。要達到在條件語句中定義函數的目的,只有使用函數表達式。
1 if (false) {
2 var f = function () {};
3 }
4
5 console.log(f()) //Uncaught TypeError: f is not a function
2.函數的部分屬性和方法
2.1:name屬性
name屬性返回緊跟在function關鍵字之後的那個函數名。
1 function k1() {};
2 console.log(k1.name); //'k1'
3
4 var k2 = function() {};
5 console.log(k2.name); //''
6
7 var k3 = function hello() {};
8 console.log(k3.name); //'hello'
上面代碼中,name屬性返回function 後面緊跟著的函數名。對於k2來說,返回一個空字符串,注意:匿名函數的name屬性總是為空字符串。對於k3來說,返回函數表達式的名字(真正的函數名為k3,hello這個函數名只能在函數內部使用。)
2.2:length屬性
length屬性返回函數預期傳入的參數個數,即函數定義之中的參數個數。返回的是個數,而不是具體參數。
1 function keith(a, b, c, d, e) {}
2 console.log(keith.length) // 5
上面代碼定義了空函數keith,它的length屬性就是定義時的參數個數。不管調用時輸入了多少個參數,length屬性始終等於5。也就是說,當調用時給實參傳遞了6個參數,length屬性會忽略掉一個。
2.3:toString()方法
函數的toString方法返回函數的代碼本身。
1 function keith(a, b, c, d, e) {
2 // 這是注釋。
3 }
4 console.log(keith.toString());
5 //function keith(a, b, c, d, e) { // 這是注釋。 }
可以看到,函數內部的注釋段也被返回了。
3.函數作用域
3.1:全局作用域和局部作用域
作用域(scope)指的是變量存在的范圍。Javascript只有兩種作用域:一種是全局作用域,變量在整個程序中一直存在,所有地方都可以讀取,在全局作用域中聲明的變量稱為全局變量;另一種是局部作用域,變量只在函數內部存在,此時的變量被稱為局部變量。
在全局作用域中聲明的變量稱為全局變量,也就是在函數外部聲明。它可以在函數內部讀取。
1 var a=1;
2 function keith(){
3 return a;
4 }
5 console.log(keith()) //1
上面代碼中,全局作用域下的函數keith可以在內部讀取全局變量a。
在函數內部定義的變量,只能在內部訪問,外部無法讀取,稱為局部變量。注意這裡必須是在函數內部聲明的變量。
1 function keith(){
2 var a=1;
3 return a;
4 }
5 console.log(a) //Uncaught ReferenceError: a is not defined
在上面代碼中,變量a在函數內部定義,所以是一個局部變量,外部無法訪問。
函數內部定義的變量,會在該作用域下覆蓋同名變量。注意以下兩個代碼段的區別。
1 var a = 2;
2
3 function keith() {
4 var a = 1;
5 console.log(a);
6 }
7 keith(); //1
8
9 var c = 2;
10
11 function rascal() {
12 var c = 1;
13 return c;
14 }
15 console.log(c); //2
16 console.log(rascal()); //1
上面代碼中,變量a和c同時在函數的外部和內部有定義。結果,在函數內部定義,局部變量a覆蓋了全局變量a。
注意,對於var命令來說,局部變量只能在函數內部聲明。在其他區塊聲明,一律都是全局變量。比如說if語句。
1 if (true) {
2 var keith=1;
3 }
4 console.log(keith); //1
從上面代碼中可以看出,變量keith在條件判斷區塊之中聲明,結果就是一個全局變量,可以在區塊之外讀取。但是這裡如果采用ES6中let關鍵字,在全局作用域下是無法訪問keith變量的。
3.2:函數內部的變量聲明提升
與全局作用域下的變量聲明提升相同,局部作用域下的局部變量在函數內部也會發生變量聲明提升。var命令聲明的變量,不管在什麼位置,變量聲明都會被提升到函數體的頭部。
1 function keith(a) {
2 if (a > 10) {
3 var b = a - 10;
4 }
5 }
6
7 function keith(a) {
8 var b;
9 if (a > 10) {
10 b = a - 10;
11 }
12 }
上面兩個函數段是相同的。
3.3:函數本身的作用域
函數本身也是一個值,也有自己的作用域。它的作用域與變量一樣,就是其聲明時所在的作用域,與其運行時所在的作用域無關。
1 var a = 1;
2 var b = function() {
3 console.log(a);
4 };
5 function c() {
6 var a = 2;
7 b();
8 }
9 c(); //1
10
11 var a = 1;
12 var b = function() {
13 return a;
14 };
15 function c() {
16 var a = 2;
17 return b();
18 }
19 console.log(c()); //1
以上兩個代碼段相同。函數b是在函數c外部聲明的。所以它的作用域綁定在函數外層,內部函數a不會到函數c體內取值,所以返回的是1,而不是2。
很容易犯錯的一點是,如果函數A調用函數B,卻沒考慮到函數B不會引用函數A的內部變量。
1 var b = function() {
2 console.log(a);
3 };
4 function c(f) {
5 var a = 1;
6 f();
7 }
8 c(b); //Uncaught ReferenceError: a is not defined
9
10
11 var b = function() {
12 return a;
13 };
14 function c(f) {
15 var a = 1;
16 return f();
17 }
18 console.log(c(b)); //Uncaught ReferenceError: a is not defined
上面代碼將函數b作為參數,傳入函數c。但是,函數b是在函數c體外聲明的,作用域綁定外層,因此找不到函數c的內部變量a,導致報錯。
同樣的,函數體內部聲明的變量,作用域綁定在函數體內部。
1 function keith() {
2 var a = 1;
3
4 function rascal() {
5 console.log(a);
6 }
7 return rascal;
8 }
9
10 var a = 2;
11 var f = keith();
12 f(); //1
上面代碼中,函數keith內部聲明了rascal變量。rascal作用域綁定在keith上。當我們在keith外部取出rascal執行時,變量a指向的是keith內部的a,而不是keith外部的a。這裡涉及到函數另外一個重要的知識點,即在一個函數內部定義另外一個函數,也就是閉包的概念。下次有機會會分享。
總之,函數執行時所在的作用域,是定義時的作用域,而不是調用時所在的作用域。
完。
感謝大家的閱讀。