深入理解javascript原型繼承

作為一門被長期誤解的編程語言,javascript一直被人所詬病.但是如果你真正的了解它之后,你會深深的愛上它.

首先,javascript是一個面向對象的編程語言,而且是一個純粹的面向對象.雖然很多人不能理解,因為在他們眼中,只有像java,c++這樣的編程語言才能稱之為面向對象.但是,我只想說,你誤解我,是因為你不懂我.

JavaScrip秘密花園在JavaScrip中,一切變量皆對象,除了兩個特殊值undefined 和 null.

什么是面向對象?

  • 一切事物皆對象
  • 對象具有封裝和繼承特性
  • 對象與對象之間使用消息通信,各自存在信息隱藏

以這三點做為依據,C++ 是半面向對象半面向過程語言,因為,雖然他實現了類的封裝、繼承和多態,但存在非對象性質的全局函數和變量。Java、C# 是完全的面向對象語言,它們通過類的形式組織函數和變量,使之不能脫離對象存在。但這里函數本身是一個過程,只是依附在某個類上。

然而,面向對象僅僅是一個概念或者編程思想而已,它不應該依賴于某個語言存在。比如 Java 采用面向對象思想構造其語言,它實現了類、繼承、派生、多態、接口等機制。但是這些機制,只是實現面向對象編程的一種手段,而非必須。換言之,一門語言可以根據其自身特性選擇合適的方式來實現面向對象。所以,由于大多數程序員首先學習或者使用的是類似 Java、C++ 等高級編譯型語言(Java 雖然是半編譯半解釋,但一般做為編譯型來講解),因而先入為主地接受了“類”這個面向對象實現方式,從而在學習腳本語言的時候,習慣性地用類式面向對象語言中的概念來判斷該語言是否是面向對象語言,或者是否具備面向對象特性。這也是阻礙程序員深入學習并掌握 JavaScript 的重要原因之一。

實際上,JavaScript語言是通過一種叫做原型(prototype)的方式來實現面向對象編程的。它和其他的面向對象類編程語言一樣,只是它的實現方式不同而已,或者說他們采用了不同的面向對象設計哲學。

在基于類的面向對象方式中,對象(object)依靠類(class)來產生。而在基于原型的面向對象方式中,對象(object)則是依靠 構造器(constructor)利用 原型(prototype)構造出來的。

舉個客觀世界的例子來說明二種方式認知的差異。例如工廠造一輛車,一方面,工人必須參照一張工程圖紙,設計規定這輛車應該如何制造。這里的工程圖紙就好比是語言中的 類 (class),而車就是按照這個 類(class)制造出來的;另一方面,工人和機器 ( 相當于 constructor)利用各種零部件如發動機,輪胎,方向盤 ( 相當于 prototype 的各個屬性 ) 將汽車構造出來。

在這里我不想討論太多這兩種面向對象究竟孰優孰劣的問題,這個討論來討論去也不會有定論.但是以為個人觀點我更傾向于基于原型的面向對象.因為一切事物皆對象.現實世界中的所有對象也都是由其他對象構造出來的,而緊緊依靠圖紙是沒法產生一個現實中的汽車的.這個設計更符合現實世界的客觀規律.

JavaScript的面向對象來源于‘self’這個牛逼但短命的編程語言。

Self語言把概念上的精簡作為設計原則。它取消了類的概念,只有對象的概念,同時把消息作為最基本的操作。把對象的屬性理解為獲取或更改屬性這兩種方法,從而把屬性的概念簡化為方法;取消了變量和賦值,并以通過消息來讀槽和寫槽的方式代之。

在 JavaScript 中,prototype 是函數的一個屬性,同時也是由構造函數創建的對象的一個屬性。 函數的原型為對象。 它主要在函數用作構造函數時使用。

在編程語言中,目前存在兩種繼承方式:類繼承和原型繼承.

  • 類繼承:

  • 原型繼承

而由于java等主流編程語言的支持使得類繼承被大眾所普遍接受

那么,JavaScript中是如何實現的基于原型的面向對象?

要理解原型繼承,首先得先熟悉幾個概念,咱們一步步說起:

如何生成對象?

-1.聲明對象直接量:JSON

    var obj = {
        name: "jack",
        eat: "bread"
    }
    console.log(typeof obj);

-2.使用構造函數生成一個新的對象

//構造函數
var Foo = function(name){
    this.name = name;  //私有屬性    
}

//原型方法和屬性,被繼承時候才會調用
Foo.prototype.run = function(){
    alert("I'm running so fast that can't stop at all!");
}

var kick = new Foo("kick");
console.log(typeof kick);
console.log(kick.name);
kick.run();

-3.使用使用Object.create創建對象
ECMAScript 5中引入了一個新方法: Object.create. 可以調用這個方法來創建一個新對象. 新對象的原型就是調用create方法時傳入的第一個參數:

先來看一下create方法是如何實現的,該方法來源于Douglas Crockford,現在已被ECMAScript 5引入:

Object.create = function (parent) {
    function F() {}
    F.prototype = parent;
    return new F();
};

這個看起來很簡潔,而且能夠完全代替new的用法,畢竟new關鍵字并不真正的屬于JavaScrip的原型模式.它先是聲明了一個構造器,然后將其原型設置為你想要的值,最后返回生成的新對象.其實就是封裝了new.

下面這段代碼就是真正的原型繼承了.look:

var Point = {
    x: 0,
    y: 0,
    print: function () { console.log(this.x, this.y); }
};
var p = Object.create(Point);  //new一個對象
p.x = 10;
p.y = 20;
p.print(); // 10 20

code:

function Plant(name,year){
    this.name = name;
    this.year = year || 0;
}
var tree.prototype = new Plant('tree');
tree.prototype.grow = function(){
    this.year ++;
}
tree.prototype.old = functiono(){
    console.log(this.year);
}

上面這段代碼使用原型實現了一個簡單的對象繼承.下面來分析下上面這段代碼
首先是聲明了一個構造函數,構造函數和普通函數有什么區別?構造函數可以使用new調用,生成一個新的對象.
如果想要在對象上添加方法,可以將方法寫在對象的原型上.
子類繼承父類,只需要把父對象復制給自對象的原型上即可.

JavaScrip原型鏈(prototype chain)

下面這段是ECMAScript關于原型的解釋

ECMAScript does not contain proper classes such as those in C++, Smalltalk, or Java, but rather, supports constructors which create objects by executing code that allocates storage for the objects and initialises all or part of them by assigning initial values to their properties. All constructors are objects, but not all objects are constructors. Each constructor has a Prototype property that is used to implement prototype-based inheritance and shared properties. Objects are created by using constructors in new expressions; for example, new String("A String") creates a new String object. Invoking a constructor without using new has consequences that depend on the constructor. For example, String("A String") produces a primitive string, not an object.

ECMAScript supports prototype-based inheritance. Every constructor has an associated prototype, and every object created by that constructor has an implicit reference to the prototype (called the object's prototype) associated with its constructor. Furthermore, a prototype may have a non-null implicit reference to its prototype, and so on; this is called the prototype chain. When a reference is made to a property in an object, that reference is to the property of that name in the first object in the prototype chain that contains a property of that name. In other words, first the object mentioned directly is examined for such a property; if that object contains the named property, that is the property to which the reference refers; if that object does not contain the named property, the prototype for that object is examined next; and so on.

依據我的理解就是說:
JavaScrip可以采用構造器(constructor)生成一個新的對象,每個構造器都擁有一個prototype屬性,而每個通過此構造器生成的對象都有一個指向該構造器原型(prototype)的內部私有的鏈接(proto),而這個prototype因為是個對象,它也擁有自己的原型,這么一級一級指導原型為null,這就構成了原型鏈.

這里我們涉及到了一個隱匿屬性proto,那么proto和prototype究竟有什么區別嘞?
注: proto 是一個不應在你代碼中出現的非正規的用法,這里僅僅用它來解釋JavaScript原型繼承的工作原理。

知道了JavaScrip原型鏈的存在之后,讓我們來看下它的實現,下面這段代碼展示了原型鏈是如何工作的.

function getProperty(obj, prop) {
    if (obj.hasOwnProperty(prop)) //首先查找自身屬性,如果有則直接返回
        return obj[prop]
    else if (obj.__proto__ !== null)
        return getProperty(obj.__proto__, prop) //如何不是私有屬性,就在原型鏈上一步步向上查找,直到找到,如果找不到就返回undefind
    else
        return undefined
}

So,如果proto可以使用的話,我們可以通過下面這種方式實現繼承:

    var person = {
        city: "Beijing",
        hate: function(){
            alert("I really hate the PM2.5 and the foggy wether!");
        }
    }
    var lee = {
        name: "lee",
        age: "18",
        __proto__: person
    }
    console.log(lee);
    lee.hate();

這都什么玩意兒,不是要用new嗎.事實上,事情不是這么簡單滴, 為了和主流的類繼承扯上那么一點兒關系,JavaScrip引入了'new'關鍵字,引入了構造函數.所以通常我們看到的是下面這樣的:

    var Person = function(name,age){
        this.name = name;
        this.age  = age;
    };
    Person.prototype = {
        city: "Beijing",
        hate: function(){
            alert("I really hate the PM2.5 and the foggy wether!");
        }
    }
    var lee = new Person('lee',18);
    console.log(lee.name);
    lee.hate();

我們需要一個像類一樣的東西,于是有了構造函數,我們得有一個通過類生成實例的過程,于是又出現了new.這么一來JavaScrip的原型繼承似乎就變得不倫不類了.雖然JavaScrip的原型繼承來源于'self',但是卻追隨了類繼承的形式.罪過,不過話說回來,也許就是因為這種妥協才讓JavaScrip能夠流行起來,并成為了現在最流行的原型繼承語言,而self,說實話,它獨特寫法確實挺難讓人接受的.

    var Foo = function(){
        this.name = "foo";
    }
    Foo.prototype.say = function(){
        alert("Hello World!");
    }
    var foo = new Foo();
    console.log(foo.__proto__); //私有鏈接,指向構造函數的原型
    console.log(Foo.prototype);  
    console.log(foo.__proto__ === Foo.prototype); //true
    console.log(foo.__proto__.constructor === Foo); //true


          
 // 聲明 Animal 對象構造器
 function Animal(name) { 
    this.name = name;
 } 
 // 將 Animal 的 prototype 屬性指向一個對象,
 // 亦可直接理解為指定 Animal 對象的原型
 Animal.prototype = {
    weight: 0, 
    eat: function() { 
        alert( "Animal is eating!" ); 
    } 
 }
 // 聲明 Mammal 對象構造器
 function Mammal() { 
    this.name = "mammal"; 
 } 
 // 指定 Mammal 對象的原型為一個 Animal 對象。
 // 實際上此處便是在創建 Mammal 對象和 Animal 對象之間的原型鏈
 Mammal.prototype = new Animal("animal"); 
 // 聲明 Horse 對象構造器
 function Horse( height, weight ) { 
    this.name = "horse"; 
    this.height = height; 
    this.weight = weight; 
 }
 // 將 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( Horse.prototype.__proto__ === Mammal.prototype ); 
 console.log( Mammal.prototype.__proto__ === Animal.prototype ); 
 //原型鏈
 Horse-->Mammal的實例
 Mammal-->Animal的實例
 Animal -->Object.prototype

在 ECMAScript 中,每個由構造器創建的對象擁有一個指向構造器 prototype 屬性值的 隱式引用(implicit reference),這個引用稱之為 原型(prototype)。進一步,每個原型可以擁有指向自己原型的 隱式引用(即該原型的原型),如此下去,這就是所謂的 原型鏈(prototype chain) 參考資源。在具體的語言實現中,每個對象都有一個 proto 屬性來實現對原型的 隱式引用。

我們已經了解了JS原型繼承是什么,以及JS如何用特定的方式來實現之。然而使用真正的原型繼承(如 Object.create 以及 proto)還是存在以下缺點:

  • 標準性差:proto 不是一個標準用法,甚至是一個不贊成使用的用法。同時原生態的 Object.create 和道爺寫的原版也不盡相同。
  • 優化性差: 不論是原生的還是自定義的 Object.create ,其性能都遠沒有 new 的優化程度高,前者要比后者慢高達10倍。

到了這里我們基本對JavaScrip的原型繼承有了一個更深層的認識了.通過歷史回溯我們也了解了為什么JavaScrip會變成現在這個不倫不類的樣子.
JavaScrip是一個完全的面向對象函數式編程語言,采用原型繼承,雖然寫法類似類繼承.但是我們不能因此就認為它不是面向對象的編程語言.而且nodejs的出現,又讓JavaScrip在編程語言界火了一把.所以是時候擁抱JavaScrip了.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,016評論 2 375

推薦閱讀更多精彩內容