天天看點

原型、原型對象、原型鍊和繼承

一、原型和原型對象

1、說到原型,我們先要明白一個概念,什麼是對象?

JavaScript 中的所有事物都是對象:字元串、數字、數組、日期,等等。

在 JavaScript 中,對象是擁有屬性和方法的資料。

例如:

* * * 屬性是與對象相關的值。

* * * 方法是能夠在對象上執行的動作。

* * * 舉例:汽車就是現實生活中的對象。

2、普通對象和函數對象的差別
var a1 = {}; 
var a2 =new Object();
var a3 = new f1();

function f1(){}; 
var f2 = function(){};
var f3 = new Function('str','console.log(str)');

console.log(typeof Object); //function 
console.log(typeof Function); //function  

console.log(typeof f1); //function 
console.log(typeof f2); //function 
console.log(typeof f3); //function   

console.log(typeof a1); //object 
console.log(typeof a2); //object 
console.log(typeof a3); //object
           

通過以上的結果我們可以知道 a1 a2 a3 為普通對象,f1 f2 f3 為函數對象。

區分:通過 new Function() 建立的對象都是函數對象,其他的都是普通對象。

其實f1,f2,歸根結底都是通過 new Function()的方式進行建立的。Function Object 也都是通過 New Function()建立的。

3、構造函數和原型對象

3.1 構造函數

1、在java中語言中,存在有類的概念,類就是對象的模闆,對象就是類的執行個體。但在js中不存在類的概念,js不是基于類,而是通過構造函數(constructor)和原型鍊(prototype chains)實作的。但在ES6中引入了類(class)這個概念,作為對象的模闆,新的class寫法知識讓原型對象的寫法更加清晰,這裡不重點談這個

2.首先我們來詳細了解下什麼是構造器

構造函數的特點:

  a:構造函數的首字母必須大寫,用來區分于普通函數

  b:内部使用的this對象,來指向即将要生成的執行個體對象

  c:使用New來生成執行個體對象

function Person(name,age){
     this.name = name;    
     this.age = age;   
     this.sayHello = function(){   
         console.log(this.name +"say hello");
    }
}
var boy1 = new Person("lily",23);    
boy.sayHello(); // lily say hello
var boy2 = new Person('huahua',20)
           

以上的兩個執行個體都有一個 constructor (構造函數)屬性,該屬性(是一個指針)指向 Person。 即:

console.log(boy1.constructor == Person); //true
  console.log(boy2.constructor == Person); //true
           

構造函數的缺點:

所有的執行個體對象都可以繼承構造器函數中的屬性和方法。但是,同一個對象執行個體之間,無法共享屬性

解決思路:

a:所有執行個體都會通過原型鍊引用到prototype

b:prototype相當于特定類型所有執行個體都可以通路到的一個公共容器

c:那麼我們就将重複的東西放到公共容易就好了

3.1 原型和原型對象

構造函數有它自己的屬性及其方法,其中包括自己定義的屬性和方法外,還有兩個特殊屬性(prototype、constructor);而每個它的執行個體都會擁有它的所有屬性和方法(包括prototype、constructor)constructor則是指向每個執行個體的構造函數,而prototype 原型則是一個位址指向原型對象,這個原型對象建立了執行個體後,隻會取得constructor屬性,其他的都是從Object繼承而來;在Firefox 、 chrome在對象上都支援一個屬性"proto";這個原型對象的屬性和方法是所有該類執行個體共享的任何該類執行個體夠可以通路該原型對象的屬性和方法

它們之間的關系如下圖:

原型、原型對象、原型鍊和繼承

我們需要了解記憶以下的邏輯順序:

Person是一個對象,它有一個prototype的原型屬性(因為所有的對象都一prototype原型!)prototype屬性有自己的prototype對象,而pototype對象肯定也有自己的constuct屬性,construct屬性有自己的constuctor對象,神奇的事情要發生了,這最後一個constructor對象就是我們構造出來的function函數本身!

prototype的特點:

1、在 JavaScript 中,每當定義一個對象(函數也是對象)時候,對象中都會包含一些預定義的屬性。其中每個函數對象都有一個prototype 屬性,這個屬性指向函數的原型對象。

2、prototype 屬性包含一個對象(以下簡稱"prototype對象"),所有執行個體對象需要共享的屬性和方法,都放在這個對象裡面;那些不需要共享的屬性和方法,就放在構造函數裡面。

3、執行個體對象一旦建立,将自動引用prototype對象的屬性和方法。也就是說,執行個體對象的屬性和方法,分成兩種,一種是本地的,另一種是引用的。

我們可以把原型對象先記為Person.prototype

Person.prototype = {
   name:  'Wendy',
   age: 20,
   sayName: function() {
     alert(this.name);
   }
}
           

下面來看個例子:

從下圖中可以看出:prototype的屬性值是一個對象,是屬性的集合。

原型、原型對象、原型鍊和繼承
function Person(name,age){
    this.name=name;
    this.age=age;
}
    Person.prototype.sayHello=function(){
    alert("使用原型得到Name:"+this.name);
}
var per=new Person("alin",21);
per.sayHello(); //輸出:使用原型得到Name:alin
           

在函數Person裡面自定義了屬性name和age,而prototype是我們的屬性集合,也就是說,我要添加sayHello這個屬性到Person,則要這樣寫:Person.prototype.sayHello,就能添加Person的屬性。

(我們可以簡單的把prototype看做是一個模闆,新建立的自定義對象都是這個模闆prototype的一個拷貝,其實準确來說,不應該說是一個拷貝,而是一個連接配接,隻不過這種連結是不可見,新執行個體化的對象内部有一個看不見的_Proto_指針,指向原型對象)。對象的 proto 屬性:我們叫它 原型

prototype和_proto_的差別

原型、原型對象、原型鍊和繼承

注:大多數情況下 _proto_可以了解為“構造器的原型”,即:

_proto_===constructor.prototype

(通過Object.create0建立的對象不适用此等式,圖2有說明)

_proto_屬性指向誰

_proto_的指向取決于對象建立時的實作方式。以下圖表列出了三種常見方式建立對象後,_proto_分别指向誰。

1、字面量方式

var a=0;

原型、原型對象、原型鍊和繼承

2、構造器方式

var A=function(){};

var a=new A();

原型、原型對象、原型鍊和繼承

3、Object.create方式

var a1={}

var a2=Object.create(a1);

原型、原型對象、原型鍊和繼承

二、原型鍊

簡單了解就是原型組成的鍊,由于__proto_是任何對象都有的屬性,而js裡萬物皆對象,對象的__proto__是原型,而原型也是一個對象,也有__proto__屬性,原型的__proto__又是原型的原型,就這樣可以一直通過__proto__想上找,這就是原型鍊,當向上找找到Object的原型的時候,這條原型鍊就算到頭了,并且最終值是null。

當is引擎查找對象的屬性時,先查找對象本身是否存在該屬性,如果不存在,會在原型鍊上查找,但不會查找自身的prototype。也就是說函數擁有 prototype 屬性,但是函數自己不用它

原型、原型對象、原型鍊和繼承

總結:

當函數對象本身的屬性或方法與原型的屬性或方法同名的時候:

1、預設調用的是函數對象本身的屬性或方法.

2、通過原型增加的屬性或方法的确是存在的.

3、函數對象本身的屬性或方法的優先級要高于原型的屬性或方法.

三、繼承

1、原型鍊繼承

核心: 将父類的執行個體作為子類的原型

function Father(name,age){
    this.name=name;
    this.age=age;
}
Father.prototype.eat=function(){ //給原型添加eat方法
    console.log(this.name+"吃飯了");
}
var f1=new Father("李四",20); //建立新對象f1, [[proto]]指向父原型
function Son(){
}
Son.prototype=f1; //将子構造函數的prototype指向了父類型的對象,這裡實作了——繼承
var s1=new Son(); // 建立子對象
s1.eat(); //李四吃飯了
           

①:當 Son.prototype指向Father的時候,他就已經是父類型的Son了。

②:s1.eat();s1中沒有此方法,該方法在父類型原型中,當s1通路時,現在s1中查找,若沒有則向他指向的原型中去查找該方法,若還是沒有,則繼續往上面的原型中查找。這樣就形成了一條原型鍊。

③:通過原型鍊實作了繼承。

簡寫形式:
    var f1=new Father;
    var Son.prototype=f1
    //可以直接簡寫成:
    var Son.prototypr=new Father(); //這個時候可以傳值進去 ,其餘地方無法傳值
           

特點:

  1. 非常純粹的繼承關系,執行個體是子類的執行個體,也是父類的執行個體
  2. 父類新增原型方法/原型屬性,子類都能通路到
  3. 簡單,易于實作

缺點:

  1. 要想為子類新增屬性和方法,必須要在new Animal()這樣的語句之後執行,不能放到構造器中
  2. 無法實作多繼承
  3. 來自原型對象的引用屬性是所有執行個體共享的(詳細請看附錄代碼: 示例1) 建立子類執行個體時,無法向父類構造函數傳參
2、構造繼承

核心:使用父類的構造函數來增強子類執行個體,等于是複制父類的執行個體屬性給子類(沒用到原型)

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
           

特點:

解決了1中,子類執行個體共享父類引用屬性的問題

建立子類執行個體時,可以向父類傳遞參數

可以實作多繼承(call多個父類對象)

缺點:

執行個體并不是父類的執行個體,隻是子類的執行個體

隻能繼承父類的執行個體屬性和方法,不能繼承原型屬性/方法

無法實作函數複用,每個子類都有父類執行個體函數的副本,影響性能

3、執行個體繼承

核心:為父類執行個體添加新特性,作為子類執行個體傳回

function Cat(name){
  var instance = new Animal();
  instance.name = name || 'Tom';
  return instance;
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // false
           

特點:

不限制調用方式,不管是new 子類()還是子類(),傳回的對象具有相同的效果

缺點:

執行個體是父類的執行個體,不是子類的執行個體

不支援多繼承

4、拷貝繼承
function Cat(name){
  var animal = new Animal();
  for(var p in animal){
    Cat.prototype[p] = animal[p];
  }
  Cat.prototype.name = name || 'Tom';
}

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
           

特點:

支援多繼承

缺點:

效率較低,記憶體占用高(因為要拷貝父類的屬性)

無法擷取父類不可枚舉的方法(不可枚舉方法,不能使用for in 通路到)

5、組合繼承

核心:通過調用父類構造,繼承父類的屬性并保留傳參的優點,然後通過将父類執行個體作為子類原型,實作函數複用

function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
Cat.prototype = new Animal();

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
           

特點:

彌補了方式2的缺陷,可以繼承執行個體屬性/方法,也可以繼承原型屬性/方法

既是子類的執行個體,也是父類的執行個體

不存在引用屬性共享問題

可傳參

函數可複用

缺點:

調用了兩次父類構造函數,生成了兩份執行個體(子類執行個體将子類原型上的那份屏蔽了)

6、寄生組合繼承

核心:通過寄生方式,砍掉父類的執行個體屬性,這樣,在調用兩次父類的構造的時候,就不會初始化兩次執行個體方法/屬性,避免的組合繼承的缺點

function Fn(name,age){
   this.name=name; //構造函數的屬性多樣
   this.age=age;
   if((typeof Fn.prototype.eat)!="funciton"){ //判斷語句中是否有該方法,沒有則建立
    Fn.prototype.eat=function(){ //原型的方法共享
      console.log(this.name+"吃了飯");
    }
   }
}
function Son(name,age,sex){ //建立子類構造函數
   Fn.call(this,name,age) //借調Fn()的屬性
   this.sex=sex;
};
Son.prototype=new Fn(); //Son.prototype指向父類對象,實作了繼承,是以能夠調用eat方法,
var s1=new Son("李四",20,"男"); //若沒有繼承,單單的使用call借調Fn繼承,子類執行個體s1無法調用eat方法
callconsole.log(s1); //因為call不是真正的繼承
s1.eat();
           

特點:

堪稱完美

缺點:

實作較為複雜

繼續閱讀