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