序文
JavaScript という大きな世界でオブジェクト指向について議論する場合、次の 2 つの点に言及する必要があります。 1. JavaScript はプロトタイプに基づくオブジェクト指向言語です。 2. クラス言語をシミュレートするオブジェクト指向のアプローチ。なぜクラス言語のオブジェクト指向の性質をシミュレートする必要があるのかというと、個人的には、場合によってはプロトタイプ パターンが一定の利便性を提供できると考えていますが、複雑なアプリケーションでは、プロトタイプ ベースのオブジェクト指向システムは、次の点で不十分です。抽象化と継承。 JavaScript は主要なブラウザでサポートされている唯一のスクリプト言語であるため、あらゆる分野の専門家が言語の利便性を向上させるためにさまざまな方法を使用する必要があり、最適化の結果、作成するコードはますますオブジェクト指向に近くなります。クラス言語のメソッドであるため、JavaScript プロトタイプ システムの本質も隠蔽されます。
プロトタイプベースのオブジェクト指向言語
プロトタイプ パターンは、クラス パターンと同様、プログラミングのジェネリック、つまりプログラミング方法論です。また、最近人気の関数型プログラミングもプログラミングジェネリックスの一種です。 JavaScript の父であるブレンダン・アイヒは、JavaScript を設計したとき、最初からクラスの概念を追加するつもりはなく、他の 2 つのプロトタイプベースの言語、Self と Smalltalk を利用しました。
どちらもオブジェクト指向言語であるため、オブジェクトを作成するメソッドが必要です。クラス言語では、まず、現実世界の抽象化としてオブジェクトが定義され、プロトタイプ言語では、オブジェクトが別のオブジェクトの複製によって作成されます。クローン化された親はプロトタイプ オブジェクトと呼ばれます。
クローン作成の鍵は、言語自体がネイティブのクローン作成方法を提供するかどうかです。 ECMAScript5 では、Object.create を使用してオブジェクトのクローンを作成できます。
var person = { name: "tree", age: 25, say: function(){ console.log("I'm tree.") } }; var cloneTree = Object.create(person); console.log(cloneTree);
プロトタイプ パターンの目的は、正確なオブジェクトを取得することではなく、オブジェクトを作成する便利な方法を提供することです (「JavaScript の設計パターンと開発実践」より)。ただし、言語設計の問題により、JavaScript のプロトタイプには多くの矛盾があり、その複雑な構文の一部はクラスベース言語の構文に似ています (「JavaScript 言語の本質」より)。例:
function Person(name, age){ this.name = name; this.age = age; } var p = new Person('tree', 25)
実際には、関数オブジェクトが作成されると、Function コンストラクターによって生成された関数オブジェクトは次のようなコードを実行します。
this.prototype = {constructor: this}
新しい関数オブジェクトにはプロトタイプ属性が割り当てられます。その値はコンストラクター属性を含むオブジェクトであり、属性値は新しい関数です。関数で new 演算子を使用すると、関数のプロトタイプ プロパティの値が、新しいオブジェクトを複製するためのプロトタイプ オブジェクトとして使用されます。新しい演算子がメソッドの場合、その実行プロセスは次のとおりです:
Function.prorotype.new = function() { //以prototype属性值作为原型对象来克隆出一个新对象 var that = Object.create(this.prorotype); //改变函数中this关键指向这个新克隆的对象 var other = this.apply(that, arguments); //如果返回值不是一个对象,则返回这个新克隆对象 return (other && typeof other === 'object') ? other : that; }
上記からわかるように、new 演算子を使用して関数を呼び出すことは、テンプレートのインスタンス化を使用してオブジェクトを作成するように見えますが、本質はプロトタイプ オブジェクトを使用して新しいオブジェクトのクローンを作成することです。
新しく複製されたオブジェクトは、新しい演算子の特性と合わせて、プロトタイプ オブジェクトのすべてのメソッドとプロパティにアクセスできるため、これがプロトタイプを使用してクラス言語をシミュレートする基礎となります。
プロトタイプを使用してクラス言語をシミュレートします
要約
プロトタイプ パターンを使用して、最初は抽象的な方法でクラスをシミュレートします。 JavaScript言語の特性上、通常はクラス(実際には擬似クラス)がコンストラクタ(実際にはnew演算子によって呼び出される関数、JavaScript自体にはコンストラクタという概念はありません)内にフィールドを配置し、メソッドを配置することが多いです。関数のプロトタイプ属性内。
function Person(name, age) { this.name = name; this.age = age; }; Person.prototype.say = function(){ console.log("Hello, I'm " + this.name); };
継承
継承は、OO 言語で最も話題になっている概念の 1 つです。多くの OO 言語は、インターフェイスの継承と実装の継承という 2 種類の継承をサポートしています。インターフェイスの継承はメソッドのシグネチャを継承しますが、実装の継承は実際のメソッドを継承します。ただし、ECMAScript ではインターフェイスの継承を実装できません。実装の継承のみがサポートされており、その実装の継承は主にプロトタイプ チェーンに依存します。 (『JavaScript 上級プログラミング』セクション 6.3 継承より) 高校 3 年生のとき、著者は結合継承、プロトタイプ継承、寄生継承、寄生組み合わせ継承など、さまざまな継承のシミュレーションを検討し、最終的には寄生組み合わせがすべてになりました。シミュレーション クラス 継承の基礎。
function Person(name, age) { this.name = name; this.age = age; }; Person.prototype.say = function(){ console.log("Hello, I'm " + this.name); }; function Employee(name, age, major) { Person.apply(this, arguments); this.major = major; }; Employee.prototype = Object.create(Person.prototype); Employee.prorotype.constructor = Employee; Employee.prorotype.sayMajor = function(){ console.log(this.major); }
高三中只给出了单继承的解决方案,关于多继承的模拟我们还得自己想办法。由于多继承有其本身的困难:面向对象语言如果支持了多继承的话,都会遇到著名的菱形问题(Diamond Problem)。假设存在一个如左图所示的继承关系,O中有一个方法foo,被A类和B类覆写,但是没有被C类覆写。那么C在调用foo方法的时候,究竟是调用A中的foo,还是调用B中的foo?
所以大多数语言并不支持多继承,如Java支持单继承+接口的形式。JavaScript并不支持接口,要在一个不支持接口的语言上去模拟接口怎么办?答案是著名的鸭式辨型。放到实际代码中就是混入(mixin)。原理很简单:
function mixin(t, s) { for (var p in s) { t[p] = s[p]; } }
值得一提的是dojo利用MRO(方法解析顺序(Method Resolution Order),即查找被调用的方法所在类时的搜索顺序)方式解决了多继承的问题。
到此,我们已经清楚了模拟类语言的基本原理。作为一个爱折腾的程序员,我希望拥有自己的方式来简化类的创建:
最终,在借鉴各位大牛的知识总结,我编写了自己的类创建工具O.js:
(function(global) { var define = global.define; if (define && define.amd) { define([], function(){ return O; }); } else { global.O = O; } function O(){}; O.derive = function(sub) { debugger; var parent = this; sub = sub ? sub : {}; var o = create(parent); var ctor = sub.constructor || function(){};//如何调用父类的构造函数? var statics = sub.statics || {}; var ms = sub.mixins || []; var attrs = sub.attributes || {}; delete sub.constructor; delete sub.mixins; delete sub.statics; delete sub.attributes; //处理继承关系 ctor.prototype = o; ctor.prototype.constructor = ctor; ctor.superClass = parent; //利用DefineProperties方法处理Attributes //for (var p in attrs) { Object.defineProperties(ctor.prototype, attrs); //} //静态属性 mixin(ctor, statics); //混入其他属性和方法,注意这里的属性是所有实例对象都能够访问并且修改的 mixin(ctor.prototype, sub); //以mixin的方式模拟多继承 for (var i = 0, len = ms.length; i < len; i++) { mixin(ctor.prototype, ms[i] || {}); } ctor.derive = parent.derive; //_super函数 ctor.prototype._super = function(f) { debugger; return parent.prototype[f].apply(this, Array.prototype.slice.call(arguments, 1)); } return ctor; } function create(clazz) { var F = function(){}; F.prototype = clazz.prototype; //F.prototype.constructor = F; //不需要 return new F(); }; function mixin(t, s) { for (var p in s) { t[p] = s[p]; } } })(window);
类创建方式如下:
var Person = O.derive({ constructor: function(name) {//构造函数 this.setInfo(name); }, statics: {//静态变量 declaredClass: "Person" }, attributes: {//模拟C#中的属性 Name: { set: function(n) { this.name = n; console.log(this.name); }, get: function() { return this.name + "Attribute"; } } }, share: "asdsaf",//变量位于原型对象上,对所有对象共享 setInfo: function(name) {//方法 this.name = name; } }); var p = new Person('lzz'); console.log(p.Name);//lzzAttribute console.log(Person);
继承:
var Employee = Person.derive({//子类有父类派生 constructor: function(name, age) { this.setInfo(name, age); }, statics: { declaredClass: "Employee" }, setInfo: function(name, age) { this._super('setInfo', name);//调用父类同名方法 this.age = age; } }); var e = new Employee('lll', 25); console.log(e.Name);//lllAttribute console.log(Employee);
以上就是本文的全部内容,希望对大家的学习有所帮助。