扩展本机对象

时间:2017-08-26 08:55:14

标签: javascript ecmascript-6 prototypal-inheritance

我试图扩展内置的Map对象。这是有效的部分:



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"




但是我希望将这个类无缝地集成到我的库中,而是公开一个工程方法,它作为构造函数提供双重用途。用户不需要知道这种特殊方法是不寻常的。这只会让事情复杂化。然而,我自己的代码需要能够使用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都会抛出各种错误。以下代码,例如抛出TypeError" this.entries(...)[Symbol.iterator]不是函数":



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] );
        }
    }
} );




我在这里错过了什么和/或误解了什么?

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



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 );




1 个答案:

答案 0 :(得分:1)

它不起作用,因为Object.assign不会复制getter / setter,而是调用它们。因此,使用 window 调用getter(获取文本),这将显然失败:

MDN:

  

Object.assign()方法仅将可枚举和自己的属性从源对象复制到目标对象。它在源上使用[[Get]],在目标上使用[[Set]],因此它将调用getter和setter。如果合并源包含getter,这可能使它不适合将新属性合并到原型中。为了将属性定义(包括它们的可枚举性)复制到原型中,应该使用Object.getOwnPropertyDescriptor()和Object.defineProperty()。

要将对象复制到地图中而不丢失设置者,可以执行以下操作:

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)使用类语法和工厂:

 {
  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");
  }
};