關於JavaScript函數調用的幾種模式

函數的調用有五種模式:方法調用模式,函數調用模式,構造器調用模式,apply/call調用模式以及回調模式,下面分別對這幾種模式進行說明。node

1.函數調用與方法調用模式:面試

1.1 聲明一個函數並調用它就是函數調用模式,這是最簡單的調用,但其中也關係到this的指向問題。普通函數是將this默認綁定到全局對象,而箭頭函數時不綁定this的,在函數所在的父做用域外面this指向哪裏,在箭頭函數內部this也指向哪裏。ajax

function show(name) {
        console.log(name);
    }

    show('shotar');                // shotar

    // 普通函數的符符做用域this指向全局做用域,在調用的時候再一次綁定this到全局做用域
    function say() {
        console.log(this);
    }

    say();                        // 瀏覽器環境輸出window  node環境輸出global

    // 箭頭函數父做用域的this指向全局對象,調用的時候並無綁定this,而是繼承父做用域後指向全局對象
    var sayName = () => {
        console.log(this);
    }

    sayName()                     // window

1.2 方法調用時將一個函數做爲對象的方法調用,做爲方法調用的函數會將this綁定到該對象,但若是方法內部再嵌套一個函數,內部函數再次調用的時候又屬於函數調用模式,此時this又將綁定到全局對象。數組

window.name = 'Jane';        // node環境下是global
    var obj = {
        name: 'shotar',
        sayName: function() {
            console.log(1, this.name);
            sayWindowName();

            function sayWindowName() {
                console.log(2, this.name);
            }
        }
    };

    obj.sayName();                // 1, shotar      2, Jane

若是想讓內部函數(sayWindowName)指向該對象也很簡單,在此列舉三種方法。第一種是在外部將this保存到一個變量裏面,再在內部函數中使用便可。瀏覽器

window.name = 'Jane';
    var obj = {
        name: 'shotar',
        sayName: function() {
            var _this = this;
            console.log(1, this.name);
            sayWindowName();

            function sayWindowName() {
                console.log(2, _this.name);
            }
        }
    };

    obj.sayName();                // 1, shotar      2, shotar

第二種解決辦法是使用ES6的箭頭函數,箭頭函數不綁定this,父做用域的this是哪一個對象在箭頭函數中的this仍然是哪一個對象(注意:箭頭函數只能使用函數字面量的形式命名函數名,調用也要在語句以後)。數據結構

window.name = 'Jane';
    var obj = {
        name: 'shotar',
        sayName: function() {
            console.log(1, this.name);

            var sayWindowName = () => {
                console.log(2, _this.name);
            };

            sayWindowName();
        }
    };

    obj.sayName();                // 1, shotar      2, shotar

第三種使用call或apply方法是改變內部函數的this值。app

window.name = 'Jane';
    var obj = {
        name: 'shotar',
        sayName: function() {
            console.log(1, this.name);
            sayWindowName.call(this);        // 或 sayWindowName.apply(this);

            function sayWindowName() {
                console.log(2, this.name);
            }
        }
    };

    obj.sayName();                // 1, shotar      2, shotar

在此說明一下阮大大在ES6標準入門裏面列舉的關於箭頭函數this指向的例子,由於在foo函數的做用域下指向window的,使用函數調用模式調用foo函數,setTimeout內的箭頭函數不綁定this,仍是指向父做用域foo函數所指向的this。foo是普通函數,他將this指向全局對象,所以箭頭函數也指向全局變量。這時會打印undefined,爲何又會打印出undefined呢,這是由於在聲明id的時候使用了var關鍵字,他是一個變量並非全局對象(window或global)的屬性,若是將var id = 21;這句改成window.id = 21;(或者global.id = 21)後將打印出21。使用call方法調用會改變this的值,下面到call/apply調用模式的時候會講到。dom

function foo() {
        setTimeout(() => {
            console.log('id:', this.id);
        }, 100);
    }

    var id = 21;

    foo();

    foo.call({ id: 42 });        // id: 42

1.3 關於函數this指向問題
普通函數的this是會被綁定的,根據調用方式的不一樣綁定不一樣的對象到this(this只能綁定對象),而箭頭函數是不綁定this的。有這樣一道面試題:異步

window.bar = 2
    var obj = {
        bar: 1,
        foo: function() {
            return this.bar;
        }
    };

    var foo = obj.foo;

    console.log(obj.foo()); // 1
    console.log(foo());        // 2

JavaScript的this設計很內存裏的數據結構有很大的關係。當把一個對象賦給一個變量的時候,你們都知道是引用關係,上面的obj是一個地址,指向那個對象,而在對象存儲的時候,其屬性(方法)的值也是一樣的存儲形式,每一個屬性對應一個屬性描述對象,舉例來說,上面obj的bar屬性實際上是如下面的形式保存起來的。函數

bar: {
        [[value]]: 1,
        [[configurable]]: true,
        [[enumerable]]: true,
        [[writable]]: true
    }

其屬性的值被保存在[[value]]中。但若是屬性的值是個對象(函數也是對象)呢?此時JavaScript引擎會將對象的地址保存在描述符對象的[[value]]位置,像上面的foo屬性(方法)則是這樣保存的:

foo: {
        [[value]]: 對象的地址,
        [[configurable]]: true,
        [[enumerable]]: true,
        [[writable]]: true
    }

函數是個單獨的值,所以他能夠在任何不一樣的上下文環境中執行,也正由於如此,有必要須要一種機制可以在函數內部得到當前的執行上下文(context),所以this就出現了。在上面的那道面試題中,是將該函數的地址賦給變量foo。經過foo變量調用時,其是在全局做用域下執行,所以this指向全局對象。如圖1:

圖片描述

而使用obj.foo執行時,函數是在obj環境下運行,如圖2,因此this是指向obj的。上面提到普通函數是綁定this值,this值指得是當前運行環境,當在obj環境下調用時指向obj,而在全局調用時指向全局對象。因此this是在調用時才肯定值,並非在聲明時就綁定值。

圖片描述

2.call/apply調用模式

call和apply都是Function.prototype中的方法,能夠經過Function.prototype.hasOwnProperty('call')驗證。所以每個函數或者方法均可經過call或apply調用,call和apply都是函數上的方法,每聲明一個函數,就像prototype屬性同樣,都會有call和apply方法。每一個函數或方法均可以經過call或者apply改變當前的執行上下文,他們的第一個參數就是要將this綁定的值。區別是後面的傳參形式不一樣,前者是將參數逐個傳入調用的函數中,而apply是將參數做爲一個數組傳給要調用的函數。就拿那道面試題作例子:

window.bar = 2
    var obj = {
        bar: 1,
        foo: function() {
            return this.bar;
        }
    };

    var foo = obj.foo;

    // ①
    foo.call(obj);            // 1
    // ②
    obj.foo.call(window);    // 2
    // ③
    foo.call({bar: 3})        // 3

①若是foo是普通的調用,其this是指向全局對象的,而經過call改變將this綁定到obj後,this將指向obj。咱們能夠這樣理解,foo是這樣調用的obj.foo()
②這種調用方式咱們能夠這樣理解,foo是obj的方法,就當他是一個普通的函數,至關於window.foo這樣調用,那麼this就是指向全局對象的。
③這種調用方式是將{bar: 3}做爲this的綁定對象,這樣調用foo就至關於{bar: 3}.foo(),this指向{bar: 3}。

3.構造器調用模式:
構造函數的new調用方式被稱爲構造器調用模式,這是模擬類繼承式語言的一種調用方式。在使用new操做符調用函數時,函數內部將this綁定到一個新對象並返回。以下

var Person = function(name) {
        this.name = name;
    };

    var shotar = new Person(shotar);
    // 爲了區別於普通函數,約定構造函數的首字母大寫。使用new操做內部會替你作如下操做:
    Person(name) {
        // 如下都是使用new操做符時內部作的事
        // var obj = new Object();
        // this = obj;
        // obj.name = name;
        // obj.prototype = Person.prototype;

        // return obj;
    }

若是構造函數內部返回了一個不是對象的值,則new會忽略其返回值而返回新建的對象,若是返回的是一個對象則將其返回。另外,若是不使用new操做符調用,並不會在編譯時報錯,這是很是糟糕的事情,所以,咱們一般會在調用的時候檢查是否爲new操做符調用,以下:

function Person(name) {
        if (this instanceof Person) {
            this.name = name;
        } else {
            return new Person(name);
        }
    }

4.回調模式
回調函數是在知足某種狀況或者達到某種要求時當即調用。回調函數一般做爲函數的參數傳入,其本質也仍是一種普通的函數,只是在特定的狀況下執行而已,先看一個例子:

function sayName(obj) {
        var fullName = '';
        if (obj.firstName && obj.lastName) {
            fullName = typeof obj.computedFullName === 'function' ?
                obj.computedFullName() :
                obj.lastName + ' ' + obj.firstName;
        return fullName;
    }

    var obj = {
        firstName: 'Sanfeng',
        lastName: 'Zhang',

        computedFullName: function() {
            return this.lastName + ' ' + this.firstName;
        }
    };

    sayName(obj);            // Zhang Sanfeng

此處的computedName就是一個回調函數,在給sayName函數傳值的時候,咱們傳入了一個對象,前兩個屬性都是直接在sayName中使用,若是知足這兩個屬性都有值,那就調用obj的computedName方法(也就是函數),在此處調用就稱他爲回調函數,回調函數經常使用於異步操做的場合,好比ajax請求,當請求成功並返回數據時再執行回調函數。通常也用於同步阻塞的場景下,好比執行某些操做後執行回調函數。請先看下面的異步狀況的例子:

function ajax(callback) {
        var xhr = new XMLHttpReauest(); 

        if (xhr.readystate === 4 && xhr.status === 200) {
            typeof callback === 'function' && callback();
        } else {
            alert('請求失敗!')
        }

        xhr.open('get', url);
        xhr.send();
    }

    var fn = function() {
        alert('請求成功!');
    };

    ajax(fn);

這裏會有一個問題,如何給回調函數傳參,讓回調函數在裏面處理一些問題,這裏咱們就能夠用到call或者apply方法了。好比有這樣一個問題:統計若干我的的考試成績,只有90分以上的才發獎學金,請看下面同步阻塞的例子:

function startGive(arr, giveMoney) {
        // 先把分數超過90分的過濾出來
        let adult = arr.filter(item => item > 90);

        // 將過濾結果傳入回調函數,發獎金給他們
        return giveMoney.call(null, adult);
    }

    let giveBonuses = function(arr) {
        return arr.map(item => item + 'giveMoney');
    };

    console.log(startGive([70, 80, 92, 96, 85], giveBonuses));        // [ '92giveMoney', '96giveMoney' ]

上面的例子主要是在將分數在90分以上的過濾出來以後再執行操做。回調傳參還能夠經過傳遞匿名函數的形式接收該參數,以下例子:

function fn(arg1, arg2, callback){
        var num = Math.ceil(Math.random() * (arg1 - arg2) + arg2);
        callback(num);
    }
     
    fn(10, 20, function(num){
        console.log("Callback called! Num: " + num); 
    }); 

5.總結
本文講了關於函數調用的五種模式。五種模式包括函數調用模式、方法調用模式、call/apply調用模式、構造器調用模式和回調模式。其中前三種調用模式相似,主要會涉及到this的指向問題,第四種調用方式總返回一個對象,並將this綁定到此對象。回調模式屬於前四種模式中的一種,能夠是函數調用模式,也能夠是方法調用模式,回調的使用很靈活,其主要場景是用於異步操做或同步阻塞操做的場合。

本文參考《JavaScript語言精粹》一書的函數章節及阮大大的《JavaScript 的 this 原理》一文撰寫而出,文中如有表述不妥或是知識點有誤之處,歡迎留言指正批評!

相關文章
相關標籤/搜索