关于继承数组
为什么要继承数组我们可以将“数组子类”定义为从原始数组中创建的对象在其原型链中 Array.prototype继承对象的过程并遵循与原始数组相似(或相同)的行为。我们以后会看到类似原始数组的行为非常重要。拥有数组的“子类”可以被认为可以创建一个数组对象而不是直接从 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 构造函数如何创建一个与数组行为相同的子对象对象具有 “length” 属性数字 “0”“1”“2” 属性并继承 Array.prototype 上的方法。 同时SubArray 是直接继承的子对象而不是 Array 。那么做这一切的目的究竟是什么 为什么以这种方式对数组进行继承通常有两个原因避免污染全局利用 Javascript 原型扩展数组对象方法很方便。 如下代码Array.prototype.last function () { return this[this.length - 1]; }; // ... [1, 2, 3].last(); // 3但是扩展 Array.prototype 有代价的。当脚本与应用程序中的其他脚本共存时这些脚本有可能相互冲突。扩展 Array.prototype 虽然诱人并且看起来很有用但不幸的是在多样化的环境中不是很安全。不同的脚本可能最终定义相同名称的方法但具有不同的行为。这种情况往往会导致不一致的行为和难以追踪的错误。使用 Array 以外的构造函数 - 但具有相同的行为 - 可以避免这种冲突。不是扩展 Array.prototype而是扩展另一个对象比如 SubArray.prototype然后用来初始化子数组对象。任何依赖 Array.prototype 方法的第三方代码仍然能够安全地使用它们。继承数组的数据结构方法例如 StackListQueueSet pushpopshiftunshift 等等方法。天真的做法我们可以使用原型式克隆方法function clone(obj) { function F() { } F.prototype obj; return new F(); }然后设置如下的继承function Child() { } Child.prototype clone(Parent.prototype);这里的原型链new Child() | | [[Prototype]] | v Child.prototype | | [[Prototype]] | v Parent.prototype | | [[Prototype]] | v Object.prototype | | [[Prototype]] | v null开始实现继承数组function SubArray() { // 将传递给构造函数的任何参数添加到实例中 this.push.apply(this, arguments); } SubArray.prototype clone(Array.prototype); var sub new SubArray(1, 2, 3);天真方法的问题那么使用克隆方法继承数组究竟有什么错误 让我们来看看之前声明的 SubArray 函数的行为。 我们将使用原生数组对象来进行比较。var arr new Array(1, 2, 3); var sub new SubArray(1, 2, 3); arr.length; // 3 sub.length; // 0 (in IE8) 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 中的错误。 但是数组中的长度和数字属性之间的这种奇怪的关系是什么 为什么不是和 Array 的行为相同 为了理解这一点我们需要查看 JavaScript 中的数组对象。数组的特殊之处在 Javascript 中的数组几乎就像普通的 Object 对象除了行为上的一点小差异。 如下引自 es 规范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§) is equal to P and ToUint32§ 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.可以概括为数组对象以特殊的方式处理 “numeric” 属性。 只要这些属性发生变化数组的 “length” 属性的值也会被调整; 它的调整是为了确保它总是比数组的最大索引大 1 。 类似地当“长度”属性发生变化时“numeric” 属性会相应地进行调整。当创建数组对象时其 “length” 属性设置为比数组最大索引大 1 。var arr [x, y, z]; arr.length; // 3 arr [foo]; arr.length; // 1当 “numeric” 属性发生变化时“长度”也会发生变化 - 以保持比最大索引大 1 的关系。var arr [x, y]; arr.length; // 2 arr[2] z; arr.length;当“length”属性改变时“numeric” 属性会进行调整使得最大索引比“length”的值小 1 。var arr [x, y, z]; arr.length 2; arr; // [x, y] arr.length 4; arr; // [x, y] // “增加”长度不会影响数字属性... arr.join(); // x,y,, // 但在其他情况下可以看到后果例如使用 Array.prototype.push 时 arr.push(z); arr; // [x, y, undefined, undefined, z]现在你知道 Javascript 中的 Array 对象的“特殊”之处了它处于 “length” 和 “numeric” 属性之间的关系中。 还有一个值得注意的细节是数组的 “length” 属性必须总是具有小于 2 ^ 32 的非负整数值。 只要违反这个条件就会引发 RangeError 。var arr []; arr.length Math.pow(2, 32); // RangeError arr.length; // 0 (长度仍然是0就像它最初一样) arr.length Math.pow(2, 32) - 1; // 将长度设置为最大允许值 arr.length; // RangeError (明确设置长度时) arr.push(1); // RangeError (或者在隐式设置长度时)函数对象和构造器为什么通过 SubArray 和 Array 函数创建对象的行为是不同的。即使 SubArray 创建了一个从 Array.prototype 继承对象对象完全没有数组的特殊行为。 SubArray 例子只是一个普通的例子 Object 对象(就像它是通过对象字面量创建的)。但为什么 SubArray 创建一个 Object 而不是一个对象 Array 对象这个问题的核心是什么 ECMAScript 中函数的工作模式。当 new 当运算符应用于对象时(例如在新的时候) SubArray 中)调用对象的内部。 [[Constructor]] 方法。在我们的例子中它就是这样 SubArray 函数的 [[Constructor]] 。 SubArray -作为本地函数 - 具有 [[Constructor]]它指定创建一个普通的产品 Object 对象并调用提供新创建对象的相应函数作为此值。任何本地函数(包括SubArray)都应该创建一个函数。 Object 对象并将其作为结果返回。现在值得一提的是可以通过从构造函数显式返回对象来对付它 [[Constructor]] 处理返回值function SubArray() { this.push.apply(this, arguments); // return Array.apply(this, arguments); return []; // 显式返回数组对象 }但在这种情况下返回的对象不会继承构造函数的“原型”在这种情况下是 SubArray.prototype; 构造函数也不会被该对象调用。var sub new SubArray(1, 2, 3); // 对象没有 123因为构造函数从未被调用返回的是 this 值引用 object sub; // [] // SubArray 不在返回对象的原型链中 sub instanceof SubArray; // false综上创建一个从 Array.prototype 继承的对象只是开始。 最大的问题是保留长度和数字属性的特殊关系。 这就是为什么使用常规克隆方法不能完成的原因。数组特殊行为的重要性“为什么数组的特殊行为很重要” 为什么当继承一个数组时我们想要保持长度和数字属性之间的关系以 Array.prototype.push 为例 要确定从哪个位置开始插入元素push 将检索数组的 “length” 值。 如果长度未正确保存则将元素插入错误的位置var arr [x, y]; arr.length 5; arr.push(z); // z 被插入到第 5 个索引处因为这是 “length” 的值 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]最后特殊行为通常在其他情况下被巧妙利用例如“清除”数组即删除其所有数字属性var arr [1, 2, 3]; arr.length 0; arr; // [] — 将长度设置为0会有效地移除数组的所有数值属性元素Dean Edwards 解决方案一个受欢迎的解决方案是 Dean Edwards。 采用了完全不同的方法 - 不是创建一个从 Array.prototype 继承的对象而是从另一个 iframe 的上下文中“借用”实际的 Array 构造函数。// 创建一个 iframe var iframe document.createElement(iframe); iframe.style.display none; document.body.appendChild(iframe); // 将脚本写入 iframe 并窃取其 Array 对象 frames[frames.length - 1].document.write( scriptparent.Array2 Array;\/script; );这种“有效”的原因是由于浏览器为文档中的每个框架创建单独的执行环境。 每个这样的环境都有一套独立的内置和宿主对象。 内置对象包括全局数组构造函数等。 一个 iframe 的数组对象与另一个 iframe 的数组对象不同。 他们也没有任何种类的等级关系// 假设 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 .prototypenew SubArray() | | [[Prototype]] | v another iframe.Array.prototype | | [[Prototype]] | v another iframe.Object.prototype | | [[Prototype]] | v null这使我们对这种方法有了一个疑问 - 难以确定从这种 iframe 派生的对象的性质。 不再可能使用 instanceof 或构造函数检查来确定对象是数组// is this object an array? sub instanceof Array; // false sub.constructor Array; // false但是仍然可以使用 [[Class]] 检查稍后我们将讨论 [[Class]]Object.prototype.toString.call(sub) [object Array]; // true这种方法的另一个比较大的缺点是它不适用于非浏览器环境或者更确切地说在任何不支持 iframe 的环境中。 鉴于服务器端 Javascript 实现速度非常快这个问题可能会变得更大。