- 函數(shù)
- 函數(shù)定義與調(diào)用
- 變量作用域
- 全局變量
- 方法
- 高階函數(shù)
- 閉包
- 箭頭函數(shù)
- $generator$
函數(shù)
函數(shù)定義與調(diào)用
定義函數(shù)
在JavaScript中伯顶,定義函數(shù)的方式如下:
function abs(x) {
if (x >= 0) {
return x;
} else {
return -x;
}
}
上述abs()
函數(shù)的定義如下:
-
function
指出這是一個(gè)函數(shù)定義琅翻; -
abs
是函數(shù)的名稱; -
(x)
括號(hào)內(nèi)列出函數(shù)的參數(shù)千元,多個(gè)參數(shù)以,
分隔擂仍; -
{ ... }
之間的代碼是函數(shù)體驼壶,可以包含若干語(yǔ)句罚拟,甚至可以沒(méi)有任何語(yǔ)句悄雅。
請(qǐng)注意驱敲,函數(shù)體內(nèi)部的語(yǔ)句在執(zhí)行時(shí),一旦執(zhí)行到return
時(shí)宽闲,函數(shù)就執(zhí)行完畢众眨,并將結(jié)果返回。因此容诬,函數(shù)內(nèi)部通過(guò)條件判斷和循環(huán)可以實(shí)現(xiàn)非常復(fù)雜的邏輯娩梨。
如果沒(méi)有return
語(yǔ)句,函數(shù)執(zhí)行完畢后也會(huì)返回結(jié)果放案,只是結(jié)果為undefined
姚建。
由于JavaScript的函數(shù)也是一個(gè)對(duì)象,上述定義的abs()
函數(shù)實(shí)際上是一個(gè)函數(shù)對(duì)象吱殉,而函數(shù)名abs
可以視為指向該函數(shù)的變量掸冤。
因此,第二種定義函數(shù)的方式如下:
var abs = function (x) {
if (x >= 0) {
return x;
} else {
return -x;
}
};
在這種方式下友雳,function (x) { ... }
是一個(gè)匿名函數(shù)稿湿,它沒(méi)有函數(shù)名。
但是押赊,這個(gè)匿名函數(shù)賦值給了變量abs
饺藤,所以,通過(guò)變量abs
就可以調(diào)用該函數(shù)流礁。
上述兩種定義完全等價(jià)涕俗,注意第二種方式按照完整語(yǔ)法需要在函數(shù)體末尾加一個(gè);
,
表示賦值語(yǔ)句結(jié)束神帅。
調(diào)用函數(shù)
調(diào)用函數(shù)時(shí)再姑,按順序傳入?yún)?shù)即可:
abs(10); // 返回10
abs(-9); // 返回9
由于JavaScript允許傳入任意個(gè)參數(shù)而不影響調(diào)用,
因此傳入的參數(shù)比定義的參數(shù)多也沒(méi)有問(wèn)題找御,雖然函數(shù)內(nèi)部并不需要這些參數(shù):
abs(10, 'blablabla'); // 返回10
abs(-9, 'haha', 'hehe', null); // 返回9
傳入的參數(shù)比定義的少也沒(méi)有問(wèn)題:
abs(); // 返回NaN
此時(shí)abs(x)
函數(shù)的參數(shù)x將收到undefined
元镀,計(jì)算結(jié)果為NaN
。
要避免收到undefined
霎桅,可以對(duì)參數(shù)進(jìn)行檢查:
function abs(x) {
if (typeof x !== 'number') {
throw 'Not a number';
}
if (x >= 0) {
return x;
} else {
return -x;
}
}
$arguments$
JavaScript還有一個(gè)免費(fèi)贈(zèng)送的關(guān)鍵字arguments
栖疑,它只在函數(shù)內(nèi)部起作用,
并且永遠(yuǎn)指向當(dāng)前函數(shù)的調(diào)用者傳入的所有參數(shù)滔驶。arguments
類(lèi)似Array
但它不是一個(gè)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ù)。也就是說(shuō),即使函數(shù)不定義任何參數(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
實(shí)際上arguments最常用于判斷傳入?yún)?shù)的個(gè)數(shù)比原。你可能會(huì)看到這樣的寫(xiě)法:
// foo(a[, b], c)
// 接收2~3個(gè)參數(shù),b是可選參數(shù)杠巡,如果只傳2個(gè)參數(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ù)氢拥,就只能通過(guò)arguments
判斷蚌铜,然后重新調(diào)整參數(shù)并賦值。
$rest$參數(shù)
由于JavaScript函數(shù)允許接收任意個(gè)參數(shù)嫩海,于是我們就不得不用arguments
來(lái)獲取所有參數(shù):
function foo(a, b) {
var i, rest = [];
if (arguments.length > 2) {
for (i = 2; i<arguments.length; i++) {
rest.push(arguments[i]);
}
}
console.log('a = ' + a);
console.log('b = ' + b);
console.log(rest);
}
為了獲取除了已定義參數(shù)a冬殃、b之外的參數(shù),我們不得不用arguments
叁怪,
并且循環(huán)要從索引2開(kāi)始以便排除前兩個(gè)參數(shù)审葬,這種寫(xiě)法很別扭,只是為了獲得額外的rest
參數(shù)奕谭,有沒(méi)有更好的方法涣觉?
ES6標(biāo)準(zhǔn)引入了rest
參數(shù),上面的函數(shù)可以改寫(xiě)為:
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ù)只能寫(xiě)在最后血柳,前面用...
標(biāo)識(shí)官册,從運(yùn)行結(jié)果可知,傳入的參數(shù)先綁定a
难捌、b
膝宁,
多余的參數(shù)以數(shù)組形式交給變量rest
,所以根吁,不再需要arguments
我們就獲取了全部參數(shù)员淫。
如果傳入的參數(shù)連正常定義的參數(shù)都沒(méi)填滿,也不要緊击敌,rest
參數(shù)會(huì)接收一個(gè)空數(shù)組(注意不是undefined
)介返。
小心你的return語(yǔ)句
前面我們講到了JavaScript引擎有一個(gè)在行末自動(dòng)添加分號(hào)的機(jī)制,
這可能讓你栽到return
語(yǔ)句的一個(gè)大坑:
function foo() {
return { name: 'foo' };
}
foo(); // { name: 'foo' }
如果把return
語(yǔ)句拆成兩行:
function foo() {
return
{ name: 'foo' };
}
foo(); // undefined
要小心了愚争,由于JavaScript引擎在行末自動(dòng)添加分號(hào)的機(jī)制,上面的代碼實(shí)際上變成了:
function foo() {
return; // 自動(dòng)添加了分號(hào)挤聘,相當(dāng)于return undefined;
{ name: 'foo' }; // 這行語(yǔ)句已經(jīng)沒(méi)法執(zhí)行到了
}
所以正確的多行寫(xiě)法是:
function foo() {
return { // 這里不會(huì)自動(dòng)加分號(hào)轰枝,因?yàn)閧表示語(yǔ)句尚未結(jié)束
name: 'foo'
};
}
變量作用域
在JavaScript中,用var
申明的變量實(shí)際上是有作用域的组去。
如果一個(gè)變量在函數(shù)體內(nèi)部申明鞍陨,則該變量的作用域?yàn)檎麄€(gè)函數(shù)體,在函數(shù)體外不可引用該變量:
'use strict';
function foo() {
var x = 1;
x = x + 1;
}
x = x + 2; // ReferenceError! 無(wú)法在函數(shù)體外引用變量x
如果兩個(gè)不同的函數(shù)各自申明了同一個(gè)變量,
那么該變量只在各自的函數(shù)體內(nèi)起作用诚撵。
換句話說(shuō)缭裆,不同函數(shù)內(nèi)部的同名變量互相獨(dú)立,互不影響:
'use strict';
function foo() {
var x = 1;
x = x + 1;
}
function bar() {
var x = 'A';
x = x + 'B';
}
由于JavaScript的函數(shù)可以嵌套寿烟,
此時(shí)澈驼,內(nèi)部函數(shù)可以訪問(wèn)外部函數(shù)定義的變量,反過(guò)來(lái)則不行:
'use strict';
function foo() {
var x = 1;
function bar() {
var y = x + 1; // bar可以訪問(wèn)foo的變量x!
}
var z = y + 1; // ReferenceError! foo不可以訪問(wèn)bar的變量y!
}
如果內(nèi)部函數(shù)和外部函數(shù)的變量名重名怎么辦筛武?
'use strict';
function foo() {
var x = 1;
function bar() {
var x = 'A';
alert('x in bar() = ' + x); // 'A'
}
alert('x in foo() = ' + x); // 1
bar();
}
這說(shuō)明JavaScript的函數(shù)在查找變量時(shí)從自身函數(shù)定義開(kāi)始缝其,
從“內(nèi)”向“外”查找。如果內(nèi)部函數(shù)定義了與外部函數(shù)重名的變量徘六,則內(nèi)部函數(shù)的變量將“屏蔽”外部函數(shù)的變量内边。
全局變量
不在任何函數(shù)內(nèi)定義的變量就具有全局作用域。實(shí)際上待锈,JavaScript默認(rèn)有一個(gè)全局對(duì)象window
漠其,全局作用域的變量實(shí)際上被綁定到window
的一個(gè)屬性:
'use strict';
var course = 'Learn JavaScript';
alert(course); // 'Learn JavaScript'
alert(window.course); // 'Learn JavaScript'
因此,直接訪問(wèn)全局變量course
和訪問(wèn)window.course
是完全一樣的竿音。
你可能猜到了和屎,由于函數(shù)定義有兩種方式,以變量方式var foo = function () {}
定義的函數(shù)實(shí)際上也是一個(gè)全局變量谍失,
因此眶俩,頂層函數(shù)的定義也被視為一個(gè)全局變量,并綁定到window對(duì)象:
'use strict';
function foo() {
alert('foo');
}
foo(); // 直接調(diào)用foo()
window.foo(); // 通過(guò)window.foo()調(diào)用
方法
在一個(gè)對(duì)象中綁定函數(shù)快鱼,稱為這個(gè)對(duì)象的方法颠印。
在JavaScript中,對(duì)象的定義是這樣的:
var xiaoming = {
name: '小明',
birth: 1990
};
但是抹竹,如果我們給xiaoming
綁定一個(gè)函數(shù)线罕,就可以做更多的事情。比如窃判,寫(xiě)個(gè)age()
方法钞楼,返回xiaoming
的年齡:
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var y = new Date().getFullYear();
return y - this.birth;
}
};
xiaoming.age; // function xiaoming.age()
xiaoming.age(); // 今年調(diào)用是25,明年調(diào)用就變成26了
綁定到對(duì)象上的函數(shù)稱為方法,和普通函數(shù)也沒(méi)啥區(qū)別袄琳,
但是它在內(nèi)部使用了一個(gè)this
關(guān)鍵字询件,這個(gè)東東是什么?
在一個(gè)方法內(nèi)部唆樊,this
是一個(gè)特殊變量宛琅,它始終指向當(dāng)前對(duì)象,
也就是xiaoming
這個(gè)變量逗旁。
所以嘿辟,this.birth
可以拿到xiaoming
的birth
屬性。
讓我們拆開(kāi)寫(xiě):
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25, 正常結(jié)果
getAge(); // NaN
單獨(dú)調(diào)用函數(shù)getAge()
怎么返回了NaN
?請(qǐng)注意红伦,我們已經(jīng)進(jìn)入到了JavaScript的一個(gè)大坑里英古。
JavaScript的函數(shù)內(nèi)部如果調(diào)用了this
,那么這個(gè)this
到底指向誰(shuí)昙读?
答案是召调,視情況而定!
如果以對(duì)象的方法形式調(diào)用箕戳,比如xiaoming.age()
某残,
該函數(shù)的this
指向被調(diào)用的對(duì)象,也就是xiaoming
陵吸,這是符合我們預(yù)期的玻墅。
如果單獨(dú)調(diào)用函數(shù),比如getAge()
壮虫,此時(shí)澳厢,該函數(shù)的this
指向全局對(duì)象,也就是window
囚似。
坑爹笆B!!
更坑爹的是饶唤,如果這么寫(xiě):
var fn = xiaoming.age; // 先拿到xiaoming的age函數(shù)
fn(); // NaN
也是不行的徐伐!要保證this
指向正確,必須用obj.xxx()
的形式調(diào)用募狂!
由于這是一個(gè)巨大的設(shè)計(jì)錯(cuò)誤办素,要想糾正可沒(méi)那么簡(jiǎn)單。
ECMA決定祸穷,在strict
模式下讓函數(shù)的this
指向undefined
性穿,
因此,在strict
模式下雷滚,你會(huì)得到一個(gè)錯(cuò)誤:
'use strict';
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
這個(gè)決定只是讓錯(cuò)誤及時(shí)暴露出來(lái)需曾,并沒(méi)有解決this
應(yīng)該指向的正確位置。
有些時(shí)候祈远,喜歡重構(gòu)的你把方法重構(gòu)了一下:
'use strict';
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é)果又報(bào)錯(cuò)了呆万!原因是this
指針只在age
方法的函數(shù)內(nèi)指向xiaoming
,
在函數(shù)內(nèi)部定義的函數(shù)车份,this
又指向undefined
了谋减!
(在非strict模式下,它重新指向全局對(duì)象window9洹)
修復(fù)的辦法也不是沒(méi)有逃顶,我們用一個(gè)that
變量首先捕獲this
:
'use strict';
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var that = this; // 在方法內(nèi)部一開(kāi)始就捕獲this
function getAgeFromBirth() {
var y = new Date().getFullYear();
return y - that.birth; // 用that而不是this
}
return getAgeFromBirth();
}
};
xiaoming.age(); // 25
用var that = this;
,
你就可以放心地在方法內(nèi)部定義其他函數(shù)充甚,而不是把所有語(yǔ)句都堆到一個(gè)方法中以政。
$apply$
雖然在一個(gè)獨(dú)立的函數(shù)調(diào)用中饼记,根據(jù)是否是strict
模式琉雳,this
指向undefined
或window
坯癣,
不過(guò)纺座,我們還是可以控制this的指向的潮秘!
要指定函數(shù)的this
指向哪個(gè)對(duì)象窘面,可以用函數(shù)本身的apply
方法憾筏,
它接收兩個(gè)參數(shù)钦奋,第一個(gè)參數(shù)就是需要綁定的this
變量衰倦,第二個(gè)參數(shù)是Array
袒炉,表示函數(shù)本身的參數(shù)。
用apply
修復(fù)getAge()
調(diào)用:
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ù)為空
另一個(gè)與apply()
類(lèi)似的方法是call()
樊零,唯一區(qū)別是:
apply()
把參數(shù)打包成Array
再傳入我磁;
call()
把參數(shù)按順序傳入。
比如調(diào)用Math.max(3, 5, 4)
驻襟,分別用apply()
和call()
實(shí)現(xiàn)如下:
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5
對(duì)普通函數(shù)調(diào)用夺艰,我們通常把this
綁定為null
。
裝飾器
利用apply()
沉衣,我們還可以動(dòng)態(tài)改變函數(shù)的行為郁副。
JavaScript的所有對(duì)象都是動(dòng)態(tài)的,即使內(nèi)置的函數(shù)豌习,我們也可以重新指向新的函數(shù)存谎。
現(xiàn)在假定我們想統(tǒng)計(jì)一下代碼一共調(diào)用了多少次parseInt()
,
可以把所有的調(diào)用都找出來(lái)斑鸦,然后手動(dòng)加上count += 1
愕贡,
不過(guò)這樣做太傻了。最佳方案是用我們自己的函數(shù)替換掉默認(rèn)的parseInt()
:
var count = 0;
var oldParseInt = parseInt; // 保存原函數(shù)
window.parseInt = function () {
count += 1;
return oldParseInt.apply(null, arguments); // 調(diào)用原函數(shù)
};
// 測(cè)試:
parseInt('10');
parseInt('20');
parseInt('30');
count; // 3
高階函數(shù)
JavaScript的函數(shù)其實(shí)都指向某個(gè)變量巷屿。既然變量可以指向函數(shù)固以,函數(shù)的參數(shù)能接收變量,
那么一個(gè)函數(shù)就可以接收另一個(gè)函數(shù)作為參數(shù)嘱巾,這種函數(shù)就稱之為高階函數(shù)憨琳。
$map/reduce$
$map$
舉例說(shuō)明,比如我們有一個(gè)函數(shù)f(x)=x2
旬昭,要把這個(gè)函數(shù)作用在一個(gè)數(shù)組[1, 2, 3, 4, 5, 6, 7, 8, 9]
上篙螟,就可以用map
實(shí)現(xiàn)。
由于map()方法定義在JavaScript的Array
中问拘,我們調(diào)用Array
的map()
方法遍略,
傳入我們自己的函數(shù)惧所,就得到了一個(gè)新的Array
作為結(jié)果:
function pow(x) {
return x * x;
}
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
map()
傳入的參數(shù)是pow
,即函數(shù)對(duì)象本身绪杏。
你可能會(huì)想下愈,不需要map()
,寫(xiě)一個(gè)循環(huán)蕾久,也可以計(jì)算出結(jié)果:
var f = function (x) {
return x * x;
};
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var result = [];
for (var i=0; i<arr.length; i++) {
result.push(f(arr[i]));
}
的確可以势似,但是,從上面的循環(huán)代碼僧著,我們無(wú)法一眼看明白“把f(x)
作用在Array
的每一個(gè)元素并把結(jié)果生成一個(gè)新的Array
”履因。
所以,map()
作為高階函數(shù)盹愚,事實(shí)上它把運(yùn)算規(guī)則抽象了栅迄,
因此,我們不但可以計(jì)算簡(jiǎn)單的f(x)=x2
皆怕,
還可以計(jì)算任意復(fù)雜的函數(shù)霞篡,比如,把Array
的所有數(shù)字轉(zhuǎn)為字符串:
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(String); // ['1', '2', '3', '4', '5', '6', '7', '8', '9']
只需要一行代碼端逼。
$reduce$
再看reduce
的用法朗兵。Array
的reduce()
把一個(gè)函數(shù)作用在這個(gè)Array
的[x1, x2, x3...]
上,這個(gè)函數(shù)必須接收兩個(gè)參數(shù)顶滩,reduce()
把結(jié)果繼續(xù)和序列的下一個(gè)元素做累積計(jì)算余掖,其效果就是:
[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)
比方說(shuō)對(duì)一個(gè)Array求和,就可以用reduce實(shí)現(xiàn):
var arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
return x + y;
}); // 25
$filter$
filter
也是一個(gè)常用的操作礁鲁,它用于把Array
的某些元素過(guò)濾掉盐欺,然后返回剩下的元素。
和map()
類(lèi)似仅醇,Array
的filter()
也接收一個(gè)函數(shù)冗美。和map()
不同的是,filter()
把傳入的函數(shù)依次作用于每個(gè)元素析二,然后根據(jù)返回值是true
還是false
決定保留還是丟棄該元素粉洼。
例如,在一個(gè)Array
中叶摄,刪掉偶數(shù)属韧,只保留奇數(shù),可以這么寫(xiě):
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]
把一個(gè)Array
中的空字符串刪掉蛤吓,可以這么寫(xiě):
var arr = ['A', '', 'B', null, undefined, 'C', ' '];
var r = arr.filter(function (s) {
return s && s.trim(); // 注意:IE9以下的版本沒(méi)有trim()方法
});
r; // ['A', 'B', 'C']
可見(jiàn)用filter()
這個(gè)高階函數(shù)宵喂,關(guān)鍵在于正確實(shí)現(xiàn)一個(gè)“篩選”函數(shù)。
回調(diào)函數(shù)
filter()
接收的回調(diào)函數(shù)会傲,其實(shí)可以有多個(gè)參數(shù)锅棕。通常我們僅使用第一個(gè)參數(shù)拙泽,表示Array
的某個(gè)元素÷懔牵回調(diào)函數(shù)還可以接收另外兩個(gè)參數(shù)奔滑,表示元素的位置和數(shù)組本身:
var arr = ['A', 'B', 'C'];
var r = arr.filter(function (element, index, self) {
console.log(element); // 依次打印'A', 'B', 'C'
console.log(index); // 依次打印0, 1, 2
console.log(self); // self就是變量arr
return true;
});
$sort$
排序也是在程序中經(jīng)常用到的算法。
無(wú)論使用冒泡排序還是快速排序顺少,排序的核心是比較兩個(gè)元素的大小。
如果是數(shù)字王浴,我們可以直接比較脆炎,但如果是字符串或者兩個(gè)對(duì)象呢?
直接比較數(shù)學(xué)上的大小是沒(méi)有意義的氓辣,因此秒裕,比較的過(guò)程必須通過(guò)函數(shù)抽象出來(lái)。
通常規(guī)定钞啸,對(duì)于兩個(gè)元素x
和y
几蜻,如果認(rèn)為x < y
,則返回-1
体斩,
如果認(rèn)為x == y
梭稚,則返回0
,如果認(rèn)為x > y
絮吵,則返回1
弧烤,
這樣,排序算法就不用關(guān)心具體的比較過(guò)程蹬敲,而是根據(jù)比較結(jié)果直接排序暇昂。
JavaScript的Array
的sort()
方法就是用于排序的,但是排序結(jié)果可能讓你大吃一驚:
// 看上去正常的結(jié)果:
['Google', 'Apple', 'Microsoft'].sort(); // ['Apple', 'Google', 'Microsoft'];
// apple排在了最后:
['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple']
// 無(wú)法理解的結(jié)果:
[10, 20, 1, 2].sort(); // [1, 10, 2, 20]
第二個(gè)排序把apple
排在了最后伴嗡,
是因?yàn)樽址鶕?jù)ASCII碼進(jìn)行排序急波,而小寫(xiě)字母a
的ASCII碼在大寫(xiě)字母之后。
第三個(gè)排序結(jié)果是什么鬼瘪校?簡(jiǎn)單的數(shù)字排序都能錯(cuò)澄暮?
這是因?yàn)?code>Array的sort()
方法默認(rèn)把所有元素先轉(zhuǎn)換為String
再排序,結(jié)果'10'
排在了'2'
的前面阱扬,因?yàn)樽址?code>'1'比字符'2'
的ASCII碼小赏寇。
如果不知道sort()
方法的默認(rèn)排序規(guī)則,直接對(duì)數(shù)字排序价认,絕對(duì)栽進(jìn)坑里嗅定!
幸運(yùn)的是,sort()
方法也是一個(gè)高階函數(shù)用踩,它還可以接收一個(gè)比較函數(shù)來(lái)實(shí)現(xiàn)自定義的排序渠退。
要按數(shù)字大小排序忙迁,我們可以這么寫(xiě):
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]
如果要倒序排序,我們可以把大的數(shù)放前面:
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]
默認(rèn)情況下碎乃,對(duì)字符串排序姊扔,是按照ASCII的大小比較的,
現(xiàn)在梅誓,我們提出排序應(yīng)該忽略大小寫(xiě)恰梢,按照字母序排序。
要實(shí)現(xiàn)這個(gè)算法梗掰,不必對(duì)現(xiàn)有代碼大加改動(dòng)嵌言,只要我們能定義出忽略大小寫(xiě)的比較算法就可以:
var arr = ['Google', 'apple', 'Microsoft'];
arr.sort(function (s1, s2) {
x1 = s1.toUpperCase();
x2 = s2.toUpperCase();
if (x1 < x2) {
return -1;
}
if (x1 > x2) {
return 1;
}
return 0;
}); // ['apple', 'Google', 'Microsoft']
忽略大小寫(xiě)來(lái)比較兩個(gè)字符串,實(shí)際上就是先把字符串都變成大寫(xiě)(或者都變成小寫(xiě))及穗,再比較摧茴。
從上述例子可以看出,高階函數(shù)的抽象能力是非常強(qiáng)大的埂陆,而且苛白,核心代碼可以保持得非常簡(jiǎn)潔。
最后友情提示焚虱,sort()
方法會(huì)直接對(duì)Array
進(jìn)行修改购裙,它返回的結(jié)果仍是當(dāng)前Array
:
var a1 = ['B', 'A', 'C'];
var a2 = a1.sort();
a1; // ['A', 'B', 'C']
a2; // ['A', 'B', 'C']
a1 === a2; // true, a1和a2是同一對(duì)象
閉包
函數(shù)作為返回值
高階函數(shù)除了可以接受函數(shù)作為參數(shù)外,還可以把函數(shù)作為結(jié)果值返回鹃栽。
我們來(lái)實(shí)現(xiàn)一個(gè)對(duì)Array
的求和缓窜。通常情況下,求和的函數(shù)是這樣定義的:
function sum(arr) {
return arr.reduce(function (x, y) {
return x + y;
});
}
sum([1, 2, 3, 4, 5]); // 15
但是谍咆,如果不需要立刻求和禾锤,而是在后面的代碼中,根據(jù)需要再計(jì)算怎么辦摹察?可以不返回求和的結(jié)果恩掷,而是返回求和的函數(shù)!
function lazy_sum(arr) {
var sum = function () {
return arr.reduce(function (x, y) {
return x + y;
});
}
return sum;
}
當(dāng)我們調(diào)用lazy_sum()
時(shí)供嚎,返回的并不是求和結(jié)果黄娘,而是求和函數(shù):
var f = lazy_sum([1, 2, 3, 4, 5]); // function sum()
調(diào)用函數(shù)f
時(shí),才真正計(jì)算求和的結(jié)果:
f(); // 15
在這個(gè)例子中克滴,我們?cè)诤瘮?shù)lazy_sum
中又定義了函數(shù)sum
逼争,
并且,內(nèi)部函數(shù)sum
可以引用外部函數(shù)lazy_sum
的參數(shù)和局部變量劝赔,
當(dāng)lazy_sum
返回函數(shù)sum
時(shí)誓焦,相關(guān)參數(shù)和變量都保存在返回的函數(shù)中,
這種稱為“閉包(Closure)”的程序結(jié)構(gòu)擁有極大的威力着帽。
請(qǐng)?jiān)僮⒁庖稽c(diǎn)杂伟,當(dāng)我們調(diào)用lazy_sum()
時(shí)移层,每次調(diào)用都會(huì)返回一個(gè)新的函數(shù),即使傳入相同的參數(shù):
var f1 = lazy_sum([1, 2, 3, 4, 5]);
var f2 = lazy_sum([1, 2, 3, 4, 5]);
f1 === f2; // false
f1()
和f2()
的調(diào)用結(jié)果互不影響赫粥。
閉包
注意到返回的函數(shù)在其定義內(nèi)部引用了局部變量arr
观话,
所以,當(dāng)一個(gè)函數(shù)返回了一個(gè)函數(shù)后越平,其內(nèi)部的局部變量還被新函數(shù)引用频蛔,
所以,閉包用起來(lái)簡(jiǎn)單秦叛,實(shí)現(xiàn)起來(lái)可不容易晦溪。
另一個(gè)需要注意的問(wèn)題是,返回的函數(shù)并沒(méi)有立刻執(zhí)行书闸,而是直到調(diào)用了f()才執(zhí)行。我們來(lái)看一個(gè)例子:
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)建了一個(gè)新的函數(shù),
然后哀澈,把創(chuàng)建的3個(gè)函數(shù)都添加到一個(gè)Array
中返回了牌借。
你可能認(rèn)為調(diào)用f1()
,f2()
和f3()
結(jié)果應(yīng)該是1割按,4膨报,9,但實(shí)際結(jié)果是:
f1(); // 16
f2(); // 16
f3(); // 16
全部都是16适荣!
原因就在于返回的函數(shù)引用了變量i现柠,但它并非立刻執(zhí)行。
等到3個(gè)函數(shù)都返回時(shí)弛矛,它們所引用的變量i已經(jīng)變成了4够吩,因此最終結(jié)果為16。
返回閉包時(shí)牢記的一點(diǎn)就是:返回函數(shù)不要引用任何循環(huán)變量丈氓,或者后續(xù)會(huì)發(fā)生變化的變量周循。
如果一定要引用循環(huán)變量怎么辦?
方法是再創(chuàng)建一個(gè)函數(shù)万俗,用該函數(shù)的參數(shù)綁定循環(huán)變量當(dāng)前的值湾笛,無(wú)論該循環(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
注意這里用了一個(gè)“創(chuàng)建一個(gè)匿名函數(shù)并立刻執(zhí)行”的語(yǔ)法:
(function (x) {
return x * x;
})(3); // 9
理論上講闰歪,創(chuàng)建一個(gè)匿名函數(shù)并立刻執(zhí)行可以這么寫(xiě):
function (x) { return x * x } (3);
但是由于JavaScript語(yǔ)法解析的問(wèn)題嚎研,會(huì)報(bào)SyntaxError
錯(cuò)誤,
因此需要用括號(hào)把整個(gè)函數(shù)定義括起來(lái):
(function (x) { return x * x }) (3);
通常库倘,一個(gè)立即執(zhí)行的匿名函數(shù)可以把函數(shù)體拆開(kāi)嘉赎,一般這么寫(xiě):
(function (x) {
return x * x;
})(3);
說(shuō)了這么多置媳,難道閉包就是為了返回一個(gè)函數(shù)然后延遲執(zhí)行嗎?
當(dāng)然不是公条!閉包有非常強(qiáng)大的功能拇囊。舉個(gè)栗子:
在面向?qū)ο蟮某绦蛟O(shè)計(jì)語(yǔ)言里,比如Java和C++靶橱,要在對(duì)象內(nèi)部封裝一個(gè)私有變量寥袭,可以用private修飾一個(gè)成員變量。
在沒(méi)有class機(jī)制关霸,只有函數(shù)的語(yǔ)言里传黄,借助閉包,同樣可以封裝一個(gè)私有變量队寇。
我們用JavaScript創(chuàng)建一個(gè)計(jì)數(shù)器:
'use strict';
function create_counter(initial) {
var x = initial || 0;
return {
inc: function () {
x += 1;
return x;
}
}
}
它用起來(lái)像這樣:
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
在返回的對(duì)象中膘掰,實(shí)現(xiàn)了一個(gè)閉包,該閉包攜帶了局部變量x佳遣,并且识埋,從外部代碼根本無(wú)法訪問(wèn)到變量x。換句話說(shuō)零渐,閉包就是攜帶狀態(tài)的函數(shù)窒舟,并且它的狀態(tài)可以完全對(duì)外隱藏起來(lái)。
閉包還可以把多參數(shù)的函數(shù)變成單參數(shù)的函數(shù)诵盼。例如惠豺,要計(jì)算xy可以用Math.pow(x, y)函數(shù),不過(guò)考慮到經(jīng)常計(jì)算x2或x3风宁,我們可以利用閉包創(chuàng)建新的函數(shù)pow2
和pow3
:
function make_pow(n) {
return function (x) {
return Math.pow(x, n);
}
}
// 創(chuàng)建兩個(gè)新函數(shù):
var pow2 = make_pow(2);
var pow3 = make_pow(3);
pow2(5); // 25
pow3(7); // 343
箭頭函數(shù)
ES6標(biāo)準(zhǔn)新增了一種新的函數(shù):Arrow Function
(箭頭函數(shù))洁墙。
為什么叫Arrow Function
?因?yàn)樗亩x用的就是一個(gè)箭頭:
x => x * x
上面的箭頭函數(shù)相當(dāng)于:
function (x) {
return x * x;
}
箭頭函數(shù)相當(dāng)于匿名函數(shù)戒财,并且簡(jiǎn)化了函數(shù)定義扫俺。
箭頭函數(shù)有兩種格式,一種像上面的固翰,只包含一個(gè)表達(dá)式狼纬,連{ ... }
和return
都省略掉了。
還有一種可以包含多條語(yǔ)句骂际,這時(shí)候就不能省略{ ... }
和return
:
x => {
if (x > 0) {
return x * x;
}
else {
return - x * x;
}
}
如果參數(shù)不是一個(gè)疗琉,就需要用括號(hào)()
括起來(lái):
// 兩個(gè)參數(shù):
(x, y) => x * x + y * y
// 無(wú)參數(shù):
() => 3.14
// 可變參數(shù):
(x, y, ...rest) => {
var i, sum = x + y;
for (i=0; i<rest.length; i++) {
sum += rest[i];
}
return sum;
}
如果要返回一個(gè)對(duì)象,就要注意歉铝,如果是單表達(dá)式盈简,這么寫(xiě)的話會(huì)報(bào)錯(cuò):
// SyntaxError:
x => { foo: x }
因?yàn)楹秃瘮?shù)體的{ ... }有語(yǔ)法沖突,所以要改為:
// ok:
x => ({ foo: x })
this
箭頭函數(shù)看上去是匿名函數(shù)的一種簡(jiǎn)寫(xiě),
但實(shí)際上柠贤,箭頭函數(shù)和匿名函數(shù)有個(gè)明顯的區(qū)別:
箭頭函數(shù)內(nèi)部的this
是詞法作用域香浩,由上下文確定。
回顧前面的例子臼勉,由于JavaScript函數(shù)對(duì)this
綁定的錯(cuò)誤處理邻吭,
下面的例子無(wú)法得到預(yù)期結(jié)果:
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = function () {
return new Date().getFullYear() - this.birth; // this指向window或undefined
};
return fn();
}
};
現(xiàn)在,箭頭函數(shù)完全修復(fù)了this
的指向宴霸,this
總是指向詞法作用域囱晴,也就是外層調(diào)用者obj
:
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = () => new Date().getFullYear() - this.birth; // this指向obj對(duì)象
return fn();
}
};
obj.getAge(); // 25
如果使用箭頭函數(shù),以前的那種hack
寫(xiě)法:
var that = this;
就不再需要了瓢谢。
由于this
在箭頭函數(shù)中已經(jīng)按照詞法作用域綁定了畸写,
所以,用call()
或者apply()
調(diào)用箭頭函數(shù)時(shí)氓扛,
無(wú)法對(duì)this進(jìn)行綁定枯芬,即傳入的第一個(gè)參數(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ù)類(lèi)型。
一個(gè)generator
看上去像一個(gè)函數(shù)采郎,但可以返回多次千所。
我們先復(fù)習(xí)函數(shù)的概念。一個(gè)函數(shù)是一段完整的代碼尉剩,調(diào)用一個(gè)函數(shù)就是傳入?yún)?shù)真慢,然后返回結(jié)果:
function foo(x) {
return x + x;
}
var r = foo(1); // 調(diào)用foo函數(shù)
函數(shù)在執(zhí)行過(guò)程中毅臊,如果沒(méi)有遇到return
語(yǔ)句
(函數(shù)末尾如果沒(méi)有return
理茎,就是隱含的return undefined;
),
控制權(quán)無(wú)法交回被調(diào)用的代碼管嬉。
generator
跟函數(shù)很像皂林,定義如下:
function* foo(x) {
yield x + 1;
yield x + 2;
return x + 3;
}
generator
和函數(shù)不同的是,generator
由function*
定義(注意多出的*
號(hào))蚯撩,
并且础倍,除了return
語(yǔ)句,還可以用yield
返回多次胎挎。
大多數(shù)同學(xué)立刻就暈了沟启,generator
就是能夠返回多次的“函數(shù)”?返回多次有啥用犹菇?
還是舉個(gè)栗子吧德迹。
我們以一個(gè)著名的斐波那契數(shù)列為例,它由0揭芍,1開(kāi)頭:
0 1 1 2 3 5 8 13 21 34 ...
要編寫(xiě)一個(gè)產(chǎn)生斐波那契數(shù)列的函數(shù)胳搞,可以這么寫(xiě):
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;
}
// 測(cè)試:
fib(5); // [0, 1, 1, 2, 3]
fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
函數(shù)只能返回一次,所以必須返回一個(gè)Array
。
但是肌毅,如果換成generator
筷转,就可以一次返回一個(gè)數(shù),
不斷返回多次悬而。用generator
改寫(xiě)如下:
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;
}
直接調(diào)用試試:
fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}
直接調(diào)用一個(gè)generator
和調(diào)用函數(shù)不一樣呜舒,
fib(5)
僅僅是創(chuàng)建了一個(gè)generator
對(duì)象,還沒(méi)有去執(zhí)行它摊滔。
調(diào)用generator
對(duì)象有兩個(gè)方法阴绢,一是不斷地調(diào)用generator
對(duì)象的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()
方法會(huì)執(zhí)行generator
的代碼,
然后艰躺,每次遇到yield x;
就返回一個(gè)對(duì)象{value: x, done: true/false}
呻袭,然后“暫停”腺兴。
返回的value
就是yield
的返回值左电,done
表示這個(gè)generator
是否已經(jīng)執(zhí)行結(jié)束了。
如果done
為true
页响,則value
就是return
的返回值篓足。
當(dāng)執(zhí)行到done
為true
時(shí),這個(gè)generator
對(duì)象就已經(jīng)全部執(zhí)行完畢闰蚕,
不要再繼續(xù)調(diào)用next()
了栈拖。
第二個(gè)方法是直接用for ... of
循環(huán)迭代generator
對(duì)象,這種方式不需要我們自己判斷done
:
for (var x of fib(5)) {
console.log(x); // 依次輸出0, 1, 1, 2, 3
}
generator
和普通函數(shù)相比没陡,有什么用涩哟?
因?yàn)?code>generator可以在執(zhí)行過(guò)程中多次返回,所以它看上去就像一個(gè)可以記住執(zhí)行狀態(tài)的函數(shù)盼玄,
利用這一點(diǎn)贴彼,寫(xiě)一個(gè)generator
就可以實(shí)現(xiàn)需要用面向?qū)ο蟛拍軐?shí)現(xiàn)的功能。
例如埃儿,用一個(gè)對(duì)象來(lái)保存狀態(tài)器仗,得這么寫(xiě):
var fib = {
a: 0,
b: 1,
n: 0,
max: 5,
next: function () {
var
r = this.a,
t = this.a + this.b;
this.a = this.b;
this.b = t;
if (this.n < this.max) {
this.n ++;
return r;
} else {
return undefined;
}
}
};
用對(duì)象的屬性來(lái)保存狀態(tài),相當(dāng)繁瑣童番。
generator
還有另一個(gè)巨大的好處精钮,就是把異步回調(diào)代碼變成“同步”代碼。
這個(gè)好處要等到后面學(xué)了AJAX以后才能體會(huì)到剃斧。
沒(méi)有generator
之前的黑暗時(shí)代轨香,用AJAX時(shí)需要這么寫(xiě)代碼:
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);
});
});
});
回調(diào)越多,代碼越難看悯衬。
有了generator
的美好時(shí)代弹沽,用AJAX時(shí)可以這么寫(xiě):
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í)行是異步的。