理解为什么真正的原型继承优于经典/伪原型继承以及为什么我不应该使用"新"

EnZ*_*nZo 9 javascript oop inheritance prototype

阅读Aadit M Shah的一些文章,比如为什么Prototypal继承很重要或者 停止在 Eric Elliott的JavaScript中使用构造函数我认为我理解他们所有的论点,在理论上.但在实践中,我没有看到这种模式的真正优势.

让我们看看两个片段中的两个实现来进行继承.

  1. 第一个是使用augment.js,它是来自Aadit M Shah的脚本
  2. 在这个例子中,我们将使用这个脚本.也是由Aadit M Shah制作的.

实施1:

    var AugmentPerson = Object.augment(function() {
      this.constructor = function(name) {
          this.name = name;
      };
      this.setAddress = function(country, city, street) {
          this.country = country;
          this.city = city;
          this.street = street;
      };
    });
    var AugmentFrenchGuy = AugmentPerson.augment(function(base) {
      this.constructor = function(name) {
          base.constructor.call(this,name);
      };
      this.setAddress = function(city, street) {
          base.setAddress.call(this, "France", city, street);
      };
    });
    var AugmentParisLover = AugmentFrenchGuy.augment(function(base) {

      this.constructor = function(name) {
          base.constructor.call(this, name);
      };

      this.setAddress = function(street) {
          base.setAddress.call(this, "Paris", street);
      };
    });
    var t = new AugmentParisLover("Mary");
    t.setAddress("CH");
    console.log(t.name, t.country, t.city, t.street); //Mary France Paris CH
Run Code Online (Sandbox Code Playgroud)

在这个例子中,我们使用函数构造函数而不是直接从对象继承.

实施2:

    var CreatePerson = {
        create: function (name) {
            this.name = name;
            return this.extend();
        },
        setAddress: function(country, city, street) {
             this.country = country;
             this.city = city;
             this.street = street;
        }
    };
    var CreateFrenchGuy  = CreatePerson.extend({
        create: function (name) {
            return CreatePerson.create.call(this,name);
        },
        setAddress: function(city, street) {
            CreatePerson.setAddress('France', city, street);
        }
    });
    var CreateParisLover  = CreateFrenchGuy.extend({
        create: function (name) {
            return CreateFrenchGuy.create.call(this,name);
        },
        setAddress: function(street) {
            CreateFrenchGuy.setAddress('Paris', street);
        }
    });

    var t = CreateParisLover.create("Mary");
    t.setAddress("CH");
    console.log(t.name, t.country, t.city, t.street); //Mary France Paris CH
Run Code Online (Sandbox Code Playgroud)

说实话,我试图看到第二个实现的好处.但我不能.我看到的唯一一点是更灵活,因为我们可以使用apply创建实例:

var t = CreateParisLover.create.apply(CreateParisLover, ["Mary"]);

这给了我们更大的灵活性,这是真的.但是,我们可以做同样的这个:

  Function.prototype.new = function () {
     function functor() { return constructor.apply(this, args); }
     var args = Array.prototype.slice.call(arguments);
     functor.prototype = this.prototype;
     var constructor = this;
     return new functor;
  };
Run Code Online (Sandbox Code Playgroud)

然后我们可以:

var t = AugmentParisLover.new.apply(AugmentParisLover, ["Mary"]);
Run Code Online (Sandbox Code Playgroud)

在灵活性,可重用性,难度方面有什么真正的好处......因为如果你检查两种情况的表现.Object.create()比新的慢得多:http://jsperf.com/inheritance-using-create-vs-new 我很困惑.

Aad*_*hah 8

编程很像时尚.潜意识里,大多数程序员编写的代码对他们来说看起来很美观.这是Java程序员想要在JavaScript中实现经典继承的主要原因.是的,尝试在JavaScript中实现经典继承是一项单一的任务,但这并不能阻止人们这样做.这是一个矫枉过正,但人们仍然这样做,因为他们只是希望他们的代码看起来像类(例如jTypes).

就像Eric和我一直试图推广使用工厂函数而不是构造函数一样.然而,从工厂到施工人员的这种转变不仅仅是出于美学原因.我们两个人正试图改变JavaScript程序员的心态,因为在某些方面我们都认为JavaScript存在根本缺陷.newJavaScript中的运算符就是这样一个方面.虽然它已经破碎但它仍然是语言的核心,因此无法避免.

底线是这个:

如果您想在JavaScript中创建原型链,那么您必须使用new.没有别的办法(除了.__proto__不赞成).

有趣的是,您既不需要原型也不需要从多个对象继承类.使用对象组合,您可以在JavaScript中实现强大的行为子类型,正如Benjamin Gruenbaum在以下答案中所述:https://stackoverflow.com/a/17008693/783743

在这个答案中,我将涉及以下主题:

  1. 我们为什么坚持new
  2. 为什么工厂比建造者更好?
  3. 我们如何充分利用这两个世界?

1.为什么我们坚持new

new关键词被放在一个JavaScript底座.没有使用,就无法在JavaScript中创建原型链new.是的,您可以更改.__proto__对象的属性,但只有在创建对象后才能更改,并且这种做法不受欢迎.甚至在内部Object.create使用new:

Object.create = function (o) {
    function F() {}
    F.prototype = o;
    return new F;
};
Run Code Online (Sandbox Code Playgroud)

正如Douglas Crockford 所说:

Object.create函数解开了JavaScript的构造函数模式,实现了真正的原型继承.它将旧对象作为参数,并返回一个从旧对象继承的空对象.如果我们尝试从新对象获取成员,并且它缺少该键,则旧对象将提供该成员.对象继承自对象.什么可能比这更面向对象?

关键是虽然newJavaScript中的关键字"纠缠不清" ,但在JavaScript中没有其他方法可以创建原型链.该Object.create功能,实现本地甚至当,仍比使用更慢new,因此性能方面的原因单独大多数人仍然使用new,即使Object.create是一个更具逻辑性的声音选项.

2.为什么工厂比建造者更好?

现在你可能想知道new是不是真的那么糟糕.在所有表现方面,它确实是最好的解决方案.但我认为不应该这样.无论您使用new还是Object.create表现都应该始终如一.这是缺乏语言实现的地方.他们应该努力Object.create加快速度.除了表演new还有其他任何救赎品质吗?在我的拙见中,它没有.

在你开始使用更好的语言之前,你通常不会真正知道语言有什么问题.那么让我们看看其他一些语言:

a)喜鹊

喜鹊Bob Nystrom创作的一种爱好语言.它有许多非常有趣的功能,彼此之间的互动非常好,即:

  1. 模式
  2. 多方法

然而,Magpie中的类更类似于JavaScript中的原型或Haskell中的数据类型.

在Magpie中,类的实例化分为两个步骤:

  1. 构建一个新实例.
  2. 初始化新构造的实例.

在JavaScript中,new关键字结合了实例的构造和初始化.这实际上是一件坏事,因为我们很快就会看到拆分构造和初始化实际上是一件好事.

考虑以下Magpie代码:

defclass Point
    var x
    var y
end

val zeroPoint = Point new(x: 0, y: 0)

def (this == Point) new (x is Int, y is Int)
    match x, y
        case 0, 0 then zeroPoint
        else this new(x: x, y: y)
    end
end

var origin = Point new(0, 0)
val point = Point new(2, 3)
Run Code Online (Sandbox Code Playgroud)

这相当于以下JavaScript代码:

function Point(x, y) {
    this.x = x;
    this.y = y;
}

var zeroPoint = new Point(0, 0);

Point.new = function (x, y) {
    return x === 0 && y === 0 ?
    zeroPoint : new Point(x, y);
};

var origin = Point.new(0, 0);
var point = Point.new(2, 3);
Run Code Online (Sandbox Code Playgroud)

正如您在这里看到的,我们将实例的构造和初始化分成两个函数.该Point函数初始化实例,Point.new函数构造实例.本质上,我们只是创建了一个工厂功能.

将构造与初始化分离是一种非常有用的模式,JavaScript室的优秀人员甚至在博客上发表了关于它的信息,称之为初始化模式.您应该阅读初始化模式.它向您展示JavaScript中的初始化与构造是分开的.

  1. Object.create(+1)这样的工厂:构造与初始化是分开的.
  2. new运营商(-1):建筑和初始化是分不开的.

b)Haskell

自从过去8年以来,JavaScript一直是我最喜欢的语言.然而,最近我开始在Haskell编程,我必须承认Haskell已经偷走了我的心脏.在Haskell中编程很有趣也很有趣.在与Haskell处于同一个联盟之前,JavaScript还有很长的路要走,而且JavaScript程序员可以从Haskell中学到很多东西.我想谈谈从Haskell apropos到这个问题的代数数据类型.

Haskell中的数据类型就像JavaScript中的原型,Haskell中的数据构造函数就像JavaScript中的工厂函数.例如,上面的Point类将在Haskell中编写如下:

data Point = Point Int Int
zeroPoint = Point 0 0
origin = zeroPoint
point = Point 2 3
Run Code Online (Sandbox Code Playgroud)

简洁不是吗?但是我不是来卖Haskell所以让我们来看看Haskell提供的其他一些功能:

data Shape = Rectangle Point Point | Circle Point Int
rectangle = Rectangle origin (Point 3 4)
circle = Circle zeroPoint 3
Run Code Online (Sandbox Code Playgroud)

这里rectanglecircle都是类型的实例Shape:

rectangle :: Shape
circle :: Shape
Run Code Online (Sandbox Code Playgroud)

在这种情况下Shape是我们的原型(Haskell中的数据类型),rectangle并且circle是该数据类型的实例.更有趣的是,Shape原型有两个构造函数(Haskell中的数据构造函数):RectangleCircle.

Rectangle :: Point -> Point -> Shape
Circle :: Point -> Int -> Shape
Run Code Online (Sandbox Code Playgroud)

Rectangle数据构造是一个函数,它接受一个Point和另一个Point,并返回Shape.类似地,Circle数据构造函数是一个带a Point和a Int并返回a 的函数Shape.在JavaScript中,这将写成如下:

var Shape = {};

Rectangle.prototype = Shape;

function Rectangle(p1, p2) {
    this.p1 = p1;
    this.p2 = p2;
}

Circle.prototype = Shape;

function Circle(p, r) {
    this.p = p;
    this.r = r;
}

var rectangle = new Rectangle(origin, Point.new(3, 4));
var circle = new Circle(zeroPoint, 3);
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,JavaScript中的原型可以有多个构造函数,这是有道理的.一个构造函数也可能在不同的时间实例中具有不同的原型,但这根本没有意义.这样做会破坏instanceof.

事实证明,在使用构造函数模式时,使用多个构造函数会很痛苦.然而,当使用原型模式时,它是天堂般的匹配:

var Shape = {
    Rectangle: function (p1, p2) {
        var rectangle = Object.create(this);
        rectangle.p1 = p1;
        rectangle.p2 = p2;
        return rectangle;
    },
    Circle: function (p, r) {
        var circle = Object.create(this);
        circle.p = p;
        circle.r = r;
        return circle;
    }
};

var rectangle = Shape.Rectangle(zeroPoint, Point.new(3, 4));
var circle = Shape.Circle(origin, 3);
Run Code Online (Sandbox Code Playgroud)

您还可以使用extend我的博客文章中关于为什么原型继承很重要的功能,使上面的代码更简洁:

var Shape = {
    Rectangle: function (p1, p2) {
        return this.extend({
            p1: p1,
            p2: p2
        });
    },
    Circle: function (p, r) {
        return this.extend({
            p: p,
            r: r
        });
    }
};

var rectangle = Shape.Rectangle(zeroPoint, Point.new(3, 4));
var circle = Shape.Circle(origin, 3);
Run Code Online (Sandbox Code Playgroud)

以这种方式编写的工厂看起来很像模块模式,编写这样的代码感觉很自然.与构造函数模式不同,一切都很好地包含在对象文字中.这里,那里和任何地方都没有任何东西悬空.

然而,如果性能是您的主要关注点,那么坚持构造函数模式和new.在我看来,现代JavaScript引擎足够快,性能不再是主要因素.相反,我认为JavaScript程序员应该花更多的时间编写可维护且健壮的代码,而原型模式确实比构造函数模式更优雅和易懂.

  1. 工厂(+1):您可以轻松地为每个原型创建多个工厂.
  2. 构造函数(-1):为每个原型创建多个构造函数是hacky和笨拙的.
  3. Prototypal Pattern(+1):所有内容都封装在一个对象文字中.看起来很像模块模式.
  4. 构造函数模式(-1):它是非结构化的,看起来不连贯.难以理解和维护.

此外,Haskell还教我们关于纯函数式编程.由于工厂只是我们可以callapply工厂的功能,组成工厂,咖喱工厂,记忆工厂,通过提升它们使工厂变得懒惰等等.因为new是操作员而不是功能,所以不能使用new.是的,你可以做一个功能相当的,new但为什么不只是使用工厂?new在某些地方使用运算符而new在其他地方使用该方法是不一致的.

3.我们如何充分利用这两个世界?

好吧所以工厂确实有自己的优势,但仍然表现Object.create糟透了吗?确实如此,其中一个原因是因为我们每次使用时都会Object.create创建一个新的构造函数,将其原型设置为我们想要的原型,使用new然后返回它来实例化新创建的构造函数:

Object.create = function (o) {
    function F() {}
    F.prototype = o;
    return new F;
};
Run Code Online (Sandbox Code Playgroud)

我们能做得比这更好吗?我们试试吧.而不是每次都创建一个新的构造.constructor函数,为什么我们不只是实例化给定原型的函数?

Object.create = function (o) {
    return new o.constructor;
};
Run Code Online (Sandbox Code Playgroud)

这在大多数情况下都有效,但存在一些问题:

  1. o.constructor可能与原型不同o.
  2. 我们只想构造一个新的实例o,但o.constructor也可能有初始化逻辑,我们无法从构造中分离出来.

解决方案非常简单:

function defclass(prototype) {
    var constructor = function () {};
    constructor.prototype = prototype;
    return constructor;
}
Run Code Online (Sandbox Code Playgroud)

使用defclass您可以创建类,如下所示:

var Shape = defclass({
    rectangle: function (p1, p2) {
        this.p1 = p1;
        this.p2 = p2;
        return this;
    },
    circle: function (p, r) {
        this.p = p;
        this.r = r;
        return this;
    }
});

var rectangle = (new Shape).rectangle(zeroPoint, Point.new(3, 4));
var circle = (new Shape).circle(origin, 3);
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,我们已经分离了构造和初始化,并且初始化可以推迟到多个构造函数.它甚至可以链接如下:(new Shape).rectangle().circle().我们Object.createnew更快的速度取而代之,我们仍然可以灵活地做任何我们想做的事情.此外,一切都很好地封装在一个对象文字中.

结论

正如你所看到的,new操作员是一个必要的邪恶.如果将new其作为工厂函数实现,那么这将是很好的但它实现为运算符而JavaScript中的运算符不是一流的.这使得用函数编程变得更加困难new.另一方面,工厂是灵活的.您可以为您的原型定制任意数量的工厂功能,并且能够做任何您想要的工作是工厂功能的最大卖点.

  • @AaditMShah是的.有.它被称为`Object.create`.对于没有实现Object.create*的浏览器,流行的shim使用`new`.这并不意味着*实现Object.create*的浏览器使用new. (6认同)

T.J*_*der 6

在JavaScript中,人们称之为"伪经典"继承典型的继承.这是JavaScript唯一的继承类型.避免使用new就像避免使用switch语句一样,因为你可以使用它来if/else if代替.当然可以,有时你应该.其他时候,switch是正确的选择.同样的,newObject.create:使用你在做什么是最好的一个.

我来说,这有点主观(在我看来,整个"伪经典继承是坏的"模因):

  1. new是因为我在做类似课堂的事情.我使用new和构造函数,因为它非常适合语言的设计方式.(是的,设计是不寻常的,但是这是怎么回事.)所以,如果我将不得不将代表人民有共同行为的对象,我会用一个Person构造函数,分配行为(函数)Person.prototype,并使用new Person以创造它们.(我使用我的Lineage脚本使这更简洁,并轻松处理一些层次结构的东西.)这是直截了当,熟悉,干净,清晰:如果你看到new Person你知道我正在创建一个新对象.(如果我不是 - 是的,可能违反了构造函数的期望 - 那么在我看来我不应该首先编写构造函数.)

    现在当然,你可以定义一个设计器功能(createPerson,buildPerson,等等),做使用同样的事情Object.create或相似.如果那是他们喜欢的东西,我对那些人没有问题(只要功能名称清楚它会创造一些东西).我确实有人说"你不应该使用new",好像这是客观的建议; 这是一种意见,这是一种风格建议.

  2. Object.create是因为我在做实例级别的东西.我工作的项目在复杂的树/图中有一堆对象.它们只是数据,没有行为.有时,我们需要拥有尚未验证的数据,因此不应覆盖以前的数据.因此容器具有对已验证数据(verified)和未验证数据()的引用current.为了避免代码中不必要的分支,容器始终具有两个引用,但在正常情况下它们引用相同的对象(container.verified = container.current = {};).几乎所有代码都使用该current对象,因为几乎所有代码都应该使用最新的信息.如果我们需要添加待处理数据,我们container.current = Object.create(container.verified);会将数据添加到container.current.由于current原型是verified,所以不需要将所有旧数据复制到它并且在整个地方都有重复的数据.例如,门面的经典用途.new对于这项工作来说,这将是错误的工具,它只会妨碍工作.

关于JavaScript的众多奇妙之处之一就是你有两种选择.我同时在同一个项目中使用它们,不同的东西.


Eri*_*ott 6

之前已经多次提出并回答了类似的问题.看到:

构造函数vs工厂函数 Classical Vs原型继承

更多学习:https : //medium.com/javascript-scene/3-different-kinds-of-prototypal-inheritance-es6-edition-32d777fa16c9#.s0r3i5w6t http://vimeo.com/69255635

TL;博士

  • 构造者打破了开放/封闭原则
  • 构造函数将对象创建与对象初始化相混淆 - 有时会妨碍代码的可重用性
  • 构造函数看起来有点像类,这令人困惑.JavaScript不需要类(我建议避免使用ES6中的class关键字).JavaScript比类更好.
  • 原型委托和动态对象扩展(连接继承)的组合比经典继承更强大和灵活.
  • 在JavaScript中,Constructor.prototype和实例之间的连接是脆弱的和不值得信任的.使用构造函数可以提供工作实例的错觉,当它在执行上下文中不起作用时可能会令人困惑,或者如果构造函数原型被换出则不起作用.
  • 构造函数更难以更换原型.您可能希望这样做以启用多态对象构造.使用工厂,热插拔原型很容易,可以使用.call()和.apply()完成.

编辑 - 回应OP发布的"答案":

关于Object.create的最好的事情是它是一个专用的低级工具,它允许您创建一个新对象并在不使用构造函数的情况下为其分配任何原型.避免构造函数有很多理由,这里有深入介绍:构造函数与工厂函数

  1. 用于演示"代码较少"的代码并没有真正证明经典和原型继承之间的区别.更典型的示例可能如下所示:

古典

var Animal = function Animal(name) {
  this.name = name;
};

Animal.prototype.walk = function walk() {
  console.log(this.name + ' goes for a walk.');
};

var Rabbit = function Rabbit(/* name */) {
  // Because construction and instantiation are conflated, you must call super().
  Animal.prototype.constructor.apply(this, arguments);
};

// Classical inheritance is really built on top of prototypal inheritance:
Rabbit.prototype = Object.create(Animal.prototype);

// Fix the .constructor property:
Rabbit.prototype.constructor = Rabbit;

Rabbit.prototype.jump = function jump() {
  console.log(this.name + ' hops around a bit.');
};

var myRabbit = new Rabbit('Bunny George');

myRabbit.walk();
// Bunny George goes for a walk.
Run Code Online (Sandbox Code Playgroud)

原型

var animalMethods =  {
  walk: function walk() {
    console.log(this.name + ' goes for a walk.');
  }
};

var animal = function animal(name) {
  var instance = Object.create(animalMethods);
  instance.name = name;
  return instance;
};

var rabbitMethods = {
  jump: function jump() {
    console.log(this.name + ' hops around a bit.');
  }
};

var rabbit = function rabbit(name) {
  var proto = rabbitMethods;

  // This is more commonly done like mixin({}, animalMethods, rabbitMethods);
  // where mixin = $.extend, _.extend, mout.object.mixIn, etc... It just copies
  // source properties to the destination object (first arg), where properties from
  // the last argument override properties from previous source arguments.
  proto.walk = animalMethods.walk;
  var instance = Object.create(rabbitMethods);

  // This could just as easily be a functional mixin,
  // shared with both animal and rabbit.
  instance.name = name;
  return instance;
};

var rabbit2 = rabbit('Bunny Bob');

rabbit2.walk();
// Bunny Bob goes for a walk.
Run Code Online (Sandbox Code Playgroud)

所需的代码量非常相似,但对我而言,原型材料的作用更为清晰,而且它也更灵活,并且没有第一个例子的经典继承关节炎包袱.

  • 我想看看这两种情况的示例,以了解您的确认书的好处。因为我认为其中大多数只是理论上的。 (2认同)
  • 同样,您的所有论据,没有能证明您可以做什么和我不能做什么的示例,对我来说都是无效的。您的独特示例是关于代码量的。是的。也许您不同意,但如果要实现经典功能所提供的其他功能,则必须更多。难?`function duck(){flying.apply(this); Swimming.apply(this); }`。 (2认同)
  • EnZo,你的`flying.apply(this); swim.apply(this);`例子不是经典继承.它是功能继承. (2认同)