繼承數(shù)組的那些事

1、為什么要繼承數(shù)組


我們可以定義“數(shù)組子類”作為創(chuàng)建從原生數(shù)組對象(在其原型鏈中具有 Array.prototype)繼承的對象的過程,并遵循與原生數(shù)組相似(或相同)的行為。

關(guān)于類似于原生數(shù)組的行為非常重要,我們后面會看到。 擁有數(shù)組的“子類”可以被認(rèn)為能夠創(chuàng)建一個數(shù)組對象,而不是直接從 Array 繼承的對象,而是從另一個對象繼承,然后才從 Array 繼承。

換句話說,我們需要類似這樣的行為:

var sub = new SubArray(1, 2, 3);
sub; // [1, 2, 3]
sub.length; // 3
sub[1]; // 2
sub.push(4);
sub; // [1, 2, 3, 4]
// 等等.
sub intanceof SubArray; // true
sub intanceof Array; // true

注意 SubArray 構(gòu)造函數(shù)如何創(chuàng)建一個與數(shù)組行為相同的子對象(對象具有 “l(fā)ength” 屬性,數(shù)字 “0”,“1”,“2” 屬性,并繼承 Array.prototype 上的方法)。 同時,SubArray 是直接繼承的子對象,而不是 Array 。

那么做這一切的目的究竟是什么? 為什么以這種方式對數(shù)組進(jìn)行繼承?

通常有兩個原因:

  • 避免污染全局

利用 Javascript 原型擴(kuò)展數(shù)組對象方法很方便。 如下代碼:

Array.prototype.last = function () {
    return this[this.length - 1];
};
// ...
[1, 2, 3].last(); // 3

但是,擴(kuò)展 Array.prototype 有代價的。當(dāng)腳本與應(yīng)用程序中的其他腳本共存時,這些腳本有可能相互沖突。擴(kuò)展 Array.prototype 雖然誘人并且看起來很有用,但不幸的是在多樣化的環(huán)境中不是很安全。不同的腳本可能最終定義相同名稱的方法,但具有不同的行為。這種情況往往會導(dǎo)致不一致的行為和難以追蹤的錯誤。

使用 Array 以外的構(gòu)造函數(shù) - 但具有相同的行為 - 可以避免這種沖突。不是擴(kuò)展 Array.prototype,而是擴(kuò)展另一個對象(比如 SubArray.prototype),然后用來初始化(子)數(shù)組對象。任何依賴 Array.prototype 方法的第三方代碼仍然能夠安全地使用它們。

  • 繼承數(shù)組的數(shù)據(jù)結(jié)構(gòu)方法

繼承數(shù)組的另一個原因是能夠使用從數(shù)組繼承的數(shù)據(jù)結(jié)構(gòu)方法; 例如 Stack,List,Queue,Set (push,pop,shift,unshift 等)等方法。

2、天真的做法


我們可以使用原型式克隆方法:

function clone(obj) {
    function F() { }
    F.prototype = obj;
    return new F();
}

然后設(shè)置如下的繼承:

function Child() { }
Child.prototype = clone(Parent.prototype);

這里的原型鏈:
 new Child() 
     | 
     | [[Prototype]] 
     | 
     v
 Child.prototype
     |
     | [[Prototype]]
     |
     v
 Parent.prototype
     |
     | [[Prototype]]
     |
     v
 Object.prototype
     |
     | [[Prototype]]
     |
     v
    null

開始實現(xiàn)繼承數(shù)組:

function SubArray() {
    // 將傳遞給構(gòu)造函數(shù)的任何參數(shù)添加到實例中
    this.push.apply(this, arguments);
}
SubArray.prototype = clone(Array.prototype);
var sub = new SubArray(1, 2, 3);

3、天真方法的問題


那么使用克隆方法繼承數(shù)組究竟有什么錯誤? 讓我們來看看之前聲明的 SubArray 函數(shù)的行為。 我們將使用原生數(shù)組對象來進(jìn)行比較。

var arr = new Array(1, 2, 3);
var sub = new SubArray(1, 2, 3);
arr.length; // 3
sub.length; // 0 (in IE<8)
arr.length = 2;
sub.length = 2;
arr; // [1, 2]
sub; // [1, 2, 3]
arr[10] = 'foo';
sub[10] = 'foo';
arr.length; // 11
sub.length; // 2
Object.prototype.toString.call(arr); // [object Array] 
Object.prototype.toString.call(sub); // [object Object]

這里顯然有些不一致。 即使我們忽略 IE < 8 中的錯誤。 但是,數(shù)組中的長度和數(shù)字屬性之間的這種奇怪的關(guān)系是什么? 為什么不是和 Array 的行為相同? 為了理解這一點,我們需要查看 JavaScript 中的數(shù)組對象。

4、數(shù)組的特殊之處


在 Javascript 中的數(shù)組幾乎就像普通的 Object 對象,除了行為上的一點小差異。 如下引自 es 規(guī)范

Array objects give special treatment to a certain class of property names. A property name P (in the form of a string value) is an array index if and only if ToString(ToUint32(P)) is equal to P and ToUint32(P) is not equal to 2^32 - 1. Every Array object has a length property whose value is always a nonnegative integer less than 2^32. The value of the length property is numerically greater than the name of every property whose name is an array index; whenever a property of an Array object is created or changed, other properties are adjusted as necessary to maintain this invariant. Specifically, whenever a property is added whose name is an array index, the length property is changed, if necessary, to be one more than the numeric value of that array index; and whenever the length property is changed, every property whose name is an array index whose value is not smaller than the new length is automatically deleted. This constraint applies only to properties of the Array object itself and is unaffected by length or array index properties that may be inherited from its prototype.

可以概括為:數(shù)組對象以特殊的方式處理 “numeric” 屬性。 只要這些屬性發(fā)生變化,數(shù)組的 “l(fā)ength” 屬性的值也會被調(diào)整; 它的調(diào)整是為了確保它總是比數(shù)組的最大索引大 1 。 類似地,當(dāng)“長度”屬性發(fā)生變化時,“numeric” 屬性會相應(yīng)地進(jìn)行調(diào)整。

4.1、當(dāng)創(chuàng)建數(shù)組對象時,其 “l(fā)ength” 屬性設(shè)置為比數(shù)組最大索引大 1 。

var arr = ['x', 'y', 'z'];
arr.length; // 3 
arr = ['foo'];
arr.length; // 1

4.2、當(dāng) “numeric” 屬性發(fā)生變化時,“長度”也會發(fā)生變化 - 以保持比最大索引大 1 的關(guān)系。

var arr = ['x', 'y'];
arr.length; // 2
arr[2] = 'z'; 
arr.length;

4.3、當(dāng)“l(fā)ength”屬性改變時,“numeric” 屬性會進(jìn)行調(diào)整,使得最大索引比“l(fā)ength”的值小 1 。

var arr = ['x', 'y', 'z'];
arr.length = 2;
arr; // ['x', 'y']
arr.length = 4;
arr; // ['x', 'y']   // “增加”長度不會影響數(shù)字屬性...
arr.join(); // "x,y,,"   // 但在其他情況下可以看到后果,例如使用 `Array.prototype.push` 時
arr.push('z');
arr; // ['x', 'y', undefined, undefined, 'z']

現(xiàn)在你知道 Javascript 中的 Array 對象的“特殊”之處了,它處于 “l(fā)ength” 和 “numeric” 屬性之間的關(guān)系中。 還有一個值得注意的細(xì)節(jié)是數(shù)組的 “l(fā)ength” 屬性必須總是具有小于 2 ^ 32 的非負(fù)整數(shù)值。 只要違反這個條件,就會引發(fā) RangeError 。

var arr = [];
arr.length = Math.pow(2, 32); // RangeError
arr.length; // 0 (長度仍然是0,就像它最初一樣)
arr.length = Math.pow(2, 32) - 1; // 將長度設(shè)置為最大允許值
arr.length++; // RangeError (明確設(shè)置長度時)
arr.push(1); // RangeError (或者在隱式設(shè)置長度時)

5、函數(shù)對象和構(gòu)造器


為什么通過 SubArray 和 Array 函數(shù)創(chuàng)建的對象的行為存在差異。即使 SubArray 創(chuàng)建了一個從 Array.prototype 繼承的對象,該對象完全沒有數(shù)組的特殊行為。 SubArray 實例只不過是一個普通的 Object 對象(就像它是通過對象字面量創(chuàng)建的一樣)。

但為什么 SubArray 創(chuàng)建一個 Object 對象而不是一個 Array 對象?這個問題的核心是 ECMAScript 中函數(shù)的工作方式。

當(dāng) new 運(yùn)算符應(yīng)用于對象時(如在新的 SubArray 中),調(diào)用該對象的內(nèi)部 [[Constructor]] 方法。在我們的例子中,它是 SubArray 函數(shù)的 [[Constructor]] 。 SubArray - 作為本地函數(shù) - 具有 [[Constructor]],它指定創(chuàng)建一個普通的 Object 對象,并調(diào)用提供新創(chuàng)建對象的相應(yīng)函數(shù)作為此值。任何本地函數(shù)(包括SubArray)都應(yīng)創(chuàng)建一個 Object 對象并將其作為結(jié)果返回。

現(xiàn)在值得一提的是,可以通過從構(gòu)造函數(shù)顯式返回對象來對 [[Constructor]] 的返回值進(jìn)行處理:

function SubArray() {
    this.push.apply(this, arguments);
    // return Array.apply(this, arguments);
    return []; // 顯式返回數(shù)組對象
}

但在這種情況下,返回的對象不會繼承構(gòu)造函數(shù)的“原型”(在這種情況下是 SubArray.prototype); 構(gòu)造函數(shù)也不會被該對象調(diào)用。

var sub = new SubArray(1, 2, 3);
// 對象沒有 1,2,3,因為構(gòu)造函數(shù)從未被調(diào)用,返回的是 this 值引用 object
sub; // []
// SubArray 不在返回對象的原型鏈中
sub instanceof SubArray; // false

綜上,創(chuàng)建一個從 Array.prototype 繼承的對象只是開始。 最大的問題是保留長度和數(shù)字屬性的特殊關(guān)系。 這就是為什么使用常規(guī)克隆方法不能完成的原因。

6、數(shù)組特殊行為的重要性


“為什么數(shù)組的特殊行為很重要”? 為什么當(dāng)繼承一個數(shù)組時,我們想要保持長度和數(shù)字屬性之間的關(guān)系?

以 Array.prototype.push 為例, 要確定從哪個位置開始插入元素,push 將檢索數(shù)組的 “l(fā)ength” 值。 如果長度未正確保存,則將元素插入錯誤的位置:

var arr = ['x', 'y'];
arr.length = 5;
arr.push('z'); // 'z' 被插入到第 5 個索引處,因為這是 “l(fā)ength” 的值
arr; // ['x', 'y', undefined, undefined, undefined, 'z']

采取另一種方法 Array.prototype.join ,Array.prototype.join 還使用 length 屬性來確定何時停止連接值:

var arr = ['x', 'y'];
arr.join(); // "x,y"
arr.length = 5;
arr.join(); // "x,y,,,"
// Array.prototype.concat 同樣適用:
var arr = ['x'];
arr.length = 3;
arr.concat('y'); // ['x', undefined, undefined, 'y']

最后,特殊行為通常在其他情況下被巧妙利用,例如“清除”數(shù)組(即刪除其所有數(shù)字屬性):

var arr = [1, 2, 3];
arr.length = 0;
arr; // [] — 將長度設(shè)置為0會有效地移除數(shù)組的所有數(shù)值屬性(元素

7、Dean Edwards 解決方案


一個受歡迎的解決方案是 Dean Edwards。 采用了完全不同的方法 - 不是創(chuàng)建一個從 Array.prototype 繼承的對象,而是從另一個 iframe 的上下文中“借用”實際的 Array 構(gòu)造函數(shù)。

// 創(chuàng)建一個 <iframe>
var iframe = document.createElement("iframe");
iframe.style.display = "none";
document.body.appendChild(iframe);
// 將腳本寫入 iframe 并竊取其 Array 對象
frames[frames.length - 1].document.write("<script>parent.Array2 = Array;<\/script>";);

這種“有效”的原因是由于瀏覽器為文檔中的每個框架創(chuàng)建單獨的執(zhí)行環(huán)境。 每個這樣的環(huán)境都有一套獨立的內(nèi)置和宿主對象。 內(nèi)置對象包括全局?jǐn)?shù)組構(gòu)造函數(shù)等。 一個 iframe 的數(shù)組對象與另一個 iframe 的數(shù)組對象不同。 他們也沒有任何種類的等級關(guān)系:

// 假設(shè) SubArray 是從另一個 iframe 借用的
var sub = new SubArray(1, 2, 3);
sub instanceof SubArray; // true
sub instanceof Array; // false
sub instanceof Object; // false

注意 sub 為什么不是 Array 的一個實例,也不是 Object 的一個實例。 這是因為 Array 和 Object 都不在子對象的原型鏈中。 相反,原型鏈包含 SubArray.prototype,接著是來自另一個 iframe 的 Object .prototype:

 new SubArray()
     |
     | [[Prototype]]
     |
     v
 <another iframe>.Array.prototype
     |
     | [[Prototype]]
     |
     v
 <another iframe>.Object.prototype
     |
     | [[Prototype]]
     |
     v 
    null

這使我們對這種方法有了一個疑問 - 難以確定從這種 iframe 派生的對象的性質(zhì)。 不再可能使用 instanceof 或構(gòu)造函數(shù)檢查來確定對象是數(shù)組:

// is this object an array?
sub instanceof Array; // false
sub.constructor === Array; // false

但是,仍然可以使用 [[Class]] 檢查(稍后我們將討論 [[Class]]:

Object.prototype.toString.call(sub) === '[object Array]'; // true

這種方法的另一個比較大的缺點是它不適用于非瀏覽器環(huán)境(或者更確切地說,在任何不支持 iframe 的環(huán)境中)。 鑒于服務(wù)器端 Javascript 實現(xiàn)速度非???,這個問題可能會變得更大。

8、ECMAScript 5 屬性訪問器


我們來談?wù)?ECMAScript 5,正如我在一開始提到的,它帶來了一些有助于繼承數(shù)組的東西。這個“東西”其實不過是屬性訪問器。這些有用的語言結(jié)構(gòu)已經(jīng)在一些流行的實現(xiàn)(SpiderMonkey,JavaScriptCore 等)中作為非標(biāo)準(zhǔn)擴(kuò)展出現(xiàn)了很長一段時間?,F(xiàn)在它們已經(jīng)在新版本實現(xiàn)了。

使用訪問器,創(chuàng)建一個具有特殊長度/索引關(guān)系的 Object 對象是相當(dāng)簡單的 - 這與 Array 對象的關(guān)系相同!而且由于我們已經(jīng)知道如何在其原型鏈中創(chuàng)建一個具有 Array.prototype 的對象,所以將這兩個方面結(jié)合起來就可以完全模擬數(shù)組。

有一個關(guān)于實施的細(xì)節(jié)。由于 ECMAScript(包括 last,5th 版本)不提供任何 catch-all(akanoSuchMethod)機(jī)制,因此在修改 numeric 屬性時無法更改對象的 length 屬性值;換句話說,我們不能攔截 '0','1','2','15' 等屬性被設(shè)置的場景。但是,訪問器允許我們截取 length 屬性的任何讀取訪問權(quán)限并返回適當(dāng)?shù)闹担唧w取決于當(dāng)時具有哪個數(shù)字屬性對象。而這是我們真正需要的。

這是它的一個實現(xiàn),大約有42行代碼:

var makeSubArray = (function() {
    var MAX_SIGNED_INT_VALUE = Math.pow(2, 32) - 1,
    hasOwnProperty = Object.prototype.hasOwnProperty;
    function ToUint32(value) {
        return value >>> 0;
    }
    function getMaxIndexProperty(object) {
        var maxIndex = -1, isValidProperty;
        for (var prop in object) {
            isValidProperty = (
            String(ToUint32(prop)) === prop && ToUint32(prop) !== MAX_SIGNED_INT_VALUE && hasOwnProperty.call(object, prop));
            if (isValidProperty && prop > maxIndex) {
                maxIndex = prop;
            }
        }
        return maxIndex;
    }
    return function(methods) {
        var length = 0;
        methods = methods || { };
        methods.length = {
            get: function() {
                var maxIndexProperty = +getMaxIndexProperty(this);
                return Math.max(length, maxIndexProperty + 1);
            },
            set: function(value) {
                var constrainedValue = ToUint32(value);
                if (constrainedValue !== +value) {
                    throw new RangeError();
                }
                for (var i = constrainedValue, len = this.length; i < len; i++) {
                    delete this[i];
                }
                length = constrainedValue;
            }
        };
        methods.toString = {
            value: Array.prototype.join
        };
        return Object.create(Array.prototype, methods);
    };
})();

我們現(xiàn)在可以通過 makeSubArray 函數(shù)創(chuàng)建“子數(shù)組”。 它接受一個參數(shù) - 一個帶有方法的對象,將其添加到 [[Prototype]] 返回的“子數(shù)組”中。

var subMethods = {
    last: {
        value: function() {
            return this[this.length - 1];
        }
    }
};
var sub = makeSubArray(subMethods);
var sub2 = makeSubArray(subMethods);
// 等等

我們也可以將這個工廠方法隱藏在構(gòu)造函數(shù)的后面,使其與 Array 的類似:

var SubArray = (function () {
    var methods = {
        last: {
            value: function() {
                return this[this.length - 1];
            }
        }
    };
    return function() {
        var arr = makeSubArray(methods);
        arr.push.apply(arr, arguments);
        return arr;
    };
})();

然后像使用常規(guī) Array 構(gòu)造函數(shù)一樣使用它:

var sub = new SubArray(1, 2, 3);
sub.length; // 3
sub; // [1, 2, 3]
sub.length = 1;
sub; // [1]
sub[10] = 'x'; // [0:1, 1:2, 2: 3, 10: "x"]
sub.push(1); // [0:1, 1:2, 2: 3, 3: 1, 10: "x"]

9、[[Class]] 限制


我們剛剛看到利用屬性訪問器的實現(xiàn)。它不需要任何主機(jī)對象(如iframe);它保留長度和數(shù)字屬性之間的關(guān)系;它甚至不允許長度或指數(shù)超出范圍的值。它只需要支持 ES5(甚至只是Object.create方法)。

但是 [[Class]] 值 - ECMAScript 仍然沒有完全控制。

在解釋如何檢測數(shù)組時,我之前曾寫過 [[Class]] 。簡而言之,[[Class]] 是 ECMAScript 中對象的內(nèi)部屬性。它的值從不直接暴露,但仍可以使用某些方法(例如 Object.prototype.toString)進(jìn)行檢查。 [[Class]] 的用處在于,它允許檢測對象的類型,而不依賴于 instanceof 運(yùn)算符或檢查對象的構(gòu)造函數(shù) - 兩者都不足以檢測來自其他上下文(例如 iframe)的對象,如前所述。

現(xiàn)在,由于 makeSubArray 創(chuàng)建的對象只是普通的 Object 對象(只有特殊長度的 getter / setter),它們的 [[Class] ]也是 “Object” 而不是 “Array”!我們已經(jīng)考慮了長度/索引關(guān)系,我們設(shè)置了 Array.prototype 繼承,但是沒有辦法改變對象的 [[Class]] 值。所以這個解決方案不能說是完美的。

10、[[Class]] 是否重要?


您可能想知道 - 這些數(shù)組對象的 [Object] 的 [[Class]] 不是 “Array” 的實際含義是什么。實際上,不能繼承[[Class]] 會有不能對象檢測的問題。

// assuming that `sub` is a pseudo-array
Object.prototype.toString.call(sub) === '[object Array]'; // false

另一個可能更重要的含義是,ECMAScript 中的一些方法實際上依賴于 [[Class]] 值。 例如,一個眾所周知 Function.prototype.apply 接受一個數(shù)組作為它的第二個參數(shù)(以及一個參數(shù)對象)。 ES3 的 15.3.4.3節(jié)說 - “如果 argArray 既不是數(shù)組也不是參數(shù)對象(見10.1.8),則拋出 TypeError 異常”。 這意味著如果我們傳遞偽數(shù)組對象作為第二個參數(shù)來應(yīng)用它將拋出 TypeError 。 應(yīng)用程序不知道或關(guān)心一個對象是否從 Array.prototype 繼承; 它也不關(guān)心實現(xiàn)特殊長度/指數(shù)行為的對象。 它所關(guān)心的只是對象是適當(dāng)?shù)念愋?- 我們很遺憾不能模擬這種類型。

// 假設(shè) `sub` 是一個偽數(shù)組
someFunction.apply(this, sub); // TypeError

這方面的規(guī)定有些模糊。 例如,在 Date.prototype.setTime spec 中說“如果這個值不是一個 Date 對象,則拋出一個 TypeError 異?!保?Date.prototype.getTime 中,它使用 [[Class]] 而不是 “not a Date 對象“ - ”如果此值不是其 [[Class]] 屬性為 “Date” 的對象,則引發(fā) TypeError 異?!?。

假設(shè)這兩個短語 - “ Date 對象”和 “Date ['Class] 中的對象”)具有相同的含義可能是安全的。 “ Array 對象”和“ Array [] 的 [[Class]] 對象”以及其他對象也是類似的。

Function.prototype.apply 不是對對象 [[Class]] 敏感的唯一方法。 例如,Array.prototype.concat 基于對象是否為數(shù)組(不管是否具有 [[Class] ]“Array”),都遵循不同的算法。

// array ([[Class]] == "Array")
var arr = ['x', 'y'];
// object with numeric properties ([[Class]] == "Object")
var obj = { '0': 'x', '1': 'y' };
[1,2,3].concat(arr); // [1, 2, 3, 'x', 'y']
[1,2,3].concat(obj); // [1, 2, 3, { '0': 'x', '1': 'y' }]

正如你所看到的,數(shù)組的值是“扁平的”,而非數(shù)組的則保持不變。 當(dāng)然可以給這些偽數(shù)組自定義 concat 實現(xiàn)(并“修復(fù)” Array.prototype 上方法中的任何其他方法),但是 Function.prototype.apply 的問題無法解決。

值得一提的是,基于存取器的數(shù)組方法的另一個缺點是性能。 我還沒有做過任何測試,但很明顯,每次訪問 length 屬性時必須枚舉所有數(shù)字屬性的實現(xiàn)并不會很好。 這就是為什么我不能推薦這個解決方案的原因。

11、包裝, 直接屬性注入


在 Javascript 中實現(xiàn)數(shù)組的繼承有些徒勞無功,通常會使替代解決方案看起來非常有吸引力。 其中一種解決方案是使用包裝。 包裝方法避免了設(shè)置繼承或模擬長度/索引關(guān)系。 相反,類似工廠的函數(shù)可以創(chuàng)建一個普通的 Array 對象,然后使用任何自定義方法直接對其進(jìn)行擴(kuò)充。 由于返回的對象是一個數(shù)組,所以它保持適當(dāng)?shù)拈L度/索引關(guān)系,以及“數(shù)組”的 [[Class]] 。 它也自然地從 Array.prototype 繼承。

function makeSubArray() {
    var arr = [ ];
    arr.push.apply(arr, arguments);
    arr.last = function() {
        return this[this.length - 1];
    };
    return arr;
}
var sub = makeSubArray(1, 2, 3);
sub instanceof Array; // true
sub.length; // 3
sub.last(); // 3

盡管數(shù)組對象的直接擴(kuò)展是一個美觀,簡單的解決方案,但它并非沒有缺點。 主要缺點是每次調(diào)用構(gòu)造函數(shù)時,需要使用 N 個方法來擴(kuò)展數(shù)組。 創(chuàng)建數(shù)組所需的時間不再是常量(如果方法在 SubArray.prototype 上),而是與需要添加的方法的數(shù)量成正比。

12、包裝, 原型鏈注入


為了克服“N方法”的問題,可以使用包裝器的另一種變體 - 其中對象的原型鏈增加的變體,而不是對象本身。 讓我們看看如何做到這一點:

function SubArray() { }
    SubArray.prototype = new Array;
    SubArray.prototype.last = function() {
        return this[this.length - 1];
    }
};
function makeSubArray() {
    var arr = [ ];
    arr.push.apply(arr, arguments);
    arr.__proto__ = SubArray.prototype;
    return arr;
}

這個想法很簡單。 當(dāng)執(zhí)行 makeSubArray 函數(shù)時,會發(fā)生兩件事:
1)創(chuàng)建一個數(shù)組對象并使用任何傳遞的參數(shù)填充;
2)對象的原型鏈以這種方式增加,以便下一個對象是 SubArray.prototype,而不是原始 Array.prototype。 原型鏈的擴(kuò)充是通過非標(biāo)準(zhǔn)proto屬性完成的。

但是,makeSubArray 函數(shù)中發(fā)生的事情當(dāng)然只是任務(wù)的一半。 為了確保該對象在其原型鏈中具有 Array.prototype,我們需要使 SubArray.prototype 從它繼承。 這正是這段代碼的第二行(SubArray.prototype = new Array)所做的。 從 makeSubArray 返回的對象的原型鏈如下所示:

 new SubArray()
     |
     | [[Prototype]]
     |
     v
 SubArray.prototype
     |
     | [[Prototype]]
     |
     v
 Array.prototype
     |
     | [[Prototype]]
     |
     v
 Object.prototype
     |
     | [[Prototype]]
     |
     v
    null

因為返回的對象實際上是一個數(shù)組,而不是對象,我們也得到長度/指數(shù)關(guān)系以及適當(dāng)?shù)?[[Class]] 值。 實際上,我們可以更進(jìn)一步并將初始化邏輯移入 SubArray 構(gòu)造函數(shù)本身:

function SubArray() {
    var arr = [ ];
    arr.push.apply(arr, arguments);
    arr.__proto__ = SubArray.prototype;
    return arr;
}
SubArray.prototype = new Array;
SubArray.prototype.last = function() {
    return this[this.length - 1];
};
var sub = new SubArray(1, 2, 3);
sub instanceof SubArray; // true
sub instanceof Array; // true

盡管擴(kuò)充原型鏈?zhǔn)且粋€更高性能的解決方案,但它有一個明顯的缺點 - 它依賴于非標(biāo)準(zhǔn)的proto屬性。 不幸的是,ECMAScript 不允許設(shè)置一個對象的 [[Prototype]] - 在其原型鏈中引用直接祖先的內(nèi)部屬性。 即使在第五版中也沒有。 盡管proto被大量的實現(xiàn)支持,但它遠(yuǎn)沒有真正兼容。

13、es6 繼承數(shù)組的實現(xiàn)


class FakeArray extends Array { 
    push (...args) {
        console.log('我被改變啦');
        return super.push(...args);
    }
}
var list = [1, 2, 3];
var arr = new FakeArray(...list);
console.log(arr.length);
arr.push(3);
console.log(arr);

14、vue 2.x 中攔截數(shù)組 7 個變異方法的實現(xiàn)


image
// 要攔截的數(shù)組變異方法
const mutationMethods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];
const arrayMethods = Object.create(Array.prototype); // 實現(xiàn) arrayMethods.__proto__ === Array.prototype
const arrayProto = Array.prototype;  // 緩存 Array.prototype
mutationMethods.forEach(method => {
  arrayMethods[method] = function (...args) {
    const result = arrayProto[method].apply(this, args);
    console.log(`執(zhí)行了代理原型的 ${method} 函數(shù)`);
    return result;
  }
});
const arr = [];
arr.__proto__ = arrayMethods;
arr.push(1);
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容