【问题标题】:Example of Javascript Duck Typing?Javascript鸭子打字的例子?
【发布时间】:2012-10-06 18:23:32
【问题描述】:

一些程序员建议不要在 Javascript 中使用伪经典继承,但建议使用鸭子类型并为每个对象提供一组功能。

有没有一个很好的例子来说明如何做到这一点?我在下面有一个示例,但它一次只分配一个功能。我们可以给一个对象分配一整组方法吗,比如我们可以设置一个OceanAnimal的原型,它可以“游泳”、“潜水”和“上升”,一个LandAnimal的原型用于“跑步”、“ walk”和“jump”,并让一个对象从其中一个或两个继承? (所以fish对象可以继承或获得OceanAnimal的能力,而乌龟可以同时获得OceanAnimalLandAnimal的能力?)

var yoyo = {
    name: "Yoyo",
    type: "turtle"
}

var simba = {
    name: "Simba",
    type: "lion"
}

var dolphy = {
    name: "Dolphy",
    type: "dolphin"
}

function swim(n) {
    console.log("My name is", this.name, ", I am a", this.type, "and I just swam", n, "feet")
}

function run(n) {
    console.log("My name is", this.name,  ", I am a", this.type, "and I just ran", n, "feet")
}

Object.prototype.respondTo = function(method) {
    return !!(this[method] && (typeof this[method] === "function"));
}

yoyo.swim = swim;
yoyo.swim(10);

dolphy.swim = swim;
dolphy.swim(80);

simba.run = run;
simba.run(200);

yoyo.run = run;
yoyo.run(2);

yoyo.walk = run;
yoyo.walk(1);

console.log(simba.respondTo("swim"));
console.log(simba.respondTo("run"));
console.log(simba.respondTo("walk"));

console.log(yoyo.respondTo("run"));
console.log(yoyo.respondTo("walk"));
console.log(yoyo.respondTo("fly"));

if(dolphy.respondTo("run")) {
    dolphy.run(10);
}

if(dolphy.respondTo("swim")) {
    dolphy.swim(10);
}

输出:

My name is Yoyo , I am a turtle and I just swam 10 feet 
My name is Dolphy , I am a dolphin and I just swam 80 feet 
My name is Simba , I am a lion and I just ran 200 feet 
My name is Yoyo , I am a turtle and I just ran 2 feet 
My name is Yoyo , I am a turtle and I just ran 1 feet 
false 
true 
false 
true 
true 
false 
My name is Dolphy , I am a dolphin and I just swam 10 feet

【问题讨论】:

  • 我不清楚这是在问什么。 nametype 的示例显示了 Duck-typing,因为方法/属性由隐式假设合约支持 - “它的行为就像鸭子,嘎嘎叫,因此......”
  • 顺便说一句,你可以写return typeof this[method]=== 'function';。那也一样。
  • 我在问...我们可以分配一整套方法而不是一个接一个...实际上是不是更好地首先创建一整套功能的原型,然后让对象拥有一整套(例如LandAnimal,或者如果是Flyable,可能更好)
  • 要不创建一次性方法,请使用 Prototypes(“javascript 原型继承”可能是一个很好的搜索短语) - 这可以与“mass assignment”属性扩展结合使用(例如,复制所有目标对象或原型对象的“基础”对象中的方法)。
  • 我们可以使用Object.create来获得一组能力,但是我们如何获得另一组呢?是否有内置或通用方法,或者我们只是编写函数来复制所有属性...但是如果A 具有属性x 并且B 也具有属性x 怎么办?

标签: javascript


【解决方案1】:

JavaScript 中的函数是通用的。它们可以用作subroutinesmethodsconstructorsnamespacesmodules 等等。

人们反对在 JavaScript 中使用伪经典继承的原因是它隐藏了 JavaScript 的真正威力。如果不比对象更具表现力,函数也同样具有表现力。 Alonzo Church 证明了这一点,他的工作 Lambda CalculusTuring Complete

为了直接回答您的问题,我将使用函数创建TurtleLionDolphin。然后我将演示乌龟如何是OceanAnimalLandAnimal,狮子如何只是LandAnimal,海豚如何只是OceanAnimal。最后,我将解释什么是鸭式打字。

首先让我们为OceanAnimal创建构造函数:

function OceanAnimal() {
    this.swim = function (n) {
        return "I am " + this.name + ", the " + this.type +
               ", and I just swam " + n + " meters.";
    };
}

接下来我们将为LandAnimal创建构造函数:

function LandAnimal() {
    this.walk = function (n) {
        return "I am " + this.name + ", the " + this.type +
               ", and I just walked " + n + " meters.";
    };
}

好的。所以现在让我们为 Turtle 创建构造函数:

Turtle.prototype.type = "turtle";

function Turtle(name) {
    this.name = name;
    LandAnimal.call(this);
    OceanAnimal.call(this);
}

这里发生了什么?好的,我们希望 Turtle 继承自 OceanAnimalLandAnimal。所以我们打电话给LandAnimal.call(this)OceanAnimal.call(this)。通过这种方式,我们将OceanAnimalLandAnimal 构造函数用作mixins。因此Turtle 继承自OceanAnimalLandAnimal,而实际上并没有成为OceanAnimalLandAnimal 的类型。

要注意的另一件事是,我们将type 属性设置在Turtleprototype 上,而不是在其中。这是因为type 对于所有海龟都是一样的。因此它是共享的。另一方面,每只海龟的name 可能会有所不同,因此它是在构造函数中设置的。

现在让我们类似地为Lion 创建构造函数:

Lion.prototype.type = "lion";

function Lion(name) {
    this.name = name;
    LandAnimal.call(this);
}

由于LionLandAnimal,我们只混入LandAnimal 构造函数。

同样适用于Dolphin

Dolphin.prototype.type = "dolphin";

function Dolphin(name) {
    this.name = name;
    OceanAnimal.call(this);
}

现在我们已经创建了所有的构造函数,让我们创建一只乌龟、一只狮子和一只海豚:

var yoyo = new Turtle("Yoyo");
var simba = new Lion("Simba");
var dolphy = new Dolphin("Dolphy");

哇,现在让我们释放它们:

alert(yoyo.walk(10));
alert(yoyo.swim(30));   // turtles are faster in the water
alert(simba.walk(20));
alert(dolphy.swim(20));

哈哈。那很有趣。我个人最喜欢悠悠球。

好的,那么什么是鸭子打字?我们知道 yoyo 是一个OceanAnimal 和一个LandAnimal。但是,如果我们执行yoyo instanceof OceanAnimalyoyo instanceof LandAnimal,那么它会返回false。什么?

  • 你:愚蠢的 JavaScript。 TurtleOceanAnimalLandAnimal
  • JavaScript: 不是从我站的地方。我只知道这是Turtle
  • 你:但如果它会游泳,那就是OceanAnimal,如果它会走路,那就是LandAnimal

因此,由于 JavaScript 是个大杂烩,我们必须创建自己的测试来检查对象是否为 OceanAnimal 以及是否为 LandAnimal

让我们从OceanAnimal开始:

function isOceanAnimal(object) {
    if (typeof object !== "object") return false;
    if (typeof object.swim !== "function") return false;
    return true;
}

同样,对于LandAnimal

function isLandAnimal(object) {
    if (typeof object !== "object") return false;
    if (typeof object.walk !== "function") return false;
    return true;
}

所以现在我们可以使用isOceanAnimal(yoyo) 代替yoyo instanceof OceanAnimalisLandAnimal(yoyo) 代替yoyo instanceof LandAnimal;这两个函数都会为我们心爱的悠悠球返回true。耶!

这是一个在 JavaScript 中进行鸭式输入的简单示例。总结:

当我看到一只像鸭子一样走路,像鸭子一样游泳,像鸭子一样嘎嘎叫的鸟时,我称那只鸟为鸭子。

同样:

当我看到一种像海洋动物一样游泳的动物时,我称它为海洋动物;当我看到一种像陆地动物一样走路的动物时,我称它为陆地动物。

编辑:你可以在这里看到上面的代码:http://jsfiddle.net/aaditmshah/X9M4G/

【讨论】:

  • 所以问题变成了:为什么你需要对你的子类进行类型检查,当你的程序应该只关心它所提供的对象是否可以做它想做的事情时,而不是它是一个应该拥有你想要的特性的类的实例?这才是多态的真正意义,不是吗?
【解决方案2】:

与其尝试子类化,为什么不使用鸭子类型倡议来扩展组件和依赖注入?

var Duck = function (flyable, movable, speakable) {

    this.speak = speakable.speak;
    this.fly = flyable.fly;
    this.position = movable.position;

    flyable.subscribe("takeoff", movable.leaveGround);
    flyable.subscribe("land",    movable.hitGround);

}


var duck = new Duck(new BirdFlier(), new BirdWalker(), new Quacker());

var plane = new Duck(new VehicleFlier(), new Drivable(), new Radio());


duck.speak(); // "quack"
plane.speak(); // "Roger"

注意plane 很高兴使用与duck 相同的构造函数。

它们甚至一开始就不需要构造函数。
工厂也可以。

DuckTyping 的重点是对象构造不是使用对象的程序所关心的。

只要具有相同的方法/属性名称,程序就会使用它们,而不管子/超/静态继承。

编辑: 添加示例组件

这里有几个不同的想法,我将它们结合在一起。 所以一次一个点:

一、duck-typing的基本前提:

// the basic nature of duck-typing
var sheriff = {
    gun : {
        aim : function () { /* point gun somewhere */ },
        bullets : 6,
        fire : function () {
            if (this.bullets === 0) { return; }
            this.bullets -= 1;
            /* ... et cetera */
        }
    },
    draw : function () {
        this.gun.aim();
        this.gun.fire();
    }
},

cartoonist = {
    pen : { scribble : function () { /* ... */ } },
    draw : function () { this.pen.scribble(); }
},

graphicsCard = {
    pixels : [ /* ... */ ],
    screen : { /* ... */ },
    draw : function () {
        pixels.forEach(function (pixel) { screen.draw(pixel); });
    }
};


// duck-typing at its finest:
sheriff.draw();
cartoonist.draw();
graphicsCard.draw();

目标是编写无需检查对象类型的函数:

function duckDraw (array) { array.forEach(function (obj) { obj.draw(); }); }
duckDraw([ sheriff, cartoonist, graphicsCard ]);

因此,如果您有海龟、海豚、鲸鱼、潜艇和鲦鱼,您的程序就不必关心如何 他们游泳。它只关心他们都可以.swim();。每个项目都可以担心自己,以及它做它需要做的特殊方式。

鸭式打字

接下来是依赖注入。 在某种程度上,依赖注入也使用鸭子类型,但是在你的类的 inside 上,而不是在外面(或者如果你像我在上面那样做,它从内部开始,然后也允许在外部进行鸭式打字)。

这样想: 与其让一个人继承某些东西,不如把它交给他们。

如果您有soldiersniperplanetank,则每个人都需要一个gun。 而不是尝试子类化以便他们都可以开火...... ...为什么不制造不同种类的枪,以满足您的需求,并为他们提供所需的一切?

var Rifle = function () {
    this.reload = function () {};
    this.fire = function () { /* ... */ };
},

SniperRifle = function () {
    this.reload = function () {};
    this.fire = function () {};
},

MachineGun = function () {
    this.reload = function () {};
    this.fire = function () {};
},

Cannon = function () {
    this.reload = function () {};
    this.fire = function () {};
};

所以现在我们有了不同种类的枪... 你可能会这样想,因为它们有相同的函数名,而且它们都处理子弹,所以你可以尝试子类化......但你不需要 - 这些枪都没有做同样的事情当它们开火或重新加载时... ...因此您最终会用其他语言将 overrides 写入 abstract virtual 方法/属性,这将毫无用处。

现在我们已经有了它们,我们可以看看如何“注入”它们,以及这对我们有什么作用:

var Soldier = function (gun) {
    this.currentGun = gun;
    this.inventory = {
        guns : [ gun ]
    };
    this.attack = function () { this.currentGun.fire(); };
};

var Sniper = function (gun) {
    this.currentGun = gun;
    this.inventory = {
        guns : [ gun ]
    };
    this.attack = function () { this.currentGun.fire(); };
};

var Plane = function (gun) {
    this.currentGun = gun;
    this.inventory = {
        guns : [ gun ]
    };
    this.attack = function () { this.currentGun.fire(); };
};

var Tank = function (gun) {
    this.currentGun = gun;
    this.inventory = {
        guns : [ gun ]
    };
    this.attack = function () { this.currentGun.fire(); };
};


var soldier = new Soldier( new Rifle() ),
    sniper  = new Sniper( new SniperRifle() ),
    plane   = new Plane( new MachineGun() ),
    tank    = new Tank( new Cannon() );

所以现在我们有了这些他们称之为枪的类——他们不在乎什么样的枪,它只是工作,因为枪知道枪是如何工作的,而战斗人员知道如何开枪,并且程序知道如何告诉战斗人员开火。

但如果你仔细观察,现在每个战斗员的内部代码都是 100% 相同的。

那么,为什么不只拥有一个可以提供特殊组件的“战斗者”呢?

var Combatant = function (gun) {
    this.currentGun = gun;
    this.inventory = {
        guns : [ gun ]
    };
    this.attack = function () { this.currentGun.fire(); };
};

var soldier = new Combatant( new Rifle() );

所以构造函数的内部是鸭子类型gun,如果你有不同的战斗员类,每个类都有一个fire方法,那么你可以在游戏逻辑中鸭子类型你的单位为好吧。

最终,构造函数将只保存模块:一个处理射击,一个处理地面移动,一个用于绘图,一个用于玩家控制,等等...... 构造者不需要做任何事情,除了让这些部件相互接触,你可以通过赋予它们不同种类的枪、不同种类的运动或不同种类的健康来使单位变得特别,这些在内部,但具有相同的属性和方法名称以供公共访问。

【讨论】:

  • 所以“方法集”是由duck.fly("land")duck.fly("takeoff") 完成的?所以就像,一个属性代表一整套能力,由方法的参数决定......
  • 不完全是。更重要的是,与其构建animal,然后将landanimal 子类化,然后使用swimmingLandAnimal 扩展一个类,然后不断……构建组件:具有您想要的功能的“类”。不要建造一条鱼并将其子类化为潜艇。相反,建立一个“潜水”类。该潜水器类将包含所有水下运动方法和属性。构建一个处理所有玩家输入的“可控”类。然后为您的鱼提供游泳所需的所有模块,并为您的潜艇做同样的事情。
  • 这与方法本身无关。我可以做this.fly = flyable.fly; this.land = flyable.land; this.pitch = flyable.pitch; 等等。或者,我可以像this.flyable = flyable; 这样构建类并调用duck.flyable.takeOff();。问题是,如果您所有的传单都将他们的takeoffland 称为同一个东西……那么您可以制作专门的flyables。现在你正在专注于功能,而不是试图将飞艇和蚊子硬塞进同一个家庭。你程序的其余部分并不关心你的BirdFlierPlaneFlier 做了什么,他们只想.fly()
  • 你所看到的,我传入的字符串是Subscriber-Pattern / Observer-Pattern - 我为步行制作的组件请求更新飞行模块何时离开地面以及何时撞击又是地面。你说jumpfly 都需要知道height... 但它们不需要相同的高度。 Flying 组件不需要知道任何其他组件的任何信息,除了它需要开始飞行。行走组件需要知道何时停止行走(因为现在您正在游泳、飞行或死亡)。所以他们来回传递信息。
  • TLDR;你能告诉我你想说什么的要点吗?所有这些鸭子、飞机、警长、漫画家、显卡、步枪、狙击步枪、机枪、大炮、士兵、狙击手和坦克似乎都让我感到困惑。
【解决方案3】:

所以你有这两个功能:

function make ( props ) {
    var obj = Object.create( {} );
    Object.keys( props ).forEach(function ( key ) {
        obj[ key ] = props[ key ];
    });
    return obj;
};

function addMethods ( obj, methods ) {
    var proto = Object.getPrototypeOf( obj );
    Object.keys( methods ).forEach(function ( key ) {
        proto[ key ] = methods[ key ];
    });        
}

用法:

var simba = make({
    name: "Simba",
    type: "lion"
});

addMethods( simba, {
    swim: function () {},
    run: function () {}
});

addMethods( simba, {
    hunt: function () {},
    kill: function () {}
});

现场演示: http://jsfiddle.net/UETVc/

【讨论】:

  • 我们如何获得第二/第三组方法,并处理这些方法需要具有的任何属性值,以及可能的属性值名称冲突? (例如,如果jump 需要height 值并且fly 也有height
  • 您想随后向对象添加更多方法?
  • 你的意思是我们可以在对象创建期间指定所有方法?可能,是的,以后能够添加更多方法会很好......
  • 在我上面的示例中,首先创建对象(通过对象文字)。然后创建方法对象。最后inherit方法用于设置继承。
  • 那么如果methodsrunjump,而method2swimdive,那么你如何继承两者?
猜你喜欢
  • 2011-03-23
  • 2011-11-25
  • 2015-01-24
  • 1970-01-01
  • 2012-10-26
  • 2011-05-11
  • 2020-02-13
  • 1970-01-01
  • 2015-08-01
相关资源
最近更新 更多