【发布时间】:2010-04-14 10:07:04
【问题描述】:
我想创建一个具有隐藏属性的对象(该属性未显示在for (var x in obj 循环中)。可以这样做吗?
【问题讨论】:
-
如果有这样的功能就好了。例如,在模型映射到数据库表的节点 JS 中,我们可以隐藏某些字段,而无需调用函数来删除/过滤属性。
标签: javascript
我想创建一个具有隐藏属性的对象(该属性未显示在for (var x in obj 循环中)。可以这样做吗?
【问题讨论】:
标签: javascript
这在 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)。
【讨论】:
defineProperty,这几乎意味着它仅在 DOM 对象上。 msdn.microsoft.com/en-us/library/dd548687(VS.85).aspx
Object.getOwnPropertyNames(obj) 仍将获得隐藏属性的名称。
obj.__age 并且通常您在遍历列表时会使用 obj.hasOwnProperty(i),但您可以改为使用 obj.hasOwnProperty(i) && i.charAt(0)!=="_" 命名约定也是记住哪些属性是公共/私有的好方法。但是,您当然可以在上面的解决方案中添加一个命名约定,它也可以正常工作。
为了保持最新状态,这是 ES6+ 中的状态。我有点超出了问题的范围,并谈论了如何隐藏一般属性,而不仅仅是来自for ... in 循环。
有几种方法可以创建所谓的“隐藏属性”,而无需查看诸如由闭包关闭的变量之类的内容,这些内容受范围规则的限制。
与以前版本的 ECMAScript 一样,您可以使用 Object.defineProperty 创建未标记为 enumerable 的属性。这使得当您使用某些方法(例如for ... in 循环和Object.keys 函数)枚举对象的属性时,不会显示该属性。
Object.defineProperty(myObject, "meaning of life", {
enumerable : false,
value : 42
});
但是,您仍然可以使用 Object.getOwnPropertyNames 函数找到它,该函数甚至返回不可枚举的属性。当然,你仍然可以通过它的键来访问属性,理论上它只是一个任何人都可以构建的字符串。
symbolproperty在 ES6 中,可以使用新的原始类型的键创建属性 -- symbol。 Javascript 本身使用这种类型来枚举一个使用 for ... of 循环的对象,并由库编写者用来做各种其他事情。
Symbols 具有描述性文本,但它们是具有唯一标识的引用类型。它们不像字符串,如果它们具有相同的值,则它们是相等的。要使两个符号相等,它们必须是完全相同事物的两个引用。
您使用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 可以解决问题。
在对象上隐藏属性的最有效方法是根本不将其存储在对象上。在 ES6 之前,这有点棘手,但现在我们的地图很弱。
弱映射基本上是Map,即键值存储,它不保留对键的(强)引用,因此它们可以被垃圾收集。弱映射非常有限,并且不允许您枚举其键(这是设计使然)。但是,如果您获得对地图键之一的引用,则可以获得相应的值。
它们的主要设计目的是允许扩展对象而不实际修改它们。
基本思路是创建一个weak map:
let weakMap = new WeakMap();
并使用要扩展的对象作为键。然后这些值将是一组属性,可以是{} 对象的形式,也可以是Map 数据结构的形式。
weakMap.set(myObject, {
"meaning of life" : 42
});
这种方法的优点是有人需要获得对您的 weakMap 实例的引用和才能获取值,甚至知道它们的存在。这是没有办法的.所以它是 100%,保证是安全的。以这种方式隐藏属性可确保用户永远不会发现它们,并且您的 Web 应用程序永远不会被黑客入侵*
当然,这一切中最大的缺陷是它并没有创建一个实际的属性。所以它不参与原型链之类的。
(*) 这是一个谎言。
最近添加到 ECMAScript 中的是私有类字段。此功能目前处于第 3 阶段,尚未进入最终标准。但是,所有现代浏览器和更新版本的 Node 都支持它。
私有类字段是类字段声明的特定变体。您只能在定义 JavaScript 类时使用它们。它们不能在其他任何地方定义。语法如下:
class Example {
#thisIsPrivate;
constructor(v) {
this.#thisIsPrivate = v;
}
}
这个字段是真正私有的。它只能通过Example 的语法定义内的代码访问,其他任何地方都无法访问。没有反射 API 或其他功能可让您访问该字段。它永远不会作为Object.getOwnPropertyNames 等函数的结果出现。字段名称始终以# 开头。
【讨论】:
这有点棘手!
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');
现在,如果您在萤火虫中查看它,它将是一个对象……但您知道得更清楚! ;-)
【讨论】:
a.constructor('hidden');。
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'
【讨论】:
试试这个:
Object.defineProperty(
objectName,
'propertiesName', {
enumerable: false
}
)
【讨论】:
) 太多了??
这是一个使用 Proxy 对象的解决方案。
一个示例事件发射器:
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
【讨论】: