整理一些刷題時學會的小技巧……
目錄:
- 即大於0又小於0的變數
- String.split() 與 正規表示式
- 快取的幾種方法
- 初始化一個陣列
即大於0又小於0的變數
問題: 設計一個變數val,使得以下表示式返回true:
val<0 && val>0;
邏輯上來說一個數是不可能即大於0又小於0的。我們能做到的只是讓一個變數一會大於0一會小於0.那麼在這個表示式裡是否存在先後順序呢?答案是肯定的。我們先判斷val是否大於0,然後才判斷它是否小於0.不過對於普通的變數而言,這個順序沒有任何意義:普通變數的值不會因為你讀取了它而發生變化。那麼,什麼樣的變數會因為讀取而發生改變呢?
沒錯,就是物件的訪問器屬性。
我們可以宣告一個物件,並給其賦予一個資料屬性,比如 _value = -3, 然後再給它設定一個訪問器屬性 val, gettar引數設定為:當我讀取 val屬性時,_value的值加2,並將 _value 的值返回。這樣一來當我第一次判斷 val<0 時,返回值為 -3+2=-1, 成立。進行第二次判斷時,再一次讀取了它,這次的返回值變成了 -1+2>0, 也成立,於是表示式為真。具體程式碼如下:
var obj = { _value: -3 }; Object.defineProperty(obj, "_val", { get: function() { this._value += 2; return this._value; } ); obj._val<0 && obj._val>0; // return true
到這裡基本完成了任務。不過這個方法還不夠完美。為什麼?看下這個問題的升級版:
問題:設計一個變數,使得以下函式返回true:
function foo(val) { return val<0&&val>0; }
看上去沒怎麼變對不對?可是如果這時候把上面的變數放進去,像這樣:
foo(obj.val); // false
是不行的。因為函式的引數是按值傳遞的。這樣呼叫只會在傳入引數時讀取一次obj.val的值,隨後的比較表示式裡只會將這個值複製過去進行比較,而不會讀取obj的屬性,也就不會觸發gettar函式。這種情況下,val的值最終一直都是-3+2=-1,所以無法通過測試。
解決辦法是傳入一個物件而不是一個值。可是傳入物件後,怎麼比較物件和數值呢?答案是Object.toString()方法。
當我們試圖比較一個物件和一個數的大小時,會呼叫物件的toString()方法,並取返回值與數字比較。通常情況下物件的toString方法返回值並不是數字,所以無法比較。在這裡,只要重寫目標物件的ttoString方法,令它返回obj._value即可。當然,也需要用到選擇器屬性。
Object.defineProperty(obj, 'toString', function(){ get: function() { this._value += 2; return this._value; });
如此一來,當我們傳入物件到函式引數時,並不會讀取toString方法,_value保持原始值。直到比較數字和它的大小時,才呼叫toString並返回相應的值。大功告成。
這個特性有什麼用呢?暫時沒想到……不過至少可以加深對於物件屬性的理解。
String.split()與正規表示式
問題:將字串“JavascriptIsSoInteresting”分割為["Javascript","Is","So","Interesting"]
字串的split方法大家都很熟悉,我們可以傳入一個字串引數以對目標字串進行分割,比如:
var str = "a=2&b=3&c=4"; console.log(str.split('&')); // ['a=2','b=3','c=4']
有過進一步瞭解的話,還可以知道它的引數可以是正規表示式,比如:
var str="a=2&b=3#c=4"; var reg = /[&#]/g; str.split(reg); // ['a=2','b=3','c=4']
另外,split還可以接受第二個引數以決定分割的長度,這個比較簡單就不說了。
現在回到問題。要分割題目中的字串,我們缺少一個分割符。那麼一個很直觀的解決方法就是,在每個大寫字母前面插入一個分隔符,然後再呼叫split方法。
var str="JavascriptIsSoInteresting"; var reg = /([A-Z][a-z])/g; str.replace(reg, '&$1').split('&'); // ["", "Javascript", "Is", "So", "Interesting"]
有點瑕疵,前面多了一個空字串,需要再處理下。不過基本的目標算是達成了。
有沒有更好的方法呢?
在這之前我們先考慮一個問題:在用split分割字串的時候,如果字串僅僅由分割符組成,結果會是什麼?比方說,對字串“aaaaa”,進行split('a')的處理,會得到怎樣一個陣列?來看下:
var str="aaa"; str.split('a'); // ['', '', '', '']
結果是由a之間的空字串組成的新陣列,也就是說當分隔符連續出現時,split會把“間隔”作為成員分配到陣列中。
用正規表示式試試看:
var str="aaa"; var reg = /a/g; str.split(reg); // ['', '', '', '']
結果是一樣的。到目前為止一切都很正常。接下來的這個特性才是我們需要用到的。
我們對上面的程式碼做一點小修改:
var str="aaa"; var reg = /(a)/g; str.split(reg); // ["", "a", "", "a", "", "a", ""]
很奇怪對不對?我只是將正規表示式用子表示式的括號括了起來,理論上沒有使用子表示式的情況下應該和上面的沒什麼區別,但是當它跑到split裡面時,奇蹟就出現了:不僅原先的分割結果還在陣列裡,本該不存在的分隔符也回來了。
經試驗證明,當split的引數是正規表示式,並且正規表示式裡包含了子表示式,那麼子表示式內的分隔符將保留在結果陣列中,而不是通常的忽略。
將這個神奇的現象應用到題目中,我們把“一個大寫字母加上若干個小寫字母”作為分隔符,並給它加上括號:
var str="JavascriptIsSoInteresting"; var reg = /([A-Z][a-z]+)/g; str.split(reg); // ["","Javascript","", "Is", "", "So", "", "Interesting"]
最後還需要去除空字串,可以使用filter方法:
str.split(reg).filter(function(val){return /\S/.test(val);});;
或者還可以更優雅一些:
str.split(reg).filter(Boolean);
這個特性的用處麼,比如你想寫一個程式碼直譯器,對於 “1+20+x+y”這樣的輸入,可能需要將它分解成["1","+","20","+","x","y"]時,就可以這麼辦了。
快取的幾種方法
問題:令以下函式返回true:
function foo() { return Math.random()*Math.random()*Math.random===0.5; }
顯然這個函式幾乎不可能返回true。其實Math.random()只是個幌子,為了達到目的我們勢必要重新Math.random()。問題就在於,怎麼寫。
很簡單地,只需要寫一個函式,返回值是0.5的三次根式就可以了。不過這個返回值不太好求。所以我決定定義一個函式,第一次呼叫時返回0.5,之後每次呼叫返回都是1.
var val = 0; function m() { if(val != 1) val += 0.5; return val; }
當然,全域性變數是魔鬼,所以需要把val封裝在一個閉包裡:
function v() { var val = 0; return function() { if(val != 1) val += 0.5; return val; } } var m = v();
m()*m()*m();
// return 0.5
這就是第一種方式。
其實說道這裡是不是覺得有點熟悉?沒錯這和我們的第一個問題其實很相似,同樣可以用物件的訪問器屬性解決。
var obj = { _value: 0 }; Object.defineProperty(obj,'val',{ get: function(){ if(this._value != 1) this._value+= 0.5; return this._value; } }); var i = function() { return obj.val; } m()*m()*m(); // return 0.5
最後一種方法,我們可以不用全域性變數,也不用閉包。把函式本身看成一個物件即可:
function m() { if(this.val != 1) this.val+=0.5; return this.val; } m.val = 0;
m()*m()*m()
// return 1
到目前為止這題貌似和快取沒有多大關係。其實只要把上面作為儲存資料的value改成一個陣列/物件,對value的操作改成為其新增一個元素,那麼它就可以作為快取使用了。
初始化陣列
問題:宣告一個長度為給定值的陣列,並初始化所有元素為0.
當然,這可以用for迴圈來做:
var arr = []; for(var i=0; i<n; i++) { arr[i] = 0; }
不過我們還可以做得更酷一點:
var n = 4; arr = Array(n+1).join('0').split('').map(Number); // [0,0,0,0]
像這樣,用一行語句就初始化了一個全為0的陣列。
不過有個缺點:如果我想初始化的值不是個位數,比如說都是12呢?
很簡單,記得上面說過的split方法了麼?可以這麼做:
var n = 4; arr = Array(n+1).join('12').split(/(12)/).map(Number).filter(Boolean); // [12,12,12,12]
其中正則部分還可以稍微改一改,改成以長度劃分,會更優雅些:
var n = 4; arr = Array(n+1).join('1222').split(/(\d{4})/).map(Number).filter(Boolean); // [1222,1222,1222,1222]
上面的方法後來考慮了一下,感覺有點繞,為何我要把一個Array先分割成字串然後再合併成陣列呢?不能直接就用map麼,像這樣:
Array(n+1).map(function(){return 0;});
試了下發現不行,對於用Array(n)這種形式建立的陣列,不管怎麼用map每個元素都仍然是undefined。
但是,
這個思路還是有擴充的餘地的,比如如果我們要宣告一個4*4的二維陣列,可以這麼做:
var n = 4; var arr = Array(n+1).join('0').split('').map(function(v){ return Array(n+1).join('0').split(''); });
更多維數可以繼續巢狀下去……
最後要考慮的問題是:這樣做雖然很酷,但是有沒有必要呢?引用了一大堆方法感覺速度會很慢。
試一試:
var arr1 = []; var arr2 = []; var n = 1e6; console.time("for"); for(var i=0; i<n; i++) arr1[i] = 0; console.timeEnd("for"); console.time("Array"); arr2 = Array(n+1).join('0').split('').map(Number); console.timeEnd("Array");
輸出是:
for: 944.83ms Array: 170.69ms
哎喲效能也不錯的樣子。上面是在Firebug下執行的資料。切換到本地node.js試試看:
for: 22.448ms Array: 124.617ms
……
所以在瀏覽器端執行程式碼時,放心大膽地用吧,瀏覽器對這些原生方法做的優化簡直不要太厲害。
而如果是在後端執行的,或者想用這些方法糊弄過leetcode的程式碼複雜度檢測的(比如我),放棄這些想法吧……
篇幅有點長了……就此打住……