为什么在构建Javascript类层次结构时不建议/不高效使用Object.setPrototypeOf?

3
我读了一篇关于在类层次结构中保持数据私有化的文章这里。我的做法不同。我使用工厂函数并使用Object.setPrototypeOf(obj, prototype)。为什么我的方法不被视为良好的实践?
这是我的做法:
我不想使用公共变量,因此我使用工厂函数创建我的狗对象:
const makeDog = (name) => {
    return { bark: () => { console.log(name) } }
}
const myDog = makeDog("sniffles")
myDog.bark() // "sniffles"

所有的动物都能进食,我希望我的狗能从动物那里继承这一特点:

const makeAnimal = () => {
    let numTimesEat = 0
    return { 
        eat: (food) => {
          numTimesEat += 1 
          console.log( "I eat " + food.toString() )
        },
        get numTimesEat() { return numTimesEat }
    }
}
const myAnimal = makeAnimal()

我的狗将会委托给我的动物来进行吃饭:

Object.setPrototypeOf(myDog, myAnimal)

现在我可以做:

myDog.eat("shoe") // "I eat shoe"
console.log( myDog.numTimesEat ) // 1
myDog.bark() // "sniffles"

请注意,myDog.numTimesEat应该指的是myDog吃饭的次数。

附言:我知道你可以使用类来实现它:

class Animal {
  constructor() {
    this.numTimesEat = 0;
  }

  eat(food) {
    this.numTimesEat += 1;
    console.log( "I eat " + food.toString() );
  }
}

class Dog extends Animal {
  constructor(myName) {
    super();
    this.name = myName;
  }
  bark() {
    console.log( this.name );
  }
}

const dog2 = new Dog("sniffles");
dog2.eat("shoe"); // "I eat shoe"
console.log( dog2.numTimesEat ); // 1
console.log( dog2.name ); // "sniffles"
dog2.bark(); // "sniffles"

但是class关键字似乎会在我的对象上产生公共变量。如果我尝试使用这些之类的技巧,它看起来有点丑(我想下划线语法看起来还不错,但它并不是真正的私有)。

解决方案:

如果我们创建10只狗,它们的原型都是同一个动物,那么“let numTimesEat”就是共享的。如果一只狗吃了一次,你不希望numTimesEat变成10。

因此,除了重复设置原型10次(这是一个缓慢的操作),你还需要为这10只狗创建10个动物来委托。


更新:相反,您可以将所有内容放在新创建的对象上。
const Dog = function(name) {
    let that = Animal()
    that.bark = () => { console.log(name) }
    return that
}

const Animal = function() {
    let numTimesEat = 0
    return { 
        eat: (food) => {
          numTimesEat += 1 
          console.log( "I eat " + food.toString() )
        },
        get numTimesEat() { return numTimesEat }
    }
}

const lab = new Dog("sniffles")
lab.bark() // sniffles
lab.eat("food") // I eat food
lab.numTimesEat // 1

这比尝试在Javascript中实现OOP要干净得多。

1
不确定问题是什么? - guest271314
“我们为什么不使用 Object.setPrototypeOf 来构建类层次结构?”谁是“我们”? Object.setPrototypeOf() 在 JavaScript 中使用该问题。 “这个问题不是为什么修改对象的 [[prototype]] 对性能有害的重复,因为问题不仅仅在于性能。” 问题中没有出现任何性能指标。与哪些比较有关的性能?您正在比较哪些基准测试?从这里的角度来看,问题陈述不够清晰。 - guest271314
3个回答

3

简单。

如果JavaScript中没有原型或this,我们仍然可以愉快地进行面向对象编程和继承,就像您描述的那样:

  • 每个对象包含其方法
  • 方法在闭包中具有共享状态

甚至可以不需要隐藏状态:

  • 每个对象包含其方法和状态
  • 方法在闭包中引用对象

现在有人可能会说:“嘿,我的对象有2个状态变量和10个方法,我需要在每次想要新的对象实例时制作10个方法的副本,这不是浪费吗?”

然后我们可以像这样:“是的,让我们实际上在相同类型的所有对象之间共享函数。”但这会产生两个问题:

  • 我们把它们放在哪里?(好的,让我们创建一个名为“原型”的对象,让每个实例都有对该原型的隐藏引用)
  • 方法如何知道它被调用的对象是哪个?我们不能再将其放在闭包中了...(让我们将this作为每个方法的隐藏参数传递)

...因此,我们到达了标准JS OOP的当前状态。在某个地方,我们牺牲了在方法闭包中保留私有状态的可能性(因为所有对象只有1个方法)。


所以您实际上不想去那里,而是停留在每个对象都有自己的方法副本的起点。这很好,您可以拥有私有状态!但是在这一点上,您为什么需要原型?它们无法发挥其原始作用。一个对象的bark()与另一个bark()完全无关,它们具有不同的闭包,不能共享。继承的方法也是如此。

在这种情况下,由于您已经复制了所有方法,因此可以将它们保留在对象本身中。为每个对象添加(不同的)原型并将超类方法放置在其中不会给您带来任何东西,它只会为方法调用添加一层间接性。


我认为使用像这样的工厂函数,您可以使用混合模式为对象添加新属性和方法,这比在每个其他工厂函数中复制执行相同操作的方法要好。 - guitarino

1
我认为普通继承比切换原型更快,并且得到更多支持(setPrototypeOf是es6功能)。此外,如果使用它,您可能会真正搞砸事情(考虑创建html元素并将其原型切换到其他内容)。
在您的情况下,有一种比setPrototypeOf更好的模式可供使用。相反,您可以在makeDog和makeAnimal工厂函数中向现有对象添加属性,而不是创建新对象。您可以使用Object.assign(可填充)来执行此操作,如下所示:
const makeDog = (name, animal = {}) => {
    return  Object.assign(animal, { bark: () => { console.log(name) } })
}
const myAnimal = makeAnimal()
const myDog = makeDog("sniffles", myAnimal)

如果你想高效(使用常规继承)并仍然保留私有变量,你仍然可以做到!我写了一篇关于此的 博客文章,这里还有一个 gist 可以让你轻松创建私有成员。最后,这里有一篇 好文章,讨论面向对象编程模式如继承是否真的好或者不好。

1
你正在创建两个实例,然后将其中一个设置为另一个的原型:
var myAnimal=makeAnimal(),myDog=makeDog();
Object.setPrototypeOf(myDog, myAnimal);

因此,我们不想使用简单的继承,而是要:

myDog -> Dog.prototype -> Animal.prototype
myDog2 ->
myDog3 ->

                   Animal ->
                  Animal2 ->

你正在做这件事:
myDog -> myAnimal
myDog1 -> myAnimal1
myDog2 -> myAnimal2
Animal
Animal2

所以,不是两个原型持有所有功能和轻量级实例只持有数据,你有2n(每只狗一个动物)实例持有绑定函数引用和数据。当构建许多元素并分配函数时,这真的不高效,而在工厂中分配函数也不是。因此,可以坚持使用类继承来解决这两个问题。或者如果想要使用setPrototype,请仅使用一次(然后它的速度影响不大):
var Animalproto = {
 birthday(){...}
}

var Dogproto={
 bark(){ ... }
}

Object.setPrototypeOf(Dogproto,Animalproto);

function makeAnimal(){
  return Object.create(Animalproto);
}

function makeDog(){
  return Object.create(Dogproto);
}

哦,我明白了。如果在对象创建时设置原型,则速度很快,如果在对象创建后设置,则速度很慢。在这种情况下,对于每个狗,我们都要设置原型,这非常慢。 - Michael Lafayette
@michael lafayette 没错。很高兴能帮到你 ;) - Jonas Wilms

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