Factory/class 扩展本机对象

Factory/class that extends native object

我正在尝试扩展内置地图对象。这是有效的部分:

var Attributes = class extends Map {

    get text () {
        let out = [];
        for ( let [ k, v ] of this.entries() ) {
            out.push( k + '="' + v + '"' );
        }
        return out.join( ' ' );
    }

    // simplified for brevity!
    set text ( raw ) {
        this.clear();
        var m, r = /(\w+)="([^"]+)"/g;
        while ( ( m = r.exec( raw ) ) ) {
            this.set( m[1], m[2] );
        }
    }
};

var a = new Attributes();
a.text = 'id="first"';
console.log( a.get( 'id' ) ); // first
a.set( 'id', 'second' );
console.log( a.text ); // id="second"

但我想将此 class 无缝集成到我的库中,而是公开一个工厂方法,该方法作为构造函数具有双重用途。用户不需要知道这个特定的方法是不寻常的。这只会使事情复杂化。但是,我自己的代码需要能够使用 instanceof 进行输入验证。这就是我想要的:

var obj = {};
obj.attr = function ( text ) {
    if ( new.target ) {
        this.text = text;
    } else {
        return new obj.attr( text );
    }
};

console.log( obj.attr() instanceof obj.attr ); // true

以上也行。但是,无论我如何尝试结合这两种方法,Chrome 和 Firefox 都会抛出各种错误。下面的代码例如抛出类型错误 "this.entries(...)[Symbol.iterator] is not a function":

var obj = {};
obj.attr = function ( text ) {
    if ( new.target ) {
        this.text = text;
    } else {
        return new obj.attr( text );
    }
};

obj.attr.prototype = Object.assign( new Map(), {
    get text () {
        let out = [];
        for ( let [ k, v ] of this.entries() ) {
            out.push( k + '="' + v + '"' );
        }
        return out.join( ' ' );
    },
    // simplified for brevity!
    set text ( raw ) {
        this.clear();
        var m, r = /(\w+)="([^"]+)"/g;
        while ( ( m = r.exec( raw ) ) ) {
            this.set( m[1], m[2] );
        }
    }
} );

我错过了什么and/or这里有误会吗?

更新:使用 Object.setPrototypeOf() 是可能的,但性能损失可能会很大。 可以使用Reflect.construct()。虽然这与 Object.setPrototypeOf() 有何不同,但对我来说并不明显。 Reflect 似乎通常很慢,因为显然当前没有浏览器对其进行优化。

var Attributes = class extends Map {
    
    constructor ( text ) {
        super();
        this.text = text;
    }
    
    get text () {
        let out = [];
        for ( let [ k, v ] of this.entries() ) {
            out.push( k + '="' + v + '"' );
        }
        return out.join( ' ' );
    }
    
    // simplified for brevity!
    set text ( raw ) {
        this.clear();
        if ( !raw ) return;
        var m, r = /(\w+)="([^"]+)"/g;
        while ( ( m = r.exec( raw ) ) ) {
            this.set( m[1], m[2] );
        }
    }
};

var obj = {};

obj.attr = function ( text ) {
    if ( new.target ) {
        return Reflect.construct( Attributes, [ text ], obj.attr );
    } else {
        return new obj.attr( text );
    }
};

obj.attr.prototype = Object.create( Attributes.prototype );

console.log( obj.attr() instanceof obj.attr );
console.log( obj.attr() instanceof Attributes );
console.log( obj.attr() instanceof Map );

var a = obj.attr();
a.text = 'id="first"';
console.log( a.get( 'id' ) );
a.set( 'id', 'second' );
console.log( a.text );

它不起作用,因为 Object.assign 不复制 getters/setters,而是调用它们。所以 getter (获取文本)被调用 thiswindow ,这显然会失败:

MDN:

The Object.assign() method only copies enumerable and own properties from a source object to a target object. It uses [[Get]] on the source and [[Set]] on the target, so it will invoke getters and setters. This may make it unsuitable for merging new properties into a prototype if the merge sources contain getters. For copying property definitions, including their enumerability, into prototypes Object.getOwnPropertyDescriptor() and Object.defineProperty() should be used instead.

要在不丢失 setter 的情况下将对象复制到 Map 中,可以这样做:

obj.attr.prototype = new Map();
var settings = {
  get text(){},
  set text(v){}
};

for(key in settings){
  Object.defineProperty(
    obj.attr.prototype,
    key,
    Object.getOwnPropertyDescriptor(settings,key)
  );
}

但是,这仍然会失败。地图不同于对象。调用 this.clear() 会崩溃,因为它期望 this 是一个映射而不是一个常规对象。所以剩下两个选项:

1) 使用 class 语法和工厂:

 {
  let internal = class extends Map {
   constructor(text){
     super();
     this.text = text;
   };
   set text(v){};
   get text(){};
  };

  var obj = {
   attr(text){
     return new internal(text);
    }
  };
}

2) 内部保留地图:

 obj.attr = function(text){
   if(this === window) return new obj.attr(text);
   this.map = new Map();
   this.text = text;
};
obj.attr.prototype = {
  set text(v){
   this.map.set("text",v);
  },
  get text(){
   return this.map.get("text");
  }
};