JS中定義函數的方式與其他語言一樣沒什么差別,但是要知道JS允許傳入任意個數參數,如果傳入的參數比定義的參數多也沒有問題,函數內部并不會調用這些參數:
function abs(x) {
if (x >= 0) {
return x;
} else {
return -x;
}
}
abs(10, 'blablabla'); // 返回10
abs(-9, 'haha', 'hehe', null); // 返回9
傳入的參數比定義的少也沒有問題:
abs(); // 返回NaN
JS關鍵字argument在函數內部指向當前函數傳入的所有參數,arguments類似Array但它不是Array:
function foo(x) {
alert(x); // 10
for (var i=0; i<arguments.length; i++) {
alert(arguments[i]); // 10, 20, 30
}
}
foo(10, 20, 30);
利用arguments可以獲得調用者傳入的所有參數,也就是說,即使函數不定義任何參數也可以拿到參數的值,而實際上arguments最常用于判斷傳入參數的個數:
function abs() {
if (arguments.length === 0) {
return 0;
}
var x = arguments[0];
return x >= 0 ? x : -x;
}
abs(); // 0
abs(10); // 10
abs(-9); // 9
// foo(a[, b], c)
// 接收2~3個參數,b是可選參數,如果只傳2個參數,b默認為null:
function foo(a, b, c) {
if (arguments.length === 2) {
// 實際拿到的參數是a和b,c為undefined
c = b; // 把b賦給c
b = null; // b變為默認值
}
// ...
}
要把中間的參數b變為“可選”參數,就只能通過arguments判斷,然后重新調整參數并賦值。
為了獲得額外的參數,ES6標準引入了rest參數:
function foo(a, b, ...rest) {
console.log('a = ' + a);
console.log('b = ' + b);
console.log(rest);
}
foo(1, 2, 3, 4, 5);
// 結果:
// a = 1
// b = 2
// Array [ 3, 4, 5 ]
foo(1);
// 結果:
// a = 1
// b = undefined
// Array []
rest參數寫在最后,前面用...標識,多余的參數以數組形式交給變量rest;如果傳入的參數連正常定義的參數都沒填滿,rest參數會接收一個空數組(注意不是undefined)。
變量作用域
JS的函數可以嵌套,內部函數可以訪問外部函數定義的變量,如果內部函數和外部函數的變量名重名怎么辦?
function foo() {
var x = 1;
function bar() {
var x = 'A';
alert('x in bar() = ' + x); // 'A'
}
alert('x in foo() = ' + x); // 1
bar();
}
這說明JS的函數在查找變量時從自身函數定義開始,由“內”向“外”查找,如果變量重名則內部變量將“屏蔽”外部變量。
JS的函數在定義時會先掃描整個函數語句,把所有申明的變量“提升”到函數頂部:
function foo() {
var x = 'Hello, ' + y;
alert(x);
var y = 'Bob';
}
foo();
雖然是strict模式,但語句var x = 'Hello, ' + y;并不報錯,原因是變量y在后面聲明了。但雖然JS引擎自動提升了變量y的聲明,卻不會提升變量y的賦值,所以在賦值前y為undefined。基于此我們在函數內部定義變量時,請嚴格遵守“在函數內部首先申明所有變量”這一規則,最常見的做法是用一個var申明函數內部用到的所有變量:
function foo() {
var
x = 1, // x初始化為1
y = x + 1, // y初始化為2
z, i; // z和i為undefined
// 其他語句:
for (i=0; i<100; i++) {
...
}
}
不在任何函數內定義的變量具有全局作用域,實際上JS默認有一個全局對象window
,全局作用域的變量實際上被作為的一個屬性綁定到window。不同的JS文件如果使用了相同的全局變量或者定義了相同名字的函數,都會造成命名沖突,并且很難被發現。
減少沖突的一個方法是把所有變量和函數全部綁定到一個全局變量中例如:
// 唯一的全局變量MYAPP:
var MYAPP = {};
// 其他變量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;
// 其他函數:
MYAPP.foo = function () {
return 'foo';
};
許多著名的JS庫都是這么做的如Query,YUI,underscore等等。此外,ES6標準引入了新的關鍵字const和let,用let替代var可以申明一個塊級作用域的變量,而const
用來聲明常量,const與let都具有塊級作用域。
方法
JS的函數內部如果調用了this,那么這個this到底指向誰?答案是視情況而定!如果以對象的方法形式調用,該函數的this指向被調用的對象;如果單獨調用函數,此時該函數的this指向全局對象,也就是window。因此ECMA決定,在strict模式下讓函數的this指向undefined:
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var y = new Date().getFullYear();
return y - this.birth;
}
};
var fn = xiaoming.age;
fn(); // Uncaught TypeError: Cannot read property 'birth' of undefined
這個決定只是讓錯誤及時暴露出來,并沒有解決this應該指向的正確位置,這時把方法重構了一下:
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
function getAgeFromBirth() {
var y = new Date().getFullYear();
return y - this.birth;
}
return getAgeFromBirth();
}
};
xiaoming.age(); // Uncaught TypeError: Cannot read property 'birth' of undefined
結果又報錯了!原因是this指針只在age方法的函數內指向xiaoming,在函數內部定義的函數,this又指向undefined了(在非strict模式下,它重新指向全局對window)!那么如何修改呢?我們用一個that變量替換this,就可以放心地在方法內部定義其他函數,而不是把所有語句都堆到一個方法中:
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var that = this; // 在方法內部一開始就捕獲this
function getAgeFromBirth() {
var y = new Date().getFullYear();
return y - that.birth; // 用that而不是this
}
return getAgeFromBirth();
}
};
xiaoming.age(); // 25
其實我們還是可以控制this指向哪個對象的,這里要用到函數本身的apply方法,它接收兩個參數,第一個參數就是需要綁定的this變量,第二個參數是Array,表示函數本身的參數:
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 參數為空
另一個與apply()類似的方法是call(),唯一區別是:
- apply()把參數打包成Array再傳入;
- call()把參數按順序傳入。
比如調用Math.max(3, 5, 4),分別用apply()和call()實現如下:
// 通常把this綁定為null
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5
利用apply(),我們還可以動態改變函數的行為。因為JS的所有對象都是動態的,即使內置的函數,我們也可以重新指向新的函數:
var count = 0;
var oldParseInt = parseInt; // 保存原函數
window.parseInt = function () {
count += 1;
return oldParseInt.apply(null, arguments); // 調用原函數
};
// 測試:
parseInt('10');
parseInt('20');
parseInt('30');
count; // 3
高階函數
map()/reduce()
map()接收一個函數并將函數作用在Array的每一個元素并把結果生成一個新的Array。而reduce()是把一個函數作用在這個Array的[x1, x2, x3...]上,這個函數必須接收兩個參數,reduce()把結果繼續和序列的下一個元素做累積計算,其效果就是:
[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)
比方說對一個Array求和,就可以用reduce()實現:
var arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
return x + y;
}); // 25
filter()
filter()用于把Array的某些元素過濾掉,然后返回剩下的元素。和map()類似,filter()也接收一個函數,但不同的是filter()把傳入的函數依次作用于每個元素,然后根據返回值是true還是false決定保留還是丟棄該元素。例如,在一個Array中,刪掉偶數只保留奇數,可以這么寫:
var arr = [1, 2, 4, 5, 6, 9, 10, 15];
var r = arr.filter(function (x) {
return x % 2 !== 0;
});
r; // [1, 5, 9, 15]
sort()
Array的sort()方法默認把所有元素先轉換為String再排序,而字符串根據ASCII碼進行排序。同時sort()方法也是一個高階函數,它還可以接收一個比較函數來實現自定義的排序,要按數字大小排序,我們可以這么寫:
// 正序
var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
if (x < y) {
return -1;
}
if (x > y) {
return 1;
}
return 0;
}); // [1, 2, 10, 20]
// 倒序
var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
if (x < y) {
return 1;
}
if (x > y) {
return -1;
}
return 0;
}); // [20, 10, 2, 1]
sort()方法會直接對Array進行修改,它返回的結果仍是當前Array。
閉包
高階函數除了可以接受函數作為參數外,還可以把函數作為返回值。注意到返回的函數在其內部引用了局部變量,當一個函數返回了一個函數后,其內部的局部變量還被新函數引用,所以閉包實現起來可不容易。另一個需要注意的問題是,返回的函數并沒有立刻執行,而是直到調用時才執行。我們來看一個例子:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push(function () {
return i * i;
});
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
在上面的例子中,每次循環都創建了一個新的函數,然后把創建的函數都添加到Array中返回。返回的函數引用了變量i,但它并非立刻執行,函數都返回時所引用的變量i已經變成了4,因此最終結果為16。所以返回閉包時牢記:返回函數不要引用任何循環變量或者后續會發生變化的變量,也可以創建一個函數,用該函數的參數綁定循環變量當前的值,無論該循環變量后續如何更改,已綁定到函數參數的值不變:
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push((function (n) {
return function () {
return n * n;
}
})(i));
}
return arr;
}
var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
f1(); // 1
f2(); // 4
f3(); // 9
JS中,創建一個匿名函數并立刻執行可以這么寫:
(function (x) { return x * x }) (3);
在沒有class機制,只有函數的語言里,借助閉包可以封裝一個私有變量。我們用JS創建一個計數器:
function create_counter(initial) {
var x = initial || 0;
return {
inc: function () {
x += 1;
return x;
}
}
}
var c1 = create_counter();
c1.inc(); // 1
c1.inc(); // 2
c1.inc(); // 3
var c2 = create_counter(10);
c2.inc(); // 11
c2.inc(); // 12
c2.inc(); // 13
在返回的對象中實現了一個閉包,該閉包攜帶了局部變量x,并且從外部根本無法訪問到變量x。換句話說,閉包就是攜帶狀態的函數,并且它的狀態可以完全對外隱藏起
來。閉包還可以把多參數的函數變成單參數的函數:
function make_pow(n) {
return function (x) {
return Math.pow(x, n);
}
}
// 創建兩個新函數:
var pow2 = make_pow(2);
var pow3 = make_pow(3);
pow2(5); // 25
pow3(7); // 343
箭頭函數
箭頭函數相當于匿名函數,并且簡化了函數定義。它有兩種格式,一種只包含一個表達式,連{ ... }和return都省略掉了;還有一種可以包含多條語句,這時候就不能省略{ ... }和return:
x => {
if (x > 0) {
return x * x;
}
else {
return - x * x;
}
}
// 兩個參數:
(x, y) => x * x + y * y
// 無參數:
() => 3.14
// 可變參數:
(x, y, ...rest) => {
var i, sum = x + y;
for (i=0; i<rest.length; i++) {
sum += rest[i];
}
return sum;
}
如果要返回一個對象,因為和函數體的{ ... }有語法沖突,要這么寫:
x => ({ foo: x })
箭頭函數內部的this是詞法作用域,總是指向外層調用者,由上下文確定:
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = () => new Date().getFullYear() - this.birth; // this指向obj對象
return fn();
}
};
obj.getAge(); // 25
由于this在箭頭函數中已經按照詞法作用域綁定了,所以用call()或者apply()調用箭頭函數時,無法對this進行綁定,即傳入的第一個參數被忽略:
var obj = {
birth: 1990,
getAge: function (year) {
var b = this.birth; // 1990
var fn = (y) => y - this.birth; // this.birth仍是1990
return fn.call({birth:2000}, year);
}
};
obj.getAge(2015); // 25
Generator(生成器)
generator是ES6標準引入的新的數據類型,一個generator看上去像一個函數,但可以返回多次。generator定義如下:
function* foo(x) {
yield x + 1;
yield x + 2;
return x + 3;
}
generator和函數不同的是,generator由function定義(注意多出的號),并且,除了return語句,還可以用yield返回多次。要編寫一個產生斐波那契數列的函數,可以這么寫:
function fib(max) {
var
t,
a = 0,
b = 1,
arr = [0, 1];
while (arr.length < max) {
t = a + b;
a = b;
b = t;
arr.push(t);
}
return arr;
}
// 測試:
fib(5); // [0, 1, 1, 2, 3]
fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
函數只能返回一次,但是如果換成generator,就可以一次返回一個數,不斷返回多次:
function* fib(max) {
var
t,
a = 0,
b = 1,
n = 1;
while (n < max) {
yield a;
t = a + b;
a = b;
b = t;
n ++;
}
return a;
}
fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}
調用generator和調用函數不一樣,fib(5)僅僅是創建了一個generator對象,還沒有去執行它。調用generator對象有兩個方法:
一是不斷地調用generator對象的next()方法;
var f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: true}
next()方法會執行generator的代碼,每次遇到yield就返回一個對象{value: x, done: true/false}并“暫停”,返回的value就是yield的返回值,done表示這個generator是否已經執行結束了。
二是直接用for ... of循環迭代generator對象:
for (var x of fib(5)) {
console.log(x); // 依次輸出0, 1, 1, 2, 3
}
generator在執行過程中多次返回,所以它可以記住執行狀態。generator還有另一個巨大的好處,就是把異步回調變成“同步”:
ajax('http://url-1', data1, function (err, result) {
if (err) {
return handle(err);
}
ajax('http://url-2', data2, function (err, result) {
if (err) {
return handle(err);
}
ajax('http://url-3', data3, function (err, result) {
if (err) {
return handle(err);
}
return success(result);
});
});
});
try {
r1 = yield ajax('http://url-1', data1);
r2 = yield ajax('http://url-2', data2);
r3 = yield ajax('http://url-3', data3);
success(r3);
}
catch (err) {
handle(err);
}
看上去是同步的代碼,實際上是異步執行。