一、函式
每個函式都是 Function型別的例項,也具有屬性和方法。由於函式也是一個物件,因此函式名實際上也是一個指向函式物件的指標,不會與某個函式繫結
1、函式的定義方式
(1)、函式宣告
function add(a, b) { return a + b; }
函式宣告提升:在執行程式碼之前,會先讀取函式宣告,也就是說,可以把函式宣告放在呼叫它的程式碼之後
fn(); // 1 function fn() {console.log(1)}
(2)、函式表示式
var add = function(a, b) { return a + b; };
函式表示式看起來像是常規的變數賦值,由於其function關鍵字後面沒有指定函式名,因此是一個匿名函式
函式表示式必須先賦值,不具備函式宣告提升的特性
fn(); // Uncaught TypeError: fn is not a function var fn = function(){console.log(1)};
由於函式宣告提升這一特性,導致在某些情況下會出現意想不到的結果,例如:
var flag = true; if(flag) { function fn() { console.log('flag 為true') } } else{ function fn() { console.log('flag 為false') } } fn(); // chrome, firefox, ie11 輸出 flag 為true // ie10及以下 輸出 flag 為false
本意是想flag為true時輸出 'flag 為true', flag為false時輸出 'flag 為false',為何結果卻不盡相同呢?究其原因就在於函式宣告提升,執行程式碼時首先讀取函式宣告,而 if...else...程式碼塊同屬於全域性作用域,因此後面的同名函式會覆蓋前面的函式,最終函式fn就只剩下一個 function fn(){console.log('flag 為false')}
由於函式宣告提升導致的這一結果令人大為意外,因此,js引擎會嘗試修正錯誤,將其轉換為合理狀態,但不同瀏覽器版本的做法並不一致
此時,函式表示式就可以解決這個問題
var flag = true; var fn; if(flag) { fn = function() { console.log('flag 為true'); } } else{ fn = function() { console.log('flag 為false'); } } fn() //chrome, firefox, ie7-11 均輸出 flag 為true
其實這個也很好理解,js預解析時,fn和flag均被初始化為undefined,然後程式碼從上到下逐行執行,首先給flag賦值為true,進入if語句,為fn賦值為 function fn(){console.log('flag 為true')}
關於函式表示式,還有一種寫法,命名函式表示式
var add = function f(a, b) { console.log(a + b); } add(1,2); // 3 f(1,2); // Uncaught ReferenceError: f is not defined var add = function f(a, b) { console.log(f); } console.log(add); add(3, 5); // ƒ f(a, b) { // console.log(f); // }
由此可見,命名函式f也是指向函式的指標,只在函式作用域內部可用
(3)、Function建構函式
var add = new Function('a', 'b', 'return a + b');
不推薦這種寫法,因為這種語句會導致解析兩次程式碼,第一次是解析js程式碼,第二次解析傳入建構函式中的字串,從而影響效能
2、沒有過載
在java中,方法具有過載的特性,即一個類中可以定義有相同名字,但引數不同的多個方法,呼叫時,會根據不同的引數選擇不同的方法
public void add(int a, int b) { System.out.println(a + b); } public void add(int a, int b, int c) { System.out.println(a * b * c); } // 呼叫時,會根據傳入引數的不同,而選擇不同的方法,例如傳入兩個引數,就會呼叫第一個add方法
而js則沒有函式過載的概念
function add(a, b) { console.log(a + b); } function add(a, b, c) { c = c || 2; console.log(a * b * c); } add(1, 2); // 4 (直接呼叫最後一個同名的函式,並沒有過載)
由於函式名可以理解成一個指向函式物件的指標,因此當出現同名函式時,指標就會指向最後一個出現的同名函式,就不存在過載了(如下圖所示)
3、呼叫匿名函式
對於函式宣告和函式表示式,呼叫函式的方式就是在函式名(或變數名)後加一對圓括號
function fn() { console.log('hello') } fn() // hello
既然fn是一個函式指標,指代函式的程式碼段,那能否直接在程式碼段後面加一對圓括號呢?
function fn() { console.log('hello') }() // Uncaught SyntaxError: Unexpected token ) var fn = function() { console.log('hello') }() // hello
分別對函式宣告和函式表示式執行這一假設,結果出人意料。另外,前面也提到函式宣告存在函式宣告提升,函式表示式不存在,如果在函式宣告前加一個合法的JS識別符號呢?
console.log(fn); // ƒ fn() {console.log('hello');} function fn() { console.log('hello'); } // 在function關鍵字前面加一個合法的字元,結果就把fn當做一個未定義的變數了 console.log(fn); // Uncaught ReferenceError: fn is not defined +function fn() { console.log('hello'); }
基於此可以大膽猜測,只要是function關鍵字開頭的程式碼段,js引擎就會將其宣告提前,所以函式宣告後加一對圓括號會認為是語法錯誤。結合函式表示式後面直接加圓括號呼叫函式成功的情況,做出如下嘗試:
+function() { console.log('hello') }() -function() { console.log('hello') }() *function() { console.log('hello') }() /function() { console.log('hello') }() %function() { console.log('hello') }() // hello // hello // hello // hello // hello
竟然全部成功了,只是這些一元運算子在此處並無實際意義,看起來令人費解。換成空格吧,又會被js引擎給直接跳過,達不到目的,因此可以用括號包裹起來
(function() { console.log('hello'); })(); (function() { console.log('hello'); }()); // hello // hello
無論怎麼包,都可以成功呼叫匿名函式了,我們也不用再困惑呼叫匿名函式時,圓括號該怎麼加了
4、遞迴呼叫
遞迴函式是在一個函式通過名字呼叫自身的情況下構成的
一個經典的例子就是計算階乘
// 3! = 3*2*1 // 4! = 4*3*2*1 = 4*3! function factorial(num) { if(num <= 1) { return 1 } return num * factorial(num - 1) } console.log(factorial(5)) // 120 console.log(factorial(4)) // 24
如果現在把函式名factorial換成了jieCheng,執行jieCheng(5) 就會報錯了,外面改了,裡面也得改,如果是遞迴的層次較深就比較麻煩。事實上,這樣的程式碼也是不夠健壯的
這裡有兩種解決方案:
(1)、使用 arguments.callee
arguments.callee 是一個指向正在執行的函式的指標,函式名也是指向函式的指標,因此,可以在函式內部用 arguments.callee 來替代函式名
function fn() { console.log(arguments.callee) } fn() // ƒ fn() { // console.log(arguments.callee) // } function factorial(num) { if(num <= 1) { return 1 } return num * arguments.callee(num - 1) } console.log(factorial(5)) // 120
但在嚴格模式下,不能通過指令碼訪問 arguments.callee,訪問這個屬性會導致錯誤
'use strict' function factorial(num) { if(num <= 1) { return 1 } return num * arguments.callee(num - 1) } console.log(factorial(5)) // Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
(2)、命名函式表示式
var factorial = function jieCheng(num) { if(num <= 1) { return 1 } return num * jieCheng(num - 1) }; console.log(factorial(5)) // 120 var result = factorial; console.log(result(4)); // 24
5、間接呼叫
apply()和 call()。這兩個方法的用途都是在特定的作用域中呼叫函式,實際上等於設定函式體內 this 物件的值。
首先,apply()方法接收兩個引數:一個是在其中執行函式的作用域,另一個是引數陣列。其中,第二個引數可以是 Array 的例項,也可以是arguments 物件。
function add(a, b) { console.log(a + b); } function sum1(a, b) { add.apply(window, [a, b]); } function sum2(a, b) { add.apply(this, arguments) } sum1(1, 2); // 3 sum2(3, 5); // 8
call()方法與 apply()方法的作用相同,它們的區別僅在於接收引數的方式不同。對於 call()方法而言,第一個引數是 this 值沒有變化,變化的是其餘引數都直接傳遞給函式。換句話說,在使用call()方法時,傳遞給函式的引數必須逐個列舉出來
var color = 'red'; var obj = { color: 'blue' }; function getColor() { console.log(this.color) } getColor.call(this) // red getColor.call(obj) // blue
二、預解析機制
第一步:js執行時,會找所有的var和function關鍵字
--、把所有var關鍵字宣告的變數提升到各自作用域的頂部並賦初始值為undefined,簡單說就是 “宣告提前,賦值留在原地”
--、函式宣告提升
第二步:從上至下逐行解析程式碼
var color = 'red'; var size = 31; function fn() { console.log(color); var color = 'blue'; var size = 29; } fn(); // undefined
// 第一步:在全域性作用域內查詢所有使用var和function關鍵字宣告的變數,把 color、size、fn 提升到全域性作用域頂端併為其賦初始值;同理,在fn函式作用域內執行此操作
// 第二步:從上至下依次執行程式碼,呼叫fn函式時,按序執行程式碼,函式作用域內的輸出語句中color此時僅賦初始值undefined
注意:
(1)、如果函式是通過 “函式宣告” 的方式定義的,遇到與函式名相同的變數時,不論函式與變數的位置順序如何,預解析時函式宣告會覆蓋掉var宣告的變數
console.log(fn) // ƒ fn() {} function fn() {} var fn = 32
(2)、如果函式是通過 “函式表示式” 的方式定義的,遇到與函式名相同的變數時,會視同兩個var宣告的變數,後者會覆蓋前者
console.log(fn); // undefined var fn = function() {}; var fn = 32; console.log(fn) // 32
(3)、兩個通過 “函式宣告” 的方式定義的同名函式,後者會覆蓋前者
console.log(fn); // ƒ fn() {console.log('你好 世界')} function fn() {console.log('hello world')} function fn() {console.log('你好 世界')}
預解析練習一:
var fn = 32 function fn() { alert('eeee') } console.log(fn) // 32 fn() // Uncaught TypeError: fn is not a function console.log(typeof fn) // number // 按照上面的預解析規則,預解析第一步時,fn會被賦值為 function fn() {alert('eeee')};第二步從上到下逐步執行時,由於函式fn宣告提前,優於var宣告的fn執行了, // 所以fn會被覆蓋為一個Number型別的基本資料型別變數,而不是一個函式,其值為32
預解析練習二:
console.log(a); // function a() {console.log(4);} var a = 1; console.log(a); // 1 function a() { console.log(2); } console.log(a); // 1 var a = 3; console.log(a); // 3 function a() { console.log(4); } console.log(a); // 3 a(); // 報錯:不是一個函式
預解析步驟:
(1)、找出當前相同作用域下所有使用var和function關鍵字宣告的變數,由於所有變數都是同名變數,按照規則,權值最高的是最後一個宣告的同名的function,所以第一行輸出 function a() {console.log(4);}
(2)、從上至下逐步執行程式碼,在第二行為變數a 賦值為1,因此輸出了一個1
(3)、執行到第一個函式a,由於沒有呼叫,直接跳過不會輸出裡面的2,執行到下一行輸出1
(4)、繼續執行,為a重新賦值為3,因此輸出了一個3
(5)、執行到第二個函式a,還是沒有呼叫,直接跳過不會輸出裡面的4,執行到下一行輸出3
(6)、最後一行呼叫函式a,但由於預解析時率先把a賦值為一個函式程式碼段,後面依次為a賦值為1和3,因此,a是一個Number型別的基本變數,而不是一個函式了
預解析練習三:
var a = 1; function fn(a) { console.log(a); // 999 a = 2; console.log(a) // 2 } fn(999); console.log(a); // 1
預解析步驟:
(1)、全域性作用域內,為a賦值為undefined,把函式fn提升到最前面;fn函式作用域內,函式引數在預解析時也視同區域性變數,為其賦初始值 undefined
(2)、執行fn函式,傳入實參999,為區域性變數a賦值為999並輸出;重新為a賦值為2,輸出2
(3)、由於全域性作用域下的a被賦值為1,而函式作用域內部的a是訪問不到的,因此直接輸出1
預解析練習四:
var a = 1; function fn() { console.log(a); var a = 2; } fn(); // undefined console.log(a); // 1
var a = 1; function fn() { console.log(a); a = 2; } fn(); // 1 console.log(a); // 2
對比兩段程式碼,唯一的區別就是fn函式內的變數a的作用域問題,前者屬於函式作用域,後者屬於全域性作用域,所以導致輸出結果完全不同