JavaScript入門03函式
函式定義和呼叫
定義函式
在JavaScript中,定義函式的方式如下:
function abs(x) {
if (x >= 0) {
return x;
} else {
return -x;
}
}
上述abs()
函式的定義如下:
function
指出這是一個函式定義;abs
是函式的名稱;(x)
括號內列出函式的引數,多個引數以,
分隔;{ ... }
之間的程式碼是函式體,可以包含若干語句,甚至可以沒有任何語句。
請注意,函式體內部的語句在執行時,一旦執行到return
時,函式就執行完畢,並將結果返回。因此,函式內部通過條件判斷和迴圈可以實現非常複雜的邏輯。
如果沒有return
語句,函式執行完畢後也會返回結果,只是結果為undefined
。
由於JavaScript的函式也是一個物件,上述定義的abs()
函式實際上是一個函式物件,而函式名abs
可以視為指向該函式的變數。
因此,第二種定義函式的方式如下:
var abs = function (x) {
if (x >= 0) {
return x;
} else {
return -x;
}
};
在這種方式下,function (x) { ... }
是一個匿名函式,它沒有函式名。但是,這個匿名函式賦值給了變數abs
,所以,通過變數abs
上述兩種定義完全等價,注意第二種方式按照完整語法需要在函式體末尾加一個;
,表示賦值語句結束。
呼叫函式
呼叫函式時,按順序傳入引數即可:
abs(10); // 返回10
abs(-9); // 返回9
由於JavaScript允許傳入任意個引數而不影響呼叫,因此傳入的引數比定義的引數多也沒有問題,雖然函式內部並不需要這些引數:
abs(10, 'blablabla'); // 返回10
abs(-9, 'haha', 'hehe', null); // 返回9
傳入的引數比定義的少也沒有問題:
abs(); // 返回NaN
此時abs(x)
函式的引數x
將收到undefined
NaN
。
要避免收到undefined
,可以對引數進行檢查:
function abs(x) {
if (typeof x !== 'number') {
throw 'Not a number';
}
if (x >= 0) {
return x;
} else {
return -x;
}
}
arguments
JavaScript還有一個免費贈送的關鍵字arguments
,它只在函式內部起作用,並且永遠指向當前函式的呼叫者傳入的所有引數。
arguments
類似Array
但它不是一個Array
:
'use strict'
function foo(x) { console.log('x = ' + x); // 10 for (var i=0; i<arguments.length; i++) { console.log('arg ' + i + ' = ' + arguments[i]); // 10, 20, 30 } } foo(10, 20, 30);
x = 10 arg 0 = 10 arg 1 = 20 arg 2 = 30
利用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
實際上arguments
最常用於判斷傳入引數的個數。你可能會看到這樣的寫法:
// 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
判斷,然後重新調整引數並賦值。
rest引數
由於JavaScript函式允許接收任意個引數,於是我們就不得不用arguments
來獲取所有引數:
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);
}
為了獲取除了已定義引數a
、b
之外的引數,我們不得不用arguments
,並且迴圈要從索引2
開始以便排除前兩個引數,這種寫法很彆扭,只是為了獲得額外的rest
引數,有沒有更好的方法?
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引數只能寫在最後,前面用...
標識,從執行結果可知,傳入的引數先繫結a
、b
,多餘的引數以陣列形式交給變數rest
,所以,不再需要arguments
我們就獲取了全部引數。
如果傳入的引數連正常定義的引數都沒填滿,也不要緊,rest引數會接收一個空陣列(注意不是undefined
)。
因為rest引數是ES6新標準,所以你需要測試一下瀏覽器是否支援。請用rest引數編寫一個sum()
函式,接收任意個引數並返回它們的和:
由於JavaScript的函式可以巢狀,此時,內部函式可以訪問外部函式定義的變數,反過來則不行:
'use strict';
function foo() {
var x = 1;
function bar() {
var y = x + 1; // bar可以訪問foo的變數x!
}
var z = y + 1; // ReferenceError! foo不可以訪問bar的變數y!
}
如果內部函式和外部函式的變數名重名怎麼辦?來測試一下:
'use strict';
function foo() { var x = 1; function bar() { var x = 'A'; console.log('x in bar() = ' + x); // 'A' } console.log('x in foo() = ' + x); // 1 bar(); }
foo();
x in foo() = 1 x in bar() = A
這說明JavaScript的函式在查詢變數時從自身函式定義開始,從“內”向“外”查詢。如果內部函式定義了與外部函式重名的變數,則內部函式的變數將“遮蔽”外部函式的變數。
變數提升
JavaScript的函式定義有個特點,它會先掃描整個函式體的語句,把所有申明的變數“提升”到函式頂部:
'use strict';
function foo() {
var x = 'Hello, ' + y;
console.log(x);
var y = 'Bob';
}
foo();
雖然是strict模式,但語句var x = 'Hello, ' + y;
並不報錯,原因是變數y
在稍後申明瞭。但是console.log
顯示Hello, undefined
,說明變數y
的值為undefined
。這正是因為JavaScript引擎自動提升了變數y
的宣告,但不會提升變數y
的賦值。
對於上述foo()
函式,JavaScript引擎看到的程式碼相當於:
function foo() {
var y; // 提升變數y的申明,此時y為undefined
var x = 'Hello, ' + y;
console.log(x);
y = 'Bob';
}
由於JavaScript的這一怪異的“特性”,我們在函式內部定義變數時,請嚴格遵守“在函式內部首先申明所有變數”這一規則。最常見的做法是用一個var
申明函式內部用到的所有變數:
function foo() {
var
x = 1, // x初始化為1
y = x + 1, // y初始化為2
z, i; // z和i為undefined
// 其他語句:
for (i=0; i<100; i++) {
...
}
}
全域性作用域
不在任何函式內定義的變數就具有全域性作用域。實際上,JavaScript預設有一個全域性物件window
,全域性作用域的變數實際上被繫結到window
的一個屬性:
'use strict';
var course = 'Learn JavaScript';
alert(course); // 'Learn JavaScript'
alert(window.course); // 'Learn JavaScript'
因此,直接訪問全域性變數course
和訪問window.course
是完全一樣的。
你可能猜到了,由於函式定義有兩種方式,以變數方式var foo = function () {}
定義的函式實際上也是一個全域性變數,因此,頂層函式的定義也被視為一個全域性變數,並繫結到window
物件:
'use strict';
function foo() {
alert('foo');
}
foo(); // 直接呼叫foo()
window.foo(); // 通過window.foo()呼叫
進一步大膽地猜測,我們每次直接呼叫的alert()
函式其實也是window
的一個變數:
這說明JavaScript實際上只有一個全域性作用域。任何變數(函式也視為變數),如果沒有在當前函式作用域中找到,就會繼續往上查詢,最後如果在全域性作用域中也沒有找到,則報ReferenceError
錯誤。
名字空間
全域性變數會繫結到window
上,不同的JavaScript檔案如果使用了相同的全域性變數,或者定義了相同名字的頂層函式,都會造成命名衝突,並且很難被發現。
減少衝突的一個方法是把自己的所有變數和函式全部繫結到一個全域性變數中。例如:
// 唯一的全域性變數MYAPP:
var MYAPP = {};
// 其他變數:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;
// 其他函式:
MYAPP.foo = function () {
return 'foo';
};
把自己的程式碼全部放入唯一的名字空間MYAPP
中,會大大減少全域性變數衝突的可能。
許多著名的JavaScript庫都是這麼幹的:jQuery,YUI,underscore等等。
區域性作用域
由於JavaScript的變數作用域實際上是函式內部,我們在for
迴圈等語句塊中是無法定義具有區域性作用域的變數的:
'use strict';
function foo() {
for (var i=0; i<100; i++) {
//
}
i += 100; // 仍然可以引用變數i
}
為了解決塊級作用域,ES6引入了新的關鍵字let
,用let
替代var
可以申明一個塊級作用域的變數:
'use strict';
function foo() {
var sum = 0;
for (let i=0; i<100; i++) {
sum += i;
}
// SyntaxError:
i += 1;
}
常量
由於var
和let
申明的是變數,如果要申明一個常量,在ES6之前是不行的,我們通常用全部大寫的變數來表示“這是一個常量,不要修改它的值”:
var PI = 3.14;
ES6標準引入了新的關鍵字const
來定義常量,const
與let
都具有塊級作用域:
'use strict';
const PI = 3.14;
PI = 3; // 某些瀏覽器不報錯,但是無效果!
PI; // 3.14
解構賦值
從ES6開始,JavaScript引入瞭解構賦值,可以同時對一組變數進行賦值。
什麼是解構賦值?我們先看看傳統的做法,如何把一個數組的元素分別賦值給幾個變數:
var array = ['hello', 'JavaScript', 'ES6'];
var x = array[0];
var y = array[1];
var z = array[2];
現在,在ES6中,可以使用解構賦值,直接對多個變數同時賦值:
/ 如果瀏覽器支援解構賦值就不會報錯: var [x, y, z] = ['hello', 'JavaScript', 'ES6'];
注意,對陣列元素進行解構賦值時,多個變數要用[...]
括起來。
如果陣列本身還有巢狀,也可以通過下面的形式進行解構賦值,注意巢狀層次和位置要保持一致:
let [x, [y, z]] = ['hello', ['JavaScript', 'ES6']];
x; // 'hello'
y; // 'JavaScript'
z; // 'ES6'
解構賦值還可以忽略某些元素:
let [, , z] = ['hello', 'JavaScript', 'ES6']; // 忽略前兩個元素,只對z賦值第三個元素
z; // 'ES6'
如果需要從一個物件中取出若干屬性,也可以使用解構賦值,便於快速獲取物件的指定屬性:
use strict'; var person = { name: '小明', age: 20, gender: 'male', passport: 'G-12345678', school: 'No.4 middle school' }; var {name, age, passport} = person;
對一個物件進行解構賦值時,同樣可以直接對巢狀的物件屬性進行賦值,只要保證對應的層次是一致的:
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school',
address: {
city: 'Beijing',
street: 'No.1 Road',
zipcode: '100001'
}
};
var {name, address: {city, zip}} = person;
name; // '小明'
city; // 'Beijing'
zip; // undefined, 因為屬性名是zipcode而不是zip
// 注意: address不是變數,而是為了讓city和zip獲得巢狀的address物件的屬性:
address; // Uncaught ReferenceError: address is not defined
使用解構賦值對物件屬性進行賦值時,如果對應的屬性不存在,變數將被賦值為undefined
,這和引用一個不存在的屬性獲得undefined
是一致的。如果要使用的變數名和屬性名不一致,可以用下面的語法獲取:
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school'
};
// 把passport屬性賦值給變數id:
let {name, passport:id} = person;
name; // '小明'
id; // 'G-12345678'
// 注意: passport不是變數,而是為了讓變數id獲得passport屬性:
passport; // Uncaught ReferenceError: passport is not defined
解構賦值還可以使用預設值,這樣就避免了不存在的屬性返回undefined
的問題:
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678'
};
// 如果person物件沒有single屬性,預設賦值為true:
var {name, single=true} = person;
name; // '小明'
single; // true
有些時候,如果變數已經被聲明瞭,再次賦值的時候,正確的寫法也會報語法錯誤:
// 宣告變數:
var x, y;
// 解構賦值:
{x, y} = { name: '小明', x: 100, y: 200};
// 語法錯誤: Uncaught SyntaxError: Unexpected token =
這是因為JavaScript引擎把{
開頭的語句當作了塊處理,於是=
不再合法。解決方法是用小括號括起來:
({x, y} = { name: '小明', x: 100, y: 200});
使用場景
解構賦值在很多時候可以大大簡化程式碼。例如,交換兩個變數x
和y
的值,可以這麼寫,不再需要臨時變數:
var x=1, y=2;
[x, y] = [y, x]
快速獲取當前頁面的域名和路徑:
var {hostname:domain, pathname:path} = location;
如果一個函式接收一個物件作為引數,那麼,可以使用解構直接把物件的屬性繫結到變數中。例如,下面的函式可以快速建立一個Date
物件:
function buildDate({year, month, day, hour=0, minute=0, second=0}) {
return new Date(year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second);
}
它的方便之處在於傳入的物件只需要year
、month
和day
這三個屬性:
buildDate({ year: 2017, month: 1, day: 1 });
// Sun Jan 01 2017 00:00:00 GMT+0800 (CST)
也可以傳入hour
、minute
和second
屬性:
buildDate({ year: 2017, month: 1, day: 1, hour: 20, minute: 15 });
// Sun Jan 01 2017 20:15:00 GMT+0800 (CST)
使用解構賦值可以減少程式碼量,但是,需要在支援ES6解構賦值特性的現代瀏覽器中才能正常執行。目前支援解構賦值的瀏覽器包括Chrome,Firefox,Edge等。
方法
在一個物件中繫結函式,稱為這個物件的方法。
在JavaScript中,物件的定義是這樣的:
var xiaoming = {
name: '小明',
birth: 1990
};
但是,如果我們給xiaoming
繫結一個函式,就可以做更多的事情。比如,寫個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(); // 今年呼叫是25,明年呼叫就變成26了
繫結到物件上的函式稱為方法,和普通函式也沒啥區別,但是它在內部使用了一個this
關鍵字,這個東東是什麼?
在一個方法內部,this
是一個特殊變數,它始終指向當前物件,也就是xiaoming
這個變數。所以,this.birth
可以拿到xiaoming
的birth
屬性。
讓我們拆開寫:
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25, 正常結果
getAge(); // NaN
單獨呼叫函式getAge()
怎麼返回了NaN
?請注意,我們已經進入到了JavaScript的一個大坑裡。
JavaScript的函式內部如果呼叫了this
,那麼這個this
到底指向誰?
答案是,視情況而定!
如果以物件的方法形式呼叫,比如xiaoming.age()
,該函式的this
指向被呼叫的物件,也就是xiaoming
,這是符合我們預期的。
如果單獨呼叫函式,比如getAge()
,此時,該函式的this
指向全域性物件,也就是window
。
坑爹啊!
更坑爹的是,如果這麼寫:
var fn = xiaoming.age; // 先拿到xiaoming的age函式
fn(); // NaN
也是不行的!要保證this
指向正確,必須用obj.xxx()
的形式呼叫!
由於這是一個巨大的設計錯誤,要想糾正可沒那麼簡單。ECMA決定,在strict模式下讓函式的this
指向undefined
,因此,在strict模式下,你會得到一個錯誤:
'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
這個決定只是讓錯誤及時暴露出來,並沒有解決this
應該指向的正確位置。
有些時候,喜歡重構的你把方法重構了一下:
'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
結果又報錯了!原因是this
指標只在age
方法的函式內指向xiaoming
,在函式內部定義的函式,this
又指向undefined
了!(在非strict模式下,它重新指向全域性物件window
!)
修復的辦法也不是沒有,我們用一個that
變數首先捕獲this
:
'use strict';
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
用var that = this;
,你就可以放心地在方法內部定義其他函式,而不是把所有語句都堆到一個方法中。
apply
雖然在一個獨立的函式呼叫中,根據是否是strict模式,this
指向undefined
或window
,不過,我們還是可以控制this
的指向的!
要指定函式的this
指向哪個物件,可以用函式本身的apply
方法,它接收兩個引數,第一個引數就是需要繫結的this
變數,第二個引數是Array
,表示函式本身的引數。
用apply
修復getAge()
呼叫:
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()
實現如下:
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5
對普通函式呼叫,我們通常把this
繫結為null
。
裝飾器
利用apply()
,我們還可以動態改變函式的行為。
JavaScript的所有物件都是動態的,即使內建的函式,我們也可以重新指向新的函式。
現在假定我們想統計一下程式碼一共呼叫了多少次parseInt()
,可以把所有的呼叫都找出來,然後手動加上count += 1
,不過這樣做太傻了。最佳方案是用我們自己的函式替換掉預設的parseInt()
:
'use strict'; var count = 0; var oldParseInt = parseInt; // 儲存原函式 window.parseInt = function () { count += 1; return oldParseInt.apply(null, arguments); // 呼叫原函式 };
高階函式
高階函式英文叫Higher-order function。那麼什麼是高階函式?
JavaScript的函式其實都指向某個變數。既然變數可以指向函式,函式的引數能接收變數,那麼一個函式就可以接收另一個函式作為引數,這種函式就稱之為高階函式。
一個最簡單的高階函式:
function add(x, y, f) {
return f(x) + f(y);
}
當我們呼叫add(-5, 6, Math.abs)
時,引數x
,y
和f
分別接收-5
,6
和函式Math.abs
,根據函式定義,我們可以推導計算過程為:
x = -5;
y = 6;
f = Math.abs;
f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11;
return 11;
map
舉例說明,比如我們有一個函式f(x)=x2,要把這個函式作用在一個數組[1, 2, 3, 4, 5, 6, 7, 8, 9]
上,就可以用map
實現如下:
由於map()
方法定義在JavaScript的Array
中,我們呼叫Array
的map()
方法,傳入我們自己的函式,就得到了一個新的Array
作為結果:
'use strict'; function pow(x) { return x * x; }
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]; var results = arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81] console.log(results);
注意:map()
傳入的引數是pow
,即函式物件本身。
你可能會想,不需要map()
,寫一個迴圈,也可以計算出結果:
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]));
}
的確可以,但是,從上面的迴圈程式碼,我們無法一眼看明白“把f(x)作用在Array的每一個元素並把結果生成一個新的Array”。
所以,map()
作為高階函式,事實上它把運算規則抽象了,因此,我們不但可以計算簡單的f(x)=x2,還可以計算任意複雜的函式,比如,把Array
的所有數字轉為字串:
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()
把一個函式作用在這個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
要把[1, 3, 5, 7, 9]
變換成整數13579,reduce()
也能派上用場:
var arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
return x * 10 + y;
}); // 13579
如果我們繼續改進這個例子,想辦法把一個字串13579
先變成Array
——[1, 3, 5, 7, 9]
,再利用reduce()
就可以寫出一個把字串轉換為Number的函式。
filter
閱讀: 98678
filter也是一個常用的操作,它用於把Array
的某些元素過濾掉,然後返回剩下的元素。
和map()
類似,Array
的filter()
也接收一個函式。和map()
不同的是,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]
把一個Array
中的空字串刪掉,可以這麼寫:
var arr = ['A', '', 'B', null, undefined, 'C', ' '];
var r = arr.filter(function (s) {
return s && s.trim(); // 注意:IE9以下的版本沒有trim()方法
});
r; // ['A', 'B', 'C']
可見用filter()
這個高階函式,關鍵在於正確實現一個“篩選”函式。
回撥函式
filter()
接收的回撥函式,其實可以有多個引數。通常我們僅使用第一個引數,表示Array
的某個元素。回撥函式還可以接收另外兩個引數,表示元素的位置和陣列本身:
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;
});
利用filter
,可以巧妙地去除Array
的重複元素:
'use strict'; var r, arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry'];
r = arr.filter(function (element, index, self) { return self.indexOf(element) === index; });
sort
閱讀: 73734
排序演算法
排序也是在程式中經常用到的演算法。無論使用氣泡排序還是快速排序,排序的核心是比較兩個元素的大小。如果是數字,我們可以直接比較,但如果是字串或者兩個物件呢?直接比較數學上的大小是沒有意義的,因此,比較的過程必須通過函式抽象出來。通常規定,對於兩個元素x
和y
,如果認為x < y
,則返回-1
,如果認為x == y
,則返回0
,如果認為x > y
,則返回1
,這樣,排序演算法就不用關心具體的比較過程,而是根據比較結果直接排序。
JavaScript的Array
的sort()
方法就是用於排序的,但是排序結果可能讓你大吃一驚:
// 看上去正常的結果:
['Google', 'Apple', 'Microsoft'].sort(); // ['Apple', 'Google', 'Microsoft'];
// apple排在了最後:
['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple']
// 無法理解的結果:
[10, 20, 1, 2].sort(); // [1, 10, 2, 20]
第二個排序把apple
排在了最後,是因為字串根據ASCII碼進行排序,而小寫字母a
的ASCII碼在大寫字母之後。
第三個排序結果是什麼鬼?簡單的數字排序都能錯?
這是因為Array
的sort()
方法預設把所有元素先轉換為String再排序,結果'10'
排在了'2'
的前面,因為字元'1'
比字元'2'
的ASCII碼小。
如果不知道sort()
方法的預設排序規則,直接對數字排序,絕對栽進坑裡!
幸運的是,sort()
方法也是一個高階函式,它還可以接收一個比較函式來實現自定義的排序。
要按數字大小排序,我們可以這麼寫:
'use strict'; var arr = [10, 20, 1, 2];
arr.sort(function (x, y) { if (x < y) { return -1; } if (x > y) { return 1; } return 0; }); console.log(arr); // [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]
預設情況下,對字串排序,是按照ASCII的大小比較的,現在,我們提出排序應該忽略大小寫,按照字母序排序。要實現這個演算法,不必對現有程式碼大加改動,只要我們能定義出忽略大小寫的比較演算法就可以:
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']
忽略大小寫來比較兩個字串,實際上就是先把字串都變成大寫(或者都變成小寫),再比較。
從上述例子可以看出,高階函式的抽象能力是非常強大的,而且,核心程式碼可以保持得非常簡潔。
最後友情提示,sort()
方法會直接對Array
進行修改,它返回的結果仍是當前Array
:
var a1 = ['B', 'A', 'C'];
var a2 = a1.sort();
a1; // ['A', 'B', 'C']
a2; // ['A', 'B', 'C']
a1 === a2; // true, a1和a2是同一物件
閉包
閱讀: 182509
函式作為返回值
高階函式除了可以接受函式作為引數外,還可以把函式作為結果值返回。
我們來實現一個對Array
的求和。通常情況下,求和的函式是這樣定義的:
function sum(arr) {
return arr.reduce(function (x, y) {
return x + y;
});
}
sum([1, 2, 3, 4, 5]); // 15
但是,如果不需要立刻求和,而是在後面的程式碼中,根據需要再計算怎麼辦?可以不返回求和的結果,而是返回求和的函式!
function lazy_sum(arr) {
var sum = function () {
return arr.reduce(function (x, y) {
return x + y;
});
}
return sum;
}
當我們呼叫lazy_sum()
時,返回的並不是求和結果,而是求和函式:
var f = lazy_sum([1, 2, 3, 4, 5]); // function sum()
呼叫函式f
時,才真正計算求和的結果:
f(); // 15
在這個例子中,我們在函式lazy_sum
中又定義了函式sum
,並且,內部函式sum
可以引用外部函式lazy_sum
的引數和區域性變數,當lazy_sum
返回函式sum
時,相關引數和變數都儲存在返回的函式中,這種稱為“閉包(Closure)”的程式結構擁有極大的威力。
請再注意一點,當我們呼叫lazy_sum()
時,每次呼叫都會返回一個新的函式,即使傳入相同的引數:
var f1 = lazy_sum([1, 2, 3, 4, 5]);
var f2 = lazy_sum([1, 2, 3, 4, 5]);
f1 === f2; // false
f1()
和f2()
的呼叫結果互不影響。
閉包
注意到返回的函式在其定義內部引用了局部變數arr
,所以,當一個函式返回了一個函式後,其內部的區域性變數還被新函式引用,所以,閉包用起來簡單,實現起來可不容易。
另一個需要注意的問題是,返回的函式並沒有立刻執行,而是直到呼叫了f()
才執行。我們來看一個例子:
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];
在上面的例子中,每次迴圈,都建立了一個新的函式,然後,把建立的3個函式都新增到一個Array
中返回了。
你可能認為呼叫f1()
,f2()
和f3()
結果應該是1
,4
,9
,但實際結果是:
f1(); // 16
f2(); // 16
f3(); // 16
全部都是16
!原因就在於返回的函式引用了變數i
,但它並非立刻執行。等到3個函式都返回時,它們所引用的變數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
注意這裡用了一個“建立一個匿名函式並立刻執行”的語法:
(function (x) {
return x * x;
})(3); // 9
理論上講,建立一個匿名函式並立刻執行可以這麼寫:
function (x) { return x * x } (3);
但是由於JavaScript語法解析的問題,會報SyntaxError錯誤,因此需要用括號把整個函式定義括起來:
(function (x) { return x * x }) (3);
通常,一個立即執行的匿名函式可以把函式體拆開,一般這麼寫:
(function (x) {
return x * x;
})(3);
說了這麼多,難道閉包就是為了返回一個函式然後延遲執行嗎?
當然不是!閉包有非常強大的功能。舉個栗子:
在面向物件的程式設計語言裡,比如Java和C++,要在物件內部封裝一個私有變數,可以用private
修飾一個成員變數。
在沒有class
機制,只有函式的語言裡,藉助閉包,同樣可以封裝一個私有變數。我們用JavaScript建立一個計數器:
'use strict';
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
。換句話說,閉包就是攜帶狀態的函式,並且它的狀態可以完全對外隱藏起來。
閉包還可以把多引數的函式變成單引數的函式。例如,要計算xy可以用Math.pow(x, y)
函式,不過考慮到經常計算x2或x3,我們可以利用閉包建立新的函式pow2
和pow3
:
'use strict'; function make_pow(n) { return function (x) { return Math.pow(x, n); } }
箭頭函式 類似lamda
ES6標準新增了一種新的函式:Arrow Function(箭頭函式)。
為什麼叫Arrow Function?因為它的定義用的就是一個箭頭:
x => x * x
上面的箭頭函式相當於:
function (x) {
return x * x;
}
箭頭函式相當於匿名函式,並且簡化了函式定義。箭頭函式有兩種格式,一種像上面的,只包含一個表示式,連{ ... }
和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;
}
如果要返回一個物件,就要注意,如果是單表示式,這麼寫的話會報錯:
// SyntaxError:
x => { foo: x }
因為和函式體的{ ... }
有語法衝突,所以要改為:
// ok:
x => ({ foo: x })
this
箭頭函式看上去是匿名函式的一種簡寫,但實際上,箭頭函式和匿名函式有個明顯的區別:箭頭函式內部的this
是詞法作用域,由上下文確定。
回顧前面的例子,由於JavaScript函式對this
繫結的錯誤處理,下面的例子無法得到預期結果:
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();
}
};
現在,箭頭函式完全修復了this
的指向,this
總是指向詞法作用域,也就是外層呼叫者obj
:
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
如果使用箭頭函式,以前的那種hack寫法:
var that = this;
就不再需要了。
由於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
閱讀: 91002
generator(生成器)是ES6標準引入的新的資料型別。一個generator看上去像一個函式,但可以返回多次。
ES6定義generator標準的哥們借鑑了Python的generator的概念和語法,如果你對Python的generator很熟悉,那麼ES6的generator就是小菜一碟了。如果你對Python還不熟,趕快惡補Python教程!。
我們先複習函式的概念。一個函式是一段完整的程式碼,呼叫一個函式就是傳入引數,然後返回結果:
function foo(x) {
return x + x;
}
var r = foo(1); // 呼叫foo函式
函式在執行過程中,如果沒有遇到return
語句(函式末尾如果沒有return
,就是隱含的return undefined;
),控制權無法交回被呼叫的程式碼。
generator跟函式很像,定義如下:
function* foo(x) {
yield x + 1;
yield x + 2;
return x + 3;
}
generator和函式不同的是,generator由function*
定義(注意多出的*
號),並且,除了return
語句,還可以用yield
返回多次。
大多數同學立刻就暈了,generator就是能夠返回多次的“函式”?返回多次有啥用?
還是舉個栗子吧。
我們以一個著名的斐波那契數列為例,它由0
,1
開頭:
0 1 1 2 3 5 8 13 21 34 ...
要編寫一個產生斐波那契數列的函式,可以這麼寫:
function fib(max) {
var
t,
a = 0,
b = 1,
arr = [0, 1];
while (arr.length < max) {
[a, b] = [b, a + b];
arr.push(b);
}
return arr;
}
// 測試:
fib(5); // [0, 1, 1, 2, 3]
fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
函式只能返回一次,所以必須返回一個Array
。但是,如果換成generator,就可以一次返回一個數,不斷返回多次。用generator改寫如下:
function* fib(max) {
var
t,
a = 0,
b = 1,
n = 0;
while (n < max) {
yield a;
[a, b] = [b, a + b];
n ++;
}
return;
}
直接呼叫試試:
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: false}
f.next(); // {value: undefined, done: true}
next()
方法會執行generator的程式碼,然後,每次遇到yield x;
就返回一個物件{value: x, done: true/false}
,然後“暫停”。返回的value
就是yield
的返回值,done
表示這個generator是否已經執行結束了。如果done
為true
,則value
就是return
的返回值。
當執行到done
為true
時,這個generator物件就已經全部執行完畢,不要再繼續呼叫next()
了。
第二個方法是直接用for ... of
迴圈迭代generator物件,這種方式不需要我們自己判斷done
:
'use strict' function* fib(max) { var t, a = 0, b = 1, n = 0; while (n < max) { yield a; [a, b] = [b, a + b]; n ++; } return; }
generator和普通函式相比,有什麼用?
因為generator可以在執行過程中多次返回,所以它看上去就像一個可以記住執行狀態的函式,利用這一點,寫一個generator就可以實現需要用面向物件才能實現的功能。例如,用一個物件來儲存狀態,得這麼寫:
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;
}
}
};
用物件的屬性來儲存狀態,相當繁瑣。
generator還有另一個巨大的好處,就是把非同步回撥程式碼變成“同步”程式碼。這個好處要等到後面學了AJAX以後才能體會到。
沒有generator之前的黑暗時代,用AJAX時需要這麼寫程式碼:
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);
});
});
});
回撥越多,程式碼越難看。
有了generator的美好時代,用AJAX時可以這麼寫:
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);
}
看上去是同步的程式碼,實際執行是非同步的。