首頁 >web前端 >js教程 >深入淺出JavaScript之原型鍊和繼承的詳解

深入淺出JavaScript之原型鍊和繼承的詳解

黄舟
黄舟原創
2017-03-03 15:25:561176瀏覽

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

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

系列目錄

  • 深入淺出JavaScript之閉包(Closure)

  • 深入淺出JavaScript之this

  • #深入淺出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以后才有的

以上就是深入浅出JavaScript之原型链和继承的详解的内容,更多相关内容请关注PHP中文网(www.php.cn)!

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