首頁 >web前端 >js教程 >全面理解物件導向的 JavaScript(來自ibm)_javascript技巧

全面理解物件導向的 JavaScript(來自ibm)_javascript技巧

WBOY
WBOY原創
2016-05-16 17:16:22982瀏覽

当今 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) 本身也是其他对象通过原型方式构造出来的对象。

再次,在類別式物件導向語言中,物件的狀態(state) 由物件實例(instance) 所持有,物件的行為方法(method) 則由宣告該物件的類別所持有,並且只有物件的結構和方法能夠被繼承;而在原型式物件導向語言中,物件的行為、狀態都屬於物件本身,並且能夠一起被繼承(參考資源),這也更貼近客觀實際。

最後,類別物件導向語言例如 Java,為了彌補無法使用過程導向語言中全域函數和變數的不便,允許在類別中聲明靜態 (static) 屬性和靜態方法。而實際上,客觀世界不存在所謂靜態概念,因為一切事物皆物件!而在原型式物件導向語言中,除內建物件 (build-in object) 外,不允許全域物件、方法或屬性的存在,也沒有靜態概念。所有語言元素 (primitive) 必須依賴物件存在。但由於函數式語言的特點,語言元素所依賴的物件是隨著執行時間 (runtime) 上下文 (context) 變化而變化的,具體體現在 this 指標的變化。正是這種特徵更貼近 「萬物皆有所屬,宇宙乃萬物生存之根本」的自然觀點。在 程序清單 1中 window 便類似與宇宙的概念。

清單 1. 物件的上下文依賴

複製程式碼 程式碼如下:

<script> <BR> var str = "我是一個String 物件, 我聲明在這裡, 但我不是獨立存在的! > var fun = function() { <BR>    console.log( "我是一個Function 物件!誰叫我,我屬於誰:", this ); <BR> }; <BR> <BR> obj.fun = fun; <P> console.log( this === window );     // 印出true  console.log( window.str === str );  // 列印true <P> console.log( window.obj == // 列印true <BR> console.log( window.obj ==window = obj );  // 列印true <BR> console.log( window.fun === fun );  // 列印true <BR> fun();        Function 物件!誰呼喚我,我屬於誰:window <BR> obj.fun();                          // 印刷 我是一個 Function 物件!誰呼喚我,我屬於誰:obj <BR> fun.apply(str);                   // 印出 我是一個 Function 物件!誰叫我,我屬於誰:str <BR> </script>


在接受了物件導向存在一種稱為基於原型實現的方式的事實之後,下面我們就可以來深入探討 ECMAScript 是如何依據這一方式構造自己的語言的。



最基本的物件導向

ECMAScript 是一門徹底的物件導向的程式語言(參考資源),JavaScript 是其中的一個變種 (variant)。它提供了 6 種基本資料類型,即 Boolean、Number、String、Null、Undefined、Object。為了實現面向對象,ECMAScript設計出了一種非常成功的數據結構- JSON(JavaScript Object Notation), 這一經典結構已經可以脫離語言而成為一種廣泛應用的數據交互格式(參考資源)。

應該說,具有基本資料型別和 JSON 建構語法的 ECMAScript 已經基本上可以實現物件導向的程式設計了。開發者可以隨意地用 

字面式聲明(literal notation方式來建構一個對象,並對其不存在的屬性直接賦值,或用delete將屬性刪除( 註:JS 中的delete 關鍵字用於刪除物件屬性,經常被誤作為C 中的delete,而後者是用於釋放不再使用的物件),如 程式清單2

清單 2. 字面式 (literal notation) 物件宣告

複製程式碼 程式碼如下:

var person = {
    name: 「張三」,
    age: 26,
    gender: 「男性」,  ( “我在吃” stuff );
    }
 };
 person.height = 176;
 delete person[ “age” ];


在實際開發過程中,大部分初學者或對 JS 應用沒有太高要求的開發者也基本上只用到 ECMAScript 定義的這一部分內容,就能滿足基本的開發需求。然而,這樣的程式碼復用性非常弱,與其他實現了繼承、衍生、多型等等的類別物件導向的強型別語言比較起來顯得有些乾癟,不能滿足複雜的 JS 應用開發。所以 ECMAScript 引進原型來解決物件繼承問題。


使用函數建構器建構物件

除了 

字面式聲明(literal notation方式之外,ECMAScript 允許透過 構造器(constructor)建立物件。每個建構器其實是一個 函數(function) 物件, 此函數物件含有一個「prototype」屬性用於實作 基於原型的繼承prototype-based inheritance和 共享屬性(shared properties物件可以由「new 關鍵字 建構器呼叫」的方式來創建,如 程式清單 3 清單 3. 使用構造器 (constructor) 建立物件

複製程式碼 程式碼如下:// 建構器Person 本身就是函數物件

// 建構器Person 本身就是一個函數物件
 function Person() {
  // 這裡可以做一些初始化工作
 }
 // 它有一個名叫prototype 的屬性
 Person.prototype = {
  三」,
    age: 26,
    gender: 「男」,
    eat: function( stuff ) {
   }  
 // 使用new 關鍵字建構物件
 var p = new Person();


由於早期JavaScript 的發明者為了使這門語言與大名鼎鼎的Java 拉上關係( 雖然現在大家知道二者是雷鋒和雷鋒塔的關係),使用了new 關鍵字來限定構造器調用並創建對象,以使其在語法上跟Java 創建物件的方式看起來類似。但要指出的是,這兩種語言的new意義毫無關係,因為其物件構造的機製完全不同。也正是因為這裡語法上的類似,眾多習慣了類式物件導向語言中物件創建方式的程式設計師,難以透徹理解JS 物件原型構造的方式,因為他們總是不明白在JS 語言中,為什麼「函數名可以作為類名」的現象。而實質上,JS 這裡只是藉用了關鍵字 new,僅此而已;換句話說,ECMAScript 完全可以用其它 非 new 表達式來用呼叫構造器建立物件。

徹底理解原型鏈 (prototype chain) 在ECMAScript 中,每個由構造器創建的物件擁有一個指向構造器prototype 屬性值的 

隱式引用(

implicit reference,這篇引用稱為 原型(prototype。進一步,每個原型可以擁有指向自己原型的 隱式引用(即該原型的原型),如此下去,這就是所謂的原型鏈(prototype chain (參考資源)。在特定的語言實作中,每個物件都有一個 __proto__ 屬性來實作原型的 隱式引用程序清單 4說明了這一點。 清單 4. 物件的 __proto__ 屬性與隱式參考

複製程式碼 程式碼如下:

 function Person( name ) {
    this.name = name;
 }
 var p = new Person();
 // 物件的隱式引用指向了建構器的prototype 屬性,所以此處列印true
 console.log( p.__proto__ === Person.prototype );

 // 原型本身是一個Object 對象,所以他的隱式引用指向了
 // Object 構造器的prototype 屬性, 故而打印true
 console.log( Person.prototype.__proto__ === Object.prototype );

 // 建構器 Person 本身是函數對象,所以此處印出 true
 console.log( Person.__proto__ === Function.prototype );

有了 原型鏈,便可以定義一種所謂的 屬性隱藏機制,並透過這個機制實現繼承。 ECMAScript 規定,當要給某個物件的屬性賦值時,解釋器會尋找該物件原型鏈中第一個含有該屬性的物件(註:原型本身就是一個對象,那麼原型鏈即為一組物件的鏈。反之,如果要取得某個物件屬性的值,解釋器自然是傳回該物件原型鏈中首先具有該屬性的物件屬性值。 圖 1說名了這中隱藏機制:


圖 1.原型鏈中的屬性隱藏機制
图 1. 原型链中的属性隐藏机制 

在圖1 中,object1->prototype1->prototype2 構成了物件object1 的原型鏈,根據上述屬性隱藏機制,可以清楚地看到prototype1 物件中的property4 屬性和prototype2 物件中的property3 屬性皆被隱藏。了解原型鏈,那麼將非常容易理解 JS 中基於原型的繼承實作原理,程式清單 5 是利用原型鏈實現繼承的簡單範例。

清單 5. 利用原型鏈 Horse->Mammal->Animal 實作繼承

複製程式碼 程式碼如下:

// () {
 }
 // 將Animal 的prototype 屬性指向一個對象,
 // 也可直接理解為指定Animal 對象的原型
 Animal.prototype = {
",
    weight: 0,
    eat: function() {
        alert( "Animal is eating!" );  function Mammal() {
    this.name = "mammal";
 }
 // 指定Mammal 物件的原型為Animal 物件。 Mammal 物件與Animal 物件之間的原型鏈
 Mammal.prototype = new Animal();

 // 宣告Horse 物件建構子
 function Horse( height, weight ) {
    this.name = "horse";
    this.height = height;
 }  // 將Horse 物件的原型指定為一個Mamal 對象,繼續建構Horse 與Mammal 之間的原型鏈

 Horse.prototype = new Mammal();

 // 重新指定eat 方法, 此方法將覆寫從Animal 原型繼承過來的eat 方法
 Horse.prototype.eat = function() {
    alert( "Horse is eating grass!" )
 }
 // 驗證並瞭解原型鏈
 var horse = new Horse( 100, 300 );

 console.log( horse.__proto__ === Horse.prototype );  console.log( Mammal.prototype.__proto__ === Animal.prototype );

理解清單 5 中物件原型繼承邏輯實作的關鍵在於 Horse.prototype = new Mammal() 和 Mammal.prototype = new Animal() 這兩句程式碼。首先,等式右邊的結果是建構出一個臨時對象,然後將這個對象賦值給等式左邊對象的 prototype 屬性。也就是說將右邊新建的物件作為左邊物件的原型。讀者可以將這兩個等式替換到對應的程式清單 5 程式碼最後兩行的等式中自行領悟。

JavaScript 類別繼承的實作方法

從程式碼清單 5 可以看出,基於原型的繼承方式,雖然實現了程式碼復用,但其行文鬆散且不夠流暢,可閱讀性差,不利於實現擴展和對原始程式碼進行有效地組織管理。不得不承認,類別繼承方式在語言實作上更具健壯性,且在建構可重複使用程式碼和組織架構程式方面具有明顯的優勢。這使得程式設計師希望尋找到一種能夠在 JavaScript 中以類別繼承風格進行編碼的方法途徑。從抽象的角度來講,既然類別繼承和原型繼承都是為實現物件導向而設計的,並且他們各自實現的載體語言在計算能力上是等價的( 因為圖靈機的計算能力與Lambda 演算的運算能力是等價的),那麼能不能找到一種變換,使得原型式繼承語言透過該變換實現具有類別繼承編碼的風格呢?

目前一些主流的 JS 框架都提供了這個轉換機制,也也就是類別式宣告方法,例如 Dojo.declare()、Ext.entend() 等等。使用者使用這些框架,可以輕易且友善地組織自己的 JS 程式碼。其實,在眾多框架出現之前,JavaScript 大師 Douglas Crockford 最早利用三個函數對Function 物件進行擴展,實現了這種變換,關於它的實現細節可以(參考資源 )。另外還有由 Dean Edwards實現的著名的 Base.js(參考資源)。值得一提的是,jQuery 之父 John Resig 在搏眾家之長之後,用不到 30 行程式碼便實現了自己的 Simple Inheritance。使用其提供的 extend 方法聲明類別非常簡單。 程式清單 6是使用了 Simple Inheritance函式庫實現類別的聲明的例子。其中最後一句列印輸出語句是對 Simple Inheritance實作類別繼承的最佳說明。

清單 6. 使用 Simple Inheritance 實作類別繼承

複製程式碼 程式碼如下:
// 聲明Person 類別


// 聲明 類型 .extend( {
    _issleeping: true,
    init: function( name ) {
        this._name = name         return this. _issleeping;
    }
 } );
 // 宣告Programmer 類別,並繼承Person
 var Programmer = Person.extend( {         this._super( name );
        //設定自己的狀態
  
 var person = new Person( "張三" );
 var diors = new Programmer( "張江男", false );
 // 印出true
 console.log( person.isSleeping() );
 console.log( person.isSleeping() );
 console.log( person.isSleeping() );
 console.log( person.isSleeping() );
 // 列印false
 console.log( diors.isSleeping() );
 // 此處全為true,故印true
 console.log( person instanceof Person && person 🎜> console.log( person instanceof Person && person 🎜> console.log( person instanceof Person && person 🎜> && diors instanceof Programmer &&
    diors instanceof Person && diors instanceof Class );


プロトタイプ、関数コンストラクター、クロージャー、およびコンテキストベースの this をすでによく理解している場合は、単純な継承がどのように機能するかを理解することはそれほど難しくありません。基本的に、ステートメント var Person = Class.extend(...) では、左側の Person は、実際には extend メソッドを呼び出す Class によって返されるコンストラクター、つまり関数オブジェクトへの参照を取得します。このアイデアに従って、単純な継承がどのようにこれを実行し、プロトタイプ継承からクラス継承への変換を実現するかを引き続き紹介します。図 2 は、Simple Inheritance のソース コードとそれに付随するコメントです。理解を容易にするために、コードを中国語で一行ずつ説明しています。

図 2. 単純な継承ソース コード分析

图 2.Simple Inheritance 源码解析

コードの 2 番目の部分を脇に置いて、最初と 3 番目の部分を全体として一貫して調べます。 extend 関数の基本的な目的は、新しいプロトタイプ プロパティを使用して新しいコンストラクターを構築することです。私たちは John Resig の見事な筆跡と、JS 言語の本質に対する彼の繊細な把握に感嘆せずにはいられません。 John Resig がこのような絶妙な実装方法をどのように思いついたのかについては、興味のある読者はこの記事 (参考資料) を読むことができます。この記事では、Simple Inheritance の初期設計の思考プロセスが詳しく説明されています。

JavaScript プライベート メンバーの実装 ここまでの説明で、オブジェクト指向 JavaScript にまだ懐疑的な人は、JavaScript がオブジェクト指向における情報の隠蔽、つまりプライベートとパブリックを実装していないのではないかという疑いがあるはずです。プライベート メンバーとパブリック メンバーを明示的に宣言する他のクラスベースのオブジェクト指向メソッドとは異なり、JavaScript の情報隠蔽はクロージャーによって実現されます。

リスト 7

: を参照してください。 リスト 7. クロージャーを使用して情報の非表示を実装する

// ユーザー コンストラクターを宣言
関数User( pwd ) {
// プライベート プロパティを定義します
varpassword = pwd;
// プライベート メソッドを定義します
function getPassword() {
// クロージャでパスワードを返します
returnpassword;
}
// オブジェクトの他のパブリック メソッドがこの特権メソッドを通じてプライベート メンバーにアクセスするために使用される特権関数宣言
this.passwordService = function() {
return getPassword( ) ;
}
}
// パブリックメンバー宣言
User.prototype.checkPassword = function( pwd ) {
return this.passwordService() === pwd; ;
// 非表示を確認します
var u = new User( "123456" ) // true を出力します
console.log( u.checkPassword( "123456" ) ); / 未定義の
console.log( u.password );
// true
console.log( typeof u.gePassword === "unknown" );



JavaScript は、関数型言語の特性によって決定される情報の隠蔽を実現するためにクロージャーに依存する必要があります。上記は JavaScript のコンテキストベースの理解を前提としているため、この記事では関数型言語とクロージャのトピックについては説明しません。 JavaScript に隠蔽された情報については、
Douglas Crockford
が記事「JavaScript のプライベート メンバー」 (
参考資料) でより信頼できる詳細な紹介をしています。

結論

JavaScript は、C 言語ファミリーの仮面をかぶっているため、世界で最も誤解されているプログラミング言語であると考えられていますが、クラスを持たず、LISP スタイルの関数型言語の機能を表現していますが、オブジェクトも完全に実装しています。指向性のある。この言語を完全に理解するには、C 言語のコートを脱ぎ、関数型プログラミングの観点に立ち戻り、同時に元のクラスのオブジェクト指向の概念を捨てて学習し、理解する必要があります。近年の Web アプリケーションの人気と JS 言語自体の急速な発展、特にバックエンド JS エンジン (V8 ベースの NodeJS など) の登場により、本来は単なる JS であったものが、ページエフェクトを書くためのおもちゃとして、より広い世界が広がります。この開発傾向により、JS プログラマーに対する要求も高まります。この言語を徹底的に理解することによってのみ、大規模な JS プロジェクトでその力を活用することができます。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn