1. 程式人生 > >JavaScript入門03函式

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);
}

為了獲取除了已定義引數ab之外的引數,我們不得不用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引數只能寫在最後,前面用...標識,從執行結果可知,傳入的引數先繫結ab,多餘的引數以陣列形式交給變數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;
}

常量

由於varlet申明的是變數,如果要申明一個常量,在ES6之前是不行的,我們通常用全部大寫的變數來表示“這是一個常量,不要修改它的值”:

var PI = 3.14;

ES6標準引入了新的關鍵字const來定義常量,constlet都具有塊級作用域:

'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});

使用場景

解構賦值在很多時候可以大大簡化程式碼。例如,交換兩個變數xy的值,可以這麼寫,不再需要臨時變數:

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);
}

它的方便之處在於傳入的物件只需要yearmonthday這三個屬性:

buildDate({ year: 2017, month: 1, day: 1 });
// Sun Jan 01 2017 00:00:00 GMT+0800 (CST)

也可以傳入hourminutesecond屬性:

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可以拿到xiaomingbirth屬性。

讓我們拆開寫:

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指向undefinedwindow,不過,我們還是可以控制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)時,引數xyf分別接收-56和函式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中,我們呼叫Arraymap()方法,傳入我們自己的函式,就得到了一個新的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()類似,Arrayfilter()也接收一個函式。和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

排序演算法

排序也是在程式中經常用到的演算法。無論使用氣泡排序還是快速排序,排序的核心是比較兩個元素的大小。如果是數字,我們可以直接比較,但如果是字串或者兩個物件呢?直接比較數學上的大小是沒有意義的,因此,比較的過程必須通過函式抽象出來。通常規定,對於兩個元素xy,如果認為x < y,則返回-1,如果認為x == y,則返回0,如果認為x > y,則返回1,這樣,排序演算法就不用關心具體的比較過程,而是根據比較結果直接排序。

JavaScript的Arraysort()方法就是用於排序的,但是排序結果可能讓你大吃一驚:

// 看上去正常的結果:
['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碼在大寫字母之後。

第三個排序結果是什麼鬼?簡單的數字排序都能錯?

這是因為Arraysort()方法預設把所有元素先轉換為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()結果應該是149,但實際結果是:

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,我們可以利用閉包建立新的函式pow2pow3

'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就是能夠返回多次的“函式”?返回多次有啥用?

還是舉個栗子吧。

我們以一個著名的斐波那契數列為例,它由01開頭:

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是否已經執行結束了。如果donetrue,則value就是return的返回值。

當執行到donetrue時,這個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);
}

看上去是同步的程式碼,實際執行是非同步的。