【问题标题】:How to add methods to a (JSON) object's prototype?如何向(JSON)对象的原型添加方法?
【发布时间】:2013-02-24 17:55:48
【问题描述】:

假设我从我的服务器接收到一些 JSON 对象,例如Person 对象的一些数据:

{firstName: "Bjarne", lastName: "Fisk"}

现在,我想要一些基于这些数据的方法,例如用于计算全名:

fullName: function() { return this.firstName + " " + this.lastName; }

这样我就可以

var personData = {firstName: "Bjarne", lastName: "Fisk"};
var person = PROFIT(personData);
person.fullName(); // => "Bjarne Fisk"

我基本上想要在这里做的是向对象的原型添加一个方法。 fullName() 方法是通用的,因此不应添加到数据对象本身。喜欢..:

personData.fullName = function() { return this.firstName + " " + this.lastName; }

... 会造成大量冗余;并且可以说“污染”数据对象。

将此类方法添加到简单数据对象的当前最佳实践方法是什么?

编辑:

有点跑题了,但是如果上面的问题能解决的话,就可以做一些不错的伪pattern matching 像这样:

if ( p = Person(data) ) {
   console.log(p.fullName());
} else if ( d = Dog(data) ) {
   console.log("I'm a dog lol. Hear me bark: "+d.bark());
} else {
   throw new Exception("Shitty object");
}

PersonDog 将添加方法,如果 data 对象具有正确的属性。如果不是,则返回 falsy(即数据确实匹配/符合)。

奖励问题:有没有人知道使用或启用此功能(即使其变得容易)的库?它已经是javascript模式了吗?如果是这样,它叫什么;你有详细说明的链接吗?谢谢:)

【问题讨论】:

  • 您在检索 JSON 数据时难道不应该知道它是什么类型的对象吗?当然可以创建一个基本的对象构造库来执行您的建议,似乎基于属性检测对象的类型并不是最好的方法..特别是因为不同的类型可以有同名的属性。为什么不直接使用 fromJSON 方法或构造函数来忽略构造函数未定义的属性?
  • @MattB :我可以想象 parseJson 函数,它采用字符串和路径/原型类映射在构造时将类分配给对象。这里的 path 可以是例如 JsonPath 表达式:goessner.net/articles/JsonPath
  • 嗯,Javascript 数据绑定框架通常使用路径将对象属性绑定到 HTML 中的某些内容,但我不确定为什么需要使用它们来构造对象,因为大多数服务器端语言现在可以序列化为 JSON。
  • @MattB:考虑这个函数调用parseJsonPlus(str, {"/*": Person} )。用简单的英语:从str 解析json,并使用new Person() 而不是new Object() 构造数据根的每个子节点。换句话说:json + 路径/类声明 = “分类”和结构化数据。
  • 啊,我明白了。不幸的是,我不知道有任何库可以做到这一点(当然可以编写一个)。我确实找到了这个:stackoverflow.com/a/12978933/560114

标签: javascript json prototype


【解决方案1】:

假设您的 Object 来自某个解析服务器输出以生成 Object 的 JSON 库,那么它的原型中通常不会有任何特殊内容;并且为不同的服务器响应生成的两个对象不会共享原型链(当然,除了 Object.prototype ;))

如果您控制从 JSON 创建“Person”的所有位置,则可以反过来做:创建一个“空”Person 对象(在其原型中使用类似 fullName 的方法),并使用从 JSON 生成的对象(使用 $.extend、_.extend 或类似的东西)。

var p = { first : "John", last : "Doe"};

function Person(data) {
   _.extend(this, data);
}

Person.prototype.fullName = function() {
   return this.first + " " + this.last;   
}

console.debug(new Person(p).fullName());

【讨论】:

  • 假设同一个 JavaScript 脚本从不同的服务器获取数据,那么是的,所有对象都将继承同一个原型 (Object.prototype)。
【解决方案2】:

这里还有另一种可能。 JSON.parse 接受第二个参数,这是一个用于恢复从叶节点到根节点遇到的对象的函数。因此,如果您可以根据它们的内在属性识别您的类型,则可以在 reviver 函数中构造它们。这是一个非常简单的示例:

var MultiReviver = function(types) {
    // todo: error checking: types must be an array, and each element
    //       must have appropriate `test` and `deserialize` functions
    return function(key, value) {
        var type;
        for (var i = 0; i < types.length; i++) {
            type = types[i];
            if (type.test(value)) {
                return type.deserialize(value);
            }
        }
        return value;
    };
};

var Person = function(first, last) {
    this.firstName = first;
    this.lastName = last;
};
Person.prototype.fullName = function() {
    return this.firstName + " " + this.lastName;
};
Person.prototype.toString = function() {return "Person: " + this.fullName();};
Person.test = function(value) {
    return typeof value.firstName == "string" && 
           typeof value.lastName == "string";
};
Person.deserialize = function(obj) {
    return new Person(obj.firstName, obj.lastName);
};

var Dog = function(breed, name) {
    this.breed = breed;
    this.name = name;
}
Dog.prototype.species = "canine";
Dog.prototype.toString = function() {
    return this.breed + " named " + this.name;
};
Dog.test = function(value) {return value.species === "canine";};
Dog.deserialize = function(obj) {return new Dog(obj.breed, obj.name);};


var reviver = new MultiReviver([Person, Dog]);

var text = '[{"firstName": "John", "lastName": "Doe"},' +
            '{"firstName": "Jane", "lastName": "Doe"},' +
            '{"firstName": "Junior", "lastName": "Doe"},' +
            '{"species": "canine", "breed": "Poodle", "name": "Puzzle"},' +
            '{"species": "canine", "breed": "Wolfhound", "name": "BJ"}]';

var family = JSON.parse(text, reviver)
family.join("\n");

// Person: John Doe
// Person: Jane Doe
// Person: Junior Doe
// Poodle named Puzzle
// Wolfhound named BJ

这取决于您能否明确识别您的类型。例如,如果有其他类型,甚至是 Person 的子类型,它也有 firstName 和 lastName 属性,这将不起作用。但它可能满足一些需求。

【讨论】:

    【解决方案3】:

    如果您处理的是纯 JSON 数据,那么每个 person 对象的原型就是 Object.prototype。为了使其成为原型为Person.prototype 的对象,您首先需要一个Person 构造函数和原型(假设您正在以传统方式执行Javascript OOP):

    function Person() {
        this.firstName = null;
        this.lastName = null;
    }
    Person.prototype.fullName = function() { return this.firstName + " " + this.lastName; }
    

    然后您需要一种将普通对象转换为 Person 对象的方法,例如如果你有一个名为 mixin 的函数,它只是将所有属性从一个对象复制到另一个对象,你可以这样做:

    //example JSON object
    var jsonPerson = {firstName: "Bjarne", lastName: "Fisk"};
    
    var person = new Person();
    mixin(person, jsonPerson);
    

    这只是解决问题的一种方法,但希望能给你一些想法。


    更新:现在 Object.assign() 在现代浏览器中可用,您可以使用它而不是编写自己的 mixin 函数。还有一个 shim 可以让 Object.assign() 在旧版浏览器上工作;见https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill

    【讨论】:

    • 这正是我想要的。唯一的问题是它有点麻烦。我想有一种更简单的方法可以做到这一点。这得到是一个普遍的问题。这是 javascript 的(许多)缺点之一吗?
    • 如果你有一个 Person 和所有其他域模型类继承自的基本 Model 或 Entity 类,它可能会变得不那么麻烦。该基础模型类可能有一个工厂方法(例如Model.prototype.new),它接受一个 JSON 对象,并与我的 mixin 示例执行相同的操作。或者您可以使用已经包含此类行为的框架,例如Can.js 的Model.init 方法:canjs.us/#can_model-init
    • 另外,Person 构造函数可以通过使用mixin 函数来稍微简洁一些,例如:function Person() { mixin(this, { firstName: null, lastName: null }); }
    • 感谢 Matt :) Can.js 看起来很有趣。会检查的。
    • 仅供参考,现在有一个标准方法Object.setPrototypeOf() 可以用作替代解决方案。
    【解决方案4】:

    你可能不应该这样做。

    JSON 允许您序列化状态,而不是类型。所以在你的用例中,你应该这样做:

    var Person = function ( data ) {
        if ( data ) {
            this.firstName = data.firstName;
            this.lastName = data.lastName;
        }
    };
    
    Person.prototype.fullName = function ( ) {
        return this.firstName + ' ' + this.lastName;
    };
    
    //
    
    var input = '{"firstName":"john", "lastName":"Doe"}';
    var myData = JSON.parse( input );
    var person = new Person( myData );
    

    【讨论】:

    • 这是我回答的一个很好的变体;我们的答案也可以合并,因此您可以在 Person 构造函数中使用 mixin 函数。
    【解决方案5】:

    换句话说,您想更改现有对象的原型(又名类)。 从技术上讲,您可以这样做:

    var Person = {
      function fullName() { return this.firstName + " " + this.lastName; }
    };
    
    // that is your PROFIT function body: 
    personData.__proto__ = Person ;
    

    之后,如果您将在personData instanceof Person 上获得true

    【讨论】:

    • 您应该提到__proto__ 是非标准的并且在大多数浏览器中已弃用。
    • __proto__ 是非标准的,在某些旧浏览器中无法使用;但是,这是更改现有对象原型的唯一方法。这是一个有趣的答案......它可以通过具有setPrototype 功能使其与所有浏览器兼容,该功能回退到仅复制不支持__proto__ 的浏览器中的所有方法。然而,对于在没有强大 Javascript 背景的情况下编写代码的任何人来说,这种方法可能看起来有点奇怪。
    • @Felix Kling:是的,IE 可能不支持__proto__(最近没有检查)。根据我的回答,我只是试图将要求形式化 - 技术上(理想情况下阅读)这正是提问者所需要的。具体实施可能会有所不同。
    • 这适用于 IE10,似乎是执行 OP 想要的最佳方式。
    【解决方案6】:

    使用新的Object.setPrototypeOf()。 (现在IE11和其他所有浏览器都支持了。)

    您可以创建一个包含所需方法的类/原型,例如您的 fullName(),然后

    Object.setPrototypeOf( personData, Person.prototype );
    

    正如警告(在上面链接的 MDN 页面上)所暗示的,不能轻易使用此函数,但当您更改现有对象的原型时,这是有意义的,这就是您所追求的。

    【讨论】:

      【解决方案7】:

      我认为用数据传输方法并不常见,但这似乎是个好主意。

      此项目允许您将函数与数据一起编码,但它不被视为标准,当然需要使用相同的库进行解码。

      https://github.com/josipk/json-plus
      

      【讨论】:

        【解决方案8】:

        匿名对象没有原型。为什么不只拥有这个:

        function fullName(obj) {
            return obj.firstName + ' ' + obj.lastName;
        }
        
        fullName(person);
        

        如果你绝对必须使用方法调用而不是函数调用,你总是可以做类似的事情,但使用一个对象。

        var Person = function (person) { this.person = person; }
        Person.prototype.fullName = function () {
            return this.person.firstName + ' ' + this.person.lastName;
        }
        var person = new Person(personData);
        person.fullName();
        

        【讨论】:

        • function fullName(obj) { return obj.firstName + ' ' + obj.lastName; } 非常非 OOP。您的第二个解决方案将使.firstName 中断。
        • 仅仅因为某些东西使用了对象并不能使它成为面向对象的。 “un-OOP”是劣质的吗? “OOP”毁掉了一代程序员……不知道你说的让.firstName破了是什么意思
        • 他的意思是,在你的第二个例子中,var person = new Person(personData);person.firstName 将不再起作用。最好在 Person 构造函数中将所有属性设置为 this 的属性,这样它的行为就像一个普通对象......代理方法使整个事情变得复杂。
        • @MattB 但您仍然可以访问 personData 对象
        • @ExplosionPills:我认为它是劣质的,是的。方法fullName 不再是透明地对象接口的一部分。如果要将人员对象传递给另一个模块中的某个其他方法,则fullName 函数不会随之传递,这会使事情变得复杂:(
        【解决方案9】:

        您无需使用原型即可在准系统对象中绑定自定义方法。

        这里有一个优雅的例子,它不会污染你的代码,避免冗余代码

        var myobj = {
          title: 'example',
          assets: 
          {
            resources: ['zero', 'one', 'two']
          }
        }
        
        var myfunc = function(index)
        {
            console.log(this.resources[index]); 
        }
        
        myobj.assets.giveme = myfunc
        
        myobj.assets.giveme(1);
        

        https://jsfiddle.net/bmde6L0r/ 中提供的示例

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-12-08
          • 1970-01-01
          • 2013-09-23
          • 2012-06-09
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多