Heim > Artikel > Web-Frontend > Umfassende Analyse der Prinzipien der JavaScript-Vererbung
Dieser Artikel bietet Ihnen eine umfassende Analyse der Prinzipien der JavaScript-Vererbung. Ich hoffe, dass er für Freunde hilfreich ist.
Wir wissen, dass JS OO-Programmierung ist, und natürlich sind die Funktionen der OO-Programmierung unverzichtbar. Nachdem wir den Prototyp kennengelernt haben, lasst uns zuschlagen, solange das Eisen heiß ist, und über eines der drei sprechen Hauptmerkmale der OO-Programmierung – Vererbung.
Das Wort „Erbe“ sollte leichter zu verstehen sein. Wir sind mit der Vererbung von Eigentum, der Vererbung von Familienunternehmen usw. vertraut. Ihre Prämisse ist, dass es einen Erben gibt, und dann sind Sie der Erbe, also gibt es ihn Nachlass. Das ist richtig, die Vererbung in JS erfolgt, wie Sie wissen, auch paarweise.
Vererbung besteht darin, die Eigenschaften eines Objekts auf das Objekt zu kopieren, das vererbt werden muss
Die OO-Sprache unterstützt zwei Vererbungsmethoden: Schnittstellenvererbung und Implementierungsvererbung, zu denen die Schnittstellenvererbung gehört Es werden nur Methodensignaturen vererbt, während die Implementierungsvererbung die eigentliche Methode erbt. Da Funktionen in ECMAScript keine Signaturen haben, kann die Schnittstellenvererbung nicht implementiert werden und es wird nur die Implementierungsvererbung unterstützt. Die Hauptmethode der Vererbung ist die Prototypenkette . Um die Prototypenkette zu verstehen, müssen Sie zunächst wissen, was a Für diejenigen, die es nicht verstehen, können Sie diesen Artikel lesen: Was ist der JavaScript-Prototyp? Ausführliche Erklärung des Javascript-Prototyps
Tatsächlich bedeutet Vererbung einfach, dass
① es ein übergeordnetes Element haben muss
② und alle Instanzen und Methoden dieses übergeordneten Elements erhält
Ein kleines Konzept wird hier populär gemacht. Das oben erwähnte keine Signatur war nicht ganz klar, als ich es zum ersten Mal sah, fand ich, dass diese Aussage durchaus akzeptabel ist.
Keine Signatur
Wir wissen, dass JS eine schwach typisierte Sprache ist und ihre Parameter durch ein Array von 0 oder mehr Werten dargestellt werden können. Dies dient nur der Bequemlichkeit. aber Es ist nicht notwendig, das heißt, Es besteht kein notwendiger Zusammenhang zwischen der Angabe unbenannter Parameter und der Übergabe von Parametern. Wir können Parameter benennen, aber nicht übergeben (der Standardwert ist derzeit nicht definiert), oder Wir können keine Parameter benennen, sondern Parameter übergeben, diese Schreibweise ist in JS zulässig. Im Gegensatz dazu gelten in stark typisierten Sprachen sehr strenge Anforderungen. Sobald mehrere Parameter definiert sind, müssen mehrere Parameter übergeben werden. Benannte Parameter müssen vorab die Erstellung einer Funktionssignatur erfordern und zukünftige Aufrufe müssen mit dieser Signatur konsistent sein. (Das heißt, wenn Sie mehrere Parameter definieren, müssen Sie mehrere Parameter übergeben)**, aber js verfügt nicht über diese Regeln und Vorschriften. Der Parser überprüft die genannten Parameter nicht, sodass js keine Signatur hat .
Gib mir ein Beispiel
function JSNoSignature () { console.log("first params" + arguments[0] + "," + "second params" + arguments[1]); } JSNoSignature ("hello", "world");
Dieses Beispiel ist offensichtlich. Der benannte Parameter ist leer, aber wir können trotzdem Parameter übergeben und die Methode aufrufen. JS kümmert sich nicht um den sogenannten Parametertyp, die Anzahl der Parameter, die Parameterposition sowie die eingehenden und ausgehenden Parameter. Wenn Sie einen Wert zurückgeben müssen, geben Sie ihn einfach ohne Deklaration zurück. Dies nennt man JS ohne Signatur.
Was ist eine Prototypenkette? Die wörtliche Bedeutung ist leicht zu verstehen, das heißt, die Aneinanderreihung aller Prototypen wird als Prototypenkette bezeichnet. Diese Erklärung dient natürlich nur dem besseren Verständnis. Die Prototypenkette ist die Hauptmethode zur Implementierung der Vererbung. Die Grundidee besteht darin, Prototypen zu verwenden, um einem Referenztyp die Eigenschaften und Methoden eines anderen Referenztyps erben zu lassen. Wir wissen, dass jeder Konstruktor ein Prototypobjekt hat, das Prototypobjekt einen Zeiger auf den Konstruktor enthält und die Instanz einen internen Zeiger auf den Prototyp enthält. Wenn wir zu diesem Zeitpunkt das Prototypobjekt einer Instanz eines anderen Typs gleichsetzen, enthält das Prototypobjekt zu diesem Zeitpunkt einen Zeiger auf einen anderen Prototyp. Dementsprechend enthält der andere Prototyp auch einen Zeiger auf einen anderen Konstruktor stellt die Kette von Instanzen und Prototypen dar, die die Prototypenkette darstellt. Um es ganz klar auszudrücken: Instanz → Prototyp → Instanz → Prototyp → Instanz ... Die Verbindung ist die Prototypenkette.
Ich denke, Vererbung ist eine Form der Prototypenkette.
Nachdem wir die Prototypenkette kennen, müssen wir wissen, wie man sie verwendet. ECMA stellt eine Reihe grundlegender Modi der Prototypenkette bereit. Die Grundmodi sind wie folgt:Der Grundmodus von die Prototypenkette
// 创建一个父类 function FatherType(){ this.fatherName = '命名最头痛'; } FatherType.prototype.getFatherValue = function() { return this.fatherName; } function ChildType(){ this.childName = 'George'; } // 继承了FatherType,即将一个实例赋值给函数原型,我们就说这个原型继承了另一个函数实例 // 将子类的原型指向这个父类的实例 ChildType.prototype = new FatherType(); ChildType.prototype.getChildValue = function() { return this.childName; } let instance = new ChildType(); console.log(instance.getFatherValue()); // 命名最头痛
Beim Aufruf von „instance.getFatherValue()“ werden drei Suchschritte durchlaufen
①Suche nach Instanz
Die Prototypenkette ist zu diesem Zeitpunkt Instanz → ChildType.prototype → FatherType.prototype
Nach der Ausführung von Instanz.getFatherValue() ist dies in getFatherValue ChildType. Zu diesem Zeitpunkt findet ChildType das entsprechende FatherName-Attribut zur Prototypenkette, die schließlich in FatherType gefunden wird.
所有的引用类型默认都继承了Object,而这个继承也是通过原型链实现的,因此,所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object。prototype,这也就是所有自定义类型都会继承toString(),valueOf()等默认方法的根本原因。
Array类型也是继承了Object类型的。
因此,我们可以总结一下,在原型链的最顶端就是Object类型,所有的函数默认都继承了Object中的属性。
在javascript原型是什么?javascript原型的详细解说中我们有提到过isPrototypeOf方法可以用于判断这个实例的指针是否指向这个原型,这一章我们学习了原型链,这里做个补充,按照原型链的先后顺序,isPrototypeOf方法可以用于判断这个实例是否属于这个原型的。
依旧用上面那个例子 // 注意,这里用的是原型,Object.prototype,FatherType.prototype,ChildType.prototype console.log(Object.prototype.isPrototypeOf(instance)); // true console.log(FatherType.prototype.isPrototypeOf(instance)); // true console.log(ChildType.prototype.isPrototypeOf(instance)); // true
下面再介绍另一种方法,通过instanceof操作符,也可以确定原型和实例之间的关系
instanceof操作符
instanceof操作符是用来测试原型链中的构造函数是否有这个实例
function FatherType(){ this.fatherName = '命名最头痛'; } FatherType.prototype.getFatherValue = function() { return this.fatherName; } function ChildType(){ this.childName = 'George'; } // 继承了FatherType ChildType.prototype = new FatherType(); // 创建实例 let instance = new ChildType(); // 为ChildType原型上添加新方法,要放在继承FatherType之后,这是因为new FatherType()会将ChildType原型上添加的新方法全部覆盖掉 ChildType.prototype.getChildValue = function() { return this.childName; } // 此时getFatherValue被重写了 ChildType.prototype.getFatherValue = function() { return true } console.log(instance.getFatherValue()); // true
②通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样会重写原型链。这部分的例子和解释在javascript原型是什么?javascript原型的详细解说中已经表述过了。一样的道理,只不过把原型换成了原型链罢了。
原型链的bug
原型链虽然强大,可以用它来实现继承,但是也是存在bug的,它最大的bug来自包含引用类型值的原型。也就是说原型链上面定义的原型属性会被所有的实例共享。
它还有另外一个bug,即在创建子类型的实例时,不能向父类型(超类型)的构造函数中传递参数。或者说没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
基于以上这两个原因,实践过程中很少会单独使用原型链
借用构造函数
其设计思想就是在子类型构造函数的内部调用父类(超类)构造函数。
由于函数只不过是在特定环境中执行代码的对象,因此通过apply()和call()方法也可以在(将来)新创建的对象上执行构造函数。
function FatherType() { this.name = 'George'; } function ChildType() { //通过call方法改变this的指向,此时FatherType中的this指的是ChildType,相当于在构造函数中定义自己的属性。 FatherType.call(this); } let instance1 = new ChildType(); instance1.name = '命名最头痛'; console.log(instance1.name); // '命名最头痛' let instance2 = new ChildType(); console.log(instance2.name); // George
通过上述方法很好解决了原型属性共享问题,此外,既然是一个函数,它也能传相应的参数,因此也能实现在子类型构造函数中向超类型构造函数传递参数。
function FatherType(name){ this.name = name } function ChildType(){ FatherType.call(this, "George"); this.age = 18 } let instance = new ChildType(); console.log(instance.name); // George console.log(instance.age); // 18
借用构造函数的问题
借用构造函数,方法都在构造函数中定义,那么函数的复用就无从谈起,而且在父类(超类型)的原型定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
组合继承
组合继承也叫伪经典继承,其设计思想是将原型链和借用构造函数的技术组合到一块,发挥二者之长的一种继承模式,其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承,这样既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
function FatherType(name){ this.name = name this.colors = ['red', 'blue', 'green'] } FatherType.prototype.sayName = function() { console.log(this.name) } // 借用构造函数实现对实例的继承 function ChildType(name, age){ // 使用call方法继承FatherType中的属性 FatherType.call(this, name); this.age = age } // 利用原型链实现对原型属性和方法的继承 ChildType.prototype = new FatherType(); //将FatherType的实例赋值给ChildType原型 ChildType.prototype.constructor = ChildType; // 让ChildType的原型指向ChildType函数 ChildType.prototype.sayAge = function(){ console.log(this.age) } let instance1 = new ChildType('命名最头痛', 18); instance1.colors.push('black'); console.log(instance1.colors); // 'red, blue, green, black' instance1.sayName(); instance1.sayAge(); var instance2 = new ChildType('命名最头痛', 18); console.log(instance2.colors); // 'red, blue, green' instance2.sayName(); // '命名最头痛' instance2.sayAge(); // 18
组合继承方式避免了原型链和借用构造函数的缺陷,是JS中常用的继承方式。
原型链继承
原型链继承没有使用严格意义上的构造函数,其思想是基于已有的对象创建新对象
// 此object函数返回一个实例, 实际上object()对传入其中的对象执行了一次浅复制. function object(o) { function F() {} // 创建一个临时构造函数 F.prototype = o; // 将传入的对象作为构造函数的原型 return new F(); // 返回这个临时构造函数的新实例 } let demo = { name: 'George', like: ['apple', 'dog'] } let demo1 = object(demo); demo1.name = '命名'; // 基本类型 demo1.like.push('cat'); // 引用类型共用一个内存地址 let demo2 = object(demo); demo2.name = '头痛'; // 基本类型 demo2.like.push('chicken') // 引用类型共用一个内存地址 console.log(demo.name) // George console.log(demo.like) // ["apple", "dog", "cat", "chicken"]
原型链继承的前提是必须要有一个对象可以作为另一个对象的基础。通过object()函数生成新对象后,再根据需求对新对象进行修改即可。 由于新对象(demo1, demo2)是将传入对象(demo)作为原型的,因此当涉及到引用类型时,他们会共用一个内存地址,引用类型会被所有实例所共享,实际上相当于创建了demo对象的两个副本。
Object.create()方法
ECMA5中新增Object.create()方法规范化了原型式继承。该方法接收两个参数
①基础对象,这个参数的实际作用是定义了模板对象中有的属性,就像上面例子中的demo,只有一个参数情况下,Object.create()与上例子中的object相同
②这个是可选参数,一个为基础对象定义额外属性的对象, 该对象的书写格式与Object.defineProperties()方法的第二个参数格式相同,每个属性都是通过自己的描述符定义的,以这种方式指定的任何属性都会覆盖原型对象上的同名属性。
// 只有一个参数 var demoObj = { name: 'George', like: ['apple', 'dog', 'cat'] } let demo1Obj = Object.create(demoObj); demo1Obj.name = '命名'; demo1Obj.like.push('banana'); let demo2Obj = Object.create(demoObj); demo2Obj.name = '头痛'; demo2Obj.like.push('walk'); console.log(demoObj.like) //["apple", "dog", "cat", "banana", "walk"] // 两个参数 var demoObj = { name: 'George', like: ['apple', 'dog', 'cat'] } let demo1Obj = Object.create(demoObj, { name: { value:'命名' }, like:{ value: ['monkey'] }, new_val: { value: 'new_val' } }); console.log(demoObj.name) // George console.log(demo1Obj.name) // 命名 console.log(demo1Obj.like) // ["monkey"] console.log(demo1Obj.new_val) // new_val console.log(Object.getOwnPropertyDescriptor(demo1Obj,'new_val')) // {value: "new_val", writable: false, enumerable: false, configurable: false}
如果只想让一个对象与另一个对象保持类型的情况下,原型式继承是完全可以胜任的,不过要注意的是,引用类型值的属性始终都会共享相应的值。
寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路,其设计思想与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数内部以某种方式来增强对象,最后返回一个对象。
// 这个函数所返回的对象,既有original的所有属性和方法,也有自己的sayHello方法 function createAnother(original) { let clone = Object.create(original); clone.sayHello = function(){ console.log('HELLO WORLD') } return clone; } let person = { name: 'George', foods: ['apple', 'banana'] } let anotherPerson = createAnother(person); anotherPerson.sayHello(); // HELLO WORLD
使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点与构造函数模式类似。
寄生组合式继承
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混合形式来继承方法。其背后思想:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。说白了就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
function inheritPrototype(childType, fatherType){ let fatherObj = Object.create(fatherType.prototype); // 创建对象 fatherObj.constructor = childType; // 弥补重写原型而失去的默认constructor属性 childType.prototype = fatherObj; // 指定对象 }
上例是寄生组合式继承最简单的形式,这个函数接受两个参数:子类型构造函数和超类型构造函数,在函数内部,①创建了父类型原型的一个副本,②为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性。③将新创建的对象(即副本)赋值给子类型的原型。
function FatherType(name){ this.name = name; this.foods = ['apple', 'banana']; } FatherType.prototype.sayName = function(){ console.log(this.name) } function ChildType(name, age){ FatherType.call(this, name); this.age = age; } inheritPrototype(ChildType, FatherType); ChildType.prototype.sayAge = function(){ console.log(this.age) }
总结
JS继承的主要方式是通过原型链实现的
实例-原型-实例-原型...无限链接下去就是原型链
所有引用类型的默认原型都是Object
instanceof操作符和isPrototypeOf方法都可以用于判断实例与原型的关系,其区别是,前者用的是原型,后者用的是构造函数
给原型添加方法的代码一定要放在继承之后,这是因为,在继承的时候被继承者会覆盖掉继承者原型上的所有方法
Object.create()方法用于创建一个新对象,其属性会放置在该对象的原型上
继承有6种方式,分别是原型链,借用构造函数,组合继承,原型式继承,寄生式继承和寄生组合式继承
相关推荐:
JavaScript中的继承之类继承_javascript技巧
Das obige ist der detaillierte Inhalt vonUmfassende Analyse der Prinzipien der JavaScript-Vererbung. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!