关于javascript:对象文字/初始化器中的自引用

Self-references in object literals / initializers

有没有办法在JavaScript中使用以下内容?

1
2
3
4
5
var foo = {
    a: 5,
    b: 6,
    c: this.a + this.b  // Doesn't work
};

在当前形式中,此代码显然会引发引用错误,因为this不引用foo。 但有没有办法让对象文字的属性中的值依赖于之前声明的其他属性?


好吧,我唯一可以告诉你的是吸气剂:

1
2
3
4
5
6
7
8
9
var foo = {
  a: 5,
  b: 6,
  get c() {
    return this.a + this.b;
  }
}

console.log(foo.c) // 11

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


你可以这样做:

1
2
3
4
5
6
7
8
var foo = {
   a: 5,
   b: 6,
   init: function() {
       this.c = this.a + this.b;
       return this;
   }
}.init();

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

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


缺少明显的简单答案,所以为了完整性:

But is there any way to have values in an object literal's properties depend on other properties declared earlier?

不会。这里的所有解决方案都会推迟到创建对象之后(以各种方式),然后分配第三个属性。最简单的方法就是这样做:

1
2
3
4
5
var foo = {
    a: 5,
    b: 6
};
foo.c = foo.a + foo.b;

所有其他人只是更间接的方式来做同样的事情。 (Felix's特别聪明,但需要创建和销毁临时函数,增加复杂性;要么在对象上留下额外的属性,要么[如果你delete该属性]影响对该对象的后续属性访问的性能。)

如果你需要它在一个表达式中,你可以在没有临时属性的情况下做到这一点:

1
2
3
4
var foo = function(o) {
    o.c = o.a + o.b;
    return o;
}({a: 5, b: 6});

或者当然,如果您需要多次执行此操作:

1
2
3
4
5
function buildFoo(a, b) {
    var o = {a: a, b: b};
    o.c = o.a + o.b;
    return o;
}

然后你需要使用它:

1
var foo = buildFoo(5, 6);


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

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


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

魔法在吸气剂中。

1
2
3
4
5
6
7
8
const foo = {
    a: 5,
    b: 6,
    get c() {
        delete this.c;
        return this.c = this.a + this.b
    }
};

在箭头getter this中选取周围的词法范围。

1
2
3
foo     // {a: 5, b: 6}
foo.c   // 11
foo     // {a: 5, b: 6 , c: 11}


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

1
2
3
4
5
6
7
8
9
10
11
var foo = function() {
    var a = 5;
    var b = 6;
    var c = a + b;

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

foo中声明的所有变量都是foo的私有变量,正如您对任何函数声明所期望的那样,并且因为它们都在范围内,所以它们都可以相互访问而无需引用this,就像您一样期望有一个功能。区别在于此函数返回一个公开私有变量的对象,并将该对象分配给foo。最后,使用return {}语句返回要作为对象公开的接口。

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


你可以这样做

1
2
3
4
5
6
var a, b
var foo = {
    a: a = 5,
    b: b = 6,
    c: a + b
}

当我不得不引用最初声明函数的对象时,该方法对我有用。以下是我如何使用它的最小例子:

1
2
3
4
5
6
7
8
9
10
11
function createMyObject() {
    var count = 0, self
    return {
        a: self = {
            log: function() {
                console.log(count++)
                return self
            }
        }
    }
}

通过将self定义为包含print函数的对象,您可以让函数引用该对象。这意味着如果您需要将打印功能传递给其他地方,则不必将打印功能"绑定"到对象上。

相反,如果您愿意,请使用this,如下图所示

1
2
3
4
5
6
7
8
9
10
11
function createMyObject() {
    var count = 0
    return {
        a: {
            log: function() {
                console.log(count++)
                return this
            }
        }
    }
}

然后,以下代码将记录0,1,2,然后给出错误

1
2
3
4
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始终返回相同的对象,而不管运行函数的上下文。当使用createMyObject()的自我版本时,上面的代码将运行正常并记录0,1,2和3。


为了完成,在ES6中我们有类(在撰写时仅支持最新的浏览器,但在Babel,TypeScript和其他转换器中可用)

1
2
3
4
5
6
7
8
9
class Foo {
  constructor(){
    this.a = 5;
    this.b = 6;
    this.c = this.a + this.b;
  }  
}

const foo = new Foo();

有几种方法可以实现这一目标;这是我会用的:

1
2
3
4
5
6
7
8
function Obj() {
 this.a = 5;
 this.b = this.a + 1;
 // return this; // commented out because this happens automatically
}

var o = new Obj();
o.b; // === 6


您可以使用模块模式执行此操作。就像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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/


只是为了思考 - 在时间轴之外放置对象的属性:

1
2
3
4
5
6
7
var foo = {
    a: function(){return 5}(),
    b: function(){return 6}(),
    c: function(){return this.a + this.b}
}

console.log(foo.c())

上面也有更好的答案。这就是我修改你所质疑的示例代码的方法。

更新:

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


在对象文字上创建新函数并调用构造函数似乎与原始问题完全不同,并且它是不必要的。

在对象文字初始化期间,您无法引用同级属性。

1
2
var x = { a: 1, b: 2, c: a + b } // not defined
var y = { a: 1, b: 2, c: y.a + y.b } // not defined

计算属性的最简单解决方案如下(没有堆,没有函数,没有构造函数):

1
2
3
var x = { a: 1, b: 2 };

x.c = x.a + x.b; // apply computed property

所有这一切的关键是SCOPE。

您需要封装要定义的属性的"父"(父对象)作为它自己的实例化对象,然后您可以使用关键字this来引用兄弟属性

非常非常重要的是要记住,如果你在没有这么做的情况下引用this,那么this将引用外部范围......这将是window对象。

1
2
3
4
5
6
7
8
9
10
11
var x = 9   //this is really window.x
var bar = {
  x: 1,
  y: 2,
  foo: new function(){
    this.a = 5, //assign value
    this.b = 6,
    this.c = this.a + this.b;  // 11
  },
  z: this.x   // 9 (not 1 as you might expect, b/c *this* refers `window` object)
};


如果您的对象被写为返回对象的函数,并且您使用ES6对象属性'methods',则可能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const module = (state) => ({
  a: 1,
  oneThing() {
    state.b = state.b + this.a
  },
  anotherThing() {
    this.oneThing();
    state.c = state.b + this.a
  },
});

const store = {b: 10};
const root = module(store);

root.oneThing();
console.log(store);

root.anotherThing();
console.log(store);

console.log(root, Object.keys(root), root.prototype);


这是一个整洁的ES6方式:

1
2
3
4
5
6
7
8
9
var foo = (o => ({
    ...o,
    c: o.a + o.b
  }))({
    a: 5,
    b: 6
  });
 
console.log(foo);

我用它来做这样的事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const constants = Object.freeze(
  (_ => ({
    ..._,
    flag_data: {
      [_.a_flag]: 'foo',
      [_.b_flag]: 'bar',
      [_.c_flag]: 'oof'
    }
  }))({
    a_flag: 5,
    b_flag: 6,
    c_flag: 7,
  })
);

console.log(constants.flag_data[constants.b_flag]);


这里发布的其他答案更好,但这里有一个替代方案:

  • 设置初始化时的值(不是getter或派生等)
  • 不需要任何类型的init()或对象文字之外的代码
  • 是一个对象文字而不是工厂函数或其他对象创建机制。
  • 不应该有任何性能影响(初始化除外)

自执行匿名功能和窗口存储

1
2
3
4
5
6
7
var foo = {
    bar:(function(){
        window.temp ="qwert";
        return window.temp;
    })(),
    baz: window.temp
};

订单有保证(bar之前baz)。

它当然会污染window,但我无法想象某人编写需要window.temp持久化的脚本。如果你是偏执狂,也许tempMyApp

它也很难看,但偶尔也很有用。例如,当您使用具有严格初始化条件的API并且不想重构时,因此范围是正确的。

当然,它很干燥。


其他方法是在为其分配属性之前先声明对象:

1
2
3
4
5
const foo = {};
foo.a = 5;
foo.b = 6;
foo.c = foo.a + foo.b;  // Does work
foo.getSum = () => foo.a + foo.b + foo.c;  // foo.getSum() === 22

这样,您可以使用对象变量名称来访问已分配的值。
最适合config.js文件。


我使用以下代码作为替代,它的工作原理。变量也可以是数组。 (@ Fausto R.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var foo = {
  a: 5,
  b: 6,
  c: function() {
    return this.a + this.b;
  },

  d: [10,20,30],
  e: function(x) {
    this.d.push(x);
    return this.d;
  }
};
foo.c(); // 11
foo.e(40); // foo.d = [10,20,30,40]

这个解决方案怎么样也适用于带有数组的嵌套对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
      Object.prototype.assignOwnProVal
     = function (to,from){
            function compose(obj,string){
                var parts = string.split('.');
                var newObj = obj[parts[0]];
                if(parts[1]){
                    parts.splice(0,1);
                    var newString = parts.join('.');
                    return compose(newObj,newString);
                }
                return newObj;
            }
            this[to] = compose(this,from);
     }
     var obj = { name : 'Gaurav', temp :
                  {id : [10,20], city:
                        {street:'Brunswick'}} }
     obj.assignOwnProVal('street','temp.city.street');
     obj.assignOwnProVal('myid','temp.id.1');


投入选项,因为我没有看到这个确切的情况。如果您不希望在ab更新时更新c,则ES6 IIFE运行良好。

1
2
3
4
5
var foo = ((a,b) => ({
    a,
    b,
    c: a + b
}))(a,b);

根据我的需要,我有一个与最终用于循环的数组有关的对象,所以我只想计算一次常用设置,所以这就是我所拥有的:

1
2
3
4
5
6
  let processingState = ((indexOfSelectedTier) => ({
      selectedTier,
      indexOfSelectedTier,
      hasUpperTierSelection: tiers.slice(0,indexOfSelectedTier)
                             .some(t => pendingSelectedFiltersState[t.name]),
  }))(tiers.indexOf(selectedTier));

由于我需要为indexOfSelectedTier设置属性,并且我需要在设置hasUpperTierSelection属性时使用该值,我首先计算该值并将其作为参数传递给IIFE


为了使事情更简单并且不在对象中定义新函数,另一种方法是使原始对象保持其属性,使用动态属性创建第二个对象,该动态属性使用第一个对象的属性,然后使用spread进行合并。

所以:

宾语

1
2
3
4
var foo = {
    a: 5,
    b: 6,
};

对象2

1
2
3
var foo2 = {
    c: foo.a + foo.b
};

合并

1
Object.assign(foo, foo2);

要么

1
foo = {...foo, ...foo2};

foo的结果:

1
2
3
4
5
{
 "a":5,
 "b":6,
 "c":11
}

所有在同一个js内,对象内没有新功能,只使用赋值或传播。


如果你想使用原生JS,其他答案提供了很好的解决方案。

但是,如果您更喜欢编写自引用对象,例如:

1
2
3
4
{
  a: ...,
  b:"${this.a + this.a}",
}

我写了一个名为self-referenced-object的npm库,它支持该语法并返回一个本机对象。


Note: This solution uses Typescript (you can use the vanilla JS which TS compiles to if needed)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class asd {
    def = new class {
        ads= 'asd';
        qwe= this.ads + '123';
    };

    // this method is just to check/test this solution
    check(){
        console.log(this.def.qwe);
    }
}

// these two lines are just to check
let instance = new asd();
instance.check();

这里使用类表达式来获取我们想要的嵌套对象文字界面。这是IMHO在创建过程中能够引用对象属性的下一个最好的事情。

需要注意的主要事项是在使用此解决方案时,您拥有与对象文字完全相同的界面。语法非常接近对象文字本身(与使用函数等)。

比较以下内容

解决方案我提出了

1
2
3
4
5
class asd {
    def = new class {
        ads= 'asd';
        qwe= this.ads + '123';
    };

解决方案,如果对象文字已经足够了

1
2
3
4
5
6
var asd = {
    def : {
        ads:'asd',
        qwe: this.ads + '123';, //ILLEGAL CODE; just to show ideal scenario
    }
}

另一个例子

在这个类中,您可以在它们之间组合多个相对路径,这对于对象文字是不可能的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CONSTANT {
    static readonly PATH = new class {
        /** private visibility because these relative paths don't make sense for direct access, they're only useful to path class
         *
         */

        private readonly RELATIVE = new class {
            readonly AFTER_EFFECTS_TEMPLATE_BINARY_VERSION: fs.PathLike = '\\assets\\aep-template\\src\\video-template.aep';
            readonly AFTER_EFFECTS_TEMPLATE_XML_VERSION: fs.PathLike = '\\assets\\aep-template\\intermediates\\video-template.aepx';
            readonly RELATIVE_PATH_TO_AFTER_EFFECTS: fs.PathLike = '\\Adobe\\Adobe After Effects CC 2018\\Support Files\\AfterFX.exe';
            readonly OUTPUT_DIRECTORY_NAME: fs.PathLike = '\\output';
            readonly INPUT_DIRECTORY_NAME: fs.PathLike = '\\input';
            readonly ASSETS_DIRECTORY_NAME: fs.PathLike = '\\assets';
        };
    }
}