如何在javascript中处理对象属性时避免编写丑陋的代码

时间:2017-11-09 23:01:03

标签: javascript prototype

我发现当我写javascript时遇到的情况是我被迫编写丑陋的代码。这是由于我无法协调以下两个标准:

1)使用速记来定义数据,例如var data = { a: 1, b: { c: 2, d: 3 ... } }

2)使用原始运算符检查属性是否存在

例如,考虑一个返回两个Object个实例的交集的函数。键作为交叉键的Array

var intersection = function(obj1, obj2) {
    var result = [];
    for (var k in obj1) if (k in obj2) result.push(k);
    return result;
};

该代码看起来很不错。但不幸的是,它并不总是按预期工作!这是由于for (x in y)if (x in y)之间的不一致:使用for (x in y)只会迭代"拥有"属性(对hasOwnProperty返回true的属性),而if (x in y)同时申请"拥有" "非自己"属性!

如果我这样打电话给intersection

var obj1 = { toString: 'hahaha' };
var obj2 = {};
var intersectingKeys = intersection(obj1, obj2);

我将结束intersectingKeys === [ 'toString' ];显然这是不正确的:涉及空集的intersection操作(如obj2所示)必须返回空集。虽然{}显然是空的"但我们的问题是('toString' in {}) === true。这也适用于'constructor''valueOf'等术语,以及将来引入Object.prototype的所有新属性。

在我看来,如果本机操作符可以提供对键的迭代,本机操作符应该能够验证键是否会出现在迭代中。对我来说,感觉不一致和丑陋,使用本机运算符作为一个,但函数调用另一个。出于这个原因,我不喜欢这个修复:

var safeIntersection = function(obj1, obj2) {
    var result = [];
    for (var k in obj1) if (obj2.hasOwnProperty(k)) result.push(k);
    return result;
};

如果必须使用if (x in y),我只会看到另一个可能的解决方案:确保传递给intersection的参数完全没有属性,除了我们的代码明确定义的属性。换句话说,确保我们只使用无原型对象:

var obj1 = Object.create(null, {
    toString: {
        configurable: true,
        enumerable: true,
        writable: true,
        value: 'hahaha'
    }
});
var obj2 = Object.create(null, {});
var intersectingKeys = intersection(obj1, obj2);

请注意,此代码使用的是intersection,而不是safeIntersection,并且仍然有效,因为obj1obj2是无原型的。但问题是,现在数据定义真的非常笨重!看一下使用单个" toString"定义一个对象需要多少代码。属性。这种方法阻止我们使用javascript的漂亮对象速记。即使我们编写一个实用程序函数来包含无原型对象创建,嵌套对象的定义仍然非常笨重:

// Utility function for prototype-less object definition
var obj = function(props) {
    return Object.create(null, props.map(function(v) {
        return {
            writable: true,
            configurable: true,
            enumerable: true,
            value: v
        };
    }));
};

// Now defining `obj1` looks ok...
var obj1 = obj({ toString: 'hahaha' });

// But for large, nested object definitions it's sooper ugly:
var big = obj({
    a: 'a value',
    b: 'b value',
    moreProps: obj({
        wheee: 'yay',
        evenMoreProps: obj({
            prop: 'propMeUp'
            /* ... */
        })
        /* ... */
    })
});

Javascript的对象定义速记是该语言的巨大特权,并且通过强制将所有{ ... }实例包装在函数调用中而将其丢弃似乎是一个非常可惜。

我对此问题的理想解决方案是将速记对象构造函数转换为生成无原型对象。也许是一个全球性的背景:

// Perhaps along with other global settings such as:
'use strict';
Error.stackTraceLimit = Infinity;

// We could also have: 
Object.shorthandIncludesPrototype = false;

虽然即使这种解决方案可用,但它会破坏大量已有的库。 :(

如何协调以下条件???:

1)编写有效的代码

2)使用原始in运算符检查属性是否存在

3)使用典型的简写

定义对象

也许不可能同时满足所有这些标准。在这种情况下,在这些情况下保持代码清洁的下一个最佳方法是什么?

5 个答案:

答案 0 :(得分:3)

您可以先使用Object.keys获取第一个对象的属性,然后使用Object.hasOwnProperty过滤第二个对象的键。



function intersection(o1, o2) {
    return Object.keys(o1).filter({}.hasOwnProperty.bind(o2));
}

console.log(intersection({ a: 10, b: 20, e: 30 }, { a: 10, c: 20, d: 30 })); // ['a']
console.log(intersection({ toString: 'hahaha' }, {}));                       // []




答案 1 :(得分:2)

所以,你已经列出了三个不兼容的要求。你说你必须使用in。你说对象必须定义为{}所以它们将有一个原型。但是,你不喜欢in的工作方式。并且,您希望代码使用这些代码,但工作方式与设计不同。这是你的两个选择。

这些要求没有答案。 in的工作方式。你无法改变它。我们无法改变它。是的,它根据上下文的不同而不同,但这就是它的实现方式。通过找到真正问题的实用解决方案来处理这个问题,或者编写自己的语言,使其按照您希望语言的工作方式运行。

仅供参考,对于实际解决方案,您可能需要考虑使用Map对象来存储数据,并在其上使用.get().has().set()。简单,清晰,有效。

之前的答案在问题被大量编辑之前

首先,您可以使用Object.create(null);创建无原型对象。所以,它所拥有的唯一属性就是你放在那里的属性。

我建议您不要直接从对象访问属性,而只需创建一个可重用的函数,在返回之前检查属性名称是否有效:

function sendValidProperty(req, res) {
    var propName = req.params.propertyName;
    if (Object.prototype.hasOwnProperty.call(dataObject, propName) {
        res.send(dataObject[propName]);
    } else {
        res.sendStatus(400);
    }
}

router.get('/propertyOfObject/:propertyName', function(req, res) {
    sendValidProperty(req, res);
});

或者,您可以封装一小部分:

function getValidProperty(obj, propName) {
    return Object.prototype.hasOwnProperty.call(dataObject, propName) ? obj[propName] : null;
}

router.get('/propertyOfObject/:propertyName', function(req, res) {
    let val = getValidProperty(dataObject, res.params.propertyName);
    if (val !== null) {
        res.send(val);
    } else {
        res.sendStatus(400);
    }
});

在任何一种情况下,您都不必重复检查财产。这是共同的共享功能。仅供参考,当您想在无原型对象上使用对象方法时,您可以使用我在上面显示的表单:Object.prototype.hasOwnProperty.call(dataObject, propName)而不是dataObject.hasOwnProperty(propName)

服务器开发的主要规则之一是永远不要相信您从请求中获得的输入。在使用之前,您必须经常检查或消毒。整个问题听起来像是你试图避免这样做。您无法快速检查输入并拥有完全可靠的服务器。

答案 2 :(得分:1)

我有两个额外的解决方案,可能值得一提:

1)递归obj函数

正如我在问题中提到的那样,使用obj方法从通过简写定义的常规Object创建非原型值时,如果需要在整个嵌套速记中应用所有内容,则会非常难看结构 - 但是如果obj方法是递归的,那么这个问题可以在某种程度上得到解决,因此它只需要应用于根Object而不是显式地应用于每个内部子属性:

var obj = function(val) {
  if (val.constructor === Object) {
    var ret = Object.create(null);
    for (var k in val) ret[k] = obj(val[k]);
  } else if (val.constructor === Array) {
    var ret = [];
    for (var i = 0; i < val.length; i++) ret.push(obj(val[i]));
  } else {
    var ret = val;
  }

  return ret;
};

var thing = obj({
  prop1: 'hello',
  prop2: 'hi',
  prop3: [
    { num: 1, val: 'lalala' },
    { num: 2, val: 'heehee' },
    { num: 3, val: 'hoho' },
  ],
  really: {
    deep: {
      property: {
        over: {
          here: { val: 'soooo deep' }
        }
      }
    }
  }
});

console.log('toString' in thing);
console.log('toString' in thing.prop3[1]);
console.log('toString' in thing.really.deep.property.over.here);

现在,所有速记对象都包含在obj中非常重要。

2)清除Object.prototype

我发现chrome和node(v0.12.0)允许我删除Object.prototype中的所有属性。如果将这些删除的属性保存在内存中,原型甚至可以在将来的任何时候恢复:

var savedPrototype = {};
var removePrototype = function() {
  savedPrototype = {};
  var props = Object.getOwnPropertyNames(Object.prototype);
  for (var i = 0; i < props.length; i++) {
    savedPrototype[props[i]] = Object.prototype[props[i]];
    delete Object.prototype[props[i]];
  }
};
var restorePrototype = function() {
  for (var k in savedPrototype) {
    Object.prototype[k] = savedPrototype[k];
  }
};

removePrototype();
var obj = { val: 'haha' };
console.log('toString' in obj); // false
restorePrototype();
console.log('toString' in obj); // true

答案 3 :(得分:0)

正如jfriend00已经写过的那样,该问题提出了当前版本的JavaScript中根本不可能的约束。我们能做的最好的事情是在语言的限制范围内写一些抽象。

我们已经研究过一些可能性,例如使用包装函数(如问题中的obj())。由于“普遍支持”不是而不是问题的标准,我将使用ES6 Proxies提出一个解决方案。我们可以使用has()的{​​{1}}处理程序方法来更改Proxy运算符的行为,以便它只考虑自己的属性:

in

这需要一个现代浏览器(没有IE; Edge OK)或Node 6.4.0+。当然,将成员添加到Object.prototype.own = function () { return new Proxy(this, { has: function (target, propertyName) { return target.hasOwnProperty(propertyName); } }); }; document.writeln('toString' in {}); document.writeln('toString' in {}.own()); document.writeln('foo' in { foo: 1 }); document.writeln('foo' in { foo: 1 }.own());的原型是一个冒险的举动。我们可以根据需要向Object添加其他处理程序,或者重写ES5及更低版本的实现(但这需要更多的代码)。

除了兼容性问题,这种方法满足了问题的要求:我们可以使用对象文字和Proxy运算符,代码简洁易读。我们只需要记得在需要时致电in

我们可以使用此约定从问题中重写own()方法:

intersection()

答案 4 :(得分:0)

你的问题可以通过一些漂亮的代码来解决。

ES5

var intersect = function(obj1, obj2) {
  var keys1 = Object.keys(obj1);
  var keys2 = Object.keys(obj2);

  return keys1.filter(function(k) {
    return keys2.indexOf(k) !== -1;
  });
}

ES6

let intersect = (obj1, obj2) => {
  let keys1 = Object.keys(obj1);
  let keys2 = Object.keys(obj2);

  return keys1.filter(k => keys2.includes(k));
}

这是解决此类问题的JavaScript方法。它更好,因为你不需要使用任何循环。 filter()indexOf()includes()等内置方法的效果优于任何循环。