In diesem Artikel wird das Thema unter folgenden vier Gesichtspunkten behandelt:
•1. Umsetzung und Probleme des gemischten Ansatzes
•2. Erwartete Aufrufmethode
•3. Detaillierte Implementierung der Vererbungsbibliothek
•4. Zusammenfassung
Interessierte Freunde können für Details weiterlesen.
Die früheste Methode, die ich beherrschte, um die Vererbung in js zu implementieren, war die Methode des Mischens von Prototypenkette und Objektidentität, die ich in xx gelernt habe. Bei der Arbeit verwende ich immer diese Methode, um sie zu implementieren. Die Implementierung ist einfach und die Idee klar: Verwenden Sie Objekte, um vorzugeben, die Eigenschaften des Konstruktors der übergeordneten Klasse zu erben, und verwenden Sie die Prototypenkette, um die Methoden des Prototypobjekts der übergeordneten Klasse zu erben, was alle Vererbungsszenarien erfüllt, auf die ich gestoßen bin. Aus diesem Grund hätte ich nie gedacht, dass ich das nächste Mal, wenn ich über Vererbung schreibe, es anders schreiben muss. Bis heute Abend habe ich eine Reihe von Artikeln über JavaScript-Vererbung auf Sanshengshi gelesen (sie wurde sehr früh veröffentlicht und ich habe sie gerade gelesen). Es ist jetzt wirklich ein bisschen. Leider habe ich festgestellt, dass der Vererbungsmechanismus in js auch so nah an der Implementierung von Back-End-Sprachen wie Java geschrieben werden kann, was in der Tat wunderbar ist! Daher möchte ich die Ideen seines Blogs vollständig verstehen und eine Vererbungsbibliothek implementieren, die ich in Zukunft verwenden kann.
1. Umsetzung und Probleme des gemischten Ansatzes
Bevor wir das Problem verstehen, werfen wir einen Blick auf seine spezifische Implementierung:
- Hide code //父类构造函数 function Employee(name, salary) { //实例属性:姓名 this.name = name; //实例属性:薪资 this.salary = salary; } //通过字面量对象设置父类的原型,给父类添加实例方法 Employee.prototype = { //由于此处添加实例方法时也是通过修改父类原型处理的, //所以必须修改父类原型的constructor指向,避免父类实例的constructor属性指向Object函数 constructor: Employee, getName: function () { return this.name; }, getSalary: function () { return this.salary; }, toString: function () { return this.name + '\'s salary is ' + this.getSalary() + '.'; } } //子类构造函数 function Manager(name, salary, percentage) { //对象冒充,实现属性继承(name, salary) Employee.apply(this, [name, salary]); //实例属性:提成 this.percentage = percentage; } //将父类的一个实例设置为子类的原型,实现方法继承 Manager.prototype = new Employee(); //修改子类原型的constructor指向,避免子类实例的constructor属性指向父类的构造函数 Manager.prototype.constructor = Manager; //给子类添加新的实例方法 Manager.prototype.getSalary = function () { return this.salary + this.salary * this.percentage; } var e = new Employee('jason', 5000); var m = new Manager('tom', 8000, 0.15); console.log(e.toString()); //jason's salary is 5000. console.log(m.toString()); //tom's salary is 9200. console.log(m instanceof Manager); //true console.log(m instanceof Employee); //true console.log(e instanceof Employee); //true console.log(e instanceof Manager); //false
In Bezug auf die Ergebnisse gibt es bei dieser Vererbungsimplementierung kein Problem. Die Instanz von Manager erbt gleichzeitig die Instanzattribute und Instanzmethoden der Employee-Klasse, und die Ergebnisse der InstanzOf-Operation sind ebenfalls korrekt. In Bezug auf die Codeorganisation und Implementierungsdetails weist diese Methode jedoch immer noch die folgenden Probleme auf:
1) Die Codeorganisation ist nicht elegant genug. Die Logik der Schlüsselteile der Vererbungsimplementierung ist universell und hat die folgende Struktur:
- Hide code //将父类的一个实例设置为子类的原型,实现方法继承 SubClass.prototype = new SuperClass(); //修改子类原型的constructor指向,避免子类实例的constructor属性指向父类的构造函数 SubClass.prototype.constructor = SubClass; //给子类添加新的实例方法 SubClass.prototype.method1 = function() { } SubClass.prototype.method2 = function() { } SubClass.prototype.method3 = function() { }
Diesem Code fehlt die Kapselung. Darüber hinaus können Sie beim Hinzufügen einer Instanzmethode einer Unterklasse diese nicht über SubClass.prototype = { method1: function() {} } festlegen, da sonst der gesamte Prototyp der Unterklasse geändert wird und keine Vererbung möglich ist Auf diese Weise Jedes Mal, wenn ich gemäß der Struktur von SubClass.prototype.method1 = function() {} schreiben muss, sieht der Code sehr diskontinuierlich aus.
Lösung: Verwenden Sie die Modularisierung, um die allgemeine Logik zu kapseln und eine einfache Schnittstelle zur Außenwelt bereitzustellen. Solange Sie sie gemäß der vereinbarten Schnittstelle aufrufen, können Sie den Aufbau und die Vererbung von Klassen vereinfachen. Die konkrete Umsetzung entnehmen Sie bitte der folgenden inhaltlichen Einleitung. Derzeit können nur theoretische Erläuterungen gegeben werden.
2) Beim Festlegen des Prototyps der Unterklasse auf eine Instanz der übergeordneten Klasse wird new SuperClass() aufgerufen. Dies ist ein parameterloser Aufruf des Konstruktors der übergeordneten Klasse, daher muss die übergeordnete Klasse über eine parameterlose Konstruktorfunktion verfügen. In JavaScript können Funktionen jedoch nicht überladen werden, sodass die übergeordnete Klasse nicht mehrere Konstruktoren bereitstellen kann. In den meisten Fällen kann der Konstruktor der übergeordneten Klasse keine Parameter haben, um die Funktion im einzigen Konstruktor zu simulieren Eine Überladung kann nur durch die Beurteilung von arguments.length bewältigt werden. Das Problem besteht darin, dass es manchmal schwierig ist sicherzustellen, dass die Beurteilungslogik arguments.length jedes Mal hinzugefügt wird, wenn der Konstruktor der übergeordneten Klasse geschrieben wird. In diesem Fall ist dieser Ansatz riskant. Wenn die Logik im Konstruktor extrahiert werden kann und alle Konstruktoren der Klasse parameterlose Funktionen sind, lässt sich dieses Problem leicht lösen.
Lösung: Machen Sie alle Konstruktoren der übergeordneten Klasse und der Unterklasse ohne Parameter und schreiben Sie keine Logik in den Konstruktor. Migrieren Sie die Logik des Konstruktors in die Init-Instanzmethode, z. B. das Beispiel „Employee“ und „The Manager“. in Folgendes:
- Hide code //无参无逻辑的父类构造函数 function Employee() {} Employee.prototype = { constructor: Employee, //把构造逻辑搬到init方法中来 init: function (name, salary) { this.name = name; this.salary = salary; }, getName: function () { return this.name; }, getSalary: function () { return this.salary; }, toString: function () { return this.name + '\'s salary is ' + this.getSalary() + '.'; } }; //无参无逻辑的子类构造函数 function Manager() {} Manager.prototype = new Employee(); Manager.prototype.constructor = Manager; //把构造逻辑搬到init方法中来 Manager.prototype.init = function (name, salary, percentage) { //借用父类的init方法,实现属性继承(name, salary) Employee.prototype.init.apply(this, [name, salary]); this.percentage = percentage; }; Manager.prototype.getSalary = function () { return this.salary + this.salary * this.percentage; };
Durch die Verwendung der Init-Methode zum Abschließen der Konstruktionsfunktion kann sichergestellt werden, dass beim Festlegen des Prototyps der Unterklasse (Manager.prototype = new Employee()) der Instanziierungsvorgang der übergeordneten Klasse nicht schief geht Wenn Sie die Klasse aufrufen, wenn Sie den Konstruktor zum Initialisieren einer Instanz verwenden, müssen Sie nach dem Aufruf des Konstruktors die Init-Methode manuell aufrufen, um die eigentliche Konstruktionslogik abzuschließen:
- Hide code var e = new Employee(); e.init('jason', 5000); var m = new Manager(); m.init('tom', 8000, 0.15); console.log(e.toString()); //jason's salary is 5000. console.log(m.toString()); //tom's salary is 9200. console.log(m instanceof Manager); //true console.log(m instanceof Employee); //true console.log(e instanceof Employee); //true console.log(e instanceof Manager); //false
Es wäre schön, wenn die Logik dieses Init innerhalb des Konstruktors platziert werden könnte, aber dies würde das zuvor erwähnte Prinzip verletzen, dass der Konstruktor keine Parameter und keine Logik hat. Denken Sie anders darüber nach. Der Zweck dieses Prinzips besteht darin, sicherzustellen, dass beim Instanziieren der übergeordneten Klasse kein Fehler beim Aufruf des Konstruktors der übergeordneten Klasse auftritt Verwenden Sie es im Konstruktor der Klasse. Fügen Sie eine kleine Menge Logik hinzu, die das Problem definitiv löst:
- Hide code //添加一个全局标识initializing,表示是否正在进行子类的构建和类的继承 var initializing = false; //可自动调用init方法的父类构造函数 function Employee() { if (!initializing) { this.init.apply(this, arguments); } } Employee.prototype = { constructor: Employee, //把构造逻辑搬到init方法中来 init: function (name, salary) { this.name = name; this.salary = salary; }, getName: function () { return this.name; }, getSalary: function () { return this.salary; }, toString: function () { return this.name + '\'s salary is ' + this.getSalary() + '.'; } }; //可自动调用init方法的子类构造函数 function Manager() { if (!initializing) { this.init.apply(this, arguments); } } //表示开始子类的构建和类的继承 initializing = true; //此时调用new Emplyee(),并不会调用Employee.prototype.init方法 Manager.prototype = new Employee(); Manager.prototype.constructor = Manager; //表示结束子类的构建和类的继承,之后调用new Employee或new Manager都会自动调用init实例方法 initializing = false; //把构造逻辑搬到init方法中来 Manager.prototype.init = function (name, salary, percentage) { //借用父类的init方法,实现属性继承(name, salary) Employee.prototype.init.apply(this, [name, salary]); this.percentage = percentage; }; Manager.prototype.getSalary = function () { return this.salary + this.salary * this.percentage; };
Das Anrufergebnis ist immer noch das gleiche wie im vorherigen Beispiel. Bei dieser Implementierung gibt es jedoch noch ein kleines Problem. Es wäre großartig, wenn diese globale Variable eingeführt werden könnte, solange wir die Konstruktion und Vererbung von Klassen kapseln Modul, und fügen Sie dann diese Variable in das Modul ein und es wird kein Problem geben.
3) Beim Erstellen einer Unterklasse wird der Prototyp der Unterklasse auf eine Instanz der übergeordneten Klasse gesetzt. Die Vererbung sollte nicht zwischen Klassen erfolgen, nicht zwischen Klassen und Instanzen. Der Grund, warum eine Instanz der übergeordneten Klasse als Prototyp der Unterklasse verwendet wird:
- Hide code SubClass.prototype = new SuperClass();
完全是因为父类的这个实例,指向父类的原型,而子类的实例又会指向子类的原型,所以最终子类的实例就能通过原型链访问到父类原型上的方法。这个做法虽然能实现实例方法的继承,但是它不符合语义,而且它还有一个很大的问题就是会增加原型链的长度,导致子类在调用父类方法时,必须通过原型链的查找到父类的方法才行。要是继承层次较深,会对js的执行性能有些影响。
解决方式:在解决这个问题之前,先想想继承能帮我们解决什么问题:从父类复用已有的实例属性和实例方法。在javascript面向对象编程中,一直有一个原则就是,实例属性都写在构造函数或者实例方法里面,实例方法写在原型上面,也就是说类的原型,按照这个原则来说,就是用来写实例方法的,而且是只用来写实例方法,那么我们完全可以在构建子类时,通过复制的方式将父类原型的所有方法全部添加到子类的原型上,不一定要把父类的一个实例设置成子类的原型,这样就能将原型链的长度大大地缩短,借助一个简短的copy函数,我们就能轻松对前面的代码进行改造:
- Hide code //用来复制父类原型,由于父类原型上约定只写实例方法,所以复制的时候不必担心引用的问题 var copy = function (source) { var target = {}; for (var i in source) { if (source.hasOwnProperty(i)) { target[i] = source[i]; } } return target; } function Employee() { this.init.apply(this, arguments); } Employee.prototype = { constructor: Employee, init: function (name, salary) { this.name = name; this.salary = salary; }, getName: function () { return this.name; }, getSalary: function () { return this.salary; }, toString: function () { return this.name + '\'s salary is ' + this.getSalary() + '.'; } }; function Manager() { this.init.apply(this, arguments); } //将父类的原型方法复制到子类的原型上 Manager.prototype = copy(Employee.prototype); //子类还是需要修改constructor指向,因为从父类原型复制出来的对象的constructor还是指向父类的构造函数 Manager.prototype.constructor = Manager; Manager.prototype.init = function (name, salary, percentage) { Employee.prototype.init.apply(this, [name, salary]); this.percentage = percentage; }; Manager.prototype.getSalary = function () { return this.salary + this.salary * this.percentage; }; var e = new Employee('jason', 5000); var m = new Manager('tom', 8000, 0.15); console.log(e.toString()); //jason's salary is 5000. console.log(m.toString()); //tom's salary is 9200. console.log(m instanceof Manager); //true console.log(m instanceof Employee); //false console.log(e instanceof Employee); //true console.log(e instanceof Manager); //false
这么做了以后,当调用m.toString的时候其实调用的是Manager类自身原型上的方法,而不是Employee类的实例方法,缩短了在原型链上查找方法的距离。这个做法在性能上有很大的优点,但不好的是通过原型链维持的继承关系其实已经断了,子类的原型和子类的实例都无法再通过js原生的属性访问到父类的原型,所以这个调用console.log(m instanceof Employee)输出的是false。不过跟性能比起来,这个都可以不算问题:一是instanceOf的运算,几乎在javascript的开发里面用不到,至少我是没碰到过;二是通过复制方式完全能够把父类的实例方法继承下来,这就已经达到了继承的最大目的。
这个方法还有一个额外的好处是,解决了第2个问题最后提到的引入initializing全局变量的问题,如果是复制的话,就不需要在构建继承关系时,去调用父类的构造函数,那么也就没有必要在构造函数内先判断initializing才能去调用init方法,上面的代码中就已经去掉了initializing这个变量的处理。
4)在子类的构造函数和实例方法内如果想要调用父类的构造函数或者方法,显得比较繁琐:
- Hide code function SuperClass() {} SuperClass.prototype = { constructor: SuperClass, method1: function () {} } function SubClass() { //调用父类构造函数 SuperClass.apply(this); } SubClass.prototype = new SuperClass(); SubClass.prototype.constructor = SubClass; SubClass.prototype.method1 = function () { //调用父类的实例方法 SuperClass.prototype.method1.apply(this, arguments); } SubClass.prototype.method2 = function () {} SubClass.prototype.method3 = function () {} 每次都得靠apply借用方法来处理。要是能改成如下的调用就好用多了: - Hide code function SubClass() { //调用父类构造函数 this.base(); } SubClass.prototype = new SuperClass(); SubClass.prototype.constructor = SubClass; SubClass.prototype.method1 = function() { //调用父类的实例方法 this.base(); }
解决方式:如果要在每个实例方法里,都能通过this.base()调用父类原型上相应的方法,那么this.base就一定不是一个固定的方法,需要在每个实例方法执行期间动态地将this.base指定为父类原型的同名方法,能够做到这个实现的方式,就只有通过方法代理了,前面的Employee和Manager的例子可以改造如下:
- Hide code //用来复制父类原型,由于父类原型上约定只写实例方法,所以复制的时候不必担心引用的问题 var copy = function (source) { var target = {}; for (var i in source) { if (source.hasOwnProperty(i)) { target[i] = source[i]; } } return target; }; function Employee() { this.init.apply(this, arguments); } Employee.prototype = { constructor: Employee, init: function (name, salary) { this.name = name; this.salary = salary; }, getName: function () { return this.name; }, getSalary: function () { return this.salary; }, toString: function () { return this.name + '\'s salary is ' + this.getSalary() + '.'; } }; function Manager() { //必须在每个实例中添加baseProto属性,以便实例内部可以通过这个属性访问到父类的原型 //因为copy函数导致原型链断裂,无法通过原型链访问到父类的原型 this.baseProto = Employee.prototype; this.init.apply(this, arguments); } Manager.prototype = copy(Employee.prototype); //子类还是需要修改constructor指向,因为从父类原型复制出来的对象的constructor还是指向父类的构造函数 Manager.prototype.constructor = Manager; Manager.prototype.init = (function (name, func) { return function () { //记录实例原有的this.base的值 var old = this.base; //将实例的this.base指向父类的原型的同名方法 this.base = this.baseProto[name]; //调用子类自身定义的init方法,也就是func参数传递进来的函数 var ret = func.apply(this, arguments); //还原实例原有的this.base的值 this.base = old; return ret; } })('init', function (name, salary, percentage) { //通过this.base调用父类的init方法 //这个函数真实的调用位置是var ret = func.apply(this, arguments); //当调用Manager实例的init方法时,其实不是调用的这个函数 //而是调用上面那个匿名函数里面return的匿名函数 //在return的匿名函数里,先把this.base指向为了父类原型的同名函数,然后在调用func //func内部再通过调用this.base时,就能调用父类的原型方法。 this.base(name, salary); this.percentage = percentage; }); Manager.prototype.getSalary = function () { return this.salary + this.salary * this.percentage; }; var e = new Employee('jason', 5000); var m = new Manager('tom', 8000, 0.15); console.log(e.toString()); //jason's salary is 5000. console.log(m.toString()); //tom's salary is 9200. console.log(m instanceof Manager); //true console.log(m instanceof Employee); //false console.log(e instanceof Employee); //true console.log(e instanceof Manager); //false
通过代理的方式,就解决了在在实例方法内部通过this.base调用父类原型同名方法的问题。可是在实际情况中,每个实例方法都有可能需要调用父类的实例,那么每个实例方法都要添加同样的代码,显然这会增加很多麻烦,好在这部分的逻辑也是同样的,我们可以把它抽象一下,最后都放到模块化的内部去,这样就能简化代理的工作。
5)未考虑静态属性和静态方法。尽管静态成员是不需要继承的,但在有些场景下,我们还是需要静态成员,所以得考虑静态成员应该添加在哪里。
解决方式:由于js原生并不支持静态成员,所以只能借助一些公共的位置来处理。最佳的位置是添加到构造函数上:
- Hide code var copy = function (source) { var target = {}; for (var i in source) { if (source.hasOwnProperty(i)) { target[i] = source[i]; } } return target; }; function Employee() { this.init.apply(this, arguments); } //添加一个静态属性 Employee.idCounter = 1; //添加一个静态方法 Employee.getId = function () { return Employee.idCounter++; }; Employee.prototype = { constructor: Employee, init: function (name, salary) { this.name = name; this.salary = salary; //调用静态方法 this.id = Employee.getId(); }, getName: function () { return this.name; }, getSalary: function () { return this.salary; }, toString: function () { return this.name + '\'s salary is ' + this.getSalary() + '.'; } }; function Manager() { this.baseProto = Employee.prototype; this.init.apply(this, arguments); } Manager.prototype = copy(Employee.prototype); Manager.prototype.constructor = Manager; Manager.prototype.init = (function (name, func) { return function () { var old = this.base; this.base = this.baseProto[name]; var ret = func.apply(this, arguments); this.base = old; return ret; } })('init', function (name, salary, percentage) { this.base(name, salary); this.percentage = percentage; }); Manager.prototype.getSalary = function () { return this.salary + this.salary * this.percentage; }; var e = new Employee('jason', 5000); var m = new Manager('tom', 8000, 0.15); console.log(e.toString()); //jason's salary is 5000. console.log(m.toString()); //tom's salary is 9200. console.log(m instanceof Manager); //true console.log(m instanceof Employee); //false console.log(e instanceof Employee); //true console.log(e instanceof Manager); //false console.log(m.id); //2 console.log(e.id); //1
最后的两行输出了正确的实例id,而这个id是通过Employee类的静态方法生成的。在java的面向对象编程中,子类跟父类都可以定义静态成员,在调用的时候还存在覆盖的问题,在js里面,因为受语言的限制,自定义的静态成员不可能实现全面的面向对象功能,就像上面这种,能够给类提供一些公共的属性和公共方法,就已经足够了。
2. 期望的调用方式
从第1部分的分析可以看出,在js里面,类的构建与继承,有很多通用的逻辑,完全可以把这些逻辑封装成一个单独的模块,形成一个通用的类库,以便在工作中有需要的时候,都可以直接拿来使用。这个类库要求能完成我们需要的功能(类的构建与继承和静态成员的添加),同时在使用时要足够简洁方便。在利用bootstrap的modal组件自定义alert,confirm和modal对话框这篇文章里,我曾说过一些从组件期望的调用方式,去反推组件实现的一些观点,当你明确你需要什么东西时,你才知道这个东西你该怎么去创造。本文要编写的这个继承组件也会采取这个方法来实现,我先用前面Employee和Manager的例子来模拟调用这个继承库的场景,通过预设的一些组件名称或者接口名称以及调用方式,来尝试走通真实使用这个继承库的流程,有了这个东西,下一步我只需要根据这个要求去实现即可,模拟如下:
- Hide code //通过调用Class函数构造一个类 var Employee = Class({ //通过instanceMembers指定这个类的实例成员 instanceMembers: { init: function (name, salary) { this.name = name; this.salary = salary; //调用静态方法 this.id = Employee.getId(); }, getName: function () { return this.name; }, getSalary: function () { return this.salary; }, toString: function () { return this.name + '\'s salary is ' + this.getSalary() + '.'; } }, //通过staticMembers指定这个类的静态成员 //静态方法内部可通过this访问其它静态成员 //在外部可通过Employee.getId这种方式访问到静态成员 staticMembers: { idCounter: 1, getId: function () { return this.idCounter++; } } }); var Manager = Class({ instanceMembers: { init: function (name, salary, percentage) { this.base(name, salary); this.percentage = percentage; Manager.count++; }, getSalary: function () { return this.salary + this.salary * this.percentage; } }, //通过extend指定要继承的类 extend: Employee });
从模拟的结果来看,我想要的继承库对外提供的名称只有Class, instanceMembers, staticMembers和extend而已,调用方式也很简单,只要传递参数给Class函数即可。接下来就按照这个目标,看看如何一步步根据第一部分罗列的那些问题和解决方式,把这个库给写出来。
3. 继承库的详细实现
根据API名称和接口以及前面第1部分提出的问题,这个继承库要完成的功能有:
1)类的构建(关键:init方法)和静态成员处理;
2)继承关系的构建(关键:父类原型的复制);
3)父类方法的简化调用(关键:父类原型上同名方法的代理)。
所以这个库的实现,可以按照这三点分成三版来开发。
1)第一版
在第一版里面,仅需要实现类的构架和静态成员添加的功能即可,细节如下:
- Hide code var Class = (function () { var hasOwn = Object.prototype.hasOwnProperty; //用来判断是否为Object的实例 function isObject(o) { return typeof (o) === 'object'; } //用来判断是否为Function的实例 function isFunction(f) { return typeof (f) === 'function'; } function ClassBuilder(options) { if (!isObject(options)) { throw new Error('Class options must be an valid object instance!'); } var instanceMembers = isObject(options) && options.instanceMembers || {}, staticMembers = isObject(options) && options.staticMembers || {}, extend = isObject(options) && isFunction(options.extend) && options.extend, prop; //表示要构建的类的构造函数 function TargetClass() { if (isFunction(this.init)) { this.init.apply(this, arguments); } } //添加静态成员,这段代码需在原型设置的前面执行,避免staticMembers中包含prototype属性,覆盖类的原型 for (prop in staticMembers) { if (hasOwn.call(staticMembers, prop)) { TargetClass[prop] = staticMembers[prop]; } } TargetClass.prototype = instanceMembers; TargetClass.prototype.constructor = TargetClass; return TargetClass; } return ClassBuilder })();
这一版核心代码在于类的构建和静态成员添加的部分,其它代码仅仅提供一些提前可以想到的赋值函数和变量(isObject, isFunction),并做一些参数合法性校验的处理。添加静态成员的代码一定要在设置原型的代码之前,否则就有原型被覆盖的风险。有了这个版本,就可以直接构建带静态成员的Employee类了:
- Hide code var Employee = Class({ instanceMembers: { init: function (name, salary) { this.name = name; this.salary = salary; //调用静态方法 this.id = Employee.getId(); }, getName: function () { return this.name; }, getSalary: function () { return this.salary; }, toString: function () { return this.name + '\'s salary is ' + this.getSalary() + '.'; } }, staticMembers: { idCounter: 1, getId: function () { return this.idCounter++; } } }); var e = new Employee('jason', 5000); console.log(e.toString()); //jason's salary is 5000. console.log(e.id); //1 console.log(e.constructor === Employee); //true
在getId方法中之所以直接使用this就能访问到构造函数Employee,是因为getId这个方法是添加到构造函数上的,所以当调用Employee.getId()时,getId方法里面的this指向的就是Employee这个函数对象。
第二版在第一版的基础上,实现继承关系的构建部分:
- Hide code var Class = (function () { var hasOwn = Object.prototype.hasOwnProperty; //用来判断是否为Object的实例 function isObject(o) { return typeof (o) === 'object'; } //用来判断是否为Function的实例 function isFunction(f) { return typeof (f) === 'function'; } //简单复制 function copy(source) { var target = {}; for (var i in source) { if (hasOwn.call(source, i)) { target[i] = source[i]; } } return target; } function ClassBuilder(options) { if (!isObject(options)) { throw new Error('Class options must be an valid object instance!'); } var instanceMembers = isObject(options) && options.instanceMembers || {}, staticMembers = isObject(options) && options.staticMembers || {}, extend = isObject(options) && isFunction(options.extend) && options.extend, prop; //表示要构建的类的构造函数 function TargetClass() { if (extend) { //如果有要继承的父类 //就在每个实例中添加baseProto属性,以便实例内部可以通过这个属性访问到父类的原型 //因为copy函数导致原型链断裂,无法通过原型链访问到父类的原型 this.baseProto = extend.prototype; } if (isFunction(this.init)) { this.init.apply(this, arguments); } } //添加静态成员,这段代码需在原型设置的前面执行,避免staticMembers中包含prototype属性,覆盖类的原型 for (prop in staticMembers) { if (hasOwn.call(staticMembers, prop)) { TargetClass[prop] = staticMembers[prop]; } } //如果有要继承的父类,先把父类的实例方法都复制过来 extend && (TargetClass.prototype = copy(extend.prototype)); //添加实例方法 for (prop in instanceMembers) { if (hasOwn.call(instanceMembers, prop)) { TargetClass.prototype[prop] = instanceMembers[prop]; } } TargetClass.prototype.constructor = TargetClass; return TargetClass; } return ClassBuilder })();
这一版关键的部分在于:
this.baseProto主要目的就是为了让子类的实例能够有一个属性可以访问到父类的原型,因为后面的继承方式是复制方式,会导致原型链断裂。有了这一版之后,就可以加入Manager类来演示效果了:
- Hide code var Employee = Class({ instanceMembers: { init: function (name, salary) { this.name = name; this.salary = salary; //调用静态方法 this.id = Employee.getId(); }, getName: function () { return this.name; }, getSalary: function () { return this.salary; }, toString: function () { return this.name + '\'s salary is ' + this.getSalary() + '.'; } }, staticMembers: { idCounter: 1, getId: function () { return this.idCounter++; } } }); var Manager = Class({ instanceMembers: { init: function (name, salary, percentage) { //借用父类的init方法,实现属性继承(name, salary) Employee.prototype.init.apply(this, [name, salary]); this.percentage = percentage; }, getSalary: function () { return this.salary + this.salary * this.percentage; } }, extend: Employee }); var e = new Employee('jason', 5000); var m = new Manager('tom', 8000, 0.15); console.log(e.toString()); //jason's salary is 5000. console.log(m.toString()); //tom's salary is 9200. console.log(e.constructor === Employee); //true console.log(m.constructor === Manager); //true console.log(e.id); //1 console.log(m.id); //2
不过在Manager内部,调用父类的方法时还是apply借用的方式,所以在最后一版里面,需要把它变成我们期望的this.base的方式,反正原理前面也已经了解了,无非是在方法同名的时候,对实例方法加一个代理而已,实现如下:
- Hide code var Class = (function () { var hasOwn = Object.prototype.hasOwnProperty; //用来判断是否为Object的实例 function isObject(o) { return typeof (o) === 'object'; } //用来判断是否为Function的实例 function isFunction(f) { return typeof (f) === 'function'; } //简单复制 function copy(source) { var target = {}; for (var i in source) { if (hasOwn.call(source, i)) { target[i] = source[i]; } } return target; } function ClassBuilder(options) { if (!isObject(options)) { throw new Error('Class options must be an valid object instance!'); } var instanceMembers = isObject(options) && options.instanceMembers || {}, staticMembers = isObject(options) && options.staticMembers || {}, extend = isObject(options) && isFunction(options.extend) && options.extend, prop; //表示要构建的类的构造函数 function TargetClass() { if (extend) { //如果有要继承的父类 //就在每个实例中添加baseProto属性,以便实例内部可以通过这个属性访问到父类的原型 //因为copy函数导致原型链断裂,无法通过原型链访问到父类的原型 this.baseProto = extend.prototype; } if (isFunction(this.init)) { this.init.apply(this, arguments); } } //添加静态成员,这段代码需在原型设置的前面执行,避免staticMembers中包含prototype属性,覆盖类的原型 for (prop in staticMembers) { if (hasOwn.call(staticMembers, prop)) { TargetClass[prop] = staticMembers[prop]; } } //如果有要继承的父类,先把父类的实例方法都复制过来 extend && (TargetClass.prototype = copy(extend.prototype)); //添加实例方法 for (prop in instanceMembers) { if (hasOwn.call(instanceMembers, prop)) { //如果有要继承的父类,且在父类的原型上存在当前实例方法同名的方法 if (extend && isFunction(instanceMembers[prop]) && isFunction(extend.prototype[prop])) { TargetClass.prototype[prop] = (function (name, func) { return function () { //记录实例原有的this.base的值 var old = this.base; //将实例的this.base指向父类的原型的同名方法 this.base = this.baseProto[name]; //调用子类自身定义的实例方法,也就是func参数传递进来的函数 var ret = func.apply(this, arguments); //还原实例原有的this.base的值 this.base = old; return ret; } })(prop, instanceMembers[prop]); } else { TargetClass.prototype[prop] = instanceMembers[prop]; } } } TargetClass.prototype.constructor = TargetClass; return TargetClass; } return ClassBuilder })();
核心部分是:
只有当需要继承父类,且父类原型中有方法与当前的实例方法同名时,才会去对当前的实例方法添加代理。更详细的原理可以回到文章第1部分回顾相关内容。至此,我们在Manager类内部调用父类的方法时,就很简单了,只要通过this.base即可:
- Hide code var Employee = Class({ instanceMembers: { init: function (name, salary) { this.name = name; this.salary = salary; //调用静态方法 this.id = Employee.getId(); }, getName: function () { return this.name; }, getSalary: function () { return this.salary; }, toString: function () { return this.name + '\'s salary is ' + this.getSalary() + '.'; } }, staticMembers: { idCounter: 1, getId: function () { return this.idCounter++; } } }); var Manager = Class({ instanceMembers: { init: function (name, salary, percentage) { //通过this.base调用父类的构造方法 this.base(name, salary); this.percentage = percentage; }, getSalary: function () { return this.base() + this.salary * this.percentage; } }, extend: Employee }); var e = new Employee('jason', 5000); var m = new Manager('tom', 8000, 0.15); console.log(e.toString()); //jason's salary is 5000. console.log(m.toString()); //tom's salary is 9200. console.log(e.constructor === Employee); //true console.log(m.constructor === Manager); //true console.log(e.id); //1 console.log(m.id); //2
Achten Sie auf diese beiden Aufrufe:
Das Obige enthält alle Details der Vererbungsbibliothek, die in diesem Artikel implementiert werden soll. Tatsächlich werden die im ersten Teil dieses Artikels genannten Lösungen für die Probleme mit den im zweiten simulierten Aufrufszenarien kombiniert Teil, und kapseln Sie sie in ein Modul. Es ist nur intern. Die Prinzipien jedes Details sind leicht zu verstehen, solange Sie die in Teil 1 zusammengefassten Lösungen verstehen. In der letzten Version der Demonstration können wir auch sehen, dass die in diesem Artikel implementierte Vererbungsbibliothek die Anforderungen des Simulationsszenarios vollständig erfüllt. Wenn es in Zukunft Szenarien gibt, die eine Vererbung erfordern, können Sie die letzte Version verwenden Umsetzung, um es zu entwickeln.
4. Zusammenfassung
Unter der Anleitung der Blogreihe über JavaScript-Vererbung auf Sanshengshi implementiert dieser Artikel eine benutzerfreundliche Vererbungsbibliothek. Sie kann zum Aufbau objektorientierter Klassen und Vererbungsbeziehungen zwischen Klassen verwendet werden, die eher der Java-Sprache ähneln Ich kann mir vorstellen, dass diese Bibliothek in meiner zukünftigen Arbeit eine sehr wichtige Rolle bei der Implementierung meines Codes und meiner Funktion spielen wird, da in der Entwicklung immer noch häufig geerbte Codierungsideen verwendet werden, insbesondere wenn wir viele Projekte durchführen Einerseits möchte ich auf jeden Fall einige öffentliche Dinge in wiederverwendbare Komponenten schreiben. Andererseits muss ich die individuellen Anforderungen jedes Projekts erfüllen, daher kann ich beim Schreiben von Komponenten nicht zu starr schreiben, mehr Schnittstellen schreiben und warten, bis die Erweitern Sie die einzigartigen Funktionen des Projekts durch Vererbung und andere Methoden, sodass die geschriebenen Komponenten flexibler und stabiler werden. Kurz gesagt, ich habe das Gefühl, dass der Code, den ich in Zukunft schreibe, mit dieser Vererbungsbibliothek viel zufriedener sein wird ~ Daher hoffe ich, dass der Inhalt dieses Artikels auch für Sie von gleicher Hilfe sein kann. Wenn es wirklich hilft, gebt mir bitte ein paar Empfehlungen :)
Der Herausgeber wird Ihnen so viel über die Implementierung der JavaScript-Vererbung vorstellen. Ich hoffe, es wird Ihnen hilfreich sein!