1. 程式人生 > >溫習JS及相關常用的ES6新語法(一)

溫習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總是指