原預計標題「你可能不知道的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
摺疊。
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
但由於傳入的第四個參數args
是Array
類型,並無法比較,所以最後才會得到NaN
。
以上分享了函式的建立方式,包含:
以及函式呼叫執行的方式:
Function.prototype.call()
Function.prototype.apply()
Array.prototype.reduce()
還分享了.call()
和.apply()
的使用差異。以及你可能不知道的Array.prototype.reduce()
技術細節。
本文同時發表於我的隨筆