iT邦幫忙

2022 iThome 鐵人賽

DAY 14
1
Modern Web

這些那些你可能不知道我不知道的Web技術細節系列 第 14

你可能不知道Function的三種用法

  • 分享至 

  • xImage
  •  

前言

原預計標題「你可能不知道的Math.max()三種用法」。因為這是在調整Math.max()時引發的話題。在這之後過了幾周,有另外一個同事詢問Function.prototype.call()Function.prototype.apply()的差異。

因此,接下來將來看看「你可能不知道Function的三種用法」。除了一般的呼叫外,還有<Fn>.call()<Fn>.apply()。試想已經有參數陣列args:

var args = Array(15).fill(0);
args.forEach((arg, i, arr) => arr[i] = Math.floor(Math.random()*50));

如果要將args傳遞給函式Math.max()執行,通常可以這麼做:

Math.max(...args);

這相當於:

Math.max.call(null, ...args);

此外你還可以這麼做:

Math.max.apply(null, args);

這三種作法都可以得到相同結果:

Math.max(...args) === Math.max.call(null, ...args); //true
Math.max(...args) === Math.max.apply(null, args); // true

除此之外,因為Math.max()的處理特性,恰好可以使用函式型開發方式中reduce的概念,也確實可以使用args.reduce()去得到與上面相同的結果:

Math.max(...args) === args.reduce((m, c) => Math.max(m, c))

接下來也會談到一些Array.prototype.reduce()的事情。

定義宣告函式

函式申明

通常我們有幾種方式可以宣告函式。最一般的方式是使用「函式申明」:

function myMax(a, b) {
    if(isNaN(a) || isNaN(b))
        return NaN
    return (a > b) ? Number(a) : Number(b);
}

這麼做的好處是函式會被提升(Hosting),可以在函式申明前就使用函式:

console.log(myMax(1, 2)); // 2

function myMax(a, b) {
    if(isNaN(a) || isNaN(b))
        return NaN;
    return (a > b) ? Number(a) : Number(b);
}

有意思的是,函式雖然被提升了,但和var一樣可能只是保留了空間^1。因此有可能同樣拿到undefined或引發錯誤:


try { // foo 放在條件內。foo可能存在也可能未定義
	foo()  
} catch { // 但不管如何,首次執行都有可能因發錯誤(似乎依賴瀏覽器實現,以我嘗試Firefox來說會進入這裡)
  console.error("happend error")
}

var hoisted = "foo" in this;

console.log(`'foo' name ${hoisted ? "is" : "is not"} hoisted. typeof foo is ${typeof foo}. And foo is ${foo}`);
if (true) {
  function foo(){ return 1; }
}

最後得到的結果是:

'foo' name is hoisted. typeof foo is undefined. And foo is undefined

可以看到不管是typeof(foo)還是foo得到的都是undefined

不過再次執行就是不同結果

var hoisted = "foo" in this;

if (true) {
  function foo(){ return 1; }
}

console.log(`'foo' name ${hoisted ? "is" : "is not"} hoisted. typeof foo is ${typeof foo}. And foo is ${foo}`);

會得到

'foo' name is hoisted. typeof foo is function. And foo is function foo(){ return 1; }

根據MDN這應該和瀏覽器實現有關:

// 在 Chrome 里:
// 'foo' 变量名被提升,但是 typeof foo 为 undefined
//
// 在 Firefox 里:
// 'foo' 变量名被提升。但是 typeof foo 为 undefined
//
// 在 Edge 里:
// 'foo' 变量名未被提升。而且 typeof foo 为 undefined
//
// 在 Safari 里:
// 'foo' 变量名被提升。而且 typeof foo 为 function

函式表達式

你可以使用函式表達式的方式建立函式物件,然後將該值賦值給變數:

var myMax = function (a, b) {
    if(isNaN(a) || isNaN(b))
        return NaN;
    return (a > b) ? Number(a) : Number(b);
}

也可以使用箭頭表達式建立函式物件,與函式表達式最大的差異影響在於this

var myMax = (a, b) => {
    if(isNaN(a) || isNaN(b))
        return NaN;
    return (a > b) ? Number(a) : Number(b);
}

透過這樣建立的函式變數同樣具有變數宣告的特性^1

函式建構式

所有函式都是Function的實例

myMax instanceof Function; // true

實際上建構式就是Function

myMax.constructor === Function; //true

因此實際上也可以利用Function建立函式物件:

    var myMax = new Function("a", "b", `
        if(isNaN(a) || isNaN(b))
            return NaN;
        return (a > b) ? Number(a) : Number(b);
    `)

與函式申明和函式表達式不同,你不能用同樣方式建立閉包。下例參考MDN

var x = 10;

function createFunction1() {
    var x = 20;
    return new Function('return x;'); // 这里的 x 指向最上面全局作用域内的 x
}

function createFunction2() {
    var x = 20;
    function f() {
        return x; // 这里的 x 指向上方本地作用域内的 x
    }
    return f;
}

var f1 = createFunction1();
console.log(f1());          // 10
var f2 = createFunction2();
console.log(f2());          // 20

一般函式呼叫方式

你可以括號加上參數執行函式:

myMax(1, 2); // 2

Function.prototype.call()

或是使用Function.prototype.call()

myMax.call(null, 1, 2); // 2

實際上myMax.call()Function.prototype.call()是相同函式物件

myMax.call === Function.prototype.call; // true

只是綁定的this不同,因此其實可以綁完this後再呼叫函式

var fn1 = myMax.call.bind(myMax);
var fn2 = Function.prototype.call.bind(myMax);

fn1(null, 1, 2); // 2
fn2(null, 1, 2); // 2

Function.prototype.apply()

也可以使用Function.prototype.apply()

myMax.apply(null, [1, 2]); // 2

實際上myMax.apply()Function.prototype.apply()是相同函式物件

myMax.apply === Function.prototype.apply; // true

只是綁定的this不同,因此其實可以綁完this後再呼叫函式

var fn1 = myMax.apply.bind(myMax);
var fn2 = Function.prototype.apply.bind(myMax);

fn1(null, [1, 2]); // 2
fn2(null, [1, 2]); // 2

.call().apply()的差異

.call()接收一個thisArg和多個參數展開;.apply()接收一個thisArg和一個參數陣列:

myMax.call(null, 1, 2); // 2
myMax.apply(null, [1, 2], 3); // 2

.call()除了多一個thisArg,其他和一般函式呼叫很像。

.apply()除了thisArg,只再多接受一個參數陣列,而且必須式陣列類型。

使用Array.prototype.reduce()

如果要從一個數字陣列中找到最大值,我們可以不斷的兩個兩個比較,保留當次比較的最大值,再跟下一個比較。這又有另外一個名字:folding摺疊。

image alt

var args = Array(15).fill(0);
args.forEach((arg, i, arr) => arr[i] = Math.floor(Math.random()*50));

var result = args.reduce(myMax);

上面結果與下面寫法的到的結果(result)是一致的:

var result = args[0]

for(let n of args) {
    result = myMax(result, n)
}

不過,有些情況myMax()Math.max()使用起來差不多:

myMax(1, 2); // 2
Math.max(1, 2); // 2

但是Math.max()使用Array.prototype.reduce卻會出問題:

console.log(args.reduce(myMax)); // 得到正確結果
console.log(args.reduce(Math.max)) // 得到 NaN

是的...這讓我蠻震驚的!這才去看了MDN的說明,才發現reduce和通常的functional programming有些不同。

那是因為Array.prototype.reduce()方法的簽名長成這樣:

arr.reduce(callback[accumulator, currentValue, currentIndex, array], initialValue)

對於callback他一定會傳入四個參數: accumulator, currentValue, currentIndex, array。和其他Array方法一樣,會傳入當前的index以及當前的array。以上例來說:當前的array就是args

由於myMax()只用到了兩個參數所以沒甚麼問題。但是Math.max()是支援不定長參數(剩餘參數)的:

Math.max(1, 2, 3, 4); // 4

因此傳入的四個參數都會嘗試判斷出最大的數字。

Math.max(args[0], args[1], 1, args); // 4

但由於傳入的第四個參數argsArray類型,並無法比較,所以最後才會得到NaN

小總結

以上分享了函式的建立方式,包含:

  • 函式申明
  • 函式表達式
  • 函式建構式

以及函式呼叫執行的方式:

  • 一般函式呼叫
  • Function.prototype.call()
  • Function.prototype.apply()
  • Array.prototype.reduce()

還分享了.call().apply()的使用差異。以及你可能不知道的Array.prototype.reduce()技術細節。

參考資料

本文同時發表於我的隨筆


上一篇
你可能不知道Array.prototype.forEach()沒跟你說的事情
下一篇
為什麼你需要知道Function的三種用法
系列文
這些那些你可能不知道我不知道的Web技術細節33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言