開發與維運

面向對象、工廠模式、構造函數、原型、原型鏈,看這一篇就夠了!

本文是博主自己歸納曾學到的知識,以及分享工廠模式、構造函數、以及原型鏈之間的用途;有很多面試者都會遇到這些問題,但是一些小白來說,還是不清楚,這些知識點到底在技術中有什麼用途,到底是幹什麼的?今天我就簡單說一說,本篇博客會從基本開始介紹,如果你對今天的內容所致甚少,並且你有耐心看完,相信你會有超值的收穫!!!

小白們都對原型鏈迷迷糊糊,經常查閱相關文章,卻還是縷不清到底是幹什麼的。因為你不知道它到底有什麼用,又是如何會用到它,所以在直接查閱原型鏈的時候,忽略了它之前很重要的知識,所以你不懂。這個很重要的知識,就是構造函數!!!

本篇博客要一環扣一環,決定幫大家捋清思路,所以我們從基本開始說,從頭到尾請跟住博主的思路。

面向對象:

對象?無論在javascipt當中,還是java當中,或其他語言中,所提及的對象,絕對不是男女朋友的對象,所以我們凡人先忘掉感情視野,再體會一下這個詞:“對象”。
所謂對象,任何事物都可以稱作是對象。
比如:

醫生給病人治療,病人對於醫生來說,就是治療的對象。
我們去商場購買蘋果,蘋果對於我們來說,就是要購買的對象。
博主寫的博客,你們就是我要展示博客的對象。
科學家要研製火箭,火箭對於科學家來說,就是要研製的對象
……

所以我們這些都是對象,有句英文說的好,Everything is object(萬物皆對象)。
知道對象了,那為什麼要“面向”呢?“面向對象又是什麼?”

這就是一種編程思想,面向對象編程。在我們的開發世界中,把所有的方法都封裝成一個一個的對象,大的功能都是由一個又一個的對象實現的,所以就叫面向對象編程思想。在程序員眼中,一切功能都是被拆分成小的單獨對象,組裝起來的。
在開發中,我們很容易可以創造出一個對象,例如:

var person = {};             // 字面量創建一個對象
var person1 = new Object();  // 構造一個對象

兩種方法,但都是創造了一個對象。對象都有自己的屬性,比如人可以吃飯,睡覺,戀愛。這些都是我們人(對象)的屬性,同樣,我們也可以在上面的例子中,加入屬性:

var person = {
  name: 'villin',
  age: 18,
  exes: ["111", "222", "333"]
};

我們也可以刪除裡邊的屬性,比如把年齡刪除:

delete person.age

這些就是對象的含義,我們在開發中,也可以創建一個函數,這個函數就看作是一個對象,裡邊些不同的方法。面向對象很簡單,反覆的看幾遍,就能把這個“對象”一詞理解明白。

this:

this,是屬於一個指向的關鍵字,一般是在函數內使用,誰調用了這個函數,this指向誰。這個不太好解釋,只能體會的去理解。看下邊這個例子,在person中定義sayHi這個函數,所以this指向了person對象中的屬性。

var person = {}

person.name = 'villin';

person.sayHi = function () {
  console.log(this.name)
}

  person.sayHi();    // villin

this重點:
1、在函數內,函數定義時無法確定this指向,所以記住:誰調用,指向誰!
2、函數如果以普通方式進行調用,this指向window
3、每個函數都有自己的this,只要進入一個函數,this就可能發生變化

那麼如果在函數中還不想讓this指向window,還想使用傳過來的參數,我們習慣於將參數賦值:

function aaa(name){
  this.name = name
}

工廠方法(factory):

我們把上邊剛才的例子,放在一個函數中,並返回這個值,就是工廠模式。具體為啥叫這個名字,我也不知道。我們封裝一個簡單的(工廠模式)函數,並且進行調用。這個函數很簡單,我標註的很詳細,仔細看,這一環很重要。

// 創建一個函數,接收兩個參數,一個name,一個age     (函數可以接收很多參數,具體幾個看你們自己的心情)

function person(name,age){
  // 1.創建一個對象
  var a = {};
  // 2.在對象上添加屬性和方法,將參數賦值給它
  a.name = name;
  a.age = age;
  // 3.和上邊一樣,這個對象a中定義一個函數sayHi
  a.sayHi = function(){
    console.log("你好,我叫"+this.name+",我今年"+this.age+"歲")
  }
  // 4.創建的對象必須要返回這個對象a,才能打印出函數中的內容;
  return a;
}

好了,這個工廠模式的函數,就封裝好了,就可以多次調用這個封裝的方法了,例如:

// 定義三個p,分別傳入兩個參數,一個name,一個age
var p1 = person("villin",25)
var p2 = person("xiaoming",26)
var p3 = person("xiaoli",18)

// 分別調用三個方法
p1.sayHi()
p2.sayHi()
p3.sayHi()

打印結果如下:

image

我們有個方法instanceof,是可以檢測一個對象是否為一個構造函數的實例,剛才寫的我們可以檢測一下:

console.log(p1 instanceof person)  // false

返回為false,說明p1並不是person函數的實例,那麼如果我在創建一個其他的函數,也是檢測不出來的,這樣在項目中,如果很多個函數,我們就無法得知,這個對象是哪個函數的實例,所以工廠模式的缺點,就是無法判斷生成的對象,是哪個構造函數的實例。
解決辦法:使用構造函數的方法來創建對象!!!

延伸:實例和繼承。
   什麼是實例?舉個例子:我們剛才說過,萬物皆對象,那麼動物就是個對象,我們再具體一點,貓、狗、老鼠這些就是動物的實例。所以person函數就是一個對象,那麼p1、p2、p3是否是person的實例,就用instanceof來驗證;
   那麼貓,狗等都繼承了動物的特徵,比如動物是活的,有眼睛耳朵鼻子等,貓狗也有這些,這就是繼承,繼承了動物的特徵。但是不能反過來說動物也繼承了貓狗的特徵,這是不對的,因為貓有毛,但是魚沒有。
   萬物皆對象,所以實例(像貓和狗)也是對象,但是對象未必是實例(例如動物是個大的框架,沒有被細分成實例)。

構造函數(constructor):

我們接著上邊的案例來說,同樣我們創建一個函數person,但是這個函數名首字母要大寫,代表構造函數,但並非規定。

// 函數名大寫
function Person(name,age){
  // 省略了創建對象 (和工廠模式對比)
  // 省略了添加屬性和方法 (和工廠模式對比)
  // 1.賦值指針this
  this.name = name;
  this.age = age;
  this.sayHi = function () {
   console.log("你好,我叫"+this.name+",我今年"+this.age+"歲")
  }
  // return返回對象,也省了 (和工廠模式對比)
}

接下來我們重新構造(new)這個Person對象,並傳入相應的參數:

p1 = new Person("villin",25)  // 不要忘記大寫
p2 = new Person("xiaoming",26)
p3 = new Person("xiaoli",18)

// 分別調用三個方法
p1.sayHi()
p2.sayHi()
p3.sayHi()

打印結果,同樣如此:
image

我們再次嘗試用instanceof驗證一下

console.log(p1 instanceof Person)  // true

顯然,可以被驗證了,這也說明了,構造函數p1為Person的實例。如果項目中不光有Person函數,或其他,也都可以驗證。(自己要回頭去看一下工廠模式和構造函數的區別)

不過構造函數依然伴有缺點:
細心發現,我們剛才構造了三個函數p1~p3,分別構造了三次,這樣每次構造一次,就會生成一個sayHi的函數(好奇者可以分別打印p1、p2、p3在控制檯看一下,會發現生成了三次相同的函數),並且這些函數對象內的方法,其代碼是一模一樣的,都是(你好,我的名字是,年齡)等,這樣重複太多,複用性不高,太浪費資源。
解決這個問題,我們就需要把這個sayHi公用的函數存放到一個位置,這個位置要確保每個對象都能訪問到。
那麼這個位置就是:prototype(原型)。

原型(prototype):

1、prototype中文含義就是“原型”,只要是函數,都有自己的原型prototype。
2、當用構造函數創建對象(new Xxx())時,瀏覽器在新創建的對象上添加了一個屬性__proto__(前後是雙下劃線。不要直接使用)。

所以要記住,對象中有一個屬性是__proto__。函數也是對象,所以它也有__proto__,但是函數還多了一個prototype屬性。
這個對象到底長什麼樣,我們打印一下這個對象p1(剛剛的p1)看一下。

function Person(name,age){
  // 省略了創建對象 (和工廠模式對比)
  // 省略了添加屬性和方法 (和工廠模式對比)
  // 1.賦值指針this
  this.name = name;
  this.age = age;
  this.sayHi = function () {
   console.log("你好,我叫"+this.name+",我今年"+this.age+"歲")
  }
  // return返回對象,也省了 (和工廠模式對比)
}

p1 = new Person("villin",25)

console.log(p1) // 打印p1

image
可以看到,我們沒有打印函數,僅僅是打印出的p1,那為什麼也會有name、age、以及sayHi屬性?這就是繼承,因為new構造出來的是Person函數的實例,所以它繼承了它構造的函數Person中的屬性。
我們可以看到這個對象p1是帶有自己的一個屬性__proto__的,並且,這個對象的__proto__指向了它的構造函數的prototype(每個函數都是有prototype的屬性的,只是沒打印而已,自己印在自己的腦海裡),並且__proto__也會繼承prototype中的屬性
這些就是概念性的東西,要試著去理解,如果不理解多看幾遍,一定要慢慢的讀,博主寫這些,也是慢慢的寫的。

那麼既然每個構造函數都會繼承它原來函數中prototype的屬性,那麼就利用這一點,寫一個可以公用的方法。
讓他們的實例公用這個sayHi函數,我們開始利用這個原型prototype去更改構造函數的不足之處:

  function Person() {}  //單獨設一個空的函數,它有一個屬性是prototype

  // 公共的部分
  // 我們可以直接更改Person的原型prototype
  Person.prototype.name = 'villin';
  Person.prototype.age = 26;

  Person.prototype.sayHi = function () {
    console.log("你好,我叫"+this.name+",我今年"+this.age+"歲")
    }

 var p1 = new Person();
 var p2 = new Person();
 p1.sayHi();
 p2.sayHi();

// 你好,我叫villin,我今年26歲
// 你好,我叫villin,我今年26歲

可以看到,雖然還是創建了兩個函數,但是p1和p2都複用了prototype中的屬性,所以打印的東西都是一樣的。
原型鏈:
疑問:同學可能會發現,我並沒有在p1和p2中定義任何的屬性,直接調用p1.sayHi為什麼可以把原型prototype中的sayHi打印出來?
p1中並沒有sayHi這個屬性。這就引入了一個詞叫:原型鏈
當訪問一個對象時,會在對象內進行查找這個屬性,比如之前定義過的this.sayHi屬性。像本次,對象內沒有定義sayHi屬性,那麼就會進入p1的原型__proto__中查找,因為__proto__指向了Person中prototype,並繼承了其中的屬性,所以prototype中有啥,__proto__就有啥,所以就找到了。這一條鏈式查找就是原型鏈。我們打印一下p1看一下:

image

不過,這個方法還是有些缺點,因為p1和p2都繼承了相同的屬性,打印出來的結果都是一樣的,這並不是我們想要的,我們想要的是,公用一些方法外,可以傳遞想要打印的參數,想打印出什麼值,就打印出什麼值。
所以,最後的解決方案:組合(構造+原型)

構造函數+原型:

如果以上的幾個方法都看明白了,這個就很簡單,只需要在原型中定義想要得到的公用方法,在函數中定義this指向,並接收參數即可:

// 需要定義兩個函數,一個是構造函數Person,一個是在Person的prototype中定義公用方法sayHi函數。

function Person(name, age, exes) {
  //將所有的參數屬性,放在構造方式中
  this.name = name;
  this.age = age;
  this.exes = exes;
}

Person.prototype.sayHi = function () {
  // this會指向Person內
  console.log("你好,我叫"+this.name+",我今年"+this.age+"歲")
}

然後照常傳遞參數,正常打印:

p1 = new Person("villin",25)
p2 = new Person("xiaoli",18)

// 你好,我叫villin,我今年25歲
// 你好,我叫xiaoli,我今年18歲

這樣既有公用的繼承函數方法,又可以傳遞參數,可以獲得自己想要的數據。

我們最後可以在驗證一下是否是Person的實例問題。

console.log(p1 instanceof person)  // true

這就是今天所寫的內容,在最後,補充一下知識,也能會成為面試的考點:

問題:
構造函數在new的時候,JS引擎做了那些事情?(new之後發生了什麼?)

  1. 創建了一個對象
  2. 將構造函數內的this,指向這個對象
  3. 執行構造函數
  4. 返回這個對象

weChat:VillinWeChat

歡迎提出寶貴意見

Leave a Reply

Your email address will not be published. Required fields are marked *