Long.js原始碼分析與學習

黃Java發表於2018-02-18

背景

由於在專案中使用到了WebSocket的自定義二進位制協議,需要將二進位制轉為後端服務中定義的Long型。而在JavaScript中的Number型別由於自身原因,並不能完全表示Long型的數字,因此需要我們通過其他的方式來對Long型值進行儲存。

目標

在GitHub中,有一個實現了在JavaScript中儲存Long型的物件,具體程式碼可以戳此。下面,我們通過簡單講解一下這個庫的具體實現來看看如何在JavaScript中實現一個Long型。如果你瞭解了這個實現原理,那麼與之類似的,在JavaScript中實現一個Long Long型或者其他型別的方法也是類似的。

具體實現

其實,Long的實現很簡單,我們現在只要迴歸到計算機的本質即可。在計算機中,其實儲存的都是01字串。例如,Int佔4個位元組(我們以32位作業系統為例),而Long則佔8個位元組。

我們在儲存中只需要將資料通過二進位制進行儲存,然後在操作中對二進位制進行操作即可。

下面我們簡單的來介紹一下Long的各個代表操作和思想。

大致步驟

資料儲存

在Long型物件中,我們採用了高32位和低32位,以及加上一個符號位判斷的值,用來進行資料的儲存,具體格式如下:

function Long(low, high, unsigned) {
    this.low = low | 0;
    this.high = high | 0;
    this.unsigned = !!unsigned;
}
複製程式碼

通過對高低位的儲存,從而讓兩個Number來同時表示一個Long型的高位和低位,從而滿足了數值的長度要求。

轉換為Long型

我們目前只介紹一個通過字串來講資料從String型轉換為Long型,其他的轉換例如從Number轉換為Long型是類似的,我們就不過多贅述了。

先看實現函式:

function fromString(str, unsigned, radix) {
	// 處理異常情況
    if (str.length === 0)
        throw Error('empty string');
        
    //處理為0的情況
    if (str === "NaN" || str === "Infinity" || str === "+Infinity" || str === "-Infinity")
        return ZERO;
        
    //處理只有兩個引數的情況
    if (typeof unsigned === 'number') { 
        // For goog.math.long compatibility
        radix = unsigned,
        unsigned = false;
    } else {
        unsigned = !! unsigned;
    }
    radix = radix || 10;
    if (radix < 2 || 36 < radix)
        throw RangeError('radix');

    var p;
    if ((p = str.indexOf('-')) > 0)
        throw Error('interior hyphen');
    else if (p === 0) {
        // 轉為正值處理
        return fromString(str.substring(1), unsigned, radix).neg();
    }
    
    // 從最高位分8位處理一次,如果長度超過8位,則先處理高位,然後將高位直接乘以進位制的8次方,再處理低後8位,迴圈到最後8位為止
    var result = ZERO;
    for (var i = 0; i < str.length; i += 8) {
        var size = Math.min(8, str.length - i),
            value = parseInt(str.substring(i, i + size), radix);
        if (size < 8) {
            var power = fromNumber(pow_dbl(radix, size));
            result = result.mul(power).add(fromNumber(value));
        } else {
            result = result.mul(radixToPower);
            result = result.add(fromNumber(value));
        }
    }
    result.unsigned = unsigned;
    return result;
}
複製程式碼

下面我們簡單的說下這個函式的實現:

  1. 對資料進行異常處理,排除一些邊界條件。
  2. 如果字串為一個帶"-"號的值,則轉換為正值進行處理。
  3. 如果字串為一個常規的Long型值,則先從最前面的8位開始處理,將其通過指定的進位制轉換為Long型的值。
  4. 處理接下來的8位,並且將之前的結果乘以進位制數的8次方,即數字高地位的合併。例如:18 = 1 * 10^1 + 8。
  5. 迴圈上面的操作,直到剩餘的字串長度小於8為止,即可結束,得到轉換之後的Long型。

轉換為字串

Long型轉換為字串的方式,與字串轉換為Long型的步驟差不多,差不多是一個相反的過程。

LongPrototype.toString = function toString(radix) {
    radix = radix || 10;
    if (radix < 2 || 36 < radix)
        throw RangeError('radix');
    if (this.isZero())
        return '0';
    //如果是負值,Unsigned型的Long值永遠不會為負值
    if (this.isNegative()) {
        if (this.eq(MIN_VALUE)) {
            // We need to change the Long value before it can be negated, so we remove
            // the bottom-most digit in this base and then recurse to do the rest.
            var radixLong = fromNumber(radix),
                div = this.div(radixLong),
                rem1 = div.mul(radixLong).sub(this);
            return div.toString(radix) + rem1.toInt().toString(radix);
        } else
            return '-' + this.neg().toString(radix);
    }

    //每次處理6位,處理方式與字串轉換過來是類似的,和數學中十進位制轉換為N進位制方法相同——相除法
    // Do several (6) digits each time through the loop, so as to
    // minimize the calls to the very expensive emulated div.
    var radixToPower = fromNumber(pow_dbl(radix, 6), this.unsigned),
        rem = this;
    var result = '';
    while (true) {
        var remDiv = rem.div(radixToPower),
            intval = rem.sub(remDiv.mul(radixToPower)).toInt() >>> 0,
            digits = intval.toString(radix);
        rem = remDiv;
        if (rem.isZero())
            return digits + result;
        else {
            while (digits.length < 6)
                digits = '0' + digits;
            result = '' + digits + result;
        }
    }
};
複製程式碼

上面這個函式的實現步驟正好相反:

  1. 處理各種邊界條件
  2. 如果Long型為一個負值,則轉換為正值進行處理,如果Long型為0x80000000時,則對它進行了單獨處理。
  3. 在處理正值Long型為字串時,操作方法與我們數學中教的轉換進位制的相除法類似,具體操作為:先除以需要轉換的進位制數,得到結果和餘數,將結果重新作為被除數相除直到被除數為0,再將餘數拼接起來即可。例如:18(10進位制)轉換為8進位制時,操作是:18 = 2 * 8 + 2; 2 = 0 * 8 + 2;,因此結果為0x22。只是,在此函式中,一次相除的是進位制數的6次方,其餘步驟是類似的。
  4. 通過上面的操作得到字串後返回即可。

Long型相加

在知道了Long型的儲存本質是使用高低各32位以後,Long型的運算其實就已經瞭解了。我們只需要針對特定的操作進行相對應的二進位制操作,那麼我們就能夠得到相對應的結果,下面的例項是Long型相加的操作,我們簡單瞭解下:

LongPrototype.add = function add(addend) {
    if (!isLong(addend))
        addend = fromValue(addend);
    // 將每個數字分成4個16位元的塊,然後將這些塊加起來

    var a48 = this.high >>> 16;
    var a32 = this.high & 0xFFFF;
    var a16 = this.low >>> 16;
    var a00 = this.low & 0xFFFF;

    var b48 = addend.high >>> 16;
    var b32 = addend.high & 0xFFFF;
    var b16 = addend.low >>> 16;
    var b00 = addend.low & 0xFFFF;

    var c48 = 0, c32 = 0, c16 = 0, c00 = 0;
    c00 += a00 + b00;
    c16 += c00 >>> 16;
    c00 &= 0xFFFF;
    c16 += a16 + b16;
    c32 += c16 >>> 16;
    c16 &= 0xFFFF;
    c32 += a32 + b32;
    c48 += c32 >>> 16;
    c32 &= 0xFFFF;
    c48 += a48 + b48;
    c48 &= 0xFFFF;
    return fromBits((c16 << 16) | c00, (c48 << 16) | c32, this.unsigned);
};
複製程式碼

通過上面的操作我們就可以知道,Long型的四則運算等操作其實都是通過二進位制和位運算來實現的。並沒有我們想象中的那麼神祕。

總結

其實,通過閱讀Long.js庫的原始碼你就會發現,在JavaScript中實現一個Long型並不難,也許還是一個聽簡單的事情,不過重要的是我們可能想象不到這種的實現方式。因此,這個也證明了我們在思考一個問題問題的同時,我們也應該多從事情的本質來考慮,這樣就有可能得到解決方案。

附錄

  • 我在Long.js的程式碼中新增了一些中文的註釋,如果有需要可以到我folk的倉庫進行閱讀學習。

相關文章