CH4. 老手看函式:理解函式呼叫


4.1 函式隱含引數

當我們在使用函式時,會傳遞 arguments 與 this 這兩個引數
arguments:函式隱含參數 ( Implicit function parameter )
this:代表這個函式所處的背景空間 ( function context )

4.1.1 arguments 參數

  • arguments 是傳遞給 function 的所有 parameter 之集合
  • arguments 並不是陣列
    function sum() {
      console.log(arguments);
    }
    sum(1, 2, 3);
    

可實現 function overloading

函式名相同,函式的引數列表不同(包括引數個數和引數型別),根據引數的不同去執行不同的操作。

function overload () {
  if (arguments.length === 1) {
    console.log('一個參數')
  }
  if (arguments.length === 2) {
    console.log('兩個參數')
  }
}

overload(1);  // 傳入一個引數
overload(1, 2);  // 傳入兩個引數

Ref: JavaScript中的函式過載(Function overloading) - IT閱讀


jQuery 的 css function

jQuery.fn.extend( {
    css: function( name, value ) {
        return access( this, function( elem, name, value ) {
            var styles, len,
                map = {},
                i = 0;

            if ( Array.isArray( name ) ) { // 判斷參數 name 是否為 array
                styles = getStyles( elem );
                len = name.length;

                for ( ; i < len; i++ ) {
                    map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
                }

                return map;
            }

            return value !== undefined ? // 判斷 value 是否為 undefined
                jQuery.style( elem, name, value ) :
                jQuery.css( elem, name );
        }, name, value, arguments.length > 1 );
    }
} );

Ref: jquery/css.js at 39c5778c649ad387dac834832799c0087b11d5fe · jquery/jquery · GitHub


array 的 slice 方法也有 overloading


我們可以使用 rest parameter 取代 arguments parameter
ref: prefer-rest-params - Rules - ESLint - Pluggable JavaScript linter

不訂參數是一個真正的陣列


Arguments 物件可作為函式參數的別名

function infiltrate(person) {
  arguments[0] = 'ninja'; // 改動物件會造成相對應的參數被異動
  console.log(person); // 'ninja'

  person = 'gardener';
  console.log(arguments[0]); // 'gardener'
}

infiltrate("gardener");

避免別名,容易造成混淆的風險,因此可設定嚴格模式禁止變動的行為

4.1.2 this 參數:介紹函式背景空間 context

  • 與函式呼叫相關聯的物件,因此通常被稱為 function context
  • this 所指向的東西會受到 函式定義的方式呼叫的方式 影響

4.2 呼叫函式 Invoking functions

呼叫函式的方式

  • 作為函式:foo(),一般呼叫函式的形式
  • 作為一個方法:foo.bar(),把呼叫綁到一個物件上
  • 作為一個建構器函式:new Foo(),創建新的物件
  • 經由函式的 apply 與 call 方法:foo.alppy(bar) or foo.call(bar)

4.2.1 作為函式呼叫

function ninja() {
  return this;
}

function samurai() {
  "use strict";
  return this;
}

ninja(); // window,function context 為 window
samurai(); // undefined, 在嚴格模式下 function context 為 undefined

4.2.2 作為方法呼叫

function whatsMyContext() {
  return this;
}

var getMyThis = whatsMyContext;
getMyThis(); // function context is window

var ninja1 = {
  getMyThis: whatsMyContext
};

var ninja2 = {
  getMyThis: whatsMyContext
};

ninja1.getMyThis(); // function context 為 ninja1
ninja2.getMyThis(); // function context 為 ninja2

函式作為方法來呼叫,可以在任何方法中引用該方法所擁有的物件,這是物件導向程式設計中的一項基本概念

4.2.3 作為建構器來呼叫 Invocation as a constructor

function Ninja() {
  this.skulk = function() {
    return this;
  };
}

var ninja1 = new Ninja();
var ninja2 = new Ninja();

assert(ninja1.skulk() === ninja1, "The 1st ninja is skulking");
assert(ninja2.skulk() === ninja2, "The 2nd ninja is skulking");

使用關鍵字 new 來呼叫函式,會觸發以下步驟

  1. 建立一個新的空物件
  2. 物件被當成 this 參數傳遞給建構器,因此成為建構器的函式背景空間
  3. new 運算子會回傳新建立的物件 (但會有例外)

constructor 的目的是建立一個新物件,設置好它,然後將物件作為建構器的回傳值。
任何會干擾這個目的的事物都不應該出現在建構器裡面。
因此要作為 constructor 的函式內不應該定義回傳值 ( return xxx )。


建構器的回傳值

// 4.8 Constructors returning primitive values, 回傳基礎型值的建構器函式
function Ninja() {
  this.skulk = function () {
    return true;
  };

  return 1;
}

// 作為 function 呼叫如期回傳 1
assert(Ninja() === 1,
  "Return value honored when not called as a constructor");

// 用 new 運算子呼叫,回傳值會被忽略
var ninja = new Ninja();

assert(typeof ninja === "object",
  "Object returned when called as a constructor");
assert(typeof ninja.skulk === "function",
  "ninja object has a skulk method");
var puppet = {
  rules: false
};

function Emperor() {
  this.rules = true;
  return puppet;
}

// 如果 constructor 回傳一個物件, 該物件將作為 new 表達式的回傳值,而一開始被作為 this 傳遞給建構器的新物件將被丟棄
var emperor = new Emperor();

assert(emperor === puppet,
  "The emperor is merely a puppet!");
assert(emperor.rules === false,
  "The puppet does not know how to rule!");

如果 constructor 回傳的不是物件,則回傳的值會被忽略,會回傳新建立的物件(this)


建構器的設計考量

  • 函式和方法通常以一個描述他們在做什麼的動詞開始,並以小寫開頭
    sum, add, sort, max, min, createObject

  • 建構器通常用物件的名詞來命名,並大寫開頭
    Car, Cat, Ball, House

4.3 修復函式背景空間的問題

  • 在建構器函式建立,this 為新建立的物件
  • 在建立物件時建立,this 指向當前的 function context
function Foo() {
  this.age = 20;

  this.showAgeUseArrowFun = () => { // 在建構器函式建立,this 為新建立的物件
    console.log('showAgeUseArrowFun', this.age);
  }

  this.showAgeUseFun = function() {
    console.log('showAgeUseFun', this.age);
  }
}

var age = 100;
var foo = new Foo();

foo.showAgeUseArrowFun(); // 20
var showAgeUseArrowFun = foo.showAgeUseArrowFun;
showAgeUseArrowFun(); // 20

foo.showAgeUseFun(); // 20
var showAgeUseFun = foo.showAgeUseFun;
showAgeUseFun(); // 100

var bar = {
  age: 999,
    showAgeUseArrowFun: () => { // 在建立物件時建立,this 指向當前的 function context
      console.log('showAgeUseArrowFun', this.age);
  },
    showAgeUseFun: function() {
      console.log('showAgeUseFun', this.age);
  }
}

bar.showAgeUseArrowFun(); // 100
bar.showAgeUseFun(); // 999

4.3.2. Using the bind method

可使用 bind 方法建立一個新函式,function context 會綁定到指定的物件上

<button id="test">Click Me!</button>
<script>
  var button = {
    clicked: false,
    click: function(){
      this.clicked = true;
      assert(button.clicked,"The button has been clicked");
    }
  };
  var elem = document.getElementById("test");
  elem.addEventListener("click", button.click.bind(button));

  var boundFunction = button.click.bind(button);
  assert(boundFunction != button.click, 
         "Calling bind creates a completly new function");
</script>

呼叫 bind 不會修改原始的函式,而是建立一個全新的函式。


寫 React Component 時,使用到 bind 的情境

import React from "react";

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };

    // 將 function context 綁定到目前的 this 上
    this.handleClick = this.handleClick.bind(this);
    console.log(this); // Button
  }

  handleClick() {
    this.setState(state => ({
      count: state.count + 1
    }));
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>click me</button> count:
        {this.state.count}
      </div>
    );
  }
}

export default Button;

如果不使用 bind,handleClick 裡面的 this 會指向 window 物件
bind example

#argument #parameter #javascript #this #constructor







你可能感興趣的文章

[Day 2] JS in Pipeline (2): Docker and Local Development Environment (2)

[Day 2] JS in Pipeline (2): Docker and Local Development Environment (2)

CSS 衍生的資安問題(中) - 不用 Hot Update 也行

CSS 衍生的資安問題(中) - 不用 Hot Update 也行

JS-[promises語法糖]-用async await來實現多個promises

JS-[promises語法糖]-用async await來實現多個promises






留言討論