前言
函式是 JavaScript 中的基本組件之一, 一個函式是 JavaScript 程序 — 一組執行任務或計算值的陳述句,要使用一個函式,你必須將其定義在你希望呼叫它的作用域內,
一個JavaScript 函式用function關鍵字定義,后面跟著函式名和圓括號,
定義函式
函式宣告
一個函式定義(也稱為函式宣告,或函式陳述句)由一系列的function關鍵字組成,依次為:
- 函式的名稱,
- 函式引數串列,包圍在括號中并由逗號分隔,
- 定義函式的 JavaScript 陳述句,用大括號
{}括起來,
例如,以下的代碼定義了一個簡單的square函式:
function square(number) {
return number * number;
}
函式square使用了一個引數,叫作number,這個函式只有一個陳述句,它說明該函式將函式的引數(即number)自乘后回傳,函式的return陳述句確定了函式的回傳值:
return number * number;
原始引數(比如一個具體的數字)被作為值傳遞給函式;值被傳遞給函式,如果被呼叫函式改變了這個引數的值,這樣的改變不會影響到全域或呼叫函式,
如果你傳遞一個物件(即一個非原始值,例如Array或用戶自定義的物件)作為引數,而函式改變了這個物件的屬性,這樣的改變對函式外部是可見的,如下面的例子所示:
function myFunc(theObject) {
theObject.make = "Toyota";
}
var mycar = {make: "Honda", model: "Accord", year: 1998};
var x, y;
x = mycar.make; // x獲取的值為 "Honda"
myFunc(mycar);
y = mycar.make; // y獲取的值為 "Toyota"
// (make屬性被函式改變了)
函式運算式
雖然上面的函式宣告在語法上是一個陳述句,但函式也可以由函式運算式創建,這樣的函式可以是匿名的;它不必有一個名稱,例如,函式square也可這樣來定義:
const square = function(number) { return number * number; };
var x = square(4); // x gets the value 16
然而,函式運算式也可以提供函式名,并且可以用于在函式內部代指其本身,或者在除錯器堆疊跟蹤中識別該函式:
const factorial = function fac(n) {return n<2 ? 1 : n*fac(n-1)};
console.log(factorial(3));
當將函式作為引數傳遞給另一個函式時,函式運算式很方便,下面的例子演示了一個叫map的函式如何被定義,而后使用一個運算式函式作為其第一個引數進行呼叫:
function map(f,a) {
let result = []; // 創建一個陣列
let i; // 宣告一個值,用來回圈
for (i = 0; i != a.length; i++)
result[i] = f(a[i]);
return result;
}
下面的代碼:
function map(f, a) {
let result = []; // 創建一個陣列
let i; // 宣告一個值,用來回圈
for (i = 0; i != a.length; i++)
result[i] = f(a[i]);
return result;
}
const f = function(x) {
return x * x * x;
}
let numbers = [0,1, 2, 5,10];
let cube = map(f,numbers);
console.log(cube);
回傳 [0, 1, 8, 125, 1000],
在 JavaScript 中,可以根據條件來定義一個函式,比如下面的代碼,當num 等于 0 的時候才會定義 myFunc :
var myFunc;
if (num == 0){
myFunc = function(theObject) {
theObject.make = "Toyota"
}
}
除了上述的定義函式方法外,你也可以在運行時用 Function 構造器由一個字串來創建一個函式 ,很像 eval()函式,
當一個函式是一個物件的屬性時,稱之為方法,了解更多關于物件和方法的知識 使用物件
呼叫函式
定義一個函式并不會自動的執行它,定義了函式僅僅是賦予函式以名稱并明確函式被呼叫時該做些什么,呼叫函式才會以給定的引數真正執行這些動作,例如,一旦你定義了函式square,你可以如下這樣呼叫它:
square(5);
上述陳述句通過提供引數 5 來呼叫函式,函式執行完它的陳述句會回傳值25,
函式一定要處于呼叫它們的域中,但是函式的宣告可以被提升(出現在呼叫陳述句之后),如下例:
console.log(square(5));
/* ... */
function square(n) { return n*n }
函式域是指函式宣告時的所在的地方,或者函式在頂層被宣告時指整個程式,
提示: 注意只有使用如上的語法形式(即 function funcName(){})才可以,而下面的代碼是無效的,就是說,函式提升僅適用于函式宣告,而不適用于函式運算式,
console.log(square); // square is hoisted with an initial value undefined.
console.log(square(5)); // Uncaught TypeError: square is not a function
const square = function (n) {
return n * n;
}
函式的引數并不局限于字串或數字,你也可以將整個物件傳遞給函式,函式 show_props
函式可以被遞回,就是說函式可以呼叫其本身,例如,下面這個函式就是用遞回計算階乘:
function factorial(n){
if ((n == 0) || (n == 1))
return 1;
else
return (n * factorial(n - 1));
}
你可以計算1-5的階乘如下:
var a, b, c, d, e;
a = factorial(1); // 1賦值給a
b = factorial(2); // 2賦值給b
c = factorial(3); // 6賦值給c
d = factorial(4); // 24賦值給d
e = factorial(5); // 120賦值給e
還有其它的方式來呼叫函式,常見的一些情形是某些地方需要動態呼叫函式,或者函式的實引數量是變化的,或者呼叫函式的背景關系需要指定為在運行時確定的特定物件,顯然,函式本身就是物件,因此這些物件也有方法,作為此中情形之一,apply()方法可以實作這些目的,
函式作用域
在函式內定義的變數不能在函式之外的任何地方訪問,因為變數僅僅在該函式的域的內部有定義,相對應的,一個函式可以訪問定義在其范圍內的任何變數和函式,換言之,定義在全域域中的函式可以訪問所有定義在全域域中的變數,在另一個函式中定義的函式也可以訪問在其父函式中定義的所有變數和父函式有權訪問的任何其他變數,
// 下面的變數定義在全域作用域(global scope)中
var num1 = 20,
num2 = 3,
name = "Chamahk";
// 本函式定義在全域作用域
function multiply() {
return num1 * num2;
}
multiply(); // 回傳 60
// 嵌套函式的例子
function getScore() {
var num1 = 2,
num2 = 3;
function add() {
return name + " scored " + (num1 + num2);
}
return add();
}
getScore(); // 回傳 "Chamahk scored 5"
作用域和函式堆疊
遞回
一個函式可以指向并呼叫自身,有三種方法可以達到這個目的:
- 函式名
- `arguments.callee
- 作用域下的一個指向該函式的變數名
例如,思考一下如下的函式定義:
var foo = function bar() {
// statements go here
};
在這個函式體內,以下的陳述句是等價的:
bar()arguments.callee()(譯者注:ES5禁止在嚴格模式下使用此屬性)foo()
呼叫自身的函式我們稱之為遞回函式,在某種意義上說,遞回近似于回圈,兩者都重復執行相同的代碼,并且兩者都需要一個終止條件(避免無限回圈或者無限遞回),例如以下的回圈:
var x = 0;
while (x < 10) { // "x < 10" 是回圈條件
// do stuff
x++;
}
可以被轉化成一個遞回函式和對其的呼叫:
function loop(x) {
if (x >= 10) // "x >= 10" 是退出條件(等同于 "!(x < 10)")
return;
// 做些什么
loop(x + 1); // 遞回呼叫
}
loop(0);
不過,有些演算法并不能簡單的用迭代來實作,例如,獲取樹結構中所有的節點時,使用遞回實作要容易得多:
function walkTree(node) {
if (node == null) //
return;
// do something with node
for (var i = 0; i < node.childNodes.length; i++) {
walkTree(node.childNodes[i]);
}
}
跟loop函式相比,這里每個遞回呼叫都產生了更多的遞回,
將遞回演算法轉換為非遞回演算法是可能的,不過邏輯上通常會更加復雜,而且需要使用堆疊,事實上,遞回函式就使用了堆疊:函式堆疊,
這種類似堆疊的行為可以在下例中看到:
function foo(i) {
if (i < 0)
return;
console.log('begin:' + i);
foo(i - 1);
console.log('end:' + i);
}
foo(3);
// 輸出:
// begin:3
// begin:2
// begin:1
// begin:0
// end:0
// end:1
// end:2
// end:3
嵌套函式和閉包
你可以在一個函式里面嵌套另外一個函式,嵌套(內部)函式對其容器(外部)函式是私有的,它自身也形成了一個閉包,一個閉包是一個可以自己擁有獨立的環境與變數的運算式(通常是函式),
既然嵌套函式是一個閉包,就意味著一個嵌套函式可以”繼承“容器函式的引數和變數,換句話說,內部函式包含外部函式的作用域,
可以總結如下:
- 內部函式只可以在外部函式中訪問,
- 內部函式形成了一個閉包:它可以訪問外部函式的引數和變數,但是外部函式卻不能使用它的引數和變數,
下面的例子展示了嵌套函式:
function addSquares(a, b) {
function square(x) {
return x * x;
}
return square(a) + square(b);
}
a = addSquares(2, 3); // returns 13
b = addSquares(3, 4); // returns 25
c = addSquares(4, 5); // returns 41
由于內部函式形成了閉包,因此你可以呼叫外部函式并為外部函式和內部函式指定引數:
function outside(x) {
function inside(y) {
return x + y;
}
return inside;
}
fn_inside = outside(3); // 可以這樣想:給一個函式,使它的值加3
result = fn_inside(5); // returns 8
result1 = outside(3)(5); // returns 8
保存變數
注意到上例中 inside 被回傳時 x 是怎么被保留下來的,一個閉包必須保存它可見作用域中所有引數和變數,因為每一次呼叫傳入的引數都可能不同,每一次對外部函式的呼叫實際上重新創建了一遍這個閉包,只有當回傳的 inside 沒有再被參考時,記憶體才會被釋放,
這與在其他物件中存盤參考沒什么不同,但是通常不太明顯,因為并不能直接設定參考,也不能檢查它們,
多層嵌套函式
函式可以被多層嵌套,例如,函式A可以包含函式B,函式B可以再包含函式C,B和C都形成了閉包,所以B可以訪問A,C可以訪問B和A,因此,閉包可以包含多個作用域;他們遞回式的包含了所有包含它的函式作用域,這個稱之為作用域鏈,(稍后會詳細解釋)
思考一下下面的例子:
function A(x) {
function B(y) {
function C(z) {
console.log(x + y + z);
}
C(3);
}
B(2);
}
A(1); // logs 6 (1 + 2 + 3)
在這個例子里面,C可以訪問B的y和A的x,這是因為:
- B形成了一個包含A的閉包,B可以訪問A的引數和變數
- C形成了一個包含B的閉包
- B包含A,所以C也包含A,C可以訪問B和A的引數和變數,換言之,C用這個順序鏈接了B和A的作用域
反過來卻不是這樣,A不能訪問C,因為A看不到B中的引數和變數,C是B中的一個變數,所以C是B私有的,
命名沖突
當同一個閉包作用域下兩個引數或者變數同名時,就會產生命名沖突,更近的作用域有更高的優先權,所以最近的優先級最高,最遠的優先級最低,這就是作用域鏈,鏈的第一個元素就是最里面的作用域,最后一個元素便是最外層的作用域,
看以下的例子:
function outside() {
var x = 5;
function inside(x) {
return x * 2;
}
return inside;
}
outside()(10); // returns 20 instead of 10
命名沖突發生在return x上,inside的引數x和outside變數x發生了沖突,這里的作用鏈域是{inside, outside, 全域物件},因此inside的x具有最高優先權,回傳了20(inside的x)而不是10(outside的x),
閉包
閉包是 JavaScript 中最強大的特性之一,JavaScript 允許函式嵌套,并且內部函式可以訪問定義在外部函式中的所有變數和函式,以及外部函式能訪問的所有變數和函式,
但是,外部函式卻不能夠訪問定義在內部函式中的變數和函式,這給內部函式的變數提供了一定的安全性,
此外,由于內部函式可以訪問外部函式的作用域,因此當內部函式生存周期大于外部函式時,外部函式中定義的變數和函式的生存周期將比內部函式執行時間長,當內部函式以某一種方式被任何一個外部函式作用域訪問時,一個閉包就產生了,
var pet = function(name) { //外部函式定義了一個變數"name"
var getName = function() {
//內部函式可以訪問 外部函式定義的"name"
return name;
}
//回傳這個內部函式,從而將其暴露在外部函式作用域
return getName;
};
myPet = pet("Vivie");
myPet(); // 回傳結果 "Vivie"
實際上可能會比上面的代碼復雜的多,在下面這種情形中,回傳了一個包含可以操作外部函式的內部變數方法的物件,
var createPet = function(name) {
var sex;
return {
setName: function(newName) {
name = newName;
},
getName: function() {
return name;
},
getSex: function() {
return sex;
},
setSex: function(newSex) {
if(typeof newSex == "string"
&& (newSex.toLowerCase() == "male" || newSex.toLowerCase() == "female")) {
sex = newSex;
}
}
}
}
var pet = createPet("Vivie");
pet.getName(); // Vivie
pet.setName("Oliver");
pet.setSex("male");
pet.getSex(); // male
pet.getName(); // Oliver
在上面的代碼中,外部函式的name變數對內嵌函式來說是可取得的,而除了通過內嵌函式本身,沒有其它任何方法可以取得內嵌的變數,內嵌函式的內嵌變數就像內嵌函式的保險柜,它們會為內嵌函式保留“穩定”——而又安全——的資料參與運行,而這些內嵌函式甚至不會被分配給一個變數,或者不必一定要有名字,
var getCode = (function(){
var secureCode = "0]Eal(eh&2"; // A code we do not want outsiders to be able to modify...
return function () {
return secureCode;
};
})();
getCode(); // Returns the secret code
盡管有上述優點,使用閉包時仍然要小心避免一些陷阱,如果一個閉包的函式定義了一個和外部函式的某個變數名稱相同的變數,那么這個閉包將無法參考外部函式的這個變數,
var createPet = function(name) { // Outer function defines a variable called "name"
return {
setName: function(name) { // Enclosed function also defines a variable called "name"
name = name; // ??? How do we access the "name" defined by the outer function ???
}
}
}
使用 arguments 物件
函式的實際引數會被保存在一個類似陣列的arguments物件中,在函式內,你可以按如下方式找出傳入的引數:
arguments[i]
其中i是引數的序數編號(譯注:陣列索引),以0開始,所以第一個傳來的引數會是arguments[0],引數的數量由arguments.length表示,
使用arguments物件,你可以處理比宣告的更多的引數來呼叫函式,這在你事先不知道會需要將多少引數傳遞給函式時十分有用,你可以用arguments.length來獲得實際傳遞給函式的引數的數量,然后用arguments物件來取得每個引數,
例如,設想有一個用來連接字串的函式,唯一事先確定的引數是在連接后的字串中用來分隔各個連接部分的字符(譯注:比如例子里的分號“;”),該函式定義如下:
function myConcat(separator) {
var result = ''; // 把值初始化成一個字串,這樣就可以用來保存字串了!!
var i;
// iterate through arguments
for (i = 1; i < arguments.length; i++) {
result += arguments[i] + separator;
}
return result;
}
你可以給這個函式傳遞任意數量的引數,它會將各個引數連接成一個字串“串列”:
// returns "red, orange, blue, "
myConcat(", ", "red", "orange", "blue");
// returns "elephant; giraffe; lion; cheetah; "
myConcat("; ", "elephant", "giraffe", "lion", "cheetah");
// returns "sage. basil. oregano. pepper. parsley. "
myConcat(". ", "sage", "basil", "oregano", "pepper", "parsley");
提示:arguments變數只是 *”*類陣列物件“,并不是一個陣列,稱其為類陣列物件是說它有一個索引編號和length屬性,盡管如此,它并不擁有全部的Array物件的操作方法,
函式引數
從ECMAScript 6開始,有兩個新的型別的引數:默認引數,剩余引數,
默認引數
在JavaScript中,函式引數的默認值是undefined,然而,在某些情況下設定不同的默認值是有用的,這時默認引數可以提供幫助,
在過去,用于設定默認引數的一般策略是在函式的主體中測驗引數值是否為undefined,如果是則賦予這個引數一個默認值,如果在下面的例子中,呼叫函式時沒有實參傳遞給b,那么它的值就是undefined,于是計算a*b得到、函式回傳的是 NaN,但是,在下面的例子中,這個已經被第二行獲取處理:
function multiply(a, b) {
b = (typeof b !== 'undefined') ? b : 1;
return a*b;
}
multiply(5); // 5
使用默認引數,在函式體的檢查就不再需要了,現在,你可以在函式頭簡單地把1設定為b的默認值:
function multiply(a, b = 1) {
return a*b;
}
multiply(5); // 5
剩余引數
剩余引數語法允許將不確定數量的引數表示為陣列,在下面的例子中,使用剩余引數收集從第二個到最后引數,然后,我們將這個陣列的每一個數與第一個引數相乘,這個例子是使用了一個箭頭函式,這將在下一節介紹,
function multiply(multiplier, ...theArgs) {
return theArgs.map(x => multiplier * x);
}
var arr = multiply(2, 1, 2, 3);
console.log(arr); // [2, 4, 6]
箭頭函式
箭頭函式運算式也稱胖箭頭函式)相比函式運算式具有較短的語法并以詞法的方式系結 this,箭頭函式總是匿名的,
有兩個因素會影響引入箭頭函式:更簡潔的函式和 this,
更簡潔的函式
在一些函式模式中,更簡潔的函式很受歡迎,對比一下:
var a = [
"Hydrogen",
"Helium",
"Lithium",
"Beryllium"
];
var a2 = a.map(function(s){ return s.length });
console.log(a2); // logs [ 8, 6, 7, 9 ]
var a3 = a.map( s => s.length );
console.log(a3); // logs [ 8, 6, 7, 9 ]
this 的詞法
在箭頭函式出現之前,每一個新函式都重新定義了自己的 [this]值(在建構式中是一個新的物件;在嚴格模式下是未定義的;在作為“物件方法”呼叫的函式中指向這個物件;等等),以面向物件的編程風格,這樣著實有點惱人,
function Person() {
// 建構式Person()將`this`定義為自身
this.age = 0;
setInterval(function growUp() {
// 在非嚴格模式下,growUp()函式將`this`定義為“全域物件”,
// 這與Person()定義的`this`不同,
// 所以下面的陳述句不會起到預期的效果,
this.age++;
}, 1000);
}
var p = new Person();
在ECMAScript 3/5里,通過把this的值賦值給一個變數可以修復這個問題,
function Person() {
var self = this; // 有的人習慣用`that`而不是`self`,
// 無論你選擇哪一種方式,請保持前后代碼的一致性
self.age = 0;
setInterval(function growUp() {
// 以下陳述句可以實作預期的功能
self.age++;
}, 1000);
}
另外,創建一個約束函式可以使得 this值被正確傳遞給 growUp() 函式,
箭頭函式捕捉閉包背景關系的this值,所以下面的代碼作業正常,
function Person(){
this.age = 0;
setInterval(() => {
this.age++; // 這里的`this`正確地指向person物件
}, 1000);
}
var p = new Person();
預定義函式
JavaScript語言有好些個頂級的內建函式:
eval()
eval()方法會對一串字串形式的JavaScript代碼字符求值,
uneval()
uneval()方法創建的一個Object的源代碼的字串表示,
isFinite()
isFinite()函式判斷傳入的值是否是有限的數值, 如果需要的話,其引數首先被轉換為一個數值,
isNaN()
isNaN()函式判斷一個值是否是NaN,注意:isNaN函式內部的強制轉換規則十分有趣; 另一個可供選擇的是ECMAScript 6 中定義Number.isNaN(), 或者使用 typeof來判斷數值型別,
parseFloat()
parseFloat() 函式決議字串引數,并回傳一個浮點數,
parseInt()
parseInt() 函式決議字串引數,并回傳指定的基數(基礎數學中的數制)的整數,
decodeURI()
decodeURI() 函式對先前經過encodeURI函式或者其他類似方法編碼過的字串進行解碼,
decodeURIComponent()
decodeURIComponent()方法對先前經過encodeURIComponent函式或者其他類似方法編碼過的字串進行解碼,
encodeURI()
**encodeURI()**方法通過用以一個,兩個,三個或四個轉義序串列示字符的UTF-8編碼替換統一資源識別符號(URI)的某些字符來進行編碼(每個字符對應四個轉義序列,這四個序列組了兩個”替代“字符),
encodeURIComponent()
encodeURIComponent() 方法通過用以一個,兩個,三個或四個轉義序串列示字符的UTF-8編碼替換統一資源識別符號(URI)的每個字符來進行編碼(每個字符對應四個轉義序列,這四個序列組了兩個”替代“字符),
escape()
已廢棄的 **escape()** 方法計算生成一個新的字串,其中的某些字符已被替換為十六進制轉義序列,使用 encodeURI或者encodeURIComponent替代本方法,
unescape()
已廢棄的 **unescape()** 方法計算生成一個新的字串,其中的十六進制轉義序列將被其表示的字符替換,上述的轉義序列就像escape里介紹的一樣,因為 unescape 已經廢棄,建議使用decodeURI()或者decodeURIComponent替代本方法,
最后
要想學好JavaScript除了基本的JavaScript知識點外,作為JavaScript的第一等公民——函式,我們要深入的了解,函式的多變來源于引數的靈活多變和回傳值的多變,如果引數是一般的資料型別或一般物件,這樣的函式就是普通函式;如果函式的引數是函式,這就是我們所要知道的高級函式;如果創建的函式呼叫另外一部分(變數和引數已經預置),這樣的函式就是偏函式,
JavaScript逐點突破系列之this,小伙伴們也可以看看嗷,點擊這里立即閱讀,文章里面也包括了JavaScript的面試題或前端面試題的,需要的去看文章就好啦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/277050.html
標籤:其他
上一篇:CSS布局(二)------display屬性(建議收藏)
下一篇:學會鏈表中LeetCode三道題
