在 JavaScript 中创建一个隐藏的属性是否可行?

41
我想创建一个包含隐藏属性的对象(这个属性不会在 for (var x in obj) 循环中显示)。是否可能实现这一点?

有这样一个功能会很不错。例如,在Node JS中,模型映射到数据库表,我们可以隐藏某些字段,而无需调用函数来删除/过滤属性。 - TheRealChx101
6个回答

52

在ECMAScript 3中是不可能的(这也是2010年提出此问题时主流浏览器所实现的版本)。但是,在所有主流浏览器当前采用的ECMAScript 5中,可以将属性设置为不可枚举:

var obj = {
   name: "Fred"
};

Object.defineProperty(obj, "age", {
    enumerable: false,
    writable: true
});

obj.age = 75;

/* The following will only log "name=>Fred" */
for (var i in obj) {
   console.log(i + "=>" + obj[i]);
}

这个方法适用于现代浏览器:详情请参考http://kangax.github.com/es5-compat-table/中旧版本浏览器的兼容性。

需要注意的是,在调用Object.defineProperty时,属性还必须设置为可写才能进行正常赋值(默认情况下是false)。


3
defineProperty 在IE8几乎被支持,这里的“几乎”是指仅在DOM对象上支持。http://msdn.microsoft.com/en-us/library/dd548687(VS.85).aspx - Andy E
2
安迪:谢谢,这很有趣。我没查过“几乎”是什么意思。 - Tim Down
4
Object.getOwnPropertyNames(obj) 仍将获取隐藏属性的名称。 - DDS
1
你也可以添加一个属性命名规范,例如,在私有参数前面使用双下划线,然后在代码中跳过这些参数。obj.__age,通常在迭代列表时会使用obj.hasOwnProperty(i),但是你可以使用obj.hasOwnProperty(i) && i.charAt(0)!=="_"。命名约定也是记住哪些属性是公共/私有的好方法。但是,你肯定可以向上面的解决方案添加命名约定,并且它将同样有效。 - John Proestakes
console.log(obj); // 仍然显示{name: "Fred", age: 75}。因为defineProperty在这里不起作用,是否有其他方法可以隐藏它? - user985399
@PauliSudarshanTerho:defineProperty 运作良好,至少就原本的问题而言是如此。在 Chrome 中,age 属性略微轻一些,表示它不可枚举。仍然可以使用 Object.getOwnPropertyNames 列出对象的非可枚举属性,因此我会认为这就是控制台所做的事情。 - Tim Down

35

为了保持内容的时效性,这里介绍的是ES6+的情况。我会在问题的范围之外讨论如何隐藏属性,而不仅仅是在for ... in循环中。

有几种方法可以创建所谓的“隐藏属性”,而不是查看像闭包中封闭的变量这样受到作用域规则限制的东西。

现在经典的非枚举属性

与ECMAScript的以���版本一样,您可以使用Object.defineProperty来创建未标记为enumerable的属性。这使得当您使用某些方法(例如for ... in循环和Object.keys函数)枚举对象的属性时,该属性不会显示出来。

Object.defineProperty(myObject, "meaning of life", {
    enumerable : false,
    value : 42
});

然而,您仍然可以使用Object.getOwnPropertyNames函数找到它,该函数返回甚至是不可枚举的属性。当然,您仍然可以通过其键访问属性,这只是一个任何人都可以构建的字符串理论上。

一个(不可枚举的)symbol属性

在ES6中,可以使用新的原始类型——symbol的键来创建属性。这种类型由Javascript本身用于使用for ... of循环枚举对象,并由库编写者用于执行各种其他操作。

Symbol具有描述性文本,但它们是引用类型,具有唯一的标识。它们不像字符串,如果它们具有相同的值,则相等。要使两个符号相等,它们必须是完全相同的两个引用。

您可以使用Symbol函数创建一个symbol

let symb = Symbol("descriptive text");

您可以使用 Object.defineProperty 函数来使用符号作为键定义属性。

let theSecretKey = Symbol("meaning of life");
Object.defineProperty(myObject, theSecretKey, {
    enumerable : false,
    value : 42
});

除非有人获取到该符号对象的引用,否则无法通过键查找属性的值。

但是您还可以使用常规语法:

let theSecretKey = Symbol("meaning of life");
myObject[theSecretKey] = 42;

具有此键类型的属性永远不会显示在for ... in循环或类似循环中,但仍可枚举和不可枚举,因为像Object.assign这样的函数对于不可枚举属性有不同的处理方式。

Object.getOwnPropertyNames无法获取对象的symbol键,但类似命名的Object.getOwnPropertySymbols可以解决问题。

Weak maps

在对象上隐藏属性的最强方法是根本不将其存储在对象上。在 ES6 之前,这有点棘手,但现在我们有了弱映射。

弱映射基本上是一个Map,即一个键值存储,它不保留(强)引用以便被垃圾回收。弱映射非常有限,不允许枚举其键(这是设计如此)。但是,如果您获取到映射的其中一个键的引用,则可以获取与之相关联的值。

它们主要设计用于扩展对象而不实际修改它们。

基本思想是创建一个弱映射:

let weakMap = new WeakMap();

并使用您要扩展的对象作为键。然后值将是属性集,可以采用{}对象或Map数据结构的形式。

weakMap.set(myObject, {
    "meaning of life" : 42
});
这种方法的优点是,如果想要获取值,甚至知道它们的存在,某人需要引用您的weakMap实例密钥。没有任何绕过它的方法。因此这是100%的安全保证。以这种方式隐藏属性可以确保没有用户会发现它们,您的Web应用程序也永远不会被黑客攻击*。
当然,所有这些中最大的缺陷是,这并没有创建一个实际的属性。因此它不参与原型链等操作。
(*) 这是个谎言。

私有类字段

ECMAScript 中相对较新的添加是私有类字段。这个功能目前处于 Stage 3 阶段,尚未成为最终标准。但是,在所有现代浏览器和更新版本的 Node 中都支持它。
私有类字段是类字段声明的特定变体。只有在定义 JavaScript 类时才能使用它们,不能在其他任何地方定义。语法如下:
class Example {
    #thisIsPrivate;
    constructor(v) {
        this.#thisIsPrivate = v;
    }
}

这个字段是真正私有的。它只能被在 Example 语法定义内的代码访问,其他地方无法访问。没有反射API或其他功能可以让您访问该字段。它永远不会出现在诸如 Object.getOwnPropertyNames 的函数的结果中。该字段的名称始终以 # 开头。


3
只是想说非常感谢您提供如此详细和写作精良的回复!我真的感觉在这里学到了很多,而且对第一次玩弱引用映射表感到非常兴奋。最终我在我的用例中使用了 Map() 而不是 WeakMap() (希望稍后能够对地图进行浅层比较的克隆),但是思路是相同的。非常激动不再改变数据对象。 - constancecchen
1
好的答案!感谢您重新审视这个旧问题并添加一个新的相关答案。私有类字段正是我要找的。 - praty

13

这有点棘手!

function secret() {
  var cache = {};
  return function(){
    if (arguments.length == 1) { return cache[arguments[0]];}
    if (arguments.length == 2) { cache[arguments[0]] = arguments[1]; }
  };
}
var a = secret();

a.hello = 'world';
a('hidden', 'from the world');

但如果你是真正的专业人士,你可以这样做!

var a = new (secret())();

a.hello = 'world';
a.constructor('hidden', 'from the world');

现在如果你在firebug中查看a,它将会是一个对象...但是你知道的更好!;-)


1
太棒了,正是我在寻找的!+1!甚至可以在IE中使用 ;-). 提示: 要检索示例中的值,请使用 a.constructor('hidden'); - Matt
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor - Muhammad Umer

0

试试这个:

Object.defineProperty(
    objectName,
    'propertiesName', {
        enumerable: false
    }
)

1
在格式化代码后,我发现有太多的 ) - Meloman

0
var Foo=function(s){
    var hidden
    this.setName=function(name){theName=name}
    this.toString=function(){return theName}
    this.public=s
}
var X=new Foo('The X')
X.setName('This is X')
X // returns 'This is X'
X.public // returns 'The X'
X.hidden // returns 'undefined'

0
这是一个使用代理对象的解决方案。
一个示例事件发射器:
class Event {
  constructor(opts = {}) {
    this.events = new Map
    this.proxy = new class {}
    Object.defineProperty(this.proxy, 'on', { value: this.on.bind(this) })
    Object.defineProperty(this.proxy, 'emit', { value: this.emit.bind(this) })
    Object.defineProperty(this.proxy, 'length', { get: () => this.length })
    Object.defineProperty(this.proxy.constructor, 'name', {
      value: this.constructor.name
    })
    return new Proxy(this.proxy, {})
  }
  on(topic, handler) {
    if (!this.events.has(topic))
      this.events.set(topic, new Set)
    this.events.get(topic).add(handler)
    return this.remove.bind(this, topic, handler)
  }
  emit(topic, ...payload) {
    if (!this.events.has(topic))
      return
    const set = this.events.get(topic)
    for (const fn of set)
      fn(...payload)
  }
  remove(topic, handler) {
    if (!this.events.has(topic))
      return
    const set = this.events.get(topic)
    if (set.has(handler))
      set.delete(handler)
  }
  get length() {
    return this.events.size
  }
}

注意,在构造函数中,它返回一个新的代理,并引用代理属性。我“装饰”了代理对象,使其看起来像原始类。你可以获取长度,因为我暴露了那个getter,但是没有办法直接访问事件Map并迭代键(就我所知道的)。我想这有点像反向闭包?关于垃圾回收,我不确定它是如何工作的。但是它确实能够将功能封装起来,使用户无法搞砸事情。
更新: 所以,那种方法会干扰原型继承。在这里,我找到了一种类似但更好的技术,通过在创建类时钩入构造方法并提升“隐藏”的变量events
let Event = 
class Event {
  on(topic, handler) {
    if (!events.has(topic))
      events.set(topic, new Set)
    events.get(topic).add(handler)
    return this.remove.bind(this, topic, handler)
  }
  emit(topic, ...payload) {
    if (!events.has(topic))
      return
    const set = events.get(topic)
    for (const fn of set)
      fn(...payload)
  }
  remove(topic, handler) {
    if (!events.has(topic))
      return
    const set = events.get(topic)
    if (typeof handler === 'undefined')
      return events.delete(topic)
    if (set.has(handler))
      set.delete(handler)
  }
  get length() {
    return events.size
  }
}
let events
Event = new Proxy(Event, {
  construct (target, args, self) {
    events = new Map
    return Reflect.construct(target, args, self)
  }
})

这是我使用这个概念的更完整特性的事件发射器的要点: https://gist.github.com/aronanda/18b6397c355da88ca170d01e0cc02628


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