对象字面量/初始化程序中的自引用

918
有没有办法在JavaScript中使以下类似代码正常运行?
var foo = {
    a: 5,
    b: 6,
    c: this.a + this.b  // Doesn't work
};

在当前形式下,这段代码显然会抛出引用错误,因为this并不指向foo。但是是否有任何方法可以使对象字面量属性中的值依赖于之前声明的其他属性呢?
32个回答

1018

嗯,我能告诉你的唯一事情就是有关getter

var foo = {
  a: 5,
  b: 6,
  get c() {
    return this.a + this.b;
  }
}

console.log(foo.c) // 11

这是ECMAScript第5版规范引入的一种语法扩展,该语法得到大多数现代浏览器(包括IE9)的支持。


38
非常有帮助的答案。有关“get”的更多信息可以在此处找到:https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/get - jake
69
请注意,使用此解决方案,如果更改foo.afoo.b的值,则foo.c的值也将同步更改。这可能是所需的,也可能不是所需的。 - HBP
4
@HBP 这将与问题中发生的完全相同,所以在我看来,这似乎正是预期结果必须达到的。 - Randy
17
请注意,this绑定到最深层嵌套的对象。例如:... x: { get c () { /*这里的this是x,不是foo*/ } } ... - Bernardo Dal Corno
3
为了补充我之前的陈述,由于foo被声明为一个变量,并且c只会在调用时被计算,因此在c内部使用foo是有效的,而不像使用this(但请小心)。 - Bernardo Dal Corno
显示剩余8条评论

352

你可以尝试这样做:

var foo = {
   a: 5,
   b: 6,
   init: function() {
       this.c = this.a + this.b;
       return this;
   }
}.init();

这将是对象的一次初始化。

请注意,您实际上正在将 init() 的返回值分配给 foo ,因此您必须返回this


111
你也可以在return this之前加上delete this.init,这样就不会污染foo - Billy Moon
17
确实如此,虽然这样做会影响该对象上所有随后的属性访问性能,在许多引擎(例如V8)上都是如此。 - T.J. Crowder
9
不确定ES6类如何与问题相关。 - Felix Kling
9
类只是构造函数的语法糖,因此它们并没有提供任何新内容。无论哪种方式,这个问题的主要焦点都是对象字面量。 - Felix Kling
3
很好 :) 因为对象字面量是一个单独的表达式,所以init()调用直接将字面量附加在其后,使其仍然保持单个表达式。但是,如果你想的话,当然可以单独调用该函数。 - Felix Kling
显示剩余10条评论

228

显而易见,简单的答案丢失了,所以为了完整起见:

但是,有没有办法让对象字面量属性中的值依赖于先前声明的其他属性呢?

没有。所有这里的解决方案都是在对象创建后(以各种方式)推迟它,然后分配第三个属性。最简单的方法是只需执行以下操作:

var foo = {
    a: 5,
    b: 6
};
foo.c = foo.a + foo.b;
所有其他方法只是以更加间接的方式实现相同的功能。(Felix的方法特别聪明,但需要创建和销毁临时函数,增加了复杂性;而且可能会在对象上留下额外的属性,或者(如果您使用delete删除该属性)对该对象后续的属性访问的性能产生影响。)
如果您需要所有内容都包含在一个表达式中,可以不使用临时属性来实现:
var foo = function(o) {
    o.c = o.a + o.b;
    return o;
}({a: 5, b: 6});

当然,如果你需要执行此操作多次:

function buildFoo(a, b) {
    var o = {a: a, b: b};
    o.c = o.a + o.b;
    return o;
}

那么你需要在哪里使用它:

var foo = buildFoo(5, 6);

为了保持自己的理智,我正在寻找某种官方文档,它基本上说了同样的事情 - 对象的 this 只能在该对象的 方法 中使用,而不能用于其他类型的属性。你知道在哪里可以找到吗?谢谢! - David Kennell
1
@DavidKennell:这里的规范已经非常正式了。 :-) 你可能会从这里开始,然后一路跟进。虽然语言有些笨拙,但基本上你会在属性定义评估的各个子条款中看到,对象对于确定属性初始化程序值的操作是不可用的。 - T.J. Crowder
我无法在这里看到browserscope结果,但现在不再是这种情况了,对吧?在我的环境中,v8:delete比gecko快10%,而gecko:delete只慢1%。 - TheMaster
1
@TheMaster - 是的,我认为BrowserScope已经不再是一个真正存在的东西了。看起来删除操作并不像以前那么糟糕,至少在V8(Chrome等)或SpiderMonkey中不是这样。虽然仍然比较慢,但只有一点点,而且这些东西现在都非常快。 - T.J. Crowder

71

只需实例化一个匿名函数:

var foo = new function () {
    this.a = 5;
    this.b = 6;
    this.c = this.a + this.b;
};

1
@zzzzBov:当然,他们可以直接克隆对象,但与IEFE解决方案(如TJCrowder答案中所述)相比,您的解决方案会泄漏构造函数并创建一个多余的原型对象。 - Bergi
8
只需使用 var foo = function() { this.…; return this; }.call({});,语法上并没有太大的不同,但语义上是合理的。 - Bergi
1
@Bergi,如果你觉得这很重要,为什么不添加自己的答案呢? - zzzzBov
1
@zzzzBov 我认为这只是TJ答案的一个变体。 - Bergi
3
没问题。我确实没有注意到 new 关键字。你能行的。 - Randy
显示剩余2条评论

42

现在在ES6中,您可以创建懒惰缓存属性。第一次使用时,该属性会评估一次以成为正常的静态属性。结果:第二次跳过了数学函数的开销。

神奇之处在于getter方法。

const foo = {
    a: 5,
    b: 6,
    get c() {
        delete this.c;
        return this.c = this.a + this.b
    }
};
在箭头函数中,this会捕获周围的词法作用域
foo     // {a: 5, b: 6}
foo.c   // 11
foo     // {a: 5, b: 6 , c: 11}  

1
ES5也有属性,你只需要使用Object.defineProperty(foo, 'c', {get:function() {...}})来定义它们。在这样的工厂中,可以轻松地以不显眼的方式完成此操作。当然,如果您可以使用get语法糖,那么代码更易读,但是该功能一直存在。 - Aluan Haddad
1
这个代码完美运行,但是我想知道为什么你要删除 this.c,因为它根本不存在? 我尝试过不写 delete this.c,但是它没有起作用。 - Ahsan Alii
2
我也对 delete 操作感到困惑。我认为它的作用是从对象中删除 get c 属性,并用标准属性覆盖它。我认为这样做只会计算一次,然后如果稍后更改 abfoo.c 将不会更新其值,但这也仅在调用 foo.c 时起作用/缓存/计算。 - CTS_AE
是的,这就是所谓的惰性求值。更多信息请参见:https://en.wikipedia.org/wiki/Lazy_evaluation。 - voscausa
delete this.c 的目的是删除 getter 并将其替换为单个数字值。 这样做可以避免每次使用 foo.c 时需要执行 getter。 return 语句既创建了替代属性 foo.c,又返回了其值。 虽然有可能使用get c() { return this.a + this + b },但这会导致每次使用 foo.c 都重新评估。 - Mikko Rantalainen

27

有些闭包应该处理这个问题;

var foo = function() {
    var a = 5;
    var b = 6;
    var c = a + b;

    return {
        a: a,
        b: b,
        c: c
    }
}();

foo中声明的所有变量都是私有的,这符合任何函数声明的预期,因为它们都在作用域内,所以它们可以相互访问而不需要引用this,就像函数一样。不同之处在于,此函数返回一个公开私有变量并将该对象分配给foo的对象。最终,您使用return {}语句仅返回要作为对象公开的接口。

然后使用()在结尾处执行函数,导致整个foo对象被评估,实例化其中所有的变量,并将返回对象添加为foo()的属性。


17
称之为“闭包”是令人困惑和误导的。虽然对于确切含义存在不同观点,但在任何人看来,从函数中返回对象值并不构成闭包。 - user663031

18

你可以像这样做

var a, b
var foo = {
    a: a = 5,
    b: b = 6,
    c: a + b
}

当我需要引用函数最初声明的对象时,该方法已被证明对我有用。以下是我如何使用它的最简示例:

function createMyObject() {
    var count = 0, self
    return {
        a: self = {
            log: function() {
                console.log(count++)
                return self
            }
        }
    }
}

通过将self定义为包含打印函数的对象,您允许该函数引用该对象。这意味着如果您需要将其传递到其他地方,则无需将打印函数绑定到对象。

如果您使用下面所示的this,则会......

function createMyObject() {
    var count = 0
    return {
        a: {
            log: function() {
                console.log(count++)
                return this
            }
        }
    }
}

那么以下代码将记录0,1,2,然后报错

var o = createMyObject()
var log = o.a.log
o.a.log().log() // this refers to the o.a object so the chaining works
log().log() // this refers to the window object so the chaining fails!

使用self方法可以确保不论函数在什么上下文中运行,print始终返回同一个对象。使用self版本的createMyObject()函数,上述代码将正常运行并输出0、1、2和3。


12

为了完整性,在ES6中我们有类(在撰写本文时仅由最新浏览器支持,但可在Babel、TypeScript和其他编译器中使用)

class Foo {
  constructor(){
    this.a = 5;
    this.b = 6;
    this.c = this.a + this.b;
  }  
}

const foo = new Foo();

8

思考之所在——将对象的属性从时间轴中分离出来:

var foo = {
    a: function(){return 5}(),
    b: function(){return 6}(),
    c: function(){return this.a + this.b}
}

console.log(foo.c())

上面有更好的答案。这是我修改了你质疑的示例代码的方式。

更新:

var foo = {
    get a(){return 5},
    get b(){return 6},
    get c(){return this.a + this.b}
}
// console.log(foo.c);

2
在ES6中,您可以使这种通用方法更加优雅:var foo = { get a(){return 5}, get b(){return 6}, get c(){return this.a + this.b} },所以现在您只需执行foo.c而不是foo.c() :)(随意将其粘贴到您的答案中,以便格式更好!) - user993683
请注意,每次使用时都会重新计算 foo.c。这可能是您要寻找的,也可能不是。 - Mikko Rantalainen

7
您可以使用模块模式来实现。就像这样:

var foo = function() {
  var that = {};

  that.a = 7;
  that.b = 6;

  that.c = function() {
    return that.a + that.b;
  }

  return that;
};
var fooObject = foo();
fooObject.c(); //13

使用此模式,您可以根据需要实例化多个foo对象。 http://jsfiddle.net/jPNxY/1/

2
这不是模块模式的示例,只是一个函数。如果foo定义的最后一行是}();,它将自我执行并返回一个对象,而不是一个函数。另外,foo.c是一个函数,因此对其进行写入会覆盖该函数,并且通过fooObject.c()的下一次调用将失败。也许这个fiddle更接近你想要的(它也是单例,不是为了实例化而设计的)。 - Hollister
2
模块模式最初是作为一种提供传统软件工程中类的私有和公共封装的方式来定义的。来源:学习JavaScript设计模式。虽然该对象遵循上述模块模式,但可能不是最好的解释方式,因为它没有显示公共和私有属性/方法。这个 http://jsfiddle.net/9nnR5/2/ 是具有公共和私有属性/方法的相同对象。因此,它们都遵循此模式。 - Rafael Rocha

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