JavaScript中的唯一对象标识符

170
我需要做一些实验,需要在JavaScript中使用某种独特的标识符来标识对象,以便我可以看到它们是否相同。
我不想使用相等运算符。相反,我需要像Python中的id()函数这样的东西。
是否存在这样的东西?

3
我有点好奇,为什么你想避免使用等于运算符? - Christian C. Salvadó
40
因为我想要简单的东西,我想看到一个数字,有些清晰易懂的东西。这种语言让小猫咪哭泣,我已经在和它作斗争了。 - Stefano Borini
5
严格相等运算符(===)可用于对象的比较(如果您正在比较数字/字符串等,那不是同一回事),这比在每个对象中构建一个秘密唯一ID更简单。请注意,此运算符将按引用比较对象,而不是按值比较。 - Ben Zotto
10
拥有唯一标识符可以用于调试或实现类似身份集的功能。@CMS @Ben - Alex Jasmin
12
我希望传达的是,我在理解 JavaScript 方面有了重大突破,有点像闭合了一个突触。现在一切都清晰了,我看到了一些事情,作为 JavaScript 程序员,我的水平提高了。 - Stefano Borini
显示剩余4条评论
14个回答

87

更新 我下面的原始答案是6年前以当时的风格和我的理解方式编写的。针对评论中的一些讨论,现代化的方法如下:

(function() {
    if ( typeof Object.id != "undefined" ) return;

    var id = 0;

    Object.id = function(o) {
        if ( typeof o.__uniqueid != "undefined" ) {
            return o.__uniqueid;
        }

        Object.defineProperty(o, "__uniqueid", {
            value: ++id,
            enumerable: false,
            // This could go either way, depending on your 
            // interpretation of what an "id" is
            writable: false
        });

        return o.__uniqueid;
    };
})();

var obj = { a: 1, b: 1 };

console.log(Object.id(obj));
console.log(Object.id([]));
console.log(Object.id({}));
console.log(Object.id(/./));
console.log(Object.id(function() {}));

for (var k in obj) {
    if (obj.hasOwnProperty(k)) {
        console.log(k);
    }
}
// Logged keys are `a` and `b`

如果您有古老的浏览器要求,请在此处检查 Object.defineProperty 的浏览器兼容性。
原始答案保留在下面(而不仅仅是在更改历史记录中),因为我认为比较具有价值。

你可以试试以下的方法。这也让你有选择地在构造函数或其他地方明确设置对象的ID。

(function() {
    if ( typeof Object.prototype.uniqueId == "undefined" ) {
        var id = 0;
        Object.prototype.uniqueId = function() {
            if ( typeof this.__uniqueid == "undefined" ) {
                this.__uniqueid = ++id;
            }
            return this.__uniqueid;
        };
    }
})();

var obj1 = {};
var obj2 = new Object();

console.log(obj1.uniqueId());
console.log(obj2.uniqueId());
console.log([].uniqueId());
console.log({}.uniqueId());
console.log(/./.uniqueId());
console.log((function() {}).uniqueId());

请注意确保您用于内部存储唯一标识符的成员不会与其他自动创建的成员名称发生冲突。


2
@Justin 给 Object.prototype 添加属性在 ECMAScript 3 中存在问题,因为这些属性会出现在所有对象的可枚举属性中。因此,如果你定义了 Object.prototype.a,那么当你执行 for (prop in {}) alert(prop); 时,“a”将可见。所以你必须在增强 Object.prototype 和能够使用 for..in 循环遍历记录对象之间做出妥协。这对于库来说是一个严重的问题。 - Alex Jasmin
30
没有妥协。在使用 for..in 循环时,始终使用 object.hasOwnProperty(member) 一直被视为最佳实践。这是一种经过充分文档化的做法,并且是由 jslint 执行的。 - Justin Johnson
3
我不会推荐这样做,至少不是针对每个对象。你只需要处理那些需要的对象即可。不幸的是,我们大多数时候必须使用外部 JavaScript 库,并且不幸的是,它们并非都编写得很好,所以除非您完全控制了网页中包含的所有库,或者至少知道它们被良好处理,否则应避免这样做。 - useless
1
@JustinJohnson:在ES5中有一个妥协方案:defineProperty(…, {enumerable:false})。而且uid方法本身应该在Object命名空间中(参见https://dev59.com/tGrWa4cB1Zd3GeqP_XvI)。 - Bergi
2
由于 Object.id 是需要手动调用的函数,因此没有理由将其放在我们不拥有的全局 Object 上,我认为这与放在 Object.prototype 上并没有什么区别。 - Emile Bergeron
显示剩余3条评论

84

就我所观察到的,在这里发布的任何答案都可能会产生意想不到的副作用。

在 ES2015 兼容环境中,您可以通过使用WeakMap 来避免任何副作用。

const id = (() => {
    let currentId = 0;
    const map = new WeakMap();

    return (object) => {
        if (!map.has(object)) {
            map.set(object, ++currentId);
        }

        return map.get(object);
    };
})();

id({}); //=> 1

2
为什么不返回 currentId - Gust van de Wal
为什么要使用WeakMap而不是Map?如果您向该函数提供字符串或数字,它将崩溃。 - Nate Symer
14
使用 Map 时,键会对你传入其中的对象保持强引用。这会防止这些对象被垃圾回收,且可能导致严重的内存泄漏问题。 - Downgoat
我认为这应该是正确的方法,因为它可以让你更好地控制哪些对象需要有一个ID。我也写了一篇关于这个主题的博客文章,链接在这里 https://developapa.com/object-ids/ - Nicolas Gehlert
1
如果能解释一下这个解决方案是如何避免其他问题的话,这个答案会更好。例如,您可以在Object.prototypeObject.seal({})上尝试它。 - user3840170

35
最新的浏览器提供了一种更清晰的扩展Object.prototype的方法。这段代码将使属性在属性枚举中隐藏(对于p in o)。
对于实现defineProperty的浏览器, 您可以像这样实现uniqueId属性:
(function() {
    var id_counter = 1;
    Object.defineProperty(Object.prototype, "__uniqueId", {
        writable: true
    });
    Object.defineProperty(Object.prototype, "uniqueId", {
        get: function() {
            if (this.__uniqueId == undefined)
                this.__uniqueId = id_counter++;
            return this.__uniqueId;
        }
    });
}());

详情请参见https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/defineProperty


2
“最新的浏览器”显然不包括Firefox 3.6。(是的,我选择退出在较新版本的Firefox中进行升级竞赛,我相信我不是唯一一个这样做的人。此外,FF3.6只有1年的历史。) - bart
8
1岁的孩子不是“仅仅”参加这项运动。网络动态演变,这是一件好事。现代浏览器具备自动更新程序,正是为了实现这一目的。 - Kos
1
几年后,这对我来说似乎比被接受的答案更有效。 - Fitter Man
请确保永远不要编写 Object.prototype.uniqueId - user3840170

13

实际上,你不需要修改 object 的原型并在那里添加一个函数。以下代码对于你的目的应该可以很好地工作。

var __next_objid=1;
function objectId(obj) {
    if (obj==null) return null;
    if (obj.__obj_id==null) obj.__obj_id=__next_objid++;
    return obj.__obj_id;
}

19
Nocase、snake_case和camelCase都已经在一个6行代码片段中出现了。这种情况并不常见。 - Gust van de Wal
4
这似乎比“对象”黑客攻击更具说服力。 只需要在需要 OP 的对象匹配时运行它,其余时间它将不会干扰。 - JL Peyret
1
但是,如果你复制对象(例如{...obj}),你也会复制obj.__obj_id。关键是要防止这种情况发生。 - Gábor Angyal

6

对于实现了Object.defineProperty()方法的浏览器,下面的代码生成并返回一个函数,您可以将其绑定到任何您拥有的对象上。

这种方法的优点是不会扩展Object.prototype

该代码的工作方式是检查给定对象是否具有__objectID__属性,并在不存在时将其定义为隐藏(不可枚举)只读属性。

因此,它针对任何尝试更改或重新定义只读的obj.__objectID__属性的企图是安全的,并且始终会抛出一个漂亮的错误而不是悄然失败。

最后,在一些极端情况下,如果其他代码已经在给定对象上定义了__objectID__,则该值将简单地被返回。

var getObjectID = (function () {

    var id = 0;    // Private ID counter

    return function (obj) {

         if(obj.hasOwnProperty("__objectID__")) {
             return obj.__objectID__;

         } else {

             ++id;
             Object.defineProperty(obj, "__objectID__", {

                 /*
                  * Explicitly sets these two attribute values to false,
                  * although they are false by default.
                  */
                 "configurable" : false,
                 "enumerable" :   false,

                 /* 
                  * This closure guarantees that different objects
                  * will not share the same id variable.
                  */
                 "get" : (function (__objectID__) {
                     return function () { return __objectID__; };
                  })(id),

                 "set" : function () {
                     throw new Error("Sorry, but 'obj.__objectID__' is read-only!");
                 }
             });

             return obj.__objectID__;

         }
    };

})();

5

这是@justin答案的TypeScript版本,兼容ES6,使用符号来防止任何键冲突,并添加到全局Object.id以方便使用。只需复制粘贴下面的代码,或将其放入一个ObjectId.ts文件中进行导入。

(enableObjectID)();

declare global {
    interface ObjectConstructor {
        id: (object: any) => number;
    }
}

const uniqueId: symbol = Symbol('The unique id of an object');

export function enableObjectID(): void {
    if (typeof Object['id'] !== 'undefined') {
        return;
    }

    let id: number = 0;

    Object['id'] = (object: any) => {
        const hasUniqueId: boolean = !!object[uniqueId];
        if (!hasUniqueId) {
            object[uniqueId] = ++id;
        }

        return object[uniqueId];
    };
}

使用示例:

console.log(Object.id(myObject));

如果你只需要一个辅助函数,你可以将上面的代码缩短如下:
let counter = 0;
const weakMap = new WeakMap();
const getObjectId = (obj) => (weakMap.get(obj) ?? (weakMap.set(obj, ++counter) && counter));

这里是游乐场


这段代码无法与 Object.seal({}) 一起使用,并且一旦将 Object.prototype 输入其中,它就会崩溃。 - user3840170
@user3840170,你很聪明,用WeakMap解决了问题(当我写这个评论时,我刚刚意识到另一个答案也建议使用WeakMap...) - Flavien Volken

4

为了比较两个对象,最简单的方法是在需要比较对象时向其中一个对象添加一个唯一属性,检查该属性是否存在于另一个对象中,然后再将其删除。这样可以避免覆盖原型。

function isSameObject(objectA, objectB) {
   unique_ref = "unique_id_" + performance.now();
   objectA[unique_ref] = true;
   isSame = objectB.hasOwnProperty(unique_ref);
   delete objectA[unique_ref];
   return isSame;
}

object1 = {something:true};
object2 = {something:true};
object3 = object1;

console.log(isSameObject(object1, object2)); //false
console.log(isSameObject(object1, object3)); //true

3

jQuery代码使用自己的data()方法作为id。

var id = $.data(object);

在后台方法中,data会在object中创建一个非常特殊的字段,称为"jQuery" + now(),并将下一个唯一ID流的ID放置其中。
id = elem[ expando ] = ++uuid;

我建议您使用与John Resig相同的方法,因为他显然了解JavaScript的所有知识,并且他的方法基于这些知识。

6
jQuery的data方法存在缺陷。例如,请参见https://dev59.com/yHI-5IYBdhLWcg3wVWli#1915699。另外,John Resig并不是关于JavaScript所有知识的权威,认为他是权威并不能帮助你成为一名JavaScript开发者。 - Tim Down
1
@Tim,就获取唯一ID而言,它与这里介绍的任何其他方法一样存在许多缺陷,因为在底层它基本上做了相同的事情。是的,我相信John Resig比我知道更多,即使他不是Douglas Crockford,我也应该从他的决策中学习。 - vava
1
据我所知,$.data 不适用于 JavaScript 对象,只适用于 DOM 元素。 - mb21

2
我面临了同样的问题,这是我用ES6实现的解决方案。
let id = 0; // This is a kind of global variable accessible for every instance 

class Animal {
    constructor(name) {
        this.name = name;
        this.id = id++; 
    }

    foo() {} // Executes some cool stuff
}

const cat = new Animal("Catty");
console.log(cat.id) // 1

1
这仅适用于您创建的类,因此它不是完全通用的解决方案,但至少它不会对系统造成积极的危害。 - user3840170

0

虽然有人建议不要修改Object.prototype,但在有限的范围内进行测试仍然非常有用。被接受的答案的作者进行了更改,但仍然设置了Object.id,这对我来说没有意义。下面是一个可以完成任务的片段:

// Generates a unique, read-only id for an object.
// The _uid is generated for the object the first time it's accessed.

(function() {
  var id = 0;
  Object.defineProperty(Object.prototype, '_uid', {
    // The prototype getter sets up a property on the instance. Because
    // the new instance-prop masks this one, we know this will only ever
    // be called at most once for any given object.
    get: function () {
      Object.defineProperty(this, '_uid', {
        value: id++,
        writable: false,
        enumerable: false,
      });
      return this._uid;
    },
    enumerable: false,
  });
})();

function assert(p) { if (!p) throw Error('Not!'); }
var obj = {};
assert(obj._uid == 0);
assert({}._uid == 1);
assert([]._uid == 2);
assert(obj._uid == 0);  // still

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接