網易前端微專業的一道js題求解?


題目有問題。

add() 返回的到底是函數還是數字?顯然你只能選擇一個。

一種勉強的方法是返回函數,且讓此函數對象的 valueOf() 返回數字結果。這樣可以做到 add() == number 的效果,但僅限於 == 而不能是 === 。這種方式在真實的代碼里是不可取的。


function add(a) {
var sum = a
function f(b) {
sum += b
return f
}
f.toString = function() { return sum }
return f
}

alert(add(10)(20)(30)) //60

//看結果的時候必須用alert ,因為alert接收的必須是字元串,所以它會後台自動調用toString()方法


我覺得從代碼來看, @skyler 的代碼已經夠好了。有個破壞習慣的瑕疵是,add(x) 返回的結果會被自己的調用改變,但有些時候這正是被需要的 ,題目本身也沒規定:

var acc = add(10)(20)(30)
alert(acc) //60
alert(acc(10)) //70 這時acc的語義改變了
alert(acc) //70

不想要這個瑕疵的可以看 @欲三更的答案。 但是我會告訴你面試的時候我喜歡更簡單的代碼嗎? 摔~

有個無傷大雅的瑕疵是,按照題意toString 改成 valueOf 更好。(這也是看了其他人的答案揣測的題意)

這裡給一個 lazy evaluation 的答案,回答 @欲三更的補充問題:

function add(a) {
function f(b) {
var exec = f.valueOf
f.valueOf = function() { return (exec() + b) }
return f
}
f.valueOf = function() { return a }
return f
}

add(1)(3) == 4
//true
add(1)(3)(2) == 6
//true
add(1)(3)(2)(-1) == 5
//true
add(1) == 1
//true

調用add的結果,並無副作用的答案

function add(a) {
function mid(f,b){
function r(c){
return mid(r,c)
}
r.valueOf = function(){
return f.valueOf() + b
}
return r
}
function start(b){
return mid(start,b)
}
start.valueOf = function() {return a}
return start
}

var acc = add(10)(20)(30)
acc==60 //true
acc(10)==70//true 這時acc的語義未改變
acc==60 //true

這裡想說的是,這個問題就是問:「用閉包保存狀態,你會么?」 說起來思想上來看可能和函數編程中的monads更像一點。(不過那畢竟是純函數的東西,也不是那麼好類比。)這根本和柯里化沒半毛錢關係。對於那些說柯里化的答主請問被柯里化的函數在哪裡?寫代碼不要捨近求遠,過早的拿各種編程概念裝逼會變成枷鎖。


為什麼valueOf比toString好呢?貼一段v8代碼

Object -&> Primitive的過程:

如果是Date類型的話,走DefaultString,否則走DefaultNumber

DefaultNumber會判斷Object有木有valueOf方法,有的話,直接調用return,沒有的話去判斷toString方法

/* -------------------------------------
- - - C o n v e r s i o n s - - -
-------------------------------------
*/

// ECMA-262, section 9.1, page 30. Use null/undefined for no hint,
// (1) for number hint, and (2) for string hint.
function ToPrimitive(x, hint) {
// Fast case check.
if (IS_STRING(x)) return x;
// Normal behavior.
if (!IS_SPEC_OBJECT(x)) return x;
if (IS_SYMBOL_WRAPPER(x)) throw MakeTypeError("symbol_to_primitive", []);
if (hint == NO_HINT) hint = (IS_DATE(x)) ? STRING_HINT : NUMBER_HINT;
return (hint == NUMBER_HINT) ? %DefaultNumber(x) : %DefaultString(x);
}
// ECMA-262, section 8.6.2.6, page 28.
function DefaultNumber(x) {
if (!IS_SYMBOL_WRAPPER(x)) {
var valueOf = x.valueOf;
if (IS_SPEC_FUNCTION(valueOf)) {
var v = %_CallFunction(x, valueOf);
if (%IsPrimitive(v)) return v;
}

var toString = x.toString;
if (IS_SPEC_FUNCTION(toString)) {
var s = %_CallFunction(x, toString);
if (%IsPrimitive(s)) return s;
}
}
throw %MakeTypeError("cannot_convert_to_primitive", []);
}


如果嚴格按照題目描述,保證沒人能實現。如果 add(1)(2) 返回 3,那 add(1)(2)(3) 怎麼可能返回6?直接報錯了好嗎?

之所以實現不了,是因為 js 不能顯式指定數據類型,也不能自定義類型轉換。要是 js 能這麼寫的話,就能實現:

int a = add(1)(2)(3);

最接近的結果,也不過是 add(1)(2)(3)() 返回 6 而已。具體寫法待我打開電腦。

==== 電腦打開了 ====

問題不難,但是我想寫的詳細點,把思路寫清楚。

首先咱們得明白一件事——add 函數及其返回值是無狀態的,所以不能把任何中間結果儲存在http://add.xxx或者全局變數中。那想必是要通過閉包儲存中間結果了。

我們先不考慮返回計算結果,只考慮返回函數,憑直覺寫一個最簡單的「描述」代碼:

function add(a) {
return function (b) {
return a + b;
}
}

這樣實現顯然不行,只能add(1)(2),再調用就不行了。那看來這裡應該是個遞歸調用。改一改:

function add(a) {
return function (b) {
return add(a, b);
}
}

可是add不接受多個參數,再改改:

function add(a, b) {
var s = a + b;
return function (c) {
return add(s, c);
}
}

哎呀~ 已經很靠譜了嘛。但是語法不符合題目要求,題目要求 add 只能有一個參數。兩個參數變一個參數,js 最擅長。於是改成這樣:

function add(a) {
function add_(a, b) {
var s = a + b;
return function (c) {
return add_(s, c);
}
}

return add_(0, a);
}

相當靠譜了!最後的問題就是怎麼通過一個無參數調用把結果返回出來,比如add(1)(2)() === 3。這個簡單,檢測一下參數就行了。

最終代碼如下:

function add(a) {
if (typeof a === "undefined") {
return 0;
}

function add_(a, b) {
var s = a + b;

return function (c) {
if (typeof c === "undefined") {
return s;
}

return add_(s, c);
}
}

return add_(0, a);
}

console.log(add());
console.log(add(1)());
console.log(add(1)(2)(3)());

思考題:

上面的方法是每次有參數調用都把中間結果算出來,無參數調用時返回。另有一種方法是把所有的加數存下來(不使用全局變數),最後無參數調用時全部加起來返回,請問如何實現?

答案:gist

擴展討論:

考慮一般情況:假設函數 f(a, b) 參數和返回值都是 number,那麼如何構造函數 g,使其滿足:

g() === 規定值;
g(a)() === f(g(), a);
g(a)(b)() === f(a, b);
g(a)(b)(c)() === f(f(a, b), c);
……

條件?

參照上面的寫法,很容易實現:

// g函數生成器,r為規定值
function generate(f, r) {
return function (a) {
if (typeof a === "undefined") {
return r;
}

function f_(a, b) {
var s = f(a, b);

return function (c) {
if (typeof c === "undefined") {
return s;
}

return f_(s, c);
}
}

return f_(r, a);
}
}

// 使用舉例

function f(a, b) {
return a * b;
}

var g = generate(f, 1, 1);

console.log(g());
console.log(g(1)());
console.log(g(1)(2)());
console.log(g(1)(2)(3)());
console.log(g(1)(2)(3)(4)());

我把加法改成了乘法,原理不變。問題來了——一眼能看清楚這裡面邏輯的請舉手?沒有吧?Ok,我們得化簡一下。先觀察兩處參數判斷,實際上都可以放在 f_ 函數中進行(不贅述了,見完整代碼gist)。再觀察,generate 的返回值和 f_ 函數顯然是個柯里化的關係,而 f_ 的返回值和 f_ 本身,也是柯里化關係,所以我們的代碼一定是冗餘的。假設我們已經擁有了curry函數實現柯里化,去掉參數判斷,核心邏輯就是這樣:

function generate(f, r) {
function f_(a, b) {
return curry(f_, f(a, b));
}
return curry(f_, r);
}

generate函數完整代碼:gist

所以,為啥有人會提到柯里化呢?就是上面的原因,這段代碼的核心變換抽取出來,就是柯里化的應用。注意其中的 f_ 函數,它是一個不變的結構,它能把一個二目運算函數轉化為一個連續運算函數。


這道題目前幾天在segmentfault上有過討論,其實是函數的柯里化問題:

Javascript 連續調用單參函數實現任意參函數

關於柯里化可以參考這篇文章:

函數式JavaScript(4):函數柯里化


沒看答案自己試著寫了下,最還是終寫出來了(本來想過來裝逼一下,我早該想到知乎上牛逼的人很多,還是匿了吧):

function add(num) {
var result = num;

function plus(num) {
result = result + num;
return plus;
}

plus.toString = function() {
return result;
};

return plus;
}

alert("add(20)(20) = " + add(20)(20));
alert("add(10)(20)(50) = " + add(10)(20)(50));
alert("add(10)(20)(50)(100) = " + add(10)(20)(50)(100));

// 除此之外,還可以
var num = add(20)/2;
alert(num); // 10

因為返回的結果可能是計算的結果,也可能是一個函數,所以利用對象toString來自動識別使用場景。。


修改了下,把計算結果掛到函數上

只能用 == 比較,調用==做了一個隱式子轉換,調用了valueOf

function add(arg){
add.r = arg + add.r||0
add.valueOf=function(){
var t=add.r
add.r=0
return t
}
return add
}

add(10)(2) == 12
add(1)(2)(3) == 6


Curry化技術是一種通過把多個參數填充到函數體中,實現將函數轉換為一個新的經過簡化的(使之接受的參數更少)函數的技術.

挺有意思的,我來寫一個

function curry(fn){
var value = undefined;

var callback = function(next){
value = typeof value === "undefined" ? next : fn.apply(null,[value,next]);
return callback;
}
callback.valueOf = callback.toString = function(){
return value;

}
return callback
}
//加
function add(x,y){
return x + y
}
//減
function minus(x,y){
return x -y
}
//乘
function time(x,y){
return x * y;
}
//除
function divide(x,y){
return x / y;
}
curry(add)(2)(3)(4)(5)(6) //2+3+4+5+6=20
curry(minus)(2)(3)(4)(5)(6) //2-3-4-5-6=-16
curry(time)(2)(3)(4)(5)(6) //2*3*4*5*6=720
curry(divide)(2)(3)(4)(5)(6) //0.00555555...


我覺得有可能是面試在試探你能否對權威提出質疑。

要麼是你記錯題目了。


柯里化的js實現


function add(num) {
var value = 0;

value += num;

function f(num) {
value += num;

return f;
}
f.valueOf = function () {
return value;
}
f.toString = function () {
return value.toString();
}

return f;
}

add(2); //2
add(2)(3); //5
add(2)(1)(3); //6


var add = function (digit) {
var sum = 0;
var t = function (digit2) {
sum+=digit2;
console.log(sum);
return arguments.callee;
}
t(digit);
return t;
}
// 貌似不太符合題主的要求
// 輸出:
// add(10) 10
// add(10)(10) 10,20
// add(10)(20)(30) 10,30,60


想法和@欲三更一樣, 沒法做到 add(1)(2) === 3 又 add(1)(2)(3) === 6, 那麼,

  • 要麼退而求 add(1)(2) == 3 或 add(1)(2).toString() === "3" (即用 valueOf 或 toString),

  • 要麼使 add(1)(2)() === 3.

用 ES6 來湊個熱鬧.

前一種, valueOf 和 toString:

let add = (n = 0) =&> {
let ret = (m = 0) =&> add(n + m);
ret.toString = () =&> n.toString();
ret.valueOf = () =&> n;
return ret;
};

後一種:

let inc = (a, b) =&> a + b,
sum = (...numbers) =&> numbers.reduce(inc);
let add = (...numbersOld) =&> {
let n = sum(...numbersOld);
return (...numbers) =&> numbers.length === 0 ? n : add(n + sum(...numbers));
};

至於柯里化,

let curry = (f, ...h) =&> (...t) =&> f(...h, ...t);

用法舉例:

let inc = (a, b) =&> a + b,
inc1 = curry(inc, 1);

起到的效果是 curry(f, ...h)(...t) === f(...h, ...t), 和這裡情況並不一樣.


丁磊只會養豬

——玉伯


推薦閱讀:

JS如何跨域操作DOM?
學習js看書籍好還是上機直接敲代碼好?
前端在什麼情況下應該跳槽?
怎麼才能在四個月內把web前端學好學深入?
如何看待Apache再次接受阿里開源產品捐贈 移動開發框架Weex進入孵化

TAG:前端開發 | JavaScript | 閉包 |