Javascript學習筆記-函數

JS中定義函數的方式與其他語言一樣沒什么差別,但是要知道JS允許傳入任意個數參數,如果傳入的參數比定義的參數多也沒有問題,函數內部并不會調用這些參數:

function abs(x) {
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
}
abs(10, 'blablabla'); // 返回10
abs(-9, 'haha', 'hehe', null); // 返回9

傳入的參數比定義的少也沒有問題:
abs(); // 返回NaN

JS關鍵字argument在函數內部指向當前函數傳入的所有參數,arguments類似Array但它不是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可以獲得調用者傳入的所有參數,也就是說,即使函數不定義任何參數也可以拿到參數的值,而實際上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

// 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判斷,然后重新調整參數并賦值。
為了獲得額外的參數,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參數寫在最后,前面用...標識,多余的參數以數組形式交給變量rest;如果傳入的參數連正常定義的參數都沒填滿,rest參數會接收一個空數組(注意不是undefined)。

變量作用域

JS的函數可以嵌套,內部函數可以訪問外部函數定義的變量,如果內部函數和外部函數的變量名重名怎么辦?

function foo() {
    var x = 1;
    function bar() {
        var x = 'A';
        alert('x in bar() = ' + x); // 'A'
    }
    alert('x in foo() = ' + x); // 1
    bar();
}

這說明JS的函數在查找變量時從自身函數定義開始,由“內”向“外”查找,如果變量重名則內部變量將“屏蔽”外部變量。
JS的函數在定義時會先掃描整個函數語句,把所有申明的變量“提升”到函數頂部:

function foo() {
    var x = 'Hello, ' + y;
    alert(x);
    var y = 'Bob';
}

foo();

雖然是strict模式,但語句var x = 'Hello, ' + y;并不報錯,原因是變量y在后面聲明了。但雖然JS引擎自動提升了變量y的聲明,卻不會提升變量y的賦值,所以在賦值前y為undefined。基于此我們在函數內部定義變量時,請嚴格遵守“在函數內部首先申明所有變量”這一規則,最常見的做法是用一個var申明函數內部用到的所有變量:

function foo() {
    var
        x = 1, // x初始化為1
        y = x + 1, // y初始化為2
        z, i; // z和i為undefined
    // 其他語句:
    for (i=0; i<100; i++) {
        ...
    }
}

不在任何函數內定義的變量具有全局作用域,實際上JS默認有一個全局對象window
,全局作用域的變量實際上被作為的一個屬性綁定到window。不同的JS文件如果使用了相同的全局變量或者定義了相同名字的函數,都會造成命名沖突,并且很難被發現。
減少沖突的一個方法是把所有變量和函數全部綁定到一個全局變量中例如:

// 唯一的全局變量MYAPP:
var MYAPP = {};
// 其他變量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;
// 其他函數:
MYAPP.foo = function () {
    return 'foo';
};

許多著名的JS庫都是這么做的如Query,YUI,underscore等等。此外,ES6標準引入了新的關鍵字const和let,用let替代var可以申明一個塊級作用域的變量,而const
用來聲明常量,const與let都具有塊級作用域。

方法

JS的函數內部如果調用了this,那么這個this到底指向誰?答案是視情況而定!如果以對象的方法形式調用,該函數的this指向被調用的對象;如果單獨調用函數,此時該函數的this指向全局對象,也就是window。因此ECMA決定,在strict模式下讓函數的this指向undefined:

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應該指向的正確位置,這時把方法重構了一下:

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,就可以放心地在方法內部定義其他函數,而不是把所有語句都堆到一個方法中:

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

其實我們還是可以控制this指向哪個對象的,這里要用到函數本身的apply方法,它接收兩個參數,第一個參數就是需要綁定的this變量,第二個參數是Array,表示函數本身的參數:

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()實現如下:

// 通常把this綁定為null
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5

利用apply(),我們還可以動態改變函數的行為。因為JS的所有對象都是動態的,即使內置的函數,我們也可以重新指向新的函數:

var count = 0;
var oldParseInt = parseInt; // 保存原函數
window.parseInt = function () {
    count += 1;
    return oldParseInt.apply(null, arguments); // 調用原函數
};
// 測試:
parseInt('10');
parseInt('20');
parseInt('30');
count; // 3

高階函數

map()/reduce()

map()接收一個函數并將函數作用在Array的每一個元素并把結果生成一個新的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
filter()

filter()用于把Array的某些元素過濾掉,然后返回剩下的元素。和map()類似,filter()也接收一個函數,但不同的是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]
sort()

Array的sort()方法默認把所有元素先轉換為String再排序,而字符串根據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;
}); // [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]

sort()方法會直接對Array進行修改,它返回的結果仍是當前Array。

閉包

高階函數除了可以接受函數作為參數外,還可以把函數作為返回值。注意到返回的函數在其內部引用了局部變量,當一個函數返回了一個函數后,其內部的局部變量還被新函數引用,所以閉包實現起來可不容易。另一個需要注意的問題是,返回的函數并沒有立刻執行,而是直到調用時才執行。我們來看一個例子:

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];

在上面的例子中,每次循環都創建了一個新的函數,然后把創建的函數都添加到Array中返回。返回的函數引用了變量i,但它并非立刻執行,函數都返回時所引用的變量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

JS中,創建一個匿名函數并立刻執行可以這么寫:
(function (x) { return x * x }) (3);

在沒有class機制,只有函數的語言里,借助閉包可以封裝一個私有變量。我們用JS創建一個計數器:

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。換句話說,閉包就是攜帶狀態的函數,并且它的狀態可以完全對外隱藏起
來。閉包還可以把多參數的函數變成單參數的函數:

function make_pow(n) {
    return function (x) {
        return Math.pow(x, n);
    }
}
// 創建兩個新函數:
var pow2 = make_pow(2);
var pow3 = make_pow(3);
pow2(5); // 25
pow3(7); // 343

箭頭函數

箭頭函數相當于匿名函數,并且簡化了函數定義。它有兩種格式,一種只包含一個表達式,連{ ... }和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;
}

如果要返回一個對象,因為和函數體的{ ... }有語法沖突,要這么寫:
x => ({ foo: x })
箭頭函數內部的this是詞法作用域,總是指向外層調用者,由上下文確定:

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

由于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(生成器)

generator是ES6標準引入的新的數據類型,一個generator看上去像一個函數,但可以返回多次。generator定義如下:

function* foo(x) {
    yield x + 1;
    yield x + 2;
    return x + 3;
}

generator和函數不同的是,generator由function定義(注意多出的號),并且,除了return語句,還可以用yield返回多次。要編寫一個產生斐波那契數列的函數,可以這么寫:

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;
}
// 測試:
fib(5); // [0, 1, 1, 2, 3]
fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

函數只能返回一次,但是如果換成generator,就可以一次返回一個數,不斷返回多次:

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;
}
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: true}

next()方法會執行generator的代碼,每次遇到yield就返回一個對象{value: x, done: true/false}并“暫停”,返回的value就是yield的返回值,done表示這個generator是否已經執行結束了。
二是直接用for ... of循環迭代generator對象:

for (var x of fib(5)) {
    console.log(x); // 依次輸出0, 1, 1, 2, 3
}

generator在執行過程中多次返回,所以它可以記住執行狀態。generator還有另一個巨大的好處,就是把異步回調變成“同步”:

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

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

看上去是同步的代碼,實際上是異步執行。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,155評論 3 425
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,255評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,646評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,838評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,399評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,146評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,338評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,565評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,059評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,296評論 2 376

推薦閱讀更多精彩內容

  • 函數函數定義與調用變量作用域全局變量方法高階函數閉包箭頭函數$generator$ 函數 函數定義與調用 定義函數...
    染微言閱讀 597評論 0 5
  • 本文是大神廖雪峰的JavaScript教程學習筆記。并不是教程,如有需要,請前往廖雪峰大神大博客. 一、函數定義和...
    0o凍僵的企鵝o0閱讀 500評論 1 3
  • 函數參數的默認值 基本用法 在ES6之前,不能直接為函數的參數指定默認值,只能采用變通的方法。 上面代碼檢查函數l...
    呼呼哥閱讀 3,433評論 0 1
  • 在此處先列下本篇文章的主要內容 簡介 next方法的參數 for...of循環 Generator.prototy...
    醉生夢死閱讀 1,463評論 3 8
  • 經過一天的培訓讓我再一次學會張開嘴與對方溝通,再一次突破了自己用了一個小時的時間收獲到了很多平時在朋友圈兒都不交流...
    ad2c0503b52d閱讀 239評論 0 0