溫習JS及相關常用的ES6新語法(一)
Map和Set
JavaScript的預設物件表示方式{}可以視為其他語言中的Map或Dictionary的資料結構,即一組鍵值對。
但是JavaScript的物件有個小問題,就是鍵必須是字串。但實際上Number或者其他資料型別作為鍵也是非常合理的。
為了解決這個問題,最新的ES6規範引入了新的資料型別Map。
Map
例子:
var m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);
m.get('Michael'); // 95
初始化Map需要一個二維陣列,或者直接初始化一個空Map。Map具有以下方法:
var m = new Map(); // 空Map
m.set('Adam', 67); // 新增新的key-value
m.set('Bob', 59);
m.has('Adam'); // 是否存在key 'Adam': true
m.get('Adam'); // 67
m.delete('Adam'); // 刪除key 'Adam'
m.get('Adam'); // undefined
由於一個key只能對應一個value,所以,多次對一個key放入value,後面的值會把前面的值沖掉:
var m = new Map();
m.set('Adam', 67);
m.set('Adam' , 88);
m.get('Adam'); // 88
Set
Set和Map類似,也是一組key的集合,但不儲存value。由於key不能重複,所以,在Set中,沒有重複的key。
要建立一個Set,需要提供一個Array作為輸入,或者直接建立一個空Set:
var s1 = new Set(); // 空Set
var s2 = new Set([1, 2, 3]); // 含1, 2, 3
重複元素在Set中自動被過濾:
var s = new Set([1, 2, 3, 3, '3']);
s; // Set {1, 2, 3, "3"}
注意數字3和字串’3’是不同的元素。
通過add(key)方法可以新增元素到Set中,可以重複新增,但不會有效果:
s.add(4);
s; // Set {1, 2, 3, 4}
s.add(4);
s; // 仍然是 Set {1, 2, 3, 4}
通過delete(key)方法可以刪除元素:
var s = new Set([1, 2, 3]);
s; // Set {1, 2, 3}
s.delete(3);
s; // Set {1, 2}
iterable
遍歷Array可以採用下標迴圈,遍歷Map和Set就無法使用下標。為了統一集合型別,ES6標準引入了新的iterable型別,Array、Map和Set都屬於iterable型別。
具有iterable型別的集合可以通過新的for … of迴圈來遍歷。
for … of迴圈是ES6引入的新的語法
用for … of迴圈遍歷集合,用法如下:
var a = ['A', 'B', 'C'];
var s = new Set(['A', 'B', 'C']);
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
for (var x of a) { // 遍歷Array
console.log(x);
}
for (var x of s) { // 遍歷Set
console.log(x);
}
for (var x of m) { // 遍歷Map
console.log(x[0] + '=' + x[1]);
}
for … of迴圈和for … in迴圈有何區別?
for … in迴圈由於歷史遺留問題,它遍歷的實際上是物件的屬性名稱。一個Array陣列實際上也是一個物件,它的每個元素的索引被視為一個屬性。
當我們手動給Array物件添加了額外的屬性後,for … in迴圈將帶來意想不到的意外效果:
var a = ['A', 'B', 'C'];
a.name = 'Hello';
for (var x in a) {
console.log(x); // '0', '1', '2', 'name'
}
for … in迴圈將把name包括在內,但Array的length屬性卻不包括在內。
for … of迴圈則完全修復了這些問題,它只迴圈集合本身的元素:
var a = ['A', 'B', 'C'];
a.name = 'Hello';
for (var x of a) {
console.log(x); // 'A', 'B', 'C'
}
然而,更好的方式是直接使用iterable內建的forEach方法,它接收一個函式,每次迭代就自動回撥該函式。以Array為例
a.forEach(function (element, index, array) {
// element: 指向當前元素的值
// index: 指向當前索引
// array: 指向Array物件本身
console.log(element + ', index = ' + index);
/*
*A, index = 0
*B, index = 1
*C, index = 2
*/
});
Set與Array類似,但Set沒有索引,因此回撥函式的前兩個引數都是元素本身:
var s = new Set(['A', 'B', 'C']);
s.forEach(function (element, sameElement, set) {
console.log(element);
});
Map的回撥函式引數依次為value、key和map本身:
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
m.forEach(function (value, key, map) {
console.log(value);
});
如果對某些引數不感興趣,由於JavaScript的函式呼叫不要求引數必須一致,因此可以忽略它們。例如,只需要獲得Array的element:
var a = ['A', 'B', 'C'];
a.forEach(function (element) {
console.log(element);
});
rest引數
例子:
function abs(x) {
if (x >= 0) {
return x;
} else {
return -x;
}
}
由於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;
}
}
由於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 []
例子:接收任意個引數,返回他們的和
function sum(...rest) {
var sum=0
for(var x of rest) {
sum+=x;
}
return sum;
}
關鍵字let
由於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中,可以使用解構賦值,直接對多個變數同時賦值:
'use strict';
// 如果瀏覽器支援解構賦值就不會報錯:
var [x, y, z] = ['hello', 'JavaScript', 'ES6'];
現在,在ES6中,可以使用解構賦值,直接對多個變數同時賦值:
'use strict';
// 如果瀏覽器支援解構賦值就不會報錯:
var [x, y, z] = ['hello', 'JavaScript', 'ES6'];
// x, y, z分別被賦值為陣列對應元素:
console.log('x = ' + x + ', y = ' + y + ', z = ' + z);
//x = hello, y = JavaScript, z = 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;
// name, age, passport分別被賦值為對應屬性:
console.log('name = ' + name + ', age = ' + age + ', passport = ' + passport);
對一個物件進行解構賦值時,同樣可以直接對巢狀的物件屬性進行賦值,只要保證對應的層次是一致的:
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等。
this指標
例子:
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是一個特殊變數,它始終指向當前物件,也就是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
如果以物件的方法形式呼叫,比如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
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); // 呼叫原函式
};
高階函式
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()作為高階函式,事實上它把運算規則抽象了,因此,我們不但可以計算簡單的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
例子:如果我們繼續改進這個例子,想辦法把一個字串13579先變成Array——[1, 3, 5, 7, 9],再利用reduce()就可以寫出一個把字串轉換為Number的函式。
function string2int(s) {
var arr=[];
for(var i in s) {
console.log(s[i]);
arr[i]=s[i];
}
var s2num = arr.map(function(x) {
return x - 0;
})
return s2num.reduce(function(x, y) {
return x*10 + y;
})
}
例子2:把使用者輸入的不規範的英文名字,變為首字母大寫,其他小寫的規範名字。輸入:[‘adam’, ‘LISA’, ‘barT’],輸出:[‘Adam’, ‘Lisa’, ‘Bart’]。
function normalize(arr) {
return arr.map(function(x){
var x_arr = x.toLowerCase().split('');
for(var i in x_arr){
if(i == 0){
x_arr[i] = x_arr[i].toUpperCase();
}
}
var aaa = x_arr.reduce(function(x,y){
return x+ y;
});
return aaa;
});
}
filter
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()接收的回撥函式,其實可以有多個引數。通常我們僅使用第一個引數,表示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的重複元素:
var r,arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry'];
r = arr.filter(function (element, index, self) {
return self.indexOf(element) === index;
});
sort
排序也是在程式中經常用到的演算法。無論使用氣泡排序還是快速排序,排序的核心是比較兩個元素的大小。如果是數字,我們可以直接比較,但如果是字串或者兩個物件呢?直接比較數學上的大小是沒有意義的,因此,比較的過程必須通過函式抽象出來。通常規定,對於兩個元素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()方法也是一個高階函式,它還可以接收一個比較函式來實現自定義的排序。
要按數字大小排序,我們可以這麼寫:
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]
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是同一物件
閉包
函式作為返回值
高階函式除了可以接受函式作為引數外,還可以把函式作為結果值返回。
我們來實現一個對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()時,每次呼叫都會返回一個新的函式,即使傳入相同的引數
閉包
注意到返回的函式在其定義內部引用了局部變數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 此時才執行陣列中的函式:i*i
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
箭頭函式
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總是指