在 JS 中實作矩陣

js 中只有一維陣列, 而且它其實不太像陣列,比較像物件。 總之,我因為 ~航遙測概論~ 的作業, 需要寫一些有關矩陣的程式, 至少需要矩陣相乘的函數, 所以就實作了一個矩陣的物件。 (其實只有 3x3 方陣。)

一開始我是用 js 傳統的建構函數做的, 就是要搭配 new 關鍵字使用的建構函數。 後來發現一些問題,需要從不同的來源產生矩陣, 或產生不同的矩陣;所以我需要各式各樣的建構函數。 覺得傳統建構函數不能滿足, 就試著用原型導向重寫了一遍。 不過這不是本文的重點,請見下一篇 js 各類物件繼承的問題

矩陣與陣列

我本來是用 coffee script 的 class 與 extends 關鍵字定義陣列, 就當成一個陣列,三個元素分別也是陣列, class Matrix3 extends Array 可以定義繼承自陣列, 但後來發現這樣沒有正確繼承到。

而且一維陣列版的 forEach 不太適合矩陣, 在定義 callback 時還得巢狀一個 forEach 很不方便。 想到既然沒有要用到陣列的 forEach, 且大部份一維陣列的 method 都不起作用了, 乾脆不要繼承陣列好了,只要 row 是陣列就好了。

後來從原型思考時,才想到可以直接把 Matrix.prototype 換掉,直接換成一個陣列實例, 看要定義什麼 method,就直接定義在她上面。

又後來看了 ES6 才知道什麼叫類陣列結構, 要有 length 屬性才能算類陣列, 所以之前一些陣列操作都結果都很奇怪。 定義 length 後就好了。

高階迴圈

我剛認識了高階的陣列操作, 像 map、reduce 等無副作用的, 或像 forEach,為了解決 JS 的 for 迴圈問題而生的。 運用 js 的 callback 特性,可以實作出 forEach; 再加上閉包,就能用 forEach 再實作出 map、reduce。 於是我就用實作一個屬於矩陣的 forEach 來練習。

forEach 我以為是因為 js 缺乏 block scope, 在迴圈中的變數不能被隱藏, 所以就把要對陣列作的事寫成函數, forEach 就是對陣每一項都呼叫該函數。 但依據 js perf 的測量結果, forEach 的效率遠差於真正的 for, 畢竟要對每一項都呼叫一次函數。

forEach 的 callback 會收到 3 個參數, 該項目的值、索引、陣列本身。 一般只會用到值,所以傳入的函數只要單參, 少數情況會用到第二個,極少數會用到第三個。

因為矩陣是二維的,不能用純量表示索引, 所以把矩陣的索引值定義成一個行數與列數的陣列, 其它則不變。 js 的 reduce 慣例則是把結果放在第一個參數, 剩下的三個參數堆在後面。

map 最難搞,因為要回傳一個新的矩陣, 所以要先 new 或 Object.create 一個新的, 我的作法是作一個新的, 再用舊的 forEach 加閉包對新的一一賦值。

this.map = function (callback) {
    var m = this.create()
    this.forEach(function (x,i,m) {
        m[i[0]][i[1]] = callback(x,i,m)
    })
    return m
}

然後後面實作時都不用 for 遍歷元素, 只用 forEach。

還發現一件事,map 超級常用, 幾乎所有創建矩陣都是用 map 實現, 這樣程式碼少,寫起來又乾淨。 有時候不見得新矩陣和舊矩陣有關, 只是用 map 已經內建創新矩陣的功能, 還能對不同位置賦值。

唯一有點擔心是 map 是又去調 forEach, 會不會太多層效率不彰。

this.createIdentity = function () {
    return @map(function(x,i){
        return (i[0] == i[1]) ? 1 : 0
    })
}

矩陣乘法

我最後採用的作法是寫一個 multiply 方法, 根據傳入的參數型態,再調用 multiplyNumber、 multiplyVector、multiplyMatrix。 而這三個也都是公有方法,可以直接調用。 又因為都是返回一個新矩陣,可以鏈式調用: m1.mulitply(-1).multiply(m2).multiply(v1)

也許 add 方法如果傳入純量,也該對所有元素加上純量, 但目前還沒實現。 另外也沒有 minus,只能乘上 -1 再用 add 的。

多型

還覆寫了一些函數,像 toString、slice。 toString 會把矩陣轉成字串, slice 則返回一個一維陣列。


這個過程很有趣,比那些示範物件導向時常舉的例子像 animal、cat、human 有趣多了。 唯一問題是需要比較純熟的技巧,與對語言的理解; 但也就是這些 hack 的過程造就了樂趣。

讓我想起有人批評過 js 社群會把 理解語言的缺陷當成技巧在炫耀與吹捧, 因為這真得是有成就感的事。 也只有 js 有這種問題,因為 js 真得是一門有缺陷的語言。

js 有時讓你感到礙手礙腳,在其它語言, 這代表這種語言不適合作這種事; 在 js 中,代表你應該定義一個物件來封裝這件事。 因為 js 是函數導向、物件導向, 有像 lisp 一樣神奇的可擴展性。