使用Object.assign和Object.create进行继承

36

我通常按照以下方式实现继承。

function Animal () { this.x = 0; this.y = 0;}

Animal.prototype.locate = function() { 
  console.log(this.x, this.y);
  return this;
};
Animal.prototype.move = function(x, y) {
  this.x = this.x + x;
  this.y = this.y + y; 
  return this;
}


function Duck () {
    Animal.call(this);
}

Duck.prototype = new Animal();
Duck.prototype.constructor = Duck;
Duck.prototype.speak = function () {
    console.log("quack");
    return this;
}

var daffy = new Duck();

daffy.move(6, 7).locate().speak();

我已经阅读了Eric Elliott的这篇文章,如果我理解正确,我可以使用Object.createObject.assign来替代吗? 难道真的那么简单吗?

var animal = {
   x : 0,
   y : 0,
   locate : function () { 
     console.log(this.x, this.y);
     return this;
   },
   move : function (x, y) { 
     this.x = this.x + x; 
     this.y = this.y + y;
     return this;
   }
}

var duck = function () {
   return Object.assign(Object.create(animal), {
     speak : function () { 
       console.log("quack");
       return this;
     }
   });
}

var daffy = duck();

daffy.move(6, 7).locate().speak();

顺便提一下,按照惯例构造函数应该大写,那么充当构造函数的对象字面量也应该大写吗?

我意识到这里有许多关于newObject.create的问题讨论,但它们通常似乎涉及 Duck.prototype = new Animal();Duck.prototype = Object.create(Animal.prototype); 的区别。


2
如果您正在使用ES2015,那么您应该使用classextend - Amit
19
@Amit,显然这不是他提出的问题。是否使用classextend是一个独立的讨论话题。 - nils
2
@Amit - 为什么我要使用classextend?如果我这样做,我不是还得使用new吗?使用classextend相比于Object.assign(Object.create(...有什么优势呢? - user5325596
3
我重新加入了ES6标签,因为主要问题是“我可以使用Object.create和Object.assign吗?” - CodingIntrigue
1
按照这个模式,你更愿意使用 var duck = Object.assign(Object.createvar daffy = Object.create(duck) - a better oliver
显示剩余4条评论
3个回答

33

是的,它就是这么简单。在你使用Object.create/Object.assign的例子中,你正在使用一个工厂函数来创建duck的新实例(类似于jQuery通过var body = $('body')选择元素时创建新实例的方式)。这种代码风格的优点是,在创建新的duck实例时,它不会强制你调用animal的构造函数(与ES2015类不同)。

初始化差异

也许有一个有趣的小细节,它与使用构造函数(或任何其他初始化函数)略有不同:

当你创建duck实例时,所有animal的属性都在duck实例的[[Prototype]]槽中。

var daffy = duck();
console.log(daffy); // Object { speak: function() }

因此,daffy目前没有任何自己的xy属性。但是,当你调用以下代码时,它们将被添加:

daffy.move(6, 7);
console.log(daffy); // Object { speak: function(), x: 6, y: 7 }

为什么?在animal.move的函数体内,我们有以下语句:

this.x = this.x + x; 

因此,当您使用daffy.move调用此函数时,this引用的是daffy。因此,它将尝试将this.x + x分配给this.x。由于this.x尚未定义,因此会沿着daffy[[Prototype]]链向下遍历,直到到达定义了animal.xanimal

因此,在第一次调用中,赋值语句右侧的this.x指的是animal.x,因为daffy.x尚未定义。第二次调用daffy.move(1,2)时,右侧的this.x将是daffy.x

备选语法

或者,您也可以使用Object.setPrototypeOf代替Object.create/Object.assignOLOO Style):

var duck = function () {
   var duckObject = {
       speak : function () { 
           console.log("quack");
           return this;
       }
   };
   return Object.setPrototypeOf(duckObject, animal);
}

命名规范

我不知道是否有任何已经确立的规范。Kyle Simpson在OLOO中使用大写字母,Eric Elliot似乎使用小写。个人认为应该坚持使用小写,因为充当构造函数的对象文字本身已经是完整的对象(不仅仅是类似于类的蓝图)。

单例模式

如果您只想要一个实例(例如用于单例模式),则可以直接调用它:

var duck = Object.assign(Object.create(animal), {
    speak : function () { 
        console.log("quack");
        return this;
    }
});

duck.move(6, 7).locate().speak();

感谢您的长篇回答。我想Object.assign(Object.create相对于setPrototypeOf的优点是,我可以更容易地进行“多重继承”(暂且这么称呼)例如Object.assign(Object.create(animal), duckObj, someOtherObj, anotherObj)?那么Object.assign(Object.create有什么缺点吗? - user5325596
如果你想在原型链中拥有多个对象,你需要将它们混合在一起使用Object.createObject.assign(Object.create({animal: animal, bird: bird}), duck); 或者 Object.setPrototypeOf(duck, {animal: animal, bird: bird}); - nils
我不知道使用Object.assign/Object.create有什么缺点,除了一个小问题,在大多数情况下可能并不重要:使用Object.create创建的对象(与new相反)在浏览器中不受hiddenClass支持:https://medium.com/javascript-scene/common-misconceptions-about-inheritance-in-javascript-d5d9bab29b0a#68b2 - nils
这条评论“如果你想在原型链中拥有多个对象”,让我想知道如果我想要animal.isPrototypeOf(daffy)bird.isPrototypeOf(daffy)都返回true,我该如何实例化daffy - user5325596
只有当animalbird[[Prototype]],而bird反过来又是duck[[Prototype]]时,你才能这样做(通过多个级别:animal-> bird-> duck)。一个对象不能有多个[[Prototype]]条目。 - nils
显示剩余3条评论

13
我已经阅读了Eric Elliott的这篇文章,如果我理解正确,我可以使用Object.createObject.assign吗?是真的那么简单吗?
是的,createassign更简单,因为它们是基本类型,而且没有进行太多的魔法操作——你所做的一切都是显式的。
然而,Eric的mouse示例有点令人困惑,因为他省略了一步,并将老鼠从动物继承与实例化混为一谈。
相反,让我们再次尝试直接转录你的小鸭子例子:
const animal = {
  constructor() {
    this.x = 0;
    this.y = 0;
    return this;
  },
  locate() { 
    console.log(this.x, this.y);
    return this;
  },
  move(x, y) {
    this.x += x;
    this.y += y; 
    return this;
  }
};
const duck = Object.assign(Object.create(animal), {
  constructor() {
    return animal.constructor.call(this);
  },
  speak() {
    console.log("quack");
    return this;
  }
});
/* alternatively: 
const duck = Object.setPrototypeOf({
  constructor() {
    return super.constructor(); // super doesn't work with `Object.assign`
  },
  speak() { … }
}, animal); */

let daffy = Object.create(duck).constructor();
daffy.move(6, 7).locate().speak();

你应该看到这里发生的事情与使用构造函数(或者说class语法)没有什么不同,我们只是直接将原型存储在变量中,并且通过显式调用createconstructor进行实例化。
现在你可以想象到我们的duck.constructor仅仅是调用它的超级方法,所以我们实际上完全可以省略它,让继承完成它的工作:
const duck = Object.assign(Object.create(animal), {
  speak() {
    console.log("quack");
    return this;
  }
});

另一个经常变化的是实例属性的初始化。如果我们不需要它们,实际上没有必要对它们进行初始化,将一些默认值放在原型上就足够了:
const animal = {
  x: 0,
  y: 0,
  locate() { 
    console.log(this.x, this.y);
  }
};
const duck = … Object.create(animal) …;

let daffy = Object.create(duck); // no constructor call any more!
daffy.x = 5; // instance initialisation by explicit assignment
daffy.locate();

这种方法的问题在于它只适用于原始值,而且会变得重复。这就是工厂函数发挥作用的地方:
function makeDuck(x, y) {
    return Object.assign(Object.create(duck), {x, y});
}
let daffy = makeDuck(5, 0);

为了方便继承,初始化通常不是在工厂中完成的,而是在专门的方法中完成,以便可以调用"子类"实例。您可以将此方法称为init,或者像我上面所做的那样将其称为constructor,基本上是相同的。

顺便提一下,按照惯例,构造函数应大写,充当构造函数的对象文字也应大写吗?

如果您没有使用任何构造函数,则可以将大写变量名称赋予新含义。但这可能会对不习惯这种方式的所有人造成困惑。顺便说一句,它们不是"充当构造函数的对象文字",它们只是原型对象

这是最好的答案。谢谢! - Sulliwane
“我已经阅读了Eric Elliott的这篇文章”是你停下来并忽略一切的地方。没有人比这个大嘴巴笨蛋更不懂好的设计。一切都可以用'new'、'call'和'object.create'来完成,其中最后一个仅用于继承。上面的例子过于冗长,以至于成为Kruger Dunning效应的明显证据。幸运的是,ES2015已经合理地添加了extend和class关键字。 - Hal50000
4
与其称呼任何人为“嘴巴大而愚笨的人”,你可能想要争论为什么 classextendsnew 是一种“更好的设计”。我甚至会认为它们与上述示例完全相同,只是使用了更多魔法般的语法糖。那么哪一部分被“覆盖”了呢? - Bergi

0

与其考虑“继承”,不如思考您打算使用哪种“实例化模式”。它们在实现上有不同的目的。

顶部示例是原型式的,底部是功能共享的。请查看此链接:JS 实例化模式

此外,这与es6无关。


这并不涵盖他的例子。是的,Object.assign 是 ES6。 - nils
ES6成为了你所使用的“类”的新标准。所张贴的代码特别与ES5有关。 - user5250554
@nils,如果你查看我最初的回复中的链接,你就会明白我在说什么。 - user5250554
不,它并不特别涉及ES5(请参见上面的两个评论,“Object.assign”在ES5中不存在)。而“Object.create/Object.assign”组合在ES6中是一个完全有效的模式。正如上面所述,您的链接与“Object.create/Object.assign”情况无关,它只谈到了“Object.create”与“x.prototype”的结合。这正是OP所问的内容。 - nils
同意!Ob.assign 是 ES6。但是,就实例化模式而言,他实际上正在比较其中的特定用例。这更多地涉及 ES6 中的“类”(就语言而言是新的):https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes - user5250554
显示剩余3条评论

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