Javascript Duck打字的例子?

时间:2012-10-06 18:23:32

标签: javascript

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

是否有一个很好的例子说明如何做到这一点?我在下面有一个例子,但它一次只分配一个函数。我们可以为对象分配一组方法,例如我们可以设置OceanAnimal的原型,它可以“游泳”,“潜水”和“上升”,LandAnimal的原型为“运行“,”“走”和“跳”,让一个对象继承一个或两个? (所以一个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

3 个答案:

答案 0 :(得分:22)

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

人们建议不要在JavaScript中使用伪经典继承是因为它隐藏了JavaScript的真正威力。如果不是比对象更具表现力,那么函数也是表达性的。 Alonzo Church的工作证明了这一点,Lambda Calculus的工作是Turing 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 prototype而不是Turtle内设置type属性。这是因为name对所有海龟都是一样的。因此它是共享的。另一方面,每只乌龟的Lion可能会有所不同,因此它会在构造函数中设置。

现在让我们类似地为Lion.prototype.type = "lion"; function Lion(name) { this.name = name; LandAnimal.call(this); } 创建构造函数:

Lion

由于LandAnimalLandAnimal,我们只会混合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));

Awww,现在我们让他们自由:

OceanAnimal

哈哈。那很有趣。就个人而言,我最爱yoyo。

好的,那么什么是鸭子打字?我们知道yoyo是LandAnimalyoyo instanceof OceanAnimal。但是,如果我们执行yoyo instanceof LandAnimalfalse,则会返回Turtle。什么?

  • 你:愚蠢的JavaScript。 OceanAnimalLandAnimalTurtle
  • JavaScript:不是我站在哪里。我所知道的只是OceanAnimal
  • 你:但如果它游了,那就是LandAnimal,如果它走了,那么它就是OceanAnimal

因此,由于JavaScript是派对者,我们必须创建自己的测试来检查对象是否为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 OceanAnimal代替isLandAnimal(yoyo)yoyo instanceof LandAnimal代替true;这两个函数都会为我们心爱的yoyo返回{{1}}。耶!

这是在JavaScript中输入duck的简单示例。总结:

  

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

类似地:

  

当我看到一种像海洋动物一样游动的动物时,我称这种动物为海洋动物;当我看到一只像陆地动物一样行走的动物时,我称这种动物为陆地动物。

修改:您可以在此处查看上述代码:http://jsfiddle.net/aaditmshah/X9M4G/

答案 1 :(得分:9)

为什么不使用duck-typing计划来扩展组件和依赖注入,而不是尝试子类化?

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的观点是对象构造不是使用该对象的程序的关注点。

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

编辑: 添加了示例组件

这里有几个不同的想法,我在一起。 所以一次只有一个点:

首先,鸭子打字的基本前提是:

// 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();。每个项目都可以担心自己,以及它做出它需要做的特殊方式。

鸭打字

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

这样想: 而不是一个人继承某些东西,只需交给他们。

如果您有soldiersniperplane以及tank,则每个人都需要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方法,那么你可以在游戏逻辑也是如此。

最终,构造函数将只保存模块:一个模块用于处理射击,一个用于处理地面移动,一个用于绘图,一个用于玩家控制等等... 构造者不必做任何事情,除了将这些碎片彼此接触,你可以通过给它们不同种类的枪,或不同种类的动作或不同类型的健康来使单位特殊,在内部,但具有相同的公共访问属性和方法名称。

答案 2 :(得分:1)

所以你有这两个功能:

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/