目錄
1. 函數
在 JavaScript
裏面,可能會定義非常多的相同代碼或者功能相似的代碼,這些代碼可能需要大量重複使用。雖然 for
循環語句也能實現一些簡單的重複操作,但是比較具有侷限性,此時我們就可以使用 JavaScript
中的函數。
函數:就是封裝了一段可被重複調用執行的代碼塊。通過此代碼塊可以實現大量代碼的重複使用。
1.1 函數的定義
在 JavaScript
中,可以使用 function
語句來定義一個函數。這種形式是由關鍵字 function
、函數名
加 一組參數
以及置於大括號中需要執行的 一段代碼
構成的。使用 function
語句定義函數的基本語法如下:
function 函數名([參數1, 參數2,……]){
語句
[return 返回值]
}
參數說明:
- 函數名:必選,用於指定函數名。在同一個頁面中,函數名必須是唯一的,並且區分大小寫。
- 參數:可選,用於指定參數列表。當使用多個參數時,參數間使用逗號進行分隔。一個函數最多可以有255個參數。
- 語句:必選,是函數體,用於實現函數功能的語句。
- 返回值:可選,用於返回函數值。返回值可以是任意的表達式、變量或常量。
例如,定義一個不帶參數的函數 hello()
,在函數體中輸出 Amo 你好~~~
字符串。示例代碼如下:
<script>
function hello() {
console.log("Amo 你好~~~");
}
</script>
例如,定義一個用於計算商品金額的函數 account()
,該函數有兩個參數,用於指定單價和數量,返回值爲計算後的金額。示例代碼如下:
<script>
function account(price, number) { // 定義有兩個參數的函數
let sum = price * number; // 計算金額
return sum; //返回計算後的金額
}
</script>
常見錯誤:在同一頁面中定義了兩個名稱相同的函數。例如,下面的代碼中定義了兩個同名的函數 hello()
。
<script>
function hello() { //定義函數名稱爲hello
document.write("Amo 你好~~~"); //定義函數體
}
function hello() { //定義同名的函數
alert("Amo 你好~~~"); //定義函數體
}
</script>
上述代碼中,由於兩個函數的名稱相同,第一個函數被第二個函數所覆蓋,所以第一個函數不會執行,因此在同一頁面中定義的函數名稱必須唯一。
1.2 函數的調用
函數定義後並不會自動執行,要執行一個函數需要在特定的位置調用函數。調用函數的過程就像是啓動一個機器一樣,機器本身是不會自動工作的,只有按下相應的開關來調用這個機器,它纔會執行相應的操作。調用函數需要創建調用語句,調用語句包含函數名稱、參數具體值。
1.2.1 函數的簡單調用
函數調用的語法如下:
函數名(傳遞給函數的參數1, 傳遞給函數的參數2, ……);
例如,定義一個函數 outputImage()
,這個函數的功能是在頁面中輸出一張圖片,然後通過調用這個函數實現圖片的輸出,示例代碼如下:
<script>
function outputImage() {
document.write("<img src='https://ss3.bdstatic.com/70cFv8Sh_" +
"Q1YnxGkpoWK1HF6hhy/it/u=1039034310,233114253&fm=26&gp=0.jpg'/>");
}
outputImage()
</script>
運行結果如下圖所示。
1.2.2 在事件響應中調用函數
當用戶單擊某個按鈕或某個複選框時都將觸發事件,通過編寫程序對事件做出反應的行爲稱爲響應事件,在 JavaScript
語言中,將函數與事件相關聯就完成了響應事件的過程。比如,按下開關按鈕打開電燈就可以看作是一個響應事件的過程,按下開關相當於觸發了單擊事件,而電燈亮起就相當於執行了相應的函數。
例如,當用戶單擊某個按鈕時執行相應的函數,可以使用如下代碼實現該功能。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>函數</title>
</head>
<body>
<input type="button" value="點我試試" onclick="test();">
</body>
</html>
<script>
function test() {
alert("Amo 好帥呀~~~");
}
</script>
在上述代碼中可以看出,首先定義一個名爲 test()
的函數,函數體比較簡單,使用 alert()
語句輸出一個字符串,最後在按鈕 onclick
事件中調用 test()
函數。當用戶單擊 點我試試
按鈕後將彈出相應對話框。運行結果如下圖所示。
1.2.3 通過鏈接調用函數
函數除了可以在響應事件中被調用之外,還可以在鏈接中被調用,在 <a>
標籤中的 href
屬性中使用 javascript:函數名()
格式來調用函數,當用戶單擊這個鏈接時,相關函數將被執行,下面的代碼實現了通過鏈接調用函數。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>函數</title>
</head>
<body>
<a href="javascript:test();">點我彈彈彈~</a>
</body>
</html>
<script>
function test() {
alert("Amo 好帥呀~~~");
}
</script>
一般我們可以使用這種方式來阻止 <a>
標籤的跳轉。示例代碼如下:
<!-- 不建議使用-->
<a href="javascript:void(0)"></a>
其它方式,示例代碼如下:
<!-- 這種a標籤會在頁面比較長的時候回到頁面頂部-->
<a href="#" class="demo"></a>
<a href="https://www.baidu.com/" onclick="return false">跳轉到百度</a>
1.3 函數的參數
我們把定義函數時指定的參數稱爲形式參數,簡稱 形參
。而把調用函數時實際傳遞的值稱爲實際參數,簡稱 實參
。如果把函數比喻成一臺生產的機器,那麼,運輸原材料的通道就可以看作形參,而實際運輸的原材料就可以看作是實參。
在 JavaScript
中定義函數參數的格式如下:
function函數名(形參1,形參2,……){
函數體
}
定義函數時,在函數名後面的圓括號內可以指定一個或多個參數(參數之間用逗號 ,
分隔)。指定參數的作用在於,當調用函數時,可以爲被調用的函數傳遞一個或多個值。如果定義的函數有參數,那麼調用該函數的語法格式如下:
函數名(實參1,實參2,……)
通常,在定義函數時使用了多少個形參,在函數調用時也會給出多少個實參,這裏需要注意的是,實參之間也必須用逗號 ,
分隔。例如,定義一個帶有三個參數的函數,這三個參數用於指定姓名和年齡以及愛好,然後對它們進行輸出,代碼如下:
<script>
function personInfo(name, age, hobby) { //定義含有三個參數的函數
alert(`我的英文名是${name},今年${age}歲,愛好${hobby}`);//輸出字符串和參數的值
}
personInfo("Amo", 18, "唱歌跳舞"); //調用函數並傳遞參數
</script>
運行結果如下圖所示。
定義一個用於輸出圖書名稱和圖書作者的函數,在調用函數時將圖書名稱和圖書作者作爲參數進行傳遞。代碼如下:
<script>
function bookInfo(bookName, author) { //定義圖書相關的信息函數
alert(`圖書名稱: ${bookName}\n圖書作者: ${author}`);
}
bookInfo("ES6標準入門", "阮一峯");
</script>
運行結果如下圖所示。
在 JavaScript
中,形參的默認值是 undefined
。函數形參和實參數量不匹配時:
參數個數 | 說明 |
---|---|
實參個數等於形參個數 | 輸出正確結果 |
實參個數多於形參個數 | 只取到形參的個數 |
實參個數小於形參個數 | 多的形參定義爲undefined,結果爲NaN |
1.4 函數的返回值
對於函數調用,一方面可以通過參數向函數傳遞數據,另一方面也可以從函數獲取數據,也就是說函數可以返回值。在 JavaScript
的函數中,可以使用 return
語句爲函數返回一個值。語法:
return 表達式;
這條語句的作用是結束函數,並把其後的表達式的值作爲函數的返回值。例如,定義一個計算兩個數的和的函數,並將計算結果作爲函數的返回值,代碼如下:
<script>
function sum(num1, num2) { //定義含有兩個參數的函數
let result = num1 + num2;//獲取兩個參數的和
return result;//將變量result的值作爲函數的返回值
}
let a = 10;
let b = 20;
alert(`${a} + ${b} = ${sum(a, b)}`);
</script>
運行結果如下圖所示。
函數返回值可以直接賦給變量或用於表達式中,也就是說函數調用可以出現在表達式中。例如,將上面示例中函數的返回值賦給變量 result
,然後再進行輸出,代碼如下:
<script>
function sum(num1, num2) { //定義含有兩個參數的函數
let result = num1 + num2;//獲取兩個參數的和
return result;//將變量result的值作爲函數的返回值
}
let a = 10;
let b = 20;
let result = sum(a, b); //將函數的返回值賦給變量result
alert(`${a} + ${b} = ${result}`);//輸出結果
</script>
1.5 函數的嵌套定義
在 JavaScript
中允許使用嵌套函數,嵌套函數就是在一個函數的函數體中使用了其他的函數。嵌套函數的使用包括函數的嵌套定義和函數的嵌套調用。函數的 嵌套定義
就是在函數內部再定義其他的函數。例如,在一個函數內部嵌套定義另一個函數的代碼如下:
<script>
function outFun() { //定義外部函數
function inFun(x, y) { //定義內部函數
alert(x + y); //輸出兩個參數的和
}
inFun(20, 50); //調用內部函數並傳遞參數
}
outFun(); //調用外部函數
</script>
運行結果如下圖所示。
在上述代碼中定義了一個外部函數 outFun()
,在該函數的內部又嵌套定義了一個函數 inFun()
,它的作用是輸出兩個參數的和,最後在外部函數中調用了內部函數。雖然在 JavaScript
中允許函數的嵌套定義,但它會使程序的可讀性降低,因此,儘量避免使用這種定義嵌套函數的方式。
1.6 函數的嵌套調用
在 JavaScript
中,允許在一個函數的函數體中對另一個函數進行調用,這就是函數的嵌套調用。例如,在函數 b()
中對函數 a()
進行調用,代碼如下:
<script>
function a() { //定義函數a()
alert("ES6標準入門"); //輸出字符串
}
function b() { //定義函數b()
a(); //在函數b()中調用函數a()
}
b(); //調用函數b()
</script>
運行結果如下圖所示。
1.7 遞歸函數
所謂 遞歸函數
就是函數在自身的函數體內調用自身,使用遞歸函數時一定要當心,處理不當將會使程序進入死循環,遞歸函數只在特定的情況下使用,比如處理 階乘/斐波那契數列等問題
。語法:
function 函數名(參數1){
函數名(參數2);
}
例如,使用遞歸函數取得 10!
的值,其中 10!=10*9!
,而9!=9*8!
,以此類推,最後 1!=1
,這樣的數學公式在 JavaScript
程序中可以很容易使用函數進行描述,可以使用 f(n)
表示 n!
的值,當 1<n<10
時,f(n)=n*f(n-1)
,當 n<=1
時,f(n)=1
。代碼如下:
<script>
function f(num) { //定義遞歸函數
if (num <= 1) { //如果參數num的值小於等於1
return 1; //返回1
} else {
return f(num - 1) * num; //調用遞歸函數
}
}
alert(`5!的結果爲: ${f(5)}`); //調用函數輸出5的階乘
</script>
上述代碼運行結果如下圖所示。
在定義遞歸函數時需要兩個必要條件:
- 包括一個結束遞歸的條件。
如上面示例中的if(num<=1)
語句,如果滿足條件則執行return 1
語句,不再遞歸。 - 包括一個遞歸調用語句。
如上面示例中的return f(num-1)*num
語句,用於實現調用遞歸函數。
1.8 變量的作用域
變量的作用域是指變量在程序中的有效範圍,在該範圍內可以使用該變量。變量的作用域取決於該變量是哪一種變量。
在 JavaScript
中,變量根據作用域可以分爲兩種:全局變量
和 局部變量
。全局變量是定義在所有函數之外的變量,作用範圍是該變量定義後的所有代碼。局部變量是定義在函數體內的變量,只有在該函數中,且該變量定義後的代碼中纔可以使用這個變量,函數的參數也是局部性的,只在函數內部起作用。如果把函數比作一臺機器,那麼,在機器外擺放的原材料就相當於全局變量,這些原材料可以爲所有機器使用,而機器內部所使用的原材料就相當於局部變量。
例如,下面的程序代碼說明了變量的作用域作用不同的有效範圍:
<script>
let a = "這是全局變量"; // 該變量在函數外聲明,作用於整個腳本
function send() { //定義函數
let b = "這是局部變量"; //該變量在函數內聲明,只作用於該函數體
document.write(a + "<br>");//輸出全局變量的值
document.write(b);//輸出局部變量的值
}
send();//調用函數
</script>
運行結果爲:
上述代碼中,局部變量 b
只作用於函數體,如果在函數之外輸出局部變量 b
的值將會出現錯誤。錯誤代碼如下:
<script>
let a = "這是全局變量"; // 該變量在函數外聲明,作用於整個腳本
function send() { //定義函數
let b = "這是局部變量"; //該變量在函數內聲明,只作用於該函數體
document.write(a + "<br>");//輸出全局變量的值
document.write(b);//輸出局部變量的值
}
send();//調用函數
document.write(b);//錯誤代碼,不允許在函數外輸出局部變量的值
</script>
運行結果爲:
如果在函數體中定義了一個與全局變量同名的局部變量,那麼該全局變量在函數體中將不起作用。例如,下面的程序代碼將輸出局部變量的值:
<script>
let a = "這是全局變量"; //聲明一個全局變量a
function send() { //定義函數
let a = "這是局部變量"; //聲明一個和全局變量同名的局部變量a
document.write(a); //輸出局部變量a的值
}
send(); //調用函數
</script>
運行結果爲:
上述代碼中,定義了一個和全局變量同名的局部變量 a
,此時在函數中輸出變量 a
的值爲局部變量的值。全局變量和局部變量的區別:
- 全局變量:在任何一個地方都可以使用,只有在瀏覽器關閉時纔會被銷燬,因此比較佔內存。
- 局部變量:只在函數內部使用,當其所在的代碼塊被執行時,會被初始化。當代碼塊運行結束後,就會被銷燬,因此更節省內存空間。
只要是代碼都在一個作用域中,寫在函數內部的爲局部作用域,未寫在任何函數內部即在全局作用域中。如果函數中還有函數,那麼在這個作用域中就又可以誕生一個作用域,根據在 內部函數可以訪問外部函數變量
的這種機制,用鏈式查找決定哪些數據能被內部函數訪問,就稱作 作用域鏈
。示例代碼如下:
<script>
function f1() {
let num = 123;
function f2() {
console.log(num);
}
f2();
}
f1(); //123
</script>
作用域鏈
:採取 就近原則
的方式來查找變量最終的值。案例如下:
<script>
let a = 1;
function fn1() {
let a = 2;
let b = '22';
fn2();
function fn2() {
let a = 3;
fn3();
function fn3() {
let a = 4;
console.log(a); //a的值 4
console.log(b); //b的值 '22'
}
}
}
fn1();
</script>
1.9 預解析
JavaScript
代碼是由瀏覽器中的 JavaScript
解析器來執行的。JavaScript
解析器在運行 JavaScript
代碼的時候分爲兩步:預解析和代碼執行。解釋如下:
- 預解析:在當前作用域下,
JS
代碼執行之前,瀏覽器會默認把帶有var
和function
聲明的變量在內存中進行提前聲明或者定義。 - 代碼執行:從上到下執行
JS
語句。
一句話: 預解析會把變量和函數的聲明在代碼執行之前執行完成.
1.9.1 變量預解析
預解析也叫做變量、函數提升。變量提升(變量預解析):變量的聲明會被提升到當前作用域的最上面,變量的賦值不會提升。示例代碼如下:
1.9.2 函數預解析
函數提升: 函數的聲明會被提升到當前作用域的最上面,但是不會調用函數。示例代碼如下:
函數聲明代表函數整體,所以函數提升後,函數名代表整個函數,但是函數並沒有被調用!函數表達式創建函數,會執行變量提升,此時接收函數的變量名無法正確的調用,示例代碼如下:
<script>
fn();
var fn = function () {
document.write("<h4>Amo好帥呀~~~~</h4>");
}
</script>
上述代碼執行結果如下圖所示:
解釋:該段代碼執行之前,會做變量聲明提升,fn
在提升之後的值是undefined
,而 fn
調用是在 fn
被賦值爲函數體之前,此時 fn
的值是 undefined
,所以無法正確調用。如下圖所示:
1.9.3 作用域練習題
第一題,示例代碼如下:
<script>
var x = 5;
a();
function a() {
alert(x)
var x = 10;
}
</script>
分析過程如下:
第二題,示例代碼如下:
<script>
a();
function a() {
alert(x)
var x = 10;
}
alert(x)
</script>
分析過程如下:
第三題,示例代碼如下:
<script>
a();
function a() {
alert(a)
}
var a;
alert(a)
</script>
分析過程如下:
第四題,示例代碼如下:
<script>
alert(a)
var a = 10;
alert(a);
function a() {
alert(20);
}
alert(a);
var a = 30;
alert(a);
function a() {
alert(40);
}
alert(a);
</script>
分析過程如下:
第五題,示例代碼如下:
<script>
var a = 10;
alert(a)
a();
function a() {
alert(20)
}
</script>
分析過程如下:
第六題,示例代碼如下:
<script>
a();
var a = function () {
alert(1)
}
a();
function a() {
alert(2)
}
a()
var a = function () {
alert(3)
}
a()
</script>
分析過程如下:
第七題,示例代碼如下:
<script>
var a = 0;
function fn() {
alert(a)
var a = 1;
alert(a)
}
fn();
alert(a)
</script>
分析過程如下:
1.10 在表達式中定義函數
在 JavaScript
中提供了一種定義匿名函數的方法,就是在表達式中直接定義函數,它的語法和 function
語句非常相似。其語法格式如下:
var 變量名 = function(參數1,參數2,……) {
函數體
};
這種定義函數的方法不需要指定函數名,把定義的函數賦值給一個變量,後面的程序就可以通過這個變量來調用這個函數,這種定義函數的方法有很好的可讀性。例如,在表達式中直接定義一個返回兩個數字和的匿名函數,示例代碼如下:
<script>
let sum = function (x, y) { //定義匿名函數
return x + y;//返回兩個參數的和
};
alert(`10+20=${sum(10, 20)}`);//調用函數並輸出結果
</script>
運行結果如下圖所示。
在以上代碼中定義了一個匿名函數,並把對它的引用存儲在變量 sum
中。該函數有兩個參數,分別爲 x
和 y
。該函數的函數體爲 return x+y
,即返回參數 x
與參數 y
的和。
1.11 使用Function()構造函數
除了在表達式中定義函數之外,還有一種定義匿名函數的方法-----使用 Function()
構造函數來定義函數。這種方式可以動態地創建函數。Function()
構造函數的語法格式如下:
let 變量名 = new Function("參數1","參數2",……"函數體");
使用 Function()
構造函數可以接收一個或多個參數作爲函數的參數,也可以一個參數也不使用。Function()
構造函數的最後一個參數爲函數體的內容。Function()
構造函數中的所有參數和函數體都必須是 字符串類型
,因此一定要用雙引號或單引號引起來。
例如:使用 Function()
構造函數定義一個計算兩個數字和的函數,代碼如下:
<script>
let sum = new Function("x", "y", "alert(x+y);"); // 使用Function構造函數定義函數
sum(10, 20); // 調用函數
</script>
運行結果如下圖所示。
上述代碼中,sum
並不是一個函數名,而是一個指向函數的變量,因此,使用 Function()
構造函數創建的函數也是匿名函數。在創建的這個構造函數中有兩個參數,分別爲 x
和 y
。該函數的函數體爲 alert(x+y)
,即輸出 x
與 y
的和。
1.12 arguments的使用
當不確定有多少個參數傳遞的時候,可以用 arguments
來獲取。JavaScript
中,arguments
實際上它是當前函數的一個內置對象。所有函數都內置了一個 arguments
對象,arguments
對象中存儲了傳遞的所有實參。arguments
展示形式是一個僞數組,因此可以進行遍歷。僞數組具有以下特點:
- 具有
length
屬性 - 按索引方式儲存數據
- 不具有數組的
push
,pop
等方法
注意:在函數內部使用該對象,用此對象獲取函數調用時傳的實參。例如,求任意個數數字之和,示例代碼如下:
<script>
function getSum() {
// console.log(arguments.length);
let sum = 0;
for (let i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
return sum;
}
let [a, b, c, d] = [1, 2, 3, 4];
let [x, y, z] = [8, 9, 10];
console.log(getSum(a, b, c, d)); // 10
console.log(getSum(x, y, z)); // 27
</script>
2. ES6 函數的擴展
2.1 基本用法
ES6
之前,不能直接爲函數的參數指定默認值,只能採用變通的方法。示例代碼如下:
<script>
function f(x, y) {
y = y || "Cool";
console.log(x, y);
}
f("Amo");
f("Amo", "So Cool~~~");
f("Amo", '');
</script>
上面代碼檢查函數 f
的參數 y
有沒有賦值,如果沒有,則指定默認值爲 Cool
。這種寫法的缺點在於,如果參數 y
賦值了,但是對應的布爾值爲 false
,則該賦值不起作用。就像上面代碼的最後一行,參數 y
等於空字符,結果被改爲默認值。ES6
允許爲函數的參數設置默認值,即直接寫在參數定義的後面。示例代碼如下:
<script>
function f(x, y = "World") {
console.log(x, y);
}
f("Amo");
f("Amo", "So Cool~~~");
f("Amo", '');
</script>
上述代碼執行結果如下:
2.2 rest參數
ES6
引入 rest
參數(形式爲 ...
變量名),用於獲取函數的多餘參數,這樣就不需要使用 arguments
對象了。rest
參數搭配的變量是一個數組,該變量將多餘的參數放入數組中。示例代碼如下:
<script>
function add(...values) {
let sum = 0;
for (let val of values) {
sum += val;
}
return sum;
}
console.log(add(2, 5, 3)); // 10
</script>
上面代碼的 add
函數是一個求和函數,利用 rest
參數,可以向該函數傳入任意數目的參數。注意,rest
參數之後不能再有其他參數(即只能是最後一個參數
),否則會報錯。示例代碼如下:
2.3 箭頭函數
ES6
允許使用 箭頭=>
定義函數。示例代碼如下:
<script>
// 傳統js函數的寫法
let f1 = function (v) {
return v;
}
console.log(f1(3));
// ES6箭頭函數
let f2 = v => v;
console.log(f2(4));
</script>
如果箭頭函數
不需要參數或需要多個參數,就使用一個圓括號代表參數部分。示例代碼如下:
<script>
let f1 = () => 5; //==> let f1 = function(){return 5};
let sum = (num1, num2) => num1 + num2;
//等同於如下代碼
/*let sum = function (num1, num2) {
return num1 + num2;
}*/
</script>
如果 箭頭函數
的代碼塊部分多於一條語句,就要使用大括號將它們括起來,並且使用 return
語句返回。示例代碼如下:
由於大括號被解釋爲代碼塊,所以如果 箭頭函數
直接返回一個對象,必須在對象外面加上括號,否則會報錯。如下圖所示:
箭頭函數的一個用處是簡化回調函數。例子如下圖所示:
2.4 箭頭函數的注意點
箭頭函數有幾個使用注意點。
- 函數體內的
this
對象,就是定義時所在的對象,而不是使用時所在的對象。 - 不可以當作構造函數,也就是說,不可以使用
new
命令,否則會拋出一個錯誤。 - 不可以使用
arguments
對象,該對象在函數體內不存在。如果要用,可以用rest
參數代替。 - 不可以使用
yield
命令,因此箭頭函數不能用作Generator
函數。
上面四點中,第一點尤其值得注意。this
對象的指向是可變的,這些 this
的指向,是當我們調用函數的時候確定的。調用方式的不同決定了 this
的指向不同,如下圖所示:
但是在箭頭函數中,它是固定的。關於 this
的指向問題,在後續高級語法中在進行詳細的講解。關於其它函數的擴展具體可以參照 ECMAScript6 入門。