当今 JavaScript 大行其道,各种应用对其依赖日深。web 程序员已逐渐习惯使用各种优秀的 JavaScript 框架快速开发 Web 应用,从而忽略了对原生 JavaScript 的学习和深入理解。所以,经常出现的情况是,很多做了多年 JS 开发的程序员对闭包、函数式编程、原型总是说不清道不明,即使使用了框架,其代码组织也非常糟糕。这都是对原生 JavaScript 语言特性理解不够的表现。要掌握好 JavaScript,首先一点是必须摒弃一些其他高级语言如 Java、C# 等类式面向对象思维的干扰,全面地从函数式语言的角度理解 JavaScript 原型式面向对象的特点。把握好这一点之后,才有可能进一步使用好这门语言。本文适合群体:使用过 JS 框架但对 JS 语言本质缺乏理解的程序员,具有 Java、C++ 等语言开发经验,准备学习并使用 JavaScript 的程序员,以及一直对 JavaScript 是否面向对象模棱两可,但希望知道真相的 JS 爱好者。
重新认识面向对象
为了说明 JavaScript 是一门彻底的面向对象的语言,首先有必要从面向对象的概念着手 , 探讨一下面向对象中的几个概念:
一切事物皆对象
对象具有封装和继承特性
对象与对象之间使用消息通信,各自存在信息隐藏
以这三点做为依据,C++ 是半面向对象半面向过程语言,因为,虽然他实现了类的封装、继承和多态,但存在非对象性质的全局函数和变量。Java、C# 是完全的面向对象语言,它们通过类的形式组织函数和变量,使之不能脱离对象存在。但这里函数本身是一个过程,只是依附在某个类上。
然而,面向对象仅仅是一个概念或者编程思想而已,它不应该依赖于某个语言存在。比如 Java 采用面向对象思想构造其语言,它实现了类、继承、派生、多态、接口等机制。但是这些机制,只是实现面向对象编程的一种手段,而非必须。换言之,一门语言可以根据其自身特性选择合适的方式来实现面向对象。所以,由于大多数程序员首先学习或者使用的是类似 Java、C++ 等高级编译型语言(Java 虽然是半编译半解释,但一般做为编译型来讲解),因而先入为主地接受了“类”这个面向对象实现方式,从而在学习脚本语言的时候,习惯性地用类式面向对象语言中的概念来判断该语言是否是面向对象语言,或者是否具备面向对象特性。这也是阻碍程序员深入学习并掌握 JavaScript 的重要原因之一。
实际上,JavaScript 语言是通过一种叫做 原型(prototype)的方式来实现面向对象编程的。下面就来讨论 基于类的(class-based)面向对象和 基于原型的 (prototype-based) 面向对象这两种方式在构造客观世界的方式上的差别。
基于类的面向对象和基于原型的面向对象方式比较
在基于类的面向对象方式中,对象(object)依靠 类(class)来产生。而在基于原型的面向对象方式中,对象(object)则是依靠 构造器(constructor)利用 原型(prototype)构造出来的。举个客观世界的例子来说明二种方式认知的差异。例如工厂造一辆车,一方面,工人必须参照一张工程图纸,设计规定这辆车应该如何制造。这里的工程图纸就好比是语言中的 类 (class),而车就是按照这个 类(class)制造出来的;另一方面,工人和机器 ( 相当于 constructor) 利用各种零部件如发动机,轮胎,方向盘 ( 相当于 prototype 的各个属性 ) 将汽车构造出来。
事实上关于这两种方式谁更为彻底地表达了面向对象的思想,目前尚有争论。但笔者认为原型式面向对象是一种更为彻底的面向对象方式,理由如下:
首先,客观世界中的对象的产生都是其它实物对象构造的结果,而抽象的“图纸”是不能产生“汽车”的,也就是说,类是一个抽象概念而并非实体,而对象的产生是一个实体的产生;
其次,按照一切事物皆对象这个最基本的面向对象的法则来看,类 (class) 本身并不是一个对象,然而原型方式中的构造器 (constructor) 和原型 (prototype) 本身也是其他对象通过原型方式构造出来的对象。
もう一度言いますが、クラスベースのオブジェクト指向言語では、オブジェクトの状態はオブジェクト インスタンスによって保持され、オブジェクトの動作メソッドはオブジェクトを宣言するクラスによって保持され、オブジェクトの構造のみが保持されます。プロトタイプのオブジェクト指向言語では、オブジェクトの動作とステータスはオブジェクト自体に属し、一緒に継承できます (参照リソース)。これも客観的な現実に近いものです。
最後に、Java などのクラスベースのオブジェクト指向言語では、手続き型言語でグローバル関数や変数を使用できない不便さを補うために、静的プロパティや静的メソッドを宣言できるようにしています。クラス。実際、すべてがオブジェクトであるため、客観的な世界にはいわゆる静的な概念は存在しません。プロトタイプのオブジェクト指向言語では、組み込みオブジェクト、グローバル オブジェクト、メソッド、またはプロパティを除いて、存在することは許可されず、静的な概念は存在しません。すべての言語要素 (プリミティブ) は、その存在のためにオブジェクトに依存する必要があります。ただし、関数型言語の特性により、言語要素が依存するオブジェクトは実行時コンテキストの変化に応じて変化し、特に this ポインターの変化に反映されます。この性質は、「すべてのものは何かに属しており、宇宙はすべてのものの生存の基盤である」という自然観に近いものです。 リスト 1 の ウィンドウ は宇宙の概念に似ています。
リスト 1. オブジェクトのコンテキスト依存関係
最も基本的なオブジェクト指向
ECMAScript は完全なオブジェクト指向プログラミング言語 (参考リソース) であり、JavaScript はそのバリアントです。これは 6 つの基本データ型、つまり Boolean、Number、String、Null、Unknown、Object を提供します。オブジェクト指向を実現するために、ECMAScript は非常に成功したデータ構造である JSON (JavaScript Object Notation) を設計しました。この古典的な構造は言語から分離でき、広く使用されるデータ対話形式 () になります。参照リソース)。
基本的なデータ型と JSON 構築構文を備えた ECMAScript は、基本的にオブジェクト指向プログラミングを実装できると言うべきです。開発者は、リテラル表記 (リテラル表記) を自由に使用してオブジェクトを構築し、その存在しないプロパティに値を直接割り当てたり、削除属性を使用したりできます。 (注: JS の delete キーワードはオブジェクト属性を削除するために使用され、C で使用されなくなったオブジェクトを解放するために使用される delete とよく間違われます) ( Program List 2 など)。
リスト 2. リテラル表記のオブジェクト宣言
リテラル表記 (リテラル表記) メソッドに加えて、ECMAScript では コンストラクター (コンストラクター) 。各コンストラクターは実際には 関数 (function) オブジェクト であり、 プロトタイプベース プロトタイプベースの継承 を実装するための「プロトタイプ」属性が含まれています。 > と 共有プロパティ 。 オブジェクトは、プログラム リスト 3: などの「新しいキーワード コンストラクター呼び出し」によって作成できます。 リスト 3. コンストラクターを使用してオブジェクトを作成する コードをコピー
コードは次のとおりです:
暗黙的参照 (
プロトタイプ) と呼ばれます。さらに、各プロトタイプは、それ自体のプロトタイプ (つまり、プロトタイプのプロトタイプ) を指す 暗黙の参照 を持つことができます。これが続くと、これがいわゆる プロトタイプ チェーン (プロトタイプ チェーン) (参考リソース)。特定の言語実装では、各オブジェクトには、プロトタイプへの 暗黙的参照 を実装するための __proto__ 属性 があります。 リスト 4 はこの点を示しています。 リスト 4. オブジェクトの __proto__ 属性と暗黙的参照 コードをコピー
コードは次のとおりです: console.log( Person.prototype.__proto__ == = オブジェクト.プロトタイプ );
// コンストラクター person 自体は関数オブジェクトであるため、ここに true が出力されます
console.log( Person.__proto__ === Function.prototype );
プロパティ隠蔽メカニズムを定義し、このメカニズムを通じて継承を実現できます。 ECMAScript では、オブジェクトの属性に値を割り当てるときに、インタプリタがオブジェクトのプロトタイプ チェーン内でその属性を含む最初のオブジェクトを検索することが規定されています (注: プロトタイプ自体がオブジェクトであり、プロトタイプ チェーンはセットのチェーンになります)オブジェクトのプロトタイプ チェーン内の最初のオブジェクトは、割り当ての対象となるオブジェクトそのものです。逆に、オブジェクト プロパティの値を取得したい場合、インタプリタは、オブジェクトのプロトタイプ チェーン内で最初にそのプロパティを持つオブジェクト プロパティ値を自然に返します。 図 1 は、隠れたメカニズムを説明しています:
図 1. プロトタイプ チェーンのプロパティ隠蔽メカニズム
図 1 では、object1->prototype1->prototype2 がオブジェクト object1 のプロトタイプ チェーンを構成しています。上記の属性隠蔽メカニズムによれば、prototype1 オブジェクトの property4 属性と、prototype1 オブジェクトの property3 属性が明確にわかります。 prototype2 オブジェクトは両方とも非表示になります。プロトタイプ チェーンを理解すると、JS でのプロトタイプベースの継承の実装原理を理解するのが非常に簡単になります。プログラム リスト 5
リスト 5. プロトタイプ チェーン Horse->Mammal->Animal を使用して継承を実装する
// Horse オブジェクトのプロトタイプを Mammal オブジェクトとして指定し、Horse と Mammal の間のプロトタイプ チェーンの構築を続行します
Horse.prototype = new Mammal();
// Eat メソッドを再指定します。このメソッドは、Animal プロトタイプから継承された Eat メソッドをオーバーライドします。
Horse.prototype.eat = function() {
alter( "Horse is eatgrass!" ) ;
}
// プロトタイプ チェーンを確認して理解する
var horse = new Horse( 100, 300 ); console.log( horse.__proto__ === Horse.prototype ); > console.log( Horse.prototype.__proto__ === Mammal.prototype );
リスト 5 のオブジェクト プロトタイプ継承ロジックの実装を理解する鍵は、Horse.prototype = new Mammal() と Mammal.prototype = new Animal() の 2 行のコードにあります。まず、方程式の右側の結果として一時オブジェクトが構築され、次にこのオブジェクトが方程式の左側のオブジェクトのプロトタイプ プロパティに割り当てられます。つまり、右側の新しく作成されたオブジェクトは、左側のオブジェクトのプロトタイプとして使用されます。読者は、これら 2 つの式をリスト 5 のコードの最後の 2 行にある対応する式に置き換えて理解することができます。
コードリスト 5 からわかるように、プロトタイプベースの継承方法はコードの再利用を実現しますが、その記述は緩くて流暢ではなく、可読性も低いため、拡張や効果的な組織と管理には役立ちません。ソースコード。クラス継承は言語実装においてより堅牢であり、再利用可能なコードの構築やプログラムの編成において明らかな利点があることを認めなければなりません。このため、プログラマは JavaScript でクラスベースの継承スタイルでコーディングする方法を模索するようになりました。抽象的な観点から見ると、クラス継承とプロトタイプ継承はどちらもオブジェクト指向を実装するように設計されており、それらによって実装されるキャリア言語は計算能力の点で同等であるため(チューリングマシンとラムダ計算の計算能力は同等であるため)累乗は等価です)、この変換を通じてプロトタイプ継承言語がクラスベースの継承コーディング スタイルを実装できるようにする変換を見つけることができるでしょうか?
現在、一部の主流の JS フレームワークは、この変換メカニズム、つまり、Dojo.declare()、Ext.entend() などのクラス宣言メソッドを提供しています。これらのフレームワークを使用すると、ユーザーは JS コードを簡単かつフレンドリーに整理できます。実際、多くのフレームワークが登場する前に、JavaScript マスター Douglas Crockford が最初に 3 つの関数を使用して Function オブジェクトを拡張し、この変換を実現しました。リソース )。 Dean Edwards によって実装された有名な Base.js もあります (参考リソース)。 jQuery の父である John Resig が、他の人の強みを活用した後、30 行未満のコードで独自の 単純な継承 を実装したことは言及する価値があります。クラスが提供する extend メソッドを使用してクラスを宣言するのは非常に簡単です。 プログラム リスト 6 は、Simple Inheritance ライブラリ実装クラス宣言の使用例です。最後の print 出力ステートメントは、クラス継承を実装するための 単純な継承 を最もよく説明しています。 リスト 6. 単純継承を使用したクラス継承の実装
プロトタイプ、関数コンストラクター、クロージャー、およびコンテキストベースの this をすでによく理解している場合は、単純な継承がどのように機能するかを理解することはそれほど難しくありません。基本的に、ステートメント var Person = Class.extend(...) では、左側の Person は、実際には extend メソッドを呼び出す Class によって返されるコンストラクター、つまり関数オブジェクトへの参照を取得します。このアイデアに従って、単純な継承がどのようにこれを実行し、プロトタイプ継承からクラス継承への変換を実現するかを引き続き紹介します。図 2 は、Simple Inheritance のソース コードとそれに付随するコメントです。理解を容易にするために、コードを中国語で一行ずつ説明しています。
図 2. 単純な継承ソース コード分析
コードの 2 番目の部分を脇に置いて、最初と 3 番目の部分を全体として一貫して調べます。 extend 関数の基本的な目的は、新しいプロトタイプ プロパティを使用して新しいコンストラクターを構築することです。私たちは John Resig の見事な筆跡と、JS 言語の本質に対する彼の繊細な把握に感嘆せずにはいられません。 John Resig がこのような絶妙な実装方法をどのように思いついたのかについては、興味のある読者はこの記事 (参考資料) を読むことができます。この記事では、Simple Inheritance の初期設計の思考プロセスが詳しく説明されています。
JavaScript プライベート メンバーの実装 ここまでの説明で、オブジェクト指向 JavaScript にまだ懐疑的な人は、JavaScript がオブジェクト指向における情報の隠蔽、つまりプライベートとパブリックを実装していないのではないかという疑いがあるはずです。プライベート メンバーとパブリック メンバーを明示的に宣言する他のクラスベースのオブジェクト指向メソッドとは異なり、JavaScript の情報隠蔽はクロージャーによって実現されます。
リスト 7: を参照してください。 リスト 7. クロージャーを使用して情報の非表示を実装する