深入淺出JavaScript之原型鏈和繼承

牧云云發表於2016-10-12

Javascript語言的繼承機制,它沒有”子類”和”父類”的概念,也沒有”類”(class)和”例項”(instance)的區分,全靠一種很奇特的”原型鏈”(prototype chain)模式,來實現繼承。

這部分知識也是JavaScript裡的核心重點之一,同時也是一個難點。我把學習筆記整理了一下,方便大家學習,同時自己也加深印象。這部分程式碼的細節很多,需要反覆推敲。那我們就開始吧。

系列目錄

小試身手

原型鏈例子(要點寫在註釋裡,可以把程式碼複製到瀏覽器裡測試,下同)

function foo(){}              //通過function foo(){}定義一個函式物件
foo.prototype.z = 3;          //函式預設帶個prototype物件屬性   (typeof foo.prototype;//"object")

var obj =new foo();           //我們通過new foo()構造器的方式構造了一個新的物件
obj.y = 2;                    //通過賦值新增兩個屬性給obj
obj.x = 1;                    //通過這種方式構造物件,物件的原型會指向建構函式的prototype屬性,也就是foo.prototype

obj.x; // 1                 //當訪問obj.x時,發現obj上有x屬性,所以返回1
obj.y; // 2                 //當訪問obj.y時,發現obj上有y屬性,所以返回2
obj.z; // 3                 //當訪問obj.z時,發現obj上沒有z屬性,那怎麼辦呢?它不會停止查詢,它會查詢它的原型,也就是foo.prototype,這時找到z了,所以返回3

//我們用字面量建立的物件或者函式的預設prototype物件,實際上它也是有原型的,它的原型指向Object.prototype,然後Object.prototype也是有原型的,它的原型指向null。
                                   //那這裡的Object.prototype有什麼作用呢?
typeof obj.toString; // ‘function'  

//我們發現typeof obj.toString是一個函式,但是不管在物件上還是物件的原型上都沒有toString方法,因為在它原型鏈的末端null之前都有個Object.prototype方法,
//而toString正是Object.prototype上面的方法。這也解釋了為什麼JS基本上所有物件都有toString方法
'z' in obj; // true               //obj.z是從foo.prototype繼承而來的,所以'z' in obj返回了true
obj.hasOwnProperty('z'); // false   //但是obj.hasOwnProperty('z')返回了false,表示z不是obj直接物件上的,而是物件的原型鏈上面的屬性。(hsaOwnProperty也是Object.prototype上的方法)

剛才我們訪問x,y和z,分別通過原型鏈去查詢,我們可以知道:當我們訪問物件的某屬性時,而該物件上沒有相應屬性時,那麼它會通過原型鏈向上查詢,一直找到null還沒有話,就會返回undefined。

基於原型的繼承

function Foo(){
   this.y = 2;     
}

Foo.prototype.x = 1;
var obj3 = new Foo();  //①當使用new去呼叫的時候,函式會作為構造器去呼叫②this會指向一個物件(這裡是obj3),而這個物件的原型會指向構造器的prototype屬性(這裡是Foo.prototype)
obj3.y; //2 
obj3.x; //1    //可以看到y是物件上的,x是原型鏈上的原型(也就是Foo.prototype上)

prototype屬性與原型

我們再來看看Foo.prototype是什麼樣的結構,當我們用函式宣告去建立一個空函式的時候,那麼這個函式就有個prototype屬性,並且它預設有兩個屬性,constructor和__proto__,

constructor屬性會指向它本身Foo,__proto__是在chrome中暴露的(不是一個標準屬性,知道就行),那麼Foo.prototype的原型會指向Object.prototype。因此Object.prototype上

的一些方法toString,valueOf才會被每個一般的物件所使用。

function Foo(){}
typeof Foo.prototype; // "object"
Foo.prototype.x = 1;
var obj3 = new Foo();

總結一下:我們這裡有個Foo函式,這個函式有個prototype的物件屬性,它的作用就是當使用new Foo()去構造例項的時候,這個構造器的prototype屬性會用作new出來的這些物件的原型。

所以我們要搞清楚,prototype和原型是兩回事,prototype是函式物件上的預設屬性,原型通常是構造器上的prototype屬性。

實現一個class繼承另外一個class

function Person(name, age) {
   this.name = name;    //直接呼叫的話,this指向全域性物件(this知識點整理)
   this.age = age;      //使用new呼叫Peoson的話,this會指向原型為Person.prototype的空物件,通過this.name給空物件賦值,最後this作為return值
}

Person.prototype.hi = function() {   //通過Person.prototype.hi建立所有Person例項共享的方法,(可以參考上節的左圖:物件的原型會指向構造器的prototype屬性,所以想讓obj1,obj2,obj3共享一些方法的話,只需在原型物件上一次性地新增屬性和方法就可以了);
   console.log('Hi, my name is ' + this.name + ',I am ' + this.age + ' years old now.')//這裡的this是全域性物件
};

Person.prototype.LEGS_NUM = 2;   //再設定一些對Person類的所有例項共享的資料
Person.prototype.ARMS_NUM = 2;
Person.prototype.walk = function() {
  console.log(this.name + ' is walking...');
};

function Student(name, age, className) {  //每個學生都屬於人
  Person.call(this, name, age);  //在Student這個子類裡面先呼叫一下父類
  this.className = className;
}

//下一步就是我們怎麼去把Student的例項繼承Person.prototype的一些方法

Student.prototype = Object.create(Person.prototype);    //Object.create():建立一個空物件,並且這個物件的原型指向它的引數  //這樣子我們可以在訪問Student.prototype的時候可以向上查詢到Person.prototype,又可以在不影響Person的情況下,建立自己的方法
Student.prototype.constructor = Student;  //保持一致性,不設定的話constructor會指向Person

Student.prototype.hi = function() {    //通過Student.prototype.hi這樣子的賦值可以覆蓋我們基類Person.prototype.hi
  console.log('Hi, my name is ' + this.name + ',I am ' + this.age + ' years old now, and from ' + this.className + '.');
}
Student.prototype.learn = function(subject) {    //同時,我們又有自己的learn方法
  console.log(this.name + 'is learning ' + subject + ' at' + this.className + '.');
};

//test
var yun = new Student('Yunyun', 22, 'Class 3,Grade 2');
yun.hi(); //Hi,my name is Yunyun,I'm 22 years old now,and from Class 3, Grade 2.
console.log(yun.ARMS_NUM); // 2     //我們本身物件是沒有的,物件的原型也就是Student.prototype也沒有,但是我們用了繼承,繼續向上查詢,找到了Person.prototype.ARMS_NUM,所以返回2
yun.walk(); //Yunyun is walking...
yun.learn('math'); //Yunyun is learning math at Class 3,Grade 2.

結合圖我們來倒過來分析一下上面程式碼:我們先通過new Student建立了一個Student的例項yun,yun的原型指向構造器的prototype屬性(這裡就是Student.prototype), Student.prototype上有hi方法和learn方法,Student.prototype是通過Object.create(Person.prototype)構造的,所以這裡的Student.prototype是空物件,並且這個物件的原型指向Person.prototype,接著我們在Person.prototype上也設定了LEGS_NUM,ARMS_NUM屬性以及hi,walk方法。然後我們直接定義了一個Person函式,Person.prototype就是一個預置的物件,它本身也會有它的原型,它的原型就是Object.prototype,也正是因為這樣,我們隨便一個物件才會有hasOwnProperty,valueOf,toString這樣些公共的函式,這些函式都是從Object.prototype上來的。這樣子就實現了基於原型鏈的繼承。       那我們呼叫hi,walk,learn方法的時候發生了什麼呢?比如我們呼叫hi方法的時候,我們首先看這個物件yun上有沒有hi方法,但是在這個例項中沒有所以會向上查詢,查詢到yun的原型也就是Student.protoype上有這hi方法,所以最終呼叫的是Student.prototype.hi,呼叫其他方法也是類似的。

改變prototype

我們知道JavaScript中的prototype原型不像Java中的class,Java中的class一旦寫好就很難動態的去改變了,但是JavaScript中的原型實際上也是普通的物件,那就意味著在程式執行的階段,我們也可以動態的給prototype新增或刪除些屬性。

在上述程式碼的基礎上,我們已經有yun這個例項了,我們接著來進行實驗:

Student.prototype.x = 101;        //通過Student.prototype.x把yun的原型動態地新增一個屬性x
yun.x;   //101                    //那我們發現所有的例項都會受到影響
//接著我們做個有趣的實驗
Student.prototype = {y:2};        //我們直接修改構造器的prototype屬性,把它賦值為一個新的物件
yun.y;  //undefined               
yun.x;  //101                     //所以我們得出:當我們修改Student.prototype值的時候,並不能修改已經例項化的物件
var Tom = new Student('Tom',3,'Class LOL KengB');  
Tom.x; //undefined                //但當我們建立一個新的例項時,這一次x就不見了,
Tom.y; //2                        //並且y是新的值

所以說當動態修改prototype的時候,是會影響所有已建立或新建立的例項的,但是修改整個prototype賦值為新的物件的話,對已建立的例項是不會影響的,但是會影響後續的例項。

實現繼承的方式

實現繼承有多種方式,下面我們還是以Person和Student來分析

function Person() {
}

function Student() {
}

Student.prototype = Person.prototype; // 我們可不可用這種方式呢?這種方法是錯誤的:因為子類Student有自己的一些方法
//,如果通過這樣子賦值,改變Student的同時也改變了Person。

Student.prototype = new Person(); //這種方式是可以實現的,但是呼叫建構函式有時候也是有問題的,比如要傳進Person一個name和age
//,這裡的Student是個類,還沒例項化,這時候有些奇怪了,傳什麼都不是。

Student.prototype = Object.create(Person.prototype); //相對來說這中方式是比較理想的,這裡我們建立了一個空的物件
//,並且物件的原型指向Person.prototype,這樣我們既保證了繼承了Person.prototype上的方法,並且Student.prototype又有自己空的物件。
//但是Object.create是ES5以後才有的

相關文章