1. 程式人生 > >6- js 函式的四種呼叫方式

6- js 函式的四種呼叫方式

6. 函式的四種呼叫方式

函式有下列呼叫模式

  1. 函式呼叫模式
  2. 方法呼叫模式
  3. 構造器模式
  4. 上下文模式

1. 函式呼叫 模式

要呼叫,就肯定要先定義,函式的定義方式:

  1. 宣告式: function fuc() {}
  2. 表示式式: var func = function() {};
  3. Function: new Function( ‘引數’,…,’函式體’ );

單獨獨立呼叫的,就是函式呼叫模式,即 函式名( 引數 ),不能加任何其他的東西, 物件 o.fuc() 就不是了。

在函式呼叫模式中, this 表示全域性物件 window

任何自呼叫函式都是函式模式。

2. 方法呼叫 模式 method

所謂方法呼叫,就是用物件的方法呼叫。方法是什麼,方法本身就是函式,但是,方法不是單獨獨立的,而是要通過一個物件引導來呼叫。

就是說方法物件一定要有宿主物件

物件.方法(引數)

this表示引導方法的物件,就是指宿主物件

對比-函式呼叫模式:

  1. 方法呼叫模式是不是獨立的,需要宿主,而函式呼叫模式是獨立的
  2. 方法呼叫模式方式:obj.fuc(); 函式呼叫模式方式: fuc();
  3. 方法呼叫模式中,this指宿主。而函式呼叫模式中 this 指 全域性物件window

美團的一道面試題

    var length = 10
; function fn() { console.log( this.length ); // 10 } var obj = { length: 5, method: function ( fn ) { fn(); // 10 前面沒有引導物件,是函式呼叫模式 arguments[ 0 ](); // 2 // arguments是一個偽陣列物件, 這裡呼叫相當於通過陣列的索引來呼叫. // 這裡 this 就是 指的這個偽陣列, 所以 this.length 為 2
} }; obj.method( fn, 1 ); // 列印 10 和 2 //obj.method( fn, 1, 2, 3 ); // 列印 10 和 4

解析:

  1. fn() 前面沒有引導物件,是函式呼叫模式, this是全域性物件,輸出 10

  2. arguments[ 0 ](),arguments是一個偽陣列物件, 這裡呼叫相當於通過陣列的索引來呼叫.

    這裡引導物件即宿主就是 arguments物件。

    所以,執行時,this 就是指 arguments,由於傳了兩個引數,所以 輸出為 arguments.length 就是 2

3. 構造器模式(建構函式模式, 構造方法模式)

constructor

  1. 特點: 使用 new 關鍵字引導

  2. 執行步驟:var p = new Person();

    new 是一個運算子, 專門用來申請建立物件, 創建出來的物件傳遞給建構函式的 this。然後利用建構函式對其初始化。

        function Person () {
            // new了 進入建構函式時, p 物件的原型 就指向了 建構函式 Person, 
            // p.__proto__.constructor = function Person() {};
            // 而 this 指的的是 p 物件
            this.name = 'jim',
            this.age = 19;
            this.gender = 'male';
        }
        var p = new Person();

    執行完 new 進入建構函式時, p 物件的原型 就指向了 建構函式 Person

    而 構造時,this 指的的是 p 物件,是通過物件動態新增屬性來構造的

    小貼士:如果呼叫建構函式的時候, 建構函式沒有引數, 圓括號是可以省略的。

        function Person() {
            this.name = 'jim';
        }
        var p = new Person; // 不傳參,可以簡寫,不影響構造
        console.log( p );   // p 含有 name屬性

    ↑ 不傳參,可以簡寫,不影響構造

  3. 返回值

    1. 不寫 return 語句, 那麼 建構函式 預設返回 this

    2. 在建構函式 return 基本型別( return num, return 1223 ). 則忽略返回型別.

    3. 在建構函式 return 引用型別, 那麼建構函式返回該引用型別資料, 而忽略 this

        function Person () {
            this.name = 'Jepson';
            return 123;
        }
        var p1 = new Person();
        console.log( p1 );

    ↑ 忽略了 123,返回 this 物件, 指向構建的例項

        function Person () {
            this.name = 'Jepson';
            return { 'peter': 'nihao' };
        }
        var p1 = new Person();
        console.log( p1 );

    ↑ 忽略了 this,返回 { ‘peter’: ‘nihao’ } 物件

建構函式結合性

  1. 如果建構函式沒有引數, 可以省略 圓括號

    var p = new Person;

  2. 如果希望建立物件並直接呼叫其方法

    ( new Person () ).sayHello()

    -> 可以省略調整結核性的圓括號 new Person().sayHello()

    -> 如果想要省略建構函式的圓括號, 就必須新增結核性的圓括號 (new Person).sayHello()

面試題

一道面試題,大家可以自己嘗試先做一下,再看下面的答案和解析

請問順序執行下面程式碼,會怎樣 alert

    function Foo(){
        getName = function(){ alert(1); };
        return this;
    }
    Foo.getName = function(){ alert(2); };
    Foo.prototype.getName = function(){ alert(3); };
    var getName = function(){ alert(4); };
    function getName(){ alert(5); }

    Foo.getName();  // alert ??
    getName();  // alert ??
    Foo().getName(); // alert ??
    getName(); // alert ??
    new Foo.getName(); // alert ??
    new Foo().getName(); // alert ??
    new new Foo().getName(); // alert ??

預解析,簡化後的程式碼,以及答案

    /* function getName(){ alert(5); } 執行到下面被覆蓋了,直接刪除 */
    function Foo() {
        getName = function () { alert(1); };
        return this;
    }
    Foo.getName = function () { alert(2); };
    Foo.prototype.getName = function () { alert(3); };
    var getName = function () { alert(4); };

    Foo.getName();  // ------- 輸出 2 -------
    getName();      // ------- 輸出 4 -------
    Foo().getName();    // ------- 輸出 1 -------
    getName();  // ------- 輸出 1 -------
    new Foo.getName();     // ------- 輸出 2 -------
    new Foo().getName();    // ------- 輸出 3 -------
    var p = new new Foo().getName();     // ------- 輸出 3 -------

全部解析過程 ↓

    function Foo() {
        getName = function () { alert(1); };
        return this;
    }
    Foo.getName = function () { alert(2); };
    Foo.prototype.getName = function () { alert(3); };
    var getName = function () { alert(4); };

    Foo.getName();  // ------- 輸出 2 -------
    // 呼叫 Foo函式 作為 物件 動態新增的屬性方法 getName
    // Foo.getName = function () { alert(2); };

    getName();      // ------- 輸出 4 -------
    // 這裡 Foo函式 還沒有執行,getName還沒有被覆蓋
    // 所以 這裡還是 最上面的 getName = function () { alert(4); };

    Foo().getName();    // ------- 輸出 1 -------
    // Foo()執行,先覆蓋全域性的 getName 再返回 this,
    // this 是 window, Foo().getName() 就是呼叫 window.getName
    // 此時 全域性的 getName已被覆蓋成 function () { alert(1); };
    // 所以 輸出 1
    /* 從這裡開始 window.getName 已被覆蓋 alert 1 */

    getName();  // -------- 輸出 1 --------
    // window.getName alert(1);

    new Foo.getName();     // ------- 輸出 2 -------
    // new 就是 找 建構函式(),由建構函式結合性,這裡即使 Foo無參,也不能省略 (),所以不是 Foo().getName()
    // 所以 Foo.getName 為一個整體,等價於 new (Foo.getName)();
    // 而 Foo.getName 其實就是函式 function () { alert(2); } 的引用
    // 那 new ( Foo.getName )(), 就是在以 Foo.getName 為建構函式 例項化物件。
    // 就 類似於 new Person(); Person 是一個建構函式

    // 總結來看 new ( Foo.getName )(); 就是在以 function () { alert(2); } 為建構函式來構造物件
    // 構造過程中 alert( 2 ),輸出 2

    new Foo().getName();    // ------- 輸出 3 -------
    // new 就是 找 建構函式(),等價於 ( new Foo() ).getName();
    // 執行 new Foo() => 以 Foo 為建構函式,例項化一個物件
    // ( new Foo() ).getName; 訪問這個例項化物件的 getName 屬性
    // 例項物件自己並沒有 getName 屬性,構造的時候也沒有 新增,找不到,就到原型中找
    // 發現 Foo.prototype.getName = function () { alert(3); };
    // 原型中有,找到了,所以 ( new Foo() ).getName(); 執行,alert(3)

    var p = new new Foo().getName();     // ------- 輸出 3 -------
    // new 就是 找 建構函式(),等價於 new ( ( new Foo() ).getName )() 輸出 3

    // 先看裡面的 ( new Foo() ).getName
    // new Foo() 以Foo為建構函式,例項化物件
    // new Foo().getName 找 例項物件的 getName屬性,自己沒有,去原型中找,
    // 發現 Foo.prototype.getName = function () { alert(3); }; 找到了

    // 所以裡層 ( new Foo() ).getName 就是 以Foo為建構函式例項出的物件的 一個原型屬性
    // 屬性值為一個函式 function () { alert(3); } 的引用

    // 所以外層 new ( (new Foo()).getName )()在以該函式 function () { alert(3); } 為建構函式,構造例項
    // 構造過程中 執行了 alert(3), 輸出 3

4. 上下文呼叫模式

就是 環境呼叫模式 => 在不同環境下的不同調用模式

簡單說就是統一一種格式, 可以實現 函式模式與方法模式

-> 語法(區分)

  1. call 形式, 函式名.call( … )
  2. apply 形式, 函式名.apply( … )

這兩種形式功能完全一樣, 唯一不同的是引數的形式. 先學習 apply, 再來看 call 形式

apply方法的呼叫形式

存在上下文呼叫的目的就是為了實現方法借用,且不會汙染物件。

  • 如果需要讓函式以函式的形式呼叫, 可以使用

    foo.apply( null ); // 上下文為 window

  • 如果希望他是方法呼叫模式, 注意需要提供一個宿主物件

    foo.apply( obj ); // 上下文 為 傳的 obj 物件

    function foo () {
        console.log( this );
    }
    var o = { name: 'jim' };

    // 如果需要讓函式以函式的形式呼叫, 可以使用
    foo.apply( null ); //  this => window  // 或 foo.apply()

    // 如果希望他是方法呼叫模式, 注意需要提供一個宿主物件
    foo.apply( o )  // this => o 物件

帶有引數的函式如何實現上下文呼叫?

    function foo ( num1, num2 ) {
        console.log( this );
        return num1 + num2;
    }

    // 函式呼叫模式
    var res1 = foo( 123, 567 );

    // 方法呼叫
    var o = { name: 'jim' };
    o.func = foo;
    var res2 = o.func( 123, 567 );

使用 apply 進行呼叫, 如果函式是帶有引數的. apply 的第一個引數要麼是 null 要麼是物件

  1. 如果是 null 就是函式呼叫

  2. 如果是 物件就是 方法呼叫, 該物件就是宿主物件, 後面緊跟一個數組引數, 將函式所有的引數依次放在陣列中.

    例如: 函式模式        foo( 123, 567 );
          apply         foo.apply( null, [ 123, 567 ] ) 以 window 為上下文執行 apply

    如果有一個函式呼叫:   func( '張三', 19, '男' ),
    將其修改成 apply 模式:  func.apply( null, [ '張三', 19, '男'] )

    方法模式:           o.func( 123, 567 )
    apply               var o = { name: 'jim' };
                        foo.apply( o, [ 123, 567 ] ); 以 o 為上下文執行 apply

方法借用的案例

需求, 獲得 div 與 p 標籤, 並新增邊框 border: 1px solid red

一般做法:

    var p_list = document.getElementsByTagName('p');
    var div_list = document.getElementsByTagName('div');

    var i = 0;
    for( ; i < p_list.length; i++ ) {
        p_list[ i ].style.border = "1px solid red";
    }
    for( i = 0; i < div_list.length; i++ ) {
        div_list[ i ].style.border = "1px solid red";
    }
  1. 利用方法借用優化,元素獲取
    var t = document.getElementsByTagName;
    var p_list = t.apply( document, [ 'p' ] );  // 方法借用
    var div_list = t.apply( document, [ 'div' ] ); // 方法借用
  1. 接下來考慮下面的優化,兩個for迴圈,只要將數組合並了,就可以只用一個 for 迴圈

    數組合並

        var arr1 = [ 1, 2, 3 ];
        var arr2 = [ 5, 6 ];
        arr1.push.apply( arr1, arr2 );  // 方法呼叫,第一個給上物件
        // 等價於 Array.prototype.push.apply( arr1, arr2 );

    所以同理,利用 apply 方法借用,將兩個偽數組合併成同一個陣列

        var arr = [];   // 偽陣列沒有 push 方法,所以這裡要 宣告一個 陣列
        arr.push.apply( arr, p_list );  // 將 p_list裡的內容,一個個當成引數放了進來,相當於不用遍歷了
        arr.push.apply( arr, div_list ); // 同上,方法借用
        console.log( arr );

將兩者綜合, 使用forEach,最終 6 行就解決了

    var t = document.getElementsByTagName, arr = [];
    arr.push.apply( arr, t.apply( document, [ 'p' ] ) );
    arr.push.apply( arr, t.apply( document, [ 'div'] ) );
    arr.forEach( function( val, index, arr ) {
        val.style.border = '1px solid red';
    });

call 呼叫

在使用 apply 呼叫的時候, 函式引數, 必須以陣列的形式存在. 但是有些時候陣列封裝比較複雜

所以引入 call 呼叫, call 呼叫與 apply 完全相同, 唯一不同是 引數不需要使用陣列

    foo( 123, 567 );

    foo.apply( null, [ 123, 567 ] );

    foo.call( null, 123, 567 );
  1. 函式呼叫: 函式名.call( null, 引數1,引數2,引數3… );

  2. 方法呼叫: 函式名.call( obj, 引數1,引數2, 引數3… );

不傳參時,apply 和 call 完全一樣

借用構造方法實現繼承

    function Person ( name, age, gender ) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    function Student ( name, age, gender, course ) {
        // 原型鏈結構不會變化,同時實現了繼承的效果
        Person.call( this, name, age, gender );// 借用Person構造方法
        this.course = course;
    }

    var p = new Student ( 'jim', 19, 'male', '前端' );
    console.log( p );

補充知識 1. 函式的 bind 方法 ( ES5 )

bind 就是 繫結

還是上面那個案例 獲得 div 與 p 標籤, 並新增邊框 border: 1px solid red

    var t = document.getElementsByTagName, arr = [];
    arr.push.apply( arr, t.call( document, 'p'  ) );
    arr.push.apply( arr, t.call( document, 'div' ) );
    arr.forEach( function( val, index, arr ) {
        val.style.border = '1px solid red';
    });

我們 讓 t 包含函式體(上面的方式),同時包含 物件,就可以更精簡

    var t = document.getElementsByTagName.bind( document ), arr = [];
    arr.push.apply( arr, t('p') );
    arr.push.apply( arr, t('div') );
    arr.forEach( function( val, index, arr ) {
        val.style.border = '1px solid red';
    });

bind : 就是讓函式繫結物件的一種用法

函式本身就是可以呼叫, 但是其如果想要作為方法呼叫, 就必須傳入宿主物件, 並且使用 call 或 apply 形式

但是 bind 使得我的函式可以與某一個物件繫結起來, 那麼在呼叫函式的時候, 就好像是該物件在呼叫方法,就可以直接傳參,而不需要傳宿主物件。

語法: 函式.bind( 物件 )

返回一個函式 foo,那麼呼叫 返回的函式 foo, 就好像 繫結的物件在呼叫 該方法一樣

     t.call( document, 'p' );

     t( 'p' ); 繫結後,就不用傳宿主物件了,這裡呼叫時上下文已經變成了 document

     bind 是 ES 5 給出的函式呼叫方法

補充知識 2. Object.prototype 的成員

Object.prototype 的成員

  1. constructor
  2. hasOwnProperty 判斷該屬性是否為自己提供
  3. propertyIsEnumerable 判斷屬性是否可以列舉
  4. isPrototypeOf 判斷是否為原型物件
  5. toString, toLocaleString, valueOf
    function Person() {
        this.name = 'jim';
    }
    Person.prototype.age = 19;
    var p = new Person();
    console.log( p.hasOwnProperty( 'name' ) );  // true; p 是否含有 name 屬性,原型上不管
    console.log( p.hasOwnProperty( 'age' ) ); // false;  p 是否含有 age 屬性

    /* Person.prototype 是 p 的原型 */
    console.log( p.isPrototypeOf( Person.prototype ) ); // false
    console.log( Person.prototype.isPrototypeOf( p ) );  // true;

用途:一般把一個物件拷貝到另一個物件時,可以進行判斷,更加嚴謹,以防把原型中的屬性也拷貝過去了…

補充知識 3. 包裝型別

字串 string 是基本型別, 理論上講不應該包含方法

那麼 charAt, substr, slice, …等等方法,理論上都不應該有,但確是有的

所以引入包裝物件的概念,在 js 中為了更好的使用資料, 為三個基本型別提供了對應的物件型別

  1. Number
  2. String
  3. Boolean

在 開發中常常會使用基本資料型別, 但是基本資料型別沒有方法, 因此 js 引擎會在需要的時候自動的將基本型別轉換成物件型別, 就是包裝物件

  • “abc”.charAt( 1 )

  • “abc” -> s = new String( “abc” )

  • s.charAt( 1 ) 返回結果以後 s 就被銷燬

當基本型別.方法 的時候. 直譯器首先將基本型別轉換成對應的物件型別, 然後呼叫方法.

方法執行結束後, 這個物件就被立刻回收

在 apply 和 call 呼叫的時候, 也會有轉換髮生. 上下文呼叫的第一個引數必須是物件. 如果傳遞的是數字就會自動轉換成對應的包裝型別

補充知識 4. getter 和 setter 的語法糖 ( ES5 )

語法糖: 為了方便開發而給出的語法結構

本身實現:

    var o = (function () {
        var num = 123;
        return {
            get_num: function () {
                return num;
            },
            set_num: function ( v ) {
                num = v;
            }
        };
    })();
    希望獲得資料  以物件的形式
    o.get_num();            => o.num 形式

    希望設定資料  以物件的形式
    o.set_num( 456 );       => o.num = 456 形式

所以 getter 和 setter 誕生 了

    var o = (function () {
        var num = 123;
        return {
            // get 名字 () { 邏輯體 }
            get num () {
                console.log( '執行 getter 讀寫器了' );
                return num;
            },

            // set 名字 ( v ) { 邏輯體 }
            set num ( v ) {
                console.log( '執行 setter 讀寫器了' );
                num = v;
            }
        };
    })();

    console.log( o.num );   // 執行 getter 讀寫器了   123
    o.num = 33; // 執行 setter 讀寫器了
    console.log( o.num ); // 執行 getter 讀寫器了 33

為什麼不直接用 物件呢 var o = { num : 123 } ,也可以讀寫呀?

因為語法糖還可以 限制其賦值的範圍,使用起來特別爽

    var o = (function () {
        var num = 13;
        return {

            // get 名字 () { 邏輯體 }
            get num () {
                console.log( '執行 getter 讀寫器了' );
                return num;
            },

            // set 名字 ( v ) { 邏輯體 }
            set num ( v ) {
                console.log( '執行 setter 讀寫器了' );

                if ( v < 0 || v > 150 ) {
                    console.log( '賦值超出範圍, 不成功 ' );
                    return;
                }
                num = v;
            }
        };
    })();
    o.num = -1;         // 執行 setter 讀寫器了
                    // 讀寫器賦值超出範圍, 不成功

補充知識 5. ES5 中引入的部分陣列方法

  1. forEach
  2. map
  3. filter
  4. some
  5. every
  6. indexOf
  7. lastIndexOf
  1. forEach, 陣列遍歷呼叫,遍歷arr,引數三個 1某項, 2索引, 3整個陣列

         var arr = [ 'hello', ' js', {  }, function () {} ];
         // 遍歷 陣列
         arr.forEach( function ( v, i, ary ) {
            console.log( i + '=====' + v );
            console.log( ary );
         });
  2. map 對映

    語法: 陣列.map( fn )

    返回一個數組, 陣列的每一個元素就是 map 函式中的 fn 的返回值

    就是對每一項都進行操作,並返回

         var arr = [ 1, 2, 3, 4 ];
         // 數學中: x -> x * x
         var a = arr.map(function ( v, i ) {
            return v * v;
         });
         // a [1, 4, 9, 16]
  3. filter 就是篩選, 函式執行結果是 false 就棄掉, true 就收著

    語法: 陣列.filter( function ( v, i ) { return true/false })

        var arr = [ 1, 2, 3, 4, 5, 6 ];
        // 篩選奇數
        var a = arr.filter( function ( v ) { return v % 2 === 1; });
        // a [ 1, 3, 5 ]
  4. some 判斷陣列中至少有一個數據複合要求 就返回 true, 否則返回 false

        var arr = [ '123', {}, function () {}, 123 ];
        // 判斷陣列中至少有一個數字
        var isTrue = arr.some( function ( v ) { return typeof v === 'number'; } );  // true;
  5. every 必須滿足所有元素都複合要求才會返回 true

        var arr = [ 1, 2, 3, 4, 5, '6' ];
        // 判斷陣列中每一個都是數字
        var isTrue = arr.every( function ( v ) { return typeof v === 'number'; } ); } ); // false;
  6. indexOf 在陣列中查詢元素, 如果含有該元素, 返回元素的需要( 索引 ), 否則返回 -1

         var arr = [ 1, 2, 3, 4, 5 ];
         var res = arr.indexOf( 4 );    // 要找 4
         console.log( res );    // 3     找 4 在 索引為 3 找到
    
         var arr = [ 1, 2, 3, 4, 5, 4, 5, 6 ];
         var res = arr.indexOf( 4, 4 ); // 要找 4, 從索引 4開始找
         console.log( res );    // 找到了 索引為 5
  7. lastIndexOf 從右往左找

         var arr = [ 1, 2, 3, 4, 5, 4, 5, 6 ];
         var res = arr.lastIndexOf( 4 );
         console.log( res );    // 索引為 5, 從最後一項開始找,即從 length-1 項開始找