## 函數
**1.函數簡介**
通過**函數**可以封裝任意多條語句,而且可以在任何地方、任何時候調用。
ECMAScript中的函數使用`function`關鍵字來聲明,后跟一組參數以及函數體,這些參數在函數體內像局部變量一樣工作。
```
function functionName(arg0, arg1....argN) {
statements
}
```
函數調用會為形參提供實參的值。函數使用它們實參的值來計算返回值,稱為該函數調用表達式的值。
```
function test(name){
return name;
}
test('tg');
```
在上面的例子中,name就是形參,調用時的'tg'就是實參。
除了實參之外,每次調用還會擁有另一個值---本次**調用的上下文**---這就是`this`關鍵字的值。
我們還可以通過在函數內添加`return`語句來實現返回值。
**注意**:遇到`return`語句時,會立即退出函數,也就是說,`return`語句后面的語句不再執行。
```
function test(){
return 1;
alert(1); //永遠不會被執行
}
```
一個函數中可以包含多個`return`語句,而且`return`語句可以不帶有任何返回值,最終將返回`undefined`。
```
function test(num){
if(num > 2){
return num;
}else{
return ;
}
}
test(3); // 3
test(1); //undefined
```
如果函數掛載在一個對象上,將作為對象的一個屬性,就稱它為對象的方法。
```
var o = {
test: function(){}
}
```
test()就是對象o的方法。
**2、函數定義(聲明)**
JavaScript有三種方法,可以定義一個函數。
**(1)function命令**
```
function name() {}
```
name是函數名稱標識符。函數名稱是函數聲明語句必需的部分。不過對于函數表達式來說,名稱是可選的:如果存在,該名字只存在于函數體內,并指向該函數對象本身。
圓括號:圓括號內可放置0個或多個用逗號隔開的標識符組成的列表,這些標識符就是函數的參數名稱。 花括號:可包含0條或多條JavaScript語句。這些語句構成了函數體。一旦調用函數,就會執行這些語句。
**(2)函數表達式**
```
var f = function(x){
console.log(x);
}
```
采用函數表達式聲明函數時,function命令后面不帶有函數名。如果加上函數名,該函數名只在函數體內部有效,在函數體外部無效。
**(3)Function()**
Function()函數定義還可以通過Function()構造函數來定義
```
var f=new Function('x','y','return x+y');
```
等價于
```
var f=function(x,y){
return x+y;
}
```
除了最后一個參數是函數體外,前面的其他參數都是函數的形參。如果函數不包含任何參數,只須給構造函數簡單的傳入一個字符串---函數體---即可。 不過,Function()構造函數在實際編程中很少會用到。
**注意點**:如果同一個函數被多次定義(聲明),后面的定義(聲明)就會覆蓋前面的定義(聲明)
```
function f(){
console.log(1);
}
f() //1
function f(){
console.log(2);
}
f() //2
```
函數可以調用自身,這就是遞歸(recursion)
```
function f(x){
if(x>2){
console.log(x);
return f(x-1);
}else{
return 1;
}
}
f(4);
// 4
//3
```
不能在條件語句中聲明函數 (在ES6中的塊級作用域是允許聲明函數的。)
**3、函數命名**
任何合法的JavaScript標識符都可以用做一個函數的名稱。
函數名稱通常是動詞或以動詞為前綴的詞組。 通常函數名的第一個字符為小寫。當函數名包含多個單詞時,可采取**下劃線法**,比如:like_this();也可以采取**駝峰法**,也就是除了第一個單詞之外的單詞首字母使用大寫字母,比如:likeThis();
**4、被提前**
就像變量的“被提前”一樣,函數聲明語句也會“被提前”到外部腳本或外部函數作用域的頂部,所以以這種方式聲明的函數,可以被在它定義之前出現的代碼所調用。
```
f()
function f(){}
```
其實JavaScript是這樣解釋的:
```
function f(){}
f()
```
注意:在函數提升中,函數體也會跟著提升(不像變量一樣,只會提升變量聲明),這也是我們可以引用后面聲明的函數的原因。
此外,以表達式定義的函數并沒有“被提前”,而是以變量的形式“被提前”。
```
f();
var f = function (){};
// TypeError: f is not a function
```
變量其實是分為聲明,賦值兩部分的,上面的代碼等同于下面的形式
```
var f;
f();
f = function() {};
```
調用f的時候,f只是被聲明了,還沒有被賦值,等于undefined,所以會報錯。
**5、嵌套函數**
JavaScript的函數可以嵌套在其他函數中定義,這樣它們就可以訪問它們被定義時所處的作用域中的任何變量,這就是JavaScript的`閉包`。
```
function test(){
var name = 'tg';
function test2(){
var age = 10;
console.log(name); // "tg"
}
console.log(age); // Uncaught ReferenceError: age is not defined
}
test();
```
從上面的例子可得,test2()可以訪問name,但是如果在test()內,test2()外訪問age,就會報錯。
**6、函數調用**
構成函數主體的JavaScript代碼在定義時并不會執行,只有調用該函數,它們才會執行。有4種方式調用JavaScript函數:
- 作為函數
- 作為方法
- 作為構造函數
- 通過它們的call()和apply()方法間接調用
**6.1函數調用**
函數可以通過函數名來調用,后跟一對圓括號和參數(圓括號中的參數如果有多個,用逗號隔開)
```
function test(){}
test()
```
**6.2方法調用**
```
var o = {
f: function(){}
}
o.f();
```
**6.3構造函數調用**
如果函數或者方法調用之前帶有關鍵字`new`,它就構成構造函數調用。 凡是沒有形參的構造函數調用都可以省略圓括號(但不推薦)。
```
var o=new Object();
var o=new Object;
```
**7、函數的實參和形參**
**7.1 可選形參**
在ECMAScript中的函數在調用時,傳遞的參數可少于函數中的參數,沒有傳入參數的命名參數的值是undefined。
為了保持好的適應性,一般應當給參數賦予一個合理的默認值。
```
function go(x,y){
x = x || 1;
y = y || 2;
}
```
注意:當用這種可選實參來實現函數時,需要將可選實參放在實參列表的最后。那些調用你的函數的程序員是沒法省略第一個參數并傳入第二個實參的。
**7.2 實參對象**
當調用函數時,傳入的實參個數超過函數定義時的形參個數時,是沒有辦法直接獲得未命名值的引用。 這時,標識符`arguments`出現了,其指向實參對象的引用,實參對象是一個類數組對象,可以通過數字下標來訪問傳入函數的實參值,而不用非要通過名字來得到實參。
```
function go(x){
console.log(arguments[0]);
console.log(arguments[1]);
}
go(1,2);
//1
//2
```
`arguments`有一個`length`屬性,用以標識其所包含元素的個數。
```
function f(x){
console.log(arguments.length);
}
f(1,2) // 2
```
注意:arguments并不是真正的數組,它是一個類數組對象。每個實參對象都包含以數字為索引的一組元素以及length屬性。
通過實參名字來修改實參值的話,通過arguments[]數組也可以獲取到更改后的值。
在函數體內,我們可以通過`arguments`對象來訪問這個參數類數組,我們可以使用方括號語法訪問它的每一個參數(比如arguments[0]),它還有一個length屬性,表示傳遞進來的參數個數。
`arguments`類數組中每一個元素的值會與對應的命名參數的值保持同步,這種影響是單向的,也可以這樣說,如果是修改`arguments`中的值,會影響到命名參數的值,但是修改命名參數的值,并不會改變`arguments`中對應的值。
```
function f(x){
console.log(x); // 1
arguments[0]=null;
console.log(x); // null
}
f(1);
```
在上面的例子中,arguments[0]和x指代同一個值,修改其中一個的值會影響到另一個。 注意:如果有同名的參數,則取最后出現的那個值。
```
function f(x,x){
console.log(x);
}
f(1,2) // 2
```
`callee`和`caller`屬性
arguments對象帶有一個`callee`屬性,返回它所對應的原函數。
在一個函數調用另一個函數時,被調用函數會自動生成一個`caller`屬性,指向調用它的函數對象。如果該函數當前未被調用,或并非被其他函數調用,則`caller`為null。
再次提醒,`arguments`并不是真正的數組,它只是類數組對象(有length屬性且可使用索引來訪問子項)。但我們可以借助Array類的原型對象的slice方法,將其轉為真正的數組:
```
Array.prototype.slice.call(arguments, 0);
//更簡潔的寫法
[].slice.call(arguments, 0);
```
**7.3 按值傳參**
ECMAScript中所有函數的參數都是`按值傳遞`的。也就是說,把函數外部的值復制給函數內部的參數,就和把值從一個變量復制到另一個變量一樣。
在向參數傳遞基本類型的值時,被傳遞的值會被復制給一個局部變量(即命名參數,或者用ECMAScript的概念來說,就是arguments對象中的一個元素。)
例子:
```
var num = 1;
function test(count){
count += 10;
return count;
}
var result = test(num);
console.log(num); // 1
console.log(result); // 11
```
在上面的例子中,我們將num作為參數傳給了test()函數,即count的值也是1,然后在函數內將count加10,但是由于傳遞的只是num的值的一個副本,并不會影響num,count和num是獨立的,所以最后num的值依舊是1.
在向參數傳遞引用類型的值時,會先把這個值在內存中的**地址**復制給一個局部變量,若局部變量變化,則局部變量和復制給局部變量路徑的全局變量也會發生改變。
```
function test(obj){
obj.name = 'tg';
}
var person = new Object();
test(person);
console.log(person.name); // "tg"
```
但是,如果局部變量指向了一個新的堆內地址,再改變局部變量的屬性時,不會影響全局變量。
看下面的例子:
```
function test(obj){
obj.name = 'tg';
obj = new Object();
obj.name = 'tg2';
}
var person = new Object();
test(person);
console.log(person.name); // "tg"
```
在上面的例子中,全局的`person`和函數內局部的`obj`在初始傳遞時,兩者指向的是內存中的同一個地址,但是,當在函數內創建了一個新的對象,并賦值給`obj`(賦值的是新對象的地址)。這個時候,`obj`指向的就不在是全局對象`person`,而是指向了新對象的地址,所以給obj添加屬性name時,全局對象person的屬性不會被改變。
對于上面的例子中的obj,也可以這樣說,一旦obj的值發生了變化,那么它就不再指向person在內存中的地址了。
**8、將對象屬性用做實參**
當一個函數包含超過三個形參時,要記住調用函數中實參的正確順序是件讓人頭疼的事。不過,我們可以通過名/值對的形式傳入參數,這樣就不需要管參數的順序了。
```
function f(params){
console.log(params.name);
}
f({name:'a'})
```
**9、作為值的函數**
在JavaScript中,我們可以將函數賦值給變量。
```
function f(){}
var a=f;
```
**10、函數作用域**
作用域(scope)指的是變量存在的范圍。
Javascript只有兩種作用域:一種是**全局作用域**,變量在整個程序中一直存在,所有地方都可以讀取;另一種是**函數作用域**,變量只在函數內部存在。 在函數外部聲明的變量就是全局變量(global variable),它可以在函數內部讀取。
```
var a=1;
function f(){
console.log(a)
}
f() //1
```
上面的代碼中,函數f內部可以讀取全局變量a。
在函數內部定義的變量,外部無法讀取,稱為“局部變量”(local variable)。
```
function f(){
var a=1;
}
v //ReferenceError: v is not defined
```
上面代碼中,變量v在函數內部定義,所以是一個局部變量,函數之外就無法讀取。
函數內部定義的變量,會在該作用域內覆蓋同名全局變量。
```
var a=1;
function f(){
var a=2;
console.log(a);
}
f() //2
a //1
```
注意:對于var命令來說,局部變量只能在函數內部聲明,在其他區塊中聲明,一律都是全局變量。
函數的執行依賴于變量作用域,這個作用域是在函數定義時決定的,而不是函數調用時決定的。
**11、函數內部的變量提升 **
與全局作用域一樣,函數作用域內部也會產生“變量提升”現象。
`var`命令聲明的變量,不管在什么位置,變量聲明都會被提升到函數體的頭部。
```
function f(x){
if(x>10){
var a = x -1;
}
}
//等同于
function f(x){
var a;
if(x>10){
a = x - 1;
}
}
```
**12、沒有重載**
ECMAScript函數沒有重載的定義。
重載是指為一個函數編寫兩個定義,只要這兩個定義的簽名(接受的參數的類型和數量)不同即可。
對于ECMAScript函數,如果定義了兩個同名的,后定義的函數會覆蓋先定義的函數。
**13、函數屬性、方法和構造函數**
**13.1 函數的屬性、方法**
**(1)name屬性**
name屬性返回緊跟在function關鍵字之后的那個函數名。
```
function f(){}
f.name //f
```
**(2)length屬性**
函數的length屬性是只讀屬性,代表函數形參的數量,也就是在函數定義時給出的形參個數。
```
function f(x,y){}
f.length //2
```
**(3)prototype屬性**
每一個函數都包含一個prototype屬性,這個屬性指向一個對象的引用,這個對象稱做“原型對象”(prototype object)。
**(4)call()**
語法:
```
call([thisObj[,arg1[, arg2[, [,.argN]]]]])
```
定義:調用一個對象的一個方法,以另一個對象替換當前對象。
說明: call 方法可以用來代替另一個對象調用一個方法。call 方法可將一個函數的對象上下文從初始的上下文改變為由 thisObj 指定的新對象。
**(5)apply()**
語法:
```
apply([thisObj[,argArray]])
```
定義:應用某一對象的一個方法,用另一個對象替換當前對象。
說明: 如果 argArray 不是一個有效的數組或者不是 arguments 對象,那么將導致一個 TypeError。 如果沒有提供 argArray 和 thisObj 任何一個參數,那么 Global 對象將被用作 thisObj, 并且無法被傳遞任何參數。 bind()方法 bind()方法是在ECMAScript 5中新增的方法。 toString()方法
函數的toString方法返回函數的源碼。
```
function f(){
return 1;
}
f.toString()
//function f(){
// return 1;
//}
```
**(6)bind()**
bind()方法會創建一個新函數,稱為綁定函數,當調用這個綁定函數時,綁定函數會以創建它時傳入 bind()方法的第一個參數作為 this,傳入 bind() 方法的第二個以及以后的參數加上綁定函數運行時本身的參數按照順序作為原函數的參數來調用原函數。
```
var bar=function(){
console.log(this.x);
}
var foo={
x:3
}
bar();
bar.bind(foo)();
/*或*/
var func=bar.bind(foo);
func();
輸出:
undefined
3
```
注意:bind()返回的是函數。
**13.2 構造函數**
構造函數和普通函數的定義并沒有太大區別,不過我們使用`new`關鍵字來生成構造函數的實例對象。
```
function Test(){}
var t = new Test();
```
對于構造函數,一般首字母大寫,便于和普通函數區別開來。
定義每個函數都會主動獲取一個`prototype`屬性,該屬性擁有一個對象--該函數的原型,該原型有一個`constructor`屬性,指向其當前所屬的函數。
**14、閉包**
JavaScript的函數可以嵌套在其他函數中定義,這樣它們就可以訪問它們被定義時所處的作用域中的任何變量,這就是JavaScript的`閉包`。
`閉包`會保存函數作用域中的狀態,即使這個函數已經執行完畢。
`閉包`的最大用處有兩個,一個是可以讀取函數內部的變量,另一個就是讓這些變量始終保持在內存中,即`閉包`可以使得它誕生環境一直存在。
`閉包`的創建依賴于函數。
```
function f(a){
return function(){
return a++;
};
}
var c=f(1);
console.log(c()); //1
console.log(c()); //2
console.log(c()); //3
```
閉包的另一個用處,是封裝對象的私有屬性和私有方法。
**15、立即調用的函數表達式(IIFE)**
在Javascript中,一對圓括號()是一種運算符,跟在函數名之后,表示調用該函數。
```
(function(){
statement
}())
```
上面的函數會立即調用。
**注意**:上面代碼的圓括號的用法,function之前的左圓括號是必需的,因為如果不寫這個左圓括號,JavaScript解釋器會試圖將關鍵字function解析為函數聲明語句。而使用圓括號,JavaScript解釋器才會正確地將其解析為函數定義表達式。
當然,下面的方法也會以表達式來處理函數定義的方法。
```
!function(){}();
~function(){}();
-function(){}();
+function(){}();
```
通常情況下,只對匿名函數使用這種“立即執行的函數表達式”。它的目的有兩個:
- 一是不必為函數命名,避免了污染全局變量;
- 二是IIFE內部形成了一個單獨的作用域,可以封裝一些外部無法讀取的私有變量。
**16、eval命令**
eval命令的作用是,將字符串當作語句執行。
```
eval('var a=1');
a //1
```
eval沒有自己的作用域,都在當前作用域內執行
JavaScript規定,如果使用嚴格模式,eval內部聲明的變量,不會影響到外部作用域。
```
(function(){
'use strict';
eval('var a=1');
console.log(a); //ReferenceError: a is not defined
})();
```
**17、嚴格模式下的函數**
- 不能把函數命名為eval或arguments
- 不能把參數命名為eval或arguments
- 不能出現兩個命名參數同名的情況
如果出現上面三種情況,都會導致語法錯誤,代碼無法執行。
- 前言
- JavaScript簡介
- 基本概念
- 語法
- 數據類型
- 運算符
- 表達式
- 語句
- 對象
- 數組
- 函數
- 引用類型(對象)
- Object對象
- Array對象
- Date對象
- RegExp對象
- 基本包裝類型(Boolean、Number、String)
- 單體內置對象(Global、Math)
- console對象
- DOM
- DOM-屬性和CSS
- BOM
- Event 事件
- 正則表達式
- JSON
- AJAX
- 表單和富文本編輯器
- 表單
- 富文本編輯器
- canvas
- 離線應用
- 客戶端存儲(Cookie、Storage、IndexedDB)
- HTML5 API
- Video/Audio
- Geolocation API
- requestAnimationFrame
- File API
- FullScreen API
- IndexedDB
- 檢測設備方向
- Blob
- vibrate
- Luminosity API
- WebRTC
- Page Visibility API
- Performance API
- Web Speech
- Notification
- 面向對象的程序設計
- 概述
- this關鍵字
- 原型鏈
- 作用域
- 常用API合集
- SVG
- 錯誤處理機制
- JavaScript開發技巧合集
- 編程風格
- 垃圾回收機制