oop - Har JavaScript typ av gränssnitt (såsom Javas 'interface')?
Jag lär mig hur man gör OOP med JavaScript. Har det konceptet interface (såsom Javas interface)?
Så jag skulle kunna skapa en lyssnare...
Svar #1
Det finns ingen uppfattning om "denna klass måste ha dessa funktioner" (det vill säga, inga gränssnitt i sig), eftersom:
- JavaScript-arv är baserat på objekt, inte klasser. Det är ingen stor sak förrän du inser:
- JavaScript är ett ytterst dynamiskt typat språk -- du kan skapa ett objekt med de rätta metoderna, vilket skulle få det att överensstämma med gränssnittet, och sedan ta bort allt som fick det att överensstämma. Det skulle vara så lätt att kringgå typsystemet -- även av misstag! -- att det inte skulle vara värt att försöka skapa ett typsystem från början.
Istället använder JavaScript det som kallas duck typing. (Om det går som en anka och kväker som en anka, så långt JavaScript bryr sig, är det en anka.) Om ditt objekt har metoderna quack(), walk() och fly(), kan koden använda det var som helst där den förväntar sig ett objekt som kan gå, kväka och flyga, utan att kräva implementering av något "Duckable"-gränssnitt. Gränssnittet är exakt den uppsättning funktioner som koden använder (och returvärdena från dessa funktioner), och med duck typing får du det gratis.
Nu betyder det inte att din kod inte kan krascha halvvägs, om du försöker anropa some_dog.quack(); du kommer få en TypeError. Ärligt talat, om du säger åt hundar att kväka har du lite större problem; duck typing fungerar bäst när du håller alla dina ankor i ordning, så att säga, och inte låter hundar och ankor blandas tillsammans om du inte behandlar dem som generella djur. Med andra ord, även om gränssnittet är flytande, finns det fortfarande där; det är ofta ett fel att skicka en hund till kod som förväntar sig att den ska kväka och flyga från början.
Men om du är säker på att du gör rätt sak, kan du kringgå problemet med kvackande hundar genom att testa om en viss metod existerar innan du försöker använda den. Något i stil med
if (typeof(someObject.quack) == "function")
{
// This thing can quack
}
Så du kan kolla alla metoder du kan använda innan du använder dem. Syntaxen är dock lite ful. Det finns ett lite snyggare sätt:
Object.prototype.can = function(methodName)
{
return ((typeof this[methodName]) == "function");
};
if (someObject.can("quack"))
{
someObject.quack();
}
Detta är standard JavaScript, så det borde fungera i vilken JS-tolk som helst värd att använda. Det har den extra fördelen att det läses som engelska.
För moderna webbläsare (det vill säga nästan alla webbläsare förutom IE 6-8), finns det till och med ett sätt att förhindra att egenskapen visas i for...in:
Object.defineProperty(Object.prototype, 'can', {
enumerable: false,
value: function(method) {
return (typeof this[method] === 'function');
}
}
Problemet är att IE7-objekt inte har .defineProperty alls, och i IE8 fungerar det enligt uppgift bara på hostobjekt (det vill säga DOM-element och liknande). Om kompatibilitet är ett problem kan du inte använda .defineProperty. (Jag kommer inte ens nämna IE6, eftersom det är ganska irrelevant nuförtiden utanför Kina.)
Ett annat problem är att vissa kodningsstilar antar att alla skriver dålig kod och förbjuder att ändra Object.prototype om någon vill använda for...in utan att tänka. Om du bryr dig om det, eller använder (enligt min åsikt trasig) kod som gör det, prova en något annorlunda version:
function can(obj, methodName)
{
return ((typeof obj[methodName]) == "function");
}
if (can(someObject, "quack"))
{
someObject.quack();
}
Svar #2
Skaffa ett exemplar av 'JavaScript design patterns' av Dustin Diaz. Det finns några kapitel som ägnas åt att implementera JavaScript-gränssnitt genom Duck Typing. Det är också en trevlig läsning. Men nej, det finns ingen språkets inbyggda implementering av ett gränssnitt, du måste Duck Typa.
// example duck typing method
var hasMethods = function(obj /*, method list as strings */){
var i = 1, methodName;
while((methodName = arguments[i++])){
if(typeof obj[methodName] != 'function') {
return false;
}
}
return true;
}
// in your code
if(hasMethods(obj, 'quak', 'flapWings','waggle')) {
// IT'S A DUCK, do your duck thang
}
Svar #3
JavaScript (ECMAScript version 3) har ett reserverat ord implements sparat för framtida användning. Jag tror att detta är avsett exakt för detta ändamål, men i en hets för att få specifikationen klar hade de inte tid att definiera vad man skulle göra med det, så för närvarande gör webbläsare ingenting annat än låter det ligga där och klagar ibland om du försöker använda det till något.
Det är möjligt och faktiskt tillräckligt enkelt att skapa din egen Object.implement(Interface)-metod med logik som vägrar så snart en viss uppsättning egenskaper/funktioner inte är implementerade i ett givet objekt.
Jag skrev en artikel om objektorientering där jag använder min egen notation enligt följande:
// Create a 'Dog' class that inherits from 'Animal'
// and implements the 'Mammal' interface
var Dog = Object.extend(Animal, {
constructor: function(name) {
Dog.superClass.call(this, name);
},
bark: function() {
alert('woof');
}
}).implement(Mammal);
Det finns många sätt att skära den här särskilda katten, men detta är den logik jag använde för min egen Interface-implementation. Jag tycker att jag föredrar detta tillvägagångssätt, och det är lätt att läsa och använda (som du kan se ovan). Det innebär att man måste lägga till en 'implement'-metod till Function.prototype vilket vissa kanske kan ha problem med, men jag tycker att det fungerar utmärkt.
Function.prototype.implement = function() {
// Loop through each interface passed in and then check
// that its members are implemented in the context object (this).
for(var i = 0; i < arguments.length; i++) {
// .. Check member's logic ..
}
// Remember to return the class being tested
return this;
}
Svar #4
JavaScript-gränssnitt:
Även om JavaScript inte har typen interface, är det ofta nödvändigt. På grund av JavaScripts dynamiska natur och användning av prototypbaserad arv är det svårt att säkerställa konsekventa gränssnitt över klasser - men det är möjligt att göra det; och det efterliknas ofta.
Vid det här laget finns det ett antal specifika sätt att emulera gränssnitt i JavaScript; variationer i tillvägagångssätt tillfredsställer vanligtvis vissa behov, medan andra lämnas obehandlade. Ofta är det mest robusta tillvägagångssättet överdrivet klumpigt och hindrar implementeraren (utvecklaren).
Här är ett tillvägagångssätt för gränssnitt/abstrakta klasser som inte är särskilt krångligt, är förklarande, håller implementationer inom abstraktioner till ett minimum och lämnar tillräckligt med utrymme för dynamiska eller egna metoder:
function resolvePrecept(interfaceName) {
var interfaceName = interfaceName;
return function curry(value) {
/* throw new Error(interfaceName + ' requires an implementation for ...'); */
console.warn('%s requires an implementation for ...', interfaceName);
return value;
};
}
var iAbstractClass = function AbstractClass() {
var defaultTo = resolvePrecept('iAbstractClass');
this.datum1 = this.datum1 || defaultTo(new Number());
this.datum2 = this.datum2 || defaultTo(new String());
this.method1 = this.method1 || defaultTo(new Function('return new Boolean();'));
this.method2 = this.method2 || defaultTo(new Function('return new Object();'));
};
var ConcreteImplementation = function ConcreteImplementation() {
this.datum1 = 1;
this.datum2 = 'str';
this.method1 = function method1() {
return true;
};
this.method2 = function method2() {
return {};
};
//Applies Interface (Implement iAbstractClass Interface)
iAbstractClass.apply(this); // .call / .apply after precept definitions
};
Deltagare
Principlösare
Funktionen resolvePrecept är ett verktyg och hjälpfunktion att använda inuti din Abstrakta Klass. Dess uppgift är att möjliggöra anpassad implementeringshantering av inkapslade Precepts (data och beteende). Den kan kasta fel eller varna -- OCH -- tilldela ett standardvärde till Implementor-klassen.
iAbstraktKlass
iAbstractClass definierar gränssnittet som ska användas. Dess tillvägagångssätt innebär en tyst överenskommelse med dess Implementor-klass. Detta gränssnitt tilldelar varje precept till samma exakta precept-namespace -- ELLER -- till vad än Precept Resolver-funktionen returnerar. Den tysta överenskommelsen löses dock till en context -- en bestämmelse av Implementor.
Implementerare
Implementeraren 'accepterar' helt enkelt ett Interface (iAbstractClass i detta fall) och tillämpar det genom användning av Constructor-Hijacking: iAbstractClass.apply(this). Genom att definiera data och beteende ovan, och sedan kapra Interfacets konstruktor - genom att skicka Implementerarens kontext till Interface-konstruktorn - kan vi säkerställa att Implementerarens överskridningar läggs till, och att Interfacet kommer att uttrycka varningar och standardvärden.
Detta är ett mycket okomplicerat tillvägagångssätt som har tjänat mitt team och mig mycket väl under tidens lopp och olika projekt. Det har dock vissa försiktighetsåtgärder och nackdelar.
Nackdelar
Även om detta hjälper till att implementera konsekvens i hela din programvara i betydande grad, implementerar det inte verkliga gränssnitt -- utan emulerar dem. Även om definitioner, standardvärden och varningar eller fel är förklarade, är förklaringen av användning tvingad & försäkrad av utvecklaren (som med mycket av JavaScript-utveckling).
Detta är till synes det bästa tillvägagångssättet för "Gränssnitt i JavaScript", men jag skulle gärna vilja se följande åtgärdat:
- Påståenden om returtyper
- Försäkran om underskrifter
- Frys objekt från
delete-åtgärder - Påståenden om något annat som är utbrett eller behövs i specifikationen av JavaScript-gemenskapen
Det sagt, jag hoppas att detta hjälper dig lika mycket som det har hjälpt mitt team och mig.
Svar #5
abstrakt gränssnitt som detta
const MyInterface = {
serialize: () => {throw "must implement serialize for MyInterface types"},
print: function() { console.log(this.serialize()) }
}
skapa en instans:
function MyType() {
this.serialize = () => "serialized "
}
MyType.prototype = MyInterface
och använd det
let x = new MyType()
x.print()
Svar #6
Hoppas att den som fortfarande letar efter ett svar finner det hjälpsamt.
Du kan prova att använda en Proxy (Det är standard sedan ECMAScript 2015): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
latLngLiteral = new Proxy({},{
set: function(obj, prop, val) {
//only these two properties can be set
if(['lng','lat'].indexOf(prop) == -1) {
throw new ReferenceError('Key must be "lat" or "lng"!');
}
//the dec format only accepts numbers
if(typeof val !== 'number') {
throw new TypeError('Value must be numeric');
}
//latitude is in range between 0 and 90
if(prop == 'lat' && !(0 < val && val < 90)) {
throw new RangeError('Position is out of range!');
}
//longitude is in range between 0 and 180
else if(prop == 'lng' && !(0 < val && val < 180)) {
throw new RangeError('Position is out of range!');
}
obj[prop] = val;
return true;
}
});
Då kan du enkelt säga:
myMap = {}
myMap.position = latLngLiteral;
Om du vill kontrollera via instanceof (frågat av @Kamaffeather), kan du linda in det i ett objekt på följande sätt:
class LatLngLiteral {
constructor(props)
{
this.proxy = new Proxy(this, {
set: function(obj, prop, val) {
//only these two properties can be set
if(['lng','lat'].indexOf(prop) == -1) {
throw new ReferenceError('Key must be "lat" or "lng"!');
}
//the dec format only accepts numbers
if(typeof val !== 'number') {
throw new TypeError('Value must be numeric');
}
//latitude is in range between 0 and 90
if(prop == 'lat' && !(0 < val && val < 90)) {
throw new RangeError('Position is out of range!');
}
//longitude is in range between 0 and 180
else if(prop == 'lng' && !(0 < val && val < 180)) {
throw new RangeError('Position is out of range!');
}
obj[prop] = val;
return true;
}
})
return this.proxy
}
}
Detta kan göras utan att använda Proxy men istället klasserna getters och setters:
class LatLngLiteral {
#latitude;
#longitude;
get lat()
{
return this.#latitude;
}
get lng()
{
return this.#longitude;
}
set lat(val)
{
//the dec format only accepts numbers
if(typeof val !== 'number') {
throw new TypeError('Value must be numeric');
}
//latitude is in range between 0 and 90
if(!(0 < val && val < 90)) {
throw new RangeError('Position is out of range!');
}
this.#latitude = val
}
set lng(val)
{
//the dec format only accepts numbers
if(typeof val !== 'number') {
throw new TypeError('Value must be numeric');
}
//longitude is in range between 0 and 180
if(!(0 < val && val < 180)) {
throw new RangeError('Position is out of range!');
}
this.#longitude = val
}
}
