【问题标题】:How can I extend Array without a class declaration?如何在没有类声明的情况下扩展 Array?
【发布时间】:2019-06-23 06:40:08
【问题描述】:

我真的很难创建一个自定义对象,它可以继承数组的所有属性,但行为也与普通实例相同,也就是说,instanceofconstructor 的行为就像你想要的那样他们到。我读过类声明只是语法糖,所以我从来没有向它们寻求解决方案(我对它们知之甚少)。

在我取得重大突破之前,我创造了这个可憎的:

function arrayLike() {
    let al = [];

    //make obj.constructor work
    Object.defineProperty(al, 'constructor', {value: arrayLike}); 

    //add methods

    //make (obj instanceof arrayLike) == true
    return new Proxy(al, {
        getPrototypeOf() {
            return arrayLike.prototype;
        },
    })
}

//make (obj instanceof Array) == true
Reflect.setPrototypeOf(arrayLike.prototype, Array.prototype);

碰巧我看到一个非常接近我想做的类示例,然后发现它非常适合这项工作:

class arrayLike extends Array {
    //add methods
}

在 Chrome DevToos 中查看,我可以看到我创建的结构与此完全不同。

如果类声明真的是语法糖,那么没有它你怎么创建这个对象呢?

【问题讨论】:

  • 我不会说类声明是 100% 的语法糖。在使用 Class 语法的新浏览器中,使用 Array 上的 .length 等神奇属性对特殊对象进行子类化变得更加容易。有什么理由不想使用 Class 语法吗?如果您尝试在旧环境中运行,您可以随时进行转译(并让转译器为您完成所有工作)。
  • @jfriend00 不不,我完全可以使用类语法。我问这个问题纯粹是出于好奇。因为我读到的内容说,没有它,这种结构应该是可能的,我想知道如何。或者我想知道我所读到的内容实际上是错误的,并且确实在类声明中添加了新功能。
  • @Mason 您可以使用Reflect.construct 来创建Array 的子类,而无需使用class 语法。是的,they're not just sugar

标签: javascript ecmascript-6 es6-class


【解决方案1】:

Javascript 是一种具有继承形式的语言,称为原型继承

其背后的想法是,给定一个对象,它有一个名为 prototype 的隐藏属性,它是对另一个对象的引用,也就是原型对象。

当您要求 javascript 引擎为您提供对象属性的值时,这种关系很重要,我们称它为 foo 只是为了解决这个问题。 javascript 引擎将首先检查您的对象以查看它是否具有名为 foo 的属性:如果该属性已在您的对象上定义,则返回其值并完成搜索。否则,如果您的对象没有名为 foo 的属性,则搜索其原型对象并再次重复相同的过程。

递归地重复此过程,直到探索完所有所谓的原型链。原型链的根是一个内置的 javascript 对象,您可以使用表达式 Object.prototype 引用它,并且是所有其他 javascript 对象派生的对象。请注意,如果在构成整个原型链的 all 对象中缺少 foo 属性,则返回值 undefined

这是内置在 javascript 中的真正继承形式,它是真正位于 ES6 class 密钥后面的业务,它很方便,可以隐藏这种混乱并给您一种 javascript 有一种形式的印象类继承(类继承更广为人知,大多数程序员发现它比原型继承更容易想到)。

为了获取一个对象并决定它应该表现得像一个数组,你可以做的最低限度如下:

const myArray = [];
const myObject = { foo: "bar" }

Object.setPrototypeOf(myObject, myArray);

myObject.push("hello");
myObject.push("world");
console.log(myObject.length); // prints 2

This book 是我所知道的 javascript 语言的最佳参考。 This is good too,不过现在有点过时了,不像以前那么容易跟上。

可以通过使用函数作为构造函数来实现比上一个更复杂的示例。这实际上是 ES5 实现类继承的老派方式,您在 ES5 时代所做的事情是为了模仿类:

function SpecialArray(name) {
  this.name = name;
}

SpecialArray.prototype = []; 

// fix the constructor property mess (see the book linked above)
Object.defineProperty(SpecialArray.prototype, "constructor", {
  value: SpecialArray,
  enumerable: false,
  writable: true
});

SpecialArray.prototype.getSalutation = function() {
  return "Hello my name is " + this.name;
};

const mySpecialArray = new SpecialArray("enrico");

// you can call the methods and properties defined on Array.prototype
mySpecialArray.push("hello");
mySpecialArray.push("world");
console.log(mySpecialArray.length); // prints 2

// you can use the methods and properties defined on SpecialArray.prototype
console.log(mySpecialArray.name); // prints enrico
console.log(mySpecialArray.getSalutation()); // prints Hello my name is enrico

// important sanity checks to be sure that everything works as expected
console.log(mySpecialArray instanceof Array); // prints true
console.log(mySpecialArray instanceof SpecialArray); // prints true
console.log(mySpecialArray.constructor === SpecialArray); // prints true

// you can iterate over the special array content
for (item of mySpecialArray){
  console.log(item);
}

// you can read special array entries
console.log(mySpecialArray[1]); // prints world

【讨论】:

  • 此代码示例实际上并未对数组进行子类化,因此您现在拥有一个包含数组方法和您自己的自定义方法的原型。它只是创建一个具有相同 Array 原型的新对象。我认为您对 OP 实际要求的答案有点过于简单了。
  • 有趣,使用push 使length 实际工作。我尝试过这种方法,但不幸的是,如果你这样做myObject[0] = "hello"myObject.length 仍然等于 0。
  • ^这是因为原型继承模型只在链上进行命名属性搜索。像myObject[whatever] 这样的读/写任意属性超出了它的范围。对于这种情况,您必须使用Proxy。当您执行myObject.push 时,推送是array.prototype.push,因此它会更改length。但是当您执行myObject[0] = value 时,该分配并未分配给数组元素,只是简单的对象属性分配,没有涉及与数组相关的功能,因此不会更改length
  • @jfriend00 感谢您的评论,我试图改进我的答案以更好地解释 class 关键字背后的内容。
  • 您的新代码仍然无法解决我在原始代码中指出的问题。
【解决方案2】:

编辑:我研究了babel的转译代码,发现要正确扩展像Array这样的内置类需要额外的触摸,我们需要先将Array构造函数包装在一个普通的Wrapper函数中,否则原型链将在构造时中断。

function _wrapNativeSuper(Class) {
  _wrapNativeSuper = function _wrapNativeSuper(Class) {
    function Wrapper() {
      var instance = Class.apply(this, arguments)
      instance.__proto__ = this.__proto__.constructor.prototype;
      return instance;
    }
    Wrapper.prototype = Object.create(Class.prototype, {
      constructor: {
        value: Wrapper,
        enumerable: false,
        writable: true,
        configurable: true
      }
    });
    Wrapper.__proto__ = Class;
    return Wrapper;
  };
  return _wrapNativeSuper(Class);
}

类声明语法做了三件事。

  1. 正确设置构造函数
  2. 正确设置原型链
  3. 继承静态属性

因此,为了重播 class Foo extends Array {} 在老式 js 中所做的事情,您需要相应地做这 3 件事。

// 0. wrap the native Array constructor
// this step is only required when extending built-in objects like Array
var _Array = _wrapNativeSuper(Array)

// 1. setup the constructor
function Foo() { return _Array.apply(this, arguments) }

// 2. setup prototype chain
function __dummy__() { this.constructor = Foo }
__dummy__.prototype = _Array.prototype
Foo.prototype = new __dummy__()

// 3. inherit static properties
Foo.__proto__ = _Array

下面的可运行示例:

function _wrapNativeSuper(Class) {
  _wrapNativeSuper = function _wrapNativeSuper(Class) {
    function Wrapper() {
      var instance = Class.apply(this, arguments)
      instance.__proto__ = this.__proto__.constructor.prototype;
      return instance;
    }
    Wrapper.prototype = Object.create(Class.prototype, {
      constructor: {
        value: Wrapper,
        enumerable: false,
        writable: true,
        configurable: true
      }
    });
    Wrapper.__proto__ = Class;
    return Wrapper;
  };
  return _wrapNativeSuper(Class);
}

// 0. wrap the native Array constructor
// this step is only required when extending built-in objects like Array
var _Array = _wrapNativeSuper(Array)

// 1. setup the constructor
function Foo() { return _Array.apply(this, arguments) }

// 2. setup prototype chain
function __dummy__() { this.constructor = Foo }
__dummy__.prototype = _Array.prototype
Foo.prototype = new __dummy__()

// 3. inherit static properties
Foo.__proto__ = _Array


// test
var foo = new Foo;
console.log('instanceof?', foo instanceof Foo);

Foo.prototype.hi = function() { return 'hello' }
console.log('method?', foo.hi());

【讨论】:

  • 在哪里/如何向 Foo 的原型添加新方法而不将它们添加到 Array.prototype?当我尝试Foo.prototype.add = function(x) {...} 时,它不起作用。当我尝试x = new Foo(); x.add("A") 时,我得到“x.add 不是函数”。
  • @jfriend00 你是对的。原来数组是非常独特的。如果您将上述代码中的Array 更改为funciton Bar() {} 之类的其他内容,则该代码有效。但看起来Array.apply(this, arguments) 部分在构建时打破了原型链。我需要修改答案。
  • 这似乎在我的测试中有效。为什么使用Wrapper.__proto__ = Class; 而不是.setPrototypeOf()?真是一团糟。谢天谢地class Foo extends Array {}
  • 请记住,我认为我已经读过这种东西只适用于较新浏览器中的数组,神奇的.length 属性甚至与这种类型的子类不兼容在早期的浏览器中。
  • Node.js 在构造Foo 的新实例时似乎给出了有用的错误:“TypeError: clazz is not a constructor”
猜你喜欢
  • 2017-04-24
  • 1970-01-01
  • 2014-03-03
  • 2022-06-15
  • 1970-01-01
  • 2023-02-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多