ES6 `static get length()` 未按预期继承

ES6 `static get length()` is not inherited as expected

我在为我的 ES6 class 扩展的 length 属性 提供静态 getter 函数时遇到问题。 事实证明,实际的 Function.length getter 总是优先于我自己的实现。

class Foo {
  static get value() {
    return 'Foo';
  }

  static get length() {
    return this.value.length;
  }
}

class Bar extends Foo {
  static get value() {
    return `${super.value}Bar`;
  }
}

console.log(Foo.value, Foo.length);  //  'Foo', 3
console.log(Bar.value, Bar.length);  //  'FooBar', 0

在上面的示例中,Foo 完全符合我的预期,Bar 没那么多。 Bar.value 确实 return 'FooBar',但 Bar.length0 让我感到惊讶。

我花了一段时间才意识到 0 的来源,因为我完全期望它是 6(并且在某种程度上会理解 3)。 事实证明 Bar.length 提供的 0 值实际上是 Barconstructor 函数的长度,我在用 ES5 表示法编写相同示例时意识到了这一点,不过,有一种快速的方法可以证明这一点;只需将 constructor 添加到 Bar.

class Bar extends Foo {
  constructor(a, b, c, d) {
    //  four configured parameters
  }

  static get value() {
    return `${super.value}Bar`;
  }
}

console.log(Foo.value, Foo.length);  //  'Foo', 3
console.log(Bar.value, Bar.length);  //  'FooBar', 4

解决这个问题的方法有:

None 这些是我想做的,如果有更合适的方法的话。

So my question is; does anyone know a proper way to have a custom property override which is inherited as expected, or am I being too stubborn?


如前所述,我通过将 class 语法编写为(我认为)将是 ES5 等价物来弄清楚出了什么问题,因为它可能对其他开发人员有益,并且可能会阐明如何我认为 ES6 class我会把它留在这里。 (如果有人知道如何在 Whosebug 上折叠这个位,请随时 edit/suggest)

我认为 ES5 语法中发生了什么

我知道 ES6 classes 主要是围绕 JS 原型继承的语法糖,所以 Bar 似乎发生的事情是这样的;

function Foo() {}
Object.defineProperties(Foo, {
  value: {
    configurable: true, 
    get: function() {
      return 'Foo';
    }
  },
  length: {
    configurable: true, 
    get: function() {
      return this.value.length;
    }
  }
});

function Bar() {}
Bar.prototype = Object.create(Object.getPrototypeOf(Foo));
Object.defineProperties(Bar, {
  value: { 
    configurable: true, 
    get: function() { 
      return 'Bar' + Foo.value; 
    }
  }
});

console.log(Foo.value, Foo.length);  //  'Foo', 3
console.log(Bar.value, Bar.length);  //  'FooBar', 0

我希望 Foo 的 属性 描述符会被考虑在内,例如:

function Bar() {}
Bar.prototype = Object.create(Object.getPrototypeOf(Foo));
Object.defineProperties(Bar, Object.assign(
  //  inherit any custom descriptors
  Object.getOwnPropertyDescriptors(Foo),
  {
    configurable: true, 
    value: { 
      get: function() { 
        return 'Bar' + Foo.value;
      }
    }
  }
));


console.log(Foo.value, Foo.length);  //  'foo', 3
console.log(Bar.value, Bar.length);  //  'bar', 6

关于静态成员

ES6 class 的静态成员实际上是 函数对象 的成员,而不是它的原型对象。考虑以下示例,其中我将使用常规方法而不是 getter,但其机制与 getters:

相同
class Foo {
    static staticMethod() {
        return 'Foo static';
    }

    nonStaticMethod() {
        return 'Foo non-static';
    }
}

staticMethod 将成为构造函数对象的成员,而 nonStaticMethod 将成为该函数对象原型的成员:

function Foo() {}

Foo.staticMethod = function() {
    return 'Foo static';
}

Foo.prototype.nonStaticMethod = function() {
    return 'Foo non-static';
}

如果你想从 Foo 'instance' 运行 staticMethod 你必须先导航到它的 constructor ,这是函数staticMethod 是以下成员的对象:

let foo = new Foo();

foo.staticMethod(); // Uncaught TypeError: foo.staticMethod is not a function

// Get the static member either on the class directly
// (this works IF you know that Foo is foo's constructor)

Foo.staticMethod(); // > 'Foo static'


// this is the same, provided that neither 'prototype' nor
// 'prototype.constructor' has been overridden afterwards:

Foo.prototype.constructor.staticMethod(); // > 'Foo static'


// ...or by getting the prototype of foo
// (If you have to perform a computed lookup of an object's constructor)
// You'll want to perform such statements in a try catch though...

Object.getPrototypeOf(foo).constructor.staticMethod(); // > 'Foo static'

function.length

所有 functions have a length property 告诉您该函数接受多少个参数:

function Foo(a, b) {}
Foo.length; // > 2

因此在您的示例中,从 Bar.lengthFoo.length 的原型查找确实不会发生,因为 length 已经直接在 Bar 上找到。简单的覆盖将不起作用:

Foo.length = 3;
Foo.length; // still 2

那是因为属性是不可写的。让我们用 getOwnPropertyDescriptor:

来验证
Object.getOwnPropertyDescriptor(Foo, 'length');
/*
    { 
        value: 0,
        writable: false,
        enumerable: false,
        configurable: true
    }
*/

此外,除了您定义的 value getter,您还可以只使用 function.name 来获取函数的名称/class 构造函数:

Foo.name; // > 'Foo'

让我们用它来覆盖 Foo 上的 length 属性。我们仍然能够覆盖 Foo.length 因为 属性 是可配置的:

Object.defineProperty(Foo, 'length', {
    get() {
        return this.name.length;
    }
});

Foo.length; // 3

这是代码膨胀

非常不希望为每个扩展 class 都执行此操作,或者为每个扩展定义一个静态 getter,这与上面的代码等效。如果不对某种函数对象进行任何修饰,就不可能完全覆盖行为。但是既然我们知道 classes 只是语法糖,而我们实际上只是在处理对象和函数,那么写一个装饰器就很容易了!

function decorateClasses(...subjects) {

    subjects.forEach(function(subject) {

        Object.defineProperty(subject, 'value', {
            get() {
                const superValue = Object.getPrototypeOf(this).value || '';

                return superValue + this.name;
            },
            enumerable: true,
            configurable: true,
        });

        Object.defineProperty(subject, 'length', {
            get() {
                return this.value.length;
            },
            enumerable: true,
            configurable: true,
        });
    })

}

此函数接受一个或多个对象,在这些对象上它将覆盖 length 属性 并设置 value 属性。两者都是 访问器 get 方法。 get value 显式执行原型查找,然后将结果与它所属的函数名称结合起来。所以,如果我们有 3 classes:

class Foo {

}

class Bar extends Foo {

}

class Baz extends Bar {

}

我们可以一次装饰所有 classes:

decorateClasses(Foo, Bar, Baz);

如果我们在所有三个 classes(函数)上访问 valuelength,我们将得到所需的结果:

Foo.value; // 'Foo'
Foo.length; // 3

Bar.value; // 'FooBar'
Bar.length; // 6

Baz.value; // 'FooBarBaz'
Baz.length; // 9