この記事では、次の 4 つの側面からこのトピックについて説明します。
•1. 混合アプローチの実装と問題点
•2. 予想される呼び出しメソッド
•3. 継承ライブラリの詳細な実装
•4. まとめ
興味のあるお友達は、詳細を読み続けてください。
私がjsで継承を実装するために最初に習得した方法は、xxで学んだプロトタイプチェーンとオブジェクト偽装を組み合わせる方法で、仕事で継承を使用するときは常にこの方法を使用して実装します。その実装はシンプルで、アイデアは明確です。オブジェクトを使用して親クラスのコンストラクターのプロパティを継承するふりをし、プロトタイプ チェーンを使用して親クラスのプロトタイプ オブジェクトのメソッドを継承します。これは、私が遭遇したすべての継承シナリオを満たします。このため、今夜、Sanshengshi で JavaScript の継承に関する一連の記事を読むまで、次回継承について書くときは、別の方法で書かなければならないとは思いませんでした (非常に早い段階で公開され、今読みました)。残念ながら、js では継承メカニズムも Java などのバックエンド言語の実装に非常に近い形で記述できることがわかりました。これは実に素晴らしいことです。なので、彼のブログの考え方をしっかり理解して、将来的に使える継承ライブラリを実装したいと思います。
1. 混合アプローチの実装と問題点
問題を理解する前に、その具体的な実装を見てみましょう:
- 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
この継承実装は結果的には問題なく、Manager のインスタンスは Employee クラスのインスタンス属性とインスタンスメソッドを同時に継承しており、instanceOf オペレーションの結果も正しいです。ただし、コードの構成と実装の詳細に関しては、この方法にはまだ次の問題があります:
1) コード構成は十分に洗練されていません。継承実装の主要部分のロジックは普遍的であり、次の構造になっています。
- Hide code //将父类的一个实例设置为子类的原型,实现方法继承 SubClass.prototype = new SuperClass(); //修改子类原型的constructor指向,避免子类实例的constructor属性指向父类的构造函数 SubClass.prototype.constructor = SubClass; //给子类添加新的实例方法 SubClass.prototype.method1 = function() { } SubClass.prototype.method2 = function() { } SubClass.prototype.method3 = function() { }
解決策: モジュール化を使用して共通ロジックをカプセル化し、合意されたインターフェースに従って呼び出す限り、クラスの構築と継承を簡素化できます。具体的な実装については、当面は理論的な説明のみを提供しますので、以下のコンテンツの紹介を参照してください。
2) サブクラスのプロトタイプを親クラスのインスタンスに設定する場合、 new SuperClass() が呼び出されます。これは親クラスのコンストラクターへのパラメーターなしの呼び出しであるため、親クラスにはパラメーターなしのコンストラクター関数が必要です。ただし、JavaScript では関数をオーバーロードできないため、実際のビジネスでは、ほとんどのシナリオで、唯一のコンストラクターで関数をシミュレートするために、親クラスのコンストラクターにパラメーターを持たせることはできません。オーバーロードは、arguments.length を判断することによってのみ処理できます。問題は、親クラスのコンストラクターが作成されるたびに、arguments.length 判定ロジックが確実に追加されるようにすることが難しい場合があることです。この場合、このアプローチは危険です。コンストラクター内のロジックが抽出でき、クラスのすべてのコンストラクターがパラメーターなし関数であれば、この問題は簡単に解決されます。
解決策: 親クラスとサブクラスのすべてのコンストラクターをパラメーターなしにし、コンストラクターのロジックを init インスタンス メソッド (Employee や The Manager の例など) に移行します。以下に変換します:
- 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; };
- 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
- 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; };
3) サブクラスを構築するとき、サブクラスのプロトタイプは親クラスのインスタンスに設定されます。これはクラス間ではなく、クラス間で継承が行われます。親クラスのインスタンスがサブクラスのプロトタイプとして使用される理由:
- 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
次の 2 つの呼び出しに注意してください:
上記は、この記事で実装する継承ライブラリの詳細です。実際、この記事の最初の部分で説明した問題の解決策と、2 番目の部分でシミュレートした呼び出しシナリオを組み合わせています。パート 1 でまとめたソリューションを理解していれば、各詳細の原則を簡単に理解できます。デモの最後のバージョンでは、この記事で実装された継承ライブラリがシミュレーション シナリオのニーズを完全に満たしていることもわかります。将来継承が必要なシナリオがある場合は、最後のバージョンを使用できます。それを開発するための実装。
4. 概要
Sanshengshi の JavaScript 継承に関する一連のブログのガイダンスに基づいて、この記事では、Java 言語に似たオブジェクト指向のクラスとクラス間の継承関係を構築するために使用できる、使いやすい継承ライブラリを実装します。私の将来の仕事では、このライブラリがコードの品質と機能の実装において非常に重要な役割を果たすことになると予測できます。開発では、特に多くのプロジェクトを実行する場合、継承されたコーディングのアイデアが依然としてよく使用されるからです。一方で、いくつかのパブリックなものを再利用可能なコンポーネントに書きたいと思っていますが、その一方で、各プロジェクトの個別の要件を満たす必要があるため、コンポーネントを作成するときは、厳密に記述しすぎたり、より多くのインターフェイスを作成したりすることはできません。特定のプロジェクトが完了すると、継承などの方法でプロジェクトの固有の機能が拡張され、作成されたコンポーネントがより柔軟で安定したものになります。つまり、この継承ライブラリを使用すると、今後書くコードがより楽しくなるような気がします〜 ということで、この記事の内容も同じようにお役に立てれば幸いです。本当に役立つ場合は、いくつかの推奨事項を教えてください:)
JavaScript の継承の実装についてエディターがここまで紹介しますので、お役に立てれば幸いです。