Skip to content

扩展内建类

内建的类,例如 ArrayMap 等也都是可以扩展的(extendable)。

例如,这里有一个继承自原生 Array 的类 PowerArray

js
// 给 PowerArray 新增了一个方法(可以增加更多)
class PowerArray extends Array {
    isEmpty() {
        return this.length === 0;
    }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
console.log(arr.isEmpty()); // false

let filteredArr = arr.filter(item => item >= 10);
console.log(filteredArr); // PowerArray(2) [ 10, 50 ]
console.log(filteredArr.isEmpty()); // false

请注意一个非常有趣的事儿。内建的方法例如 filtermap 等 — 返回的正是子类 PowerArray 的新对象。它们内部使用了对象的 constructor 属性来实现这一功能。

在上面的例子中,

js
arr.constructor === PowerArray

arr.filter() 被调用时,它的内部使用的是 arr.constructor 来创建新的结果数组,而不是使用原生的 Array。这真的很酷,因为我们可以在结果数组上继续使用 PowerArray 的方法。

甚至,我们可以定制这种行为。

我们可以给这个类添加一个特殊的静态 getter Symbol.species。如果存在,则应返回 JavaScript 在内部用来在 mapfilter 等方法中创建新实体的 constructor

如果我们希望像 mapfilter 这样的内建方法返回常规数组,我们可以在 Symbol.species 中返回 Array,就像这样:

js
class PowerArray extends Array {
    isEmpty() {
        return this.length === 0;
    }

    // 内建方法将使用这个作为 constructor
    static get [Symbol.species]() {
        return Array;
    }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
console.log(arr.isEmpty()); // false

// filter 使用 arr.constructor[Symbol.species] 作为 constructor 创建新数组
let filteredArr = arr.filter(item => item >= 10);

// filteredArr 不是 PowerArray,而是 Array
console.log(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function

正如你所看到的,现在 .filter 返回 Array。所以扩展的功能不再传递。

其他集合的工作方式类似

其他集合,例如 MapSet 的工作方式类似。它们也使用 Symbol.species

内建类没有静态方法继承

内建对象有它们自己的静态方法,例如 Object.keysArray.isArray 等。

如我们所知道的,原生的类互相扩展。例如,Array 扩展自 Object

通常,当一个类扩展另一个类时,静态方法和非静态方法都会被继承。这已经在 静态属性和静态方法 中详细地解释过了。

但内建类却是一个例外。它们相互间不继承静态方法。

例如,ArrayDate 都继承自 Object,所以它们的实例都有来自 Object.prototype 的方法。但 Array.[[Prototype]] 并不指向 Object,所以它们没有例如 Array.keys()(或 Date.keys())这些静态方法。

这里有一张 DateObject 的结构关系图:

image-20220314150356731

正如你所看到的,DateObject 之间没有连结。它们是独立的,只有 Date.prototype 继承自 Object.prototype,仅此而已。

与我们所了解的通过 extends 获得的继承相比,这是内建对象之间继承的一个重要区别。

总结

  1. 内建类,例如Array、Map,等都是可以扩展的(extendable)

    js
    // 给 PowerArray 新增了一个方法(可以增加更多)
    class PowerArray extends Array {
        isEmpty() {
            return this.length === 0;
        }
    }
    
    let arr = new PowerArray(1, 2, 5, 10, 50);
    console.log(arr.isEmpty()); // false
    
    let filteredArr = arr.filter(item => item >= 10);
    console.log(filteredArr); // PowerArray(2) [ 10, 50 ]
    console.log(filteredArr.isEmpty()); // false
  2. 在上面的例子中,当arr.filter()被调用时,是使用的PowerArray(扩展的数组类的构造方法)来构建的数组,所以打印的PowerArray(2) [ 10, 50 ],所以返回的结果依然能使用我们扩展类PowerArray的方法。

  3. 但是如果我们想使用Array来构建返回的结果时,其实可以实现一个Symbol.species的getter

    js
    class PowerArray extends Array {
        isEmpty() {
            return this.length === 0;
        }
    
        // 内建方法将使用这个作为 constructor
        static get [Symbol.species]() {
            return Array;
        }
    }
    
    let arr = new PowerArray(1, 2, 5, 10, 50);
    console.log(arr.isEmpty()); // false
    
    // filter 使用 arr.constructor[Symbol.species] 作为 constructor 创建新数组
    let filteredArr = arr.filter(item => item >= 10);
    
    // filteredArr 不是 PowerArray,而是 Array
    console.log(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function
  4. 其他集合,例如Map和Set工作方式类似,它们也使用Symbol.species

  5. 扩展的内建类,并不会继承原先类的静态方法,这点应该特殊记忆一下,因为在上一节中,使用extends继承普通类的时候,也会把原来类的静态属性和方法继承过来,会把派生类的__proto__执向父类。但是扩展内建类不会有这一步。

参考

https://zh.javascript.info/extend-natives