輕鬆理解JS中的面向對象,順便搞懂prototype和__proto__

這篇文章主要講一下JS中面向對象以及 __proto__ptototypeconstructor,這幾個概念都是相關的,因此一塊兒講了。javascript

在講這個以前咱們先來講說類,瞭解面向對象的朋友應該都知道,若是我要定義一個通用的類型我可使用類(class)。好比在java中咱們能夠這樣定義一個類:java

public class Puppy{
    int puppyAge;

    public Puppy(age){
      puppyAge = age;
    }
  
    public void say() {
      System.out.println("汪汪汪"); 
    }
}

上述代碼咱們定義了一個Puppy類,這個類有一個屬性是puppyAge,也就是小狗的年齡,而後有一個構造函數Puppy(),這個構造函數接收一個參數,能夠設置小狗的年齡,另外還有一個說話的函數say。這是一個通用的類,當咱們須要一個兩歲的小狗實例是直接這樣寫,這個實例同時具備父類的方法:git

Puppy myPuppy = new Puppy( 2 );
myPuppy.say();     // 汪汪汪

可是早期的JS沒有class關鍵字啊(如下說JS沒有class關鍵字都是指ES6以前的JS,主要幫助你們理解概念),JS爲了支持面向對象,使用了一種比較曲折的方式,這也是致使你們迷惑的地方,其實咱們將這種方式跟通常的面向對象類比起來就很清晰了。下面咱們來看看JS爲了支持面向對象須要解決哪些問題,都用了什麼曲折的方式來解決。github

沒有class,用函數代替

首先JS連class關鍵字都沒有,怎麼辦呢?用函數代替,JS中最不缺的就是函數,函數不只可以執行普通功能,還能當class使用。好比咱們要用JS建一個小狗的類怎麼寫呢?直接寫一個函數就行:ajax

function Puppy() {}

這個函數能夠直接用new關鍵字生成實例:babel

const myPuppy = new Puppy();

這樣咱們也有了一個小狗實例,可是咱們沒有構造函數,不能設置小狗年齡啊。app

函數自己就是構造函數

當作類用的函數自己也是一個函數,並且他就是默認的構造函數。咱們想讓Puppy函數可以設置實例的年齡,只要讓他接收參數就好了。函數

function Puppy(age) {
  this.puppyAge = age;
}

// 實例化時能夠傳年齡參數了
const myPuppy = new Puppy(2);

注意上面代碼的this,被做爲類使用的函數裏面this老是指向實例化對象,也就是myPuppy。這麼設計的目的就是讓使用者能夠經過構造函數給實例對象設置屬性,這時候console出來看myPuppy.puppyAge就是2。this

console.log(myPuppy.puppyAge);   // 輸出是 2

實例方法用prototype

上面咱們實現了類和構造函數,可是類方法呢?Java版小狗還能夠「汪汪汪」叫呢,JS版怎麼辦呢?JS給出的解決方案是給方法添加一個prototype屬性,掛載在這上面的方法,在實例化的時候會給到實例對象。咱們想要myPuppy能說話,就須要往Puppy.prototype添加說話的方法。prototype

Puppy.prototype.say = function() {
  console.log("汪汪汪");
}

使用new關鍵字產生的實例都有類的prototype上的屬性和方法,咱們在Puppy.prototype上添加了say方法,myPuppy就能夠說話了,我麼來試一下:

myPuppy.say();    // 汪汪汪

實例方法查找用__proto__

那myPuppy怎麼就可以調用say方法了呢,咱們把他打印出來看下,這個對象上並無say啊,這是從哪裏來的呢?

image-20200221180325943

這就該__proto__上場了,當你訪問一個對象上沒有的屬性時,好比myPuppy.say,對象會去__proto__查找。__proto__的值就等於父類的prototype, myPuppy.__proto__指向了Puppy.prototype

image-20200221181132495

若是你訪問的屬性在Puppy.prototype也不存在,那又會繼續往Puppy.prototype.__proto__上找,這時候其實就找到了Object.prototype了,Object.prototype再往上找就沒有了,也就是null,這其實就是原型鏈

image-20200221181533277

constructor

咱們說的constructor通常指類的prototype.constructorprototype.constructor是prototype上的一個保留屬性,這個屬性就指向類函數自己,用於指示當前類的構造函數。

image-20200221183238691

image-20200221182045545

既然prototype.constructor是指向構造函數的一個指針,那咱們是否是能夠經過它來修改構造函數呢?咱們來試試就知道了。咱們先修改下這個函數,而後新建一個實例看看效果:

function Puppy(age) {
  this.puppyAge = age;
}

Puppy.prototype.constructor = function myConstructor(age) {
  this.puppyAge = age + 1;
}

const myPuppy2 = new Puppy(2);
console.log(myPuppy2.puppyAge);    // 輸出是2

上例說明,咱們修改prototype.constructor只是修改了這個指針而已,並無修改真正的構造函數。

可能有的朋友會說我打印myPuppy2.constructor也有值啊,那constructor是否是也是對象自己的一個屬性呢?其實不是的,之因此你能打印出這個值,是由於你打印的時候,發現myPuppy2自己並不具備這個屬性,又去原型鏈上找了,找到了prototype.constructor。咱們能夠用hasOwnProperty看一下就知道了:

image-20200222152216426

上面咱們其實已經說清楚了prototype__proto__constructor幾者之間的關係,下面畫一張圖來更直觀的看下:

image-20200222153906550

靜態方法

咱們知道不少面向對象有靜態方法這個概念,好比Java直接是加一個static關鍵字就能將一個方法定義爲靜態方法。JS中定義一個靜態方法更簡單,直接將它做爲類函數的屬性就行:

Puppy.statciFunc = function() {    // statciFunc就是一個靜態方法
  console.log('我是靜態方法,this拿不到實例對象');
}      

Puppy.statciFunc();            // 直接經過類名調用

靜態方法和實例方法最主要的區別就是實例方法能夠訪問到實例,能夠對實例進行操做,而靜態方法通常用於跟實例無關的操做。這兩種方法在jQuery中有大量應用,在jQuery中$(selector)其實拿到的就是實例對象,經過$(selector)進行操做的方法就是實例方法。好比$(selector).append(),這會往這個實例DOM添加新元素,他須要這個DOM實例才知道怎麼操做,將append做爲一個實例方法,他裏面的this就會指向這個實例,就能夠經過this操做DOM實例。那什麼方法適合做爲靜態方法呢?好比$.ajax,這裏的ajax跟DOM實例不要緊,不須要這個this,能夠直接掛載在$上做爲靜態方法。

繼承

面向對象怎麼能沒有繼承呢,根據前面所講的知識,咱們其實已經可以本身寫一個繼承了。所謂繼承不就是子類可以繼承父類的屬性和方法嗎?換句話說就是子類可以找到父類的prototype,最簡單的方法就是子類原型的__proto__指向父類原型就好了。

function Parent() {}
function Child() {}

Child.prototype.__proto__ = Parent.prototype;

const obj = new Child();
console.log(obj instanceof Child );   // true
console.log(obj instanceof Parent );   // true

上述繼承方法只是讓Child訪問到了Parent原型鏈,可是沒有執行Parent的構造函數:

function Parent() {
  this.parentAge = 50;
}
function Child() {}

Child.prototype.__proto__ = Parent.prototype;

const obj = new Child();
console.log(obj.parentAge);    // undefined

爲了解決這個問題,咱們不能單純的修改Child.prototype.__proto__指向,還須要用new執行下Parent的構造函數:

function Parent() {
  this.parentAge = 50;
}
function Child() {}

Child.prototype.__proto__ = new Parent();

const obj = new Child();
console.log(obj.parentAge);    // 50

上述方法會多一個__proto__層級,能夠換成修改Child.prototype的指向來解決,注意將Child.prototype.constructor重置回來:

function Parent() {
  this.parentAge = 50;
}
function Child() {}

Child.prototype = new Parent();
Child.prototype.constructor = Child;      // 注意重置constructor

const obj = new Child();
console.log(obj.parentAge);    // 50

固然還有不少其餘的繼承方式,他們的原理都差很少,只是實現方式不同,核心都是讓子類擁有父類的方法和屬性,感興趣的朋友能夠自行查閱。

本身實現一個new

結合上面講的,咱們知道new其實就是生成了一個對象,這個對象可以訪問類的原型,知道了原理,咱們就能夠本身實現一個new了。

function myNew(func, ...args) {
  const obj = {};     // 新建一個空對象
  const result = func.call(obj, ...args);  // 執行構造函數
  obj.__proto__ = func.prototype;    // 設置原型鏈
  
  // 注意若是原構造函數有Object類型的返回值,包括Functoin, Array, Date, RegExg, Error
  // 那麼應該返回這個返回值
  const isObject = typeof result === 'object' && result !== null;
  const isFunction = typeof result === 'function';
  if(isObject || isFunction) {
    return result;
  }
  
  // 原構造函數沒有Object類型的返回值,返回咱們的新對象
  return obj;
}

function Puppy(age) {
  this.puppyAge = age;
}

Puppy.prototype.say = function() {
  console.log("汪汪汪");
}

const myPuppy3 = myNew(Puppy, 2);

console.log(myPuppy3.puppyAge);  // 2
console.log(myPuppy3.say());     // 汪汪汪

本身實現一個instanceof

知道了原理,其實咱們也知道了instanceof是幹啥的。instanceof不就是檢查一個對象是否是某個類的實例嗎?換句話說就是檢查一個對象的的原型鏈上有沒有這個類的prototype,知道了這個咱們就能夠本身實現一個了:

function myInstanceof(targetObj, targetClass) {
  // 參數檢查
  if(!targetObj || !targetClass || !targetObj.__proto__ || !targetClass.prototype){
    return false;
  }
  
  let current = targetObj;
  
  while(current) {   // 一直往原型鏈上面找
    if(current.__proto__ === targetClass.prototype) {
      return true;    // 找到了返回true
    }
    
    current = current.__proto__;
  }
  
  return false;     // 沒找到返回false
}

// 用咱們前面的繼承實驗下
function Parent() {}
function Child() {}

Child.prototype.__proto__ = Parent.prototype;

const obj = new Child();
console.log(myInstanceof(obj, Child) );   // true
console.log(myInstanceof(obj, Parent) );   // true
console.log(myInstanceof({}, Parent) );   // false

ES6的class

最後仍是提一嘴ES6的class,其實ES6的class就是前面說的函數類的語法糖,好比咱們的Puppy用ES6的class寫就是這樣:

class Puppy {
  // 構造函數
  constructor(age) {            
    this.puppyAge = age;
  }
  
  // 實例方法
  say() {
    console.log("汪汪汪")
  }
  
  // 靜態方法
  static statciFunc() {
    console.log('我是靜態方法,this拿不到實例對象');
  }
}

const myPuppy = new Puppy(2);
console.log(myPuppy.puppyAge);    // 2
console.log(myPuppy.say());       // 汪汪汪
console.log(Puppy.statciFunc());  // 我是靜態方法,this拿不到實例對象

使用class可讓咱們的代碼看起來更像標準的面向對象,構造函數,實例方法,靜態方法都有明確的標識。可是他本質只是改變了一種寫法,因此能夠看作是一種語法糖,若是你去看babel編譯後的代碼,你會發現他其實也是把class編譯成了咱們前面的函數類,extends關鍵字也是使用咱們前面的原型繼承的方式實現的。

總結

最後來個總結,其實前面小節的標題就是核心了,咱們再來總結下:

  1. JS中的函數能夠做爲函數使用,也能夠做爲類使用
  2. 做爲類使用的函數實例化時須要使用new
  3. 爲了讓函數具備類的功能,函數都具備prototype屬性。
  4. 爲了讓實例化出來的對象可以訪問到prototype上的屬性和方法,實例對象的__proto__指向了類的prototype。因此prototype是函數的屬性,不是對象的。對象擁有的是__proto__,是用來查找prototype的。
  5. prototype.constructor指向的是構造函數,也就是類函數自己。改變這個指針並不能改變構造函數。
  6. 對象自己並無constructor屬性,你訪問到的是原型鏈上的prototype.constructor
  7. 函數自己也是對象,也具備__proto__,他指向的是JS內置對象Function的原型Function.prototype。因此你才能調用func.call,func.apply這些方法,你調用的實際上是Function.prototype.callFunction.prototype.apply
  8. prototype自己也是對象,因此他也有__proto__,指向了他父級的prototype__proto__prototype的這種鏈式指向構成了JS的原型鏈。原型鏈的最終指向是Object的原型。Object上面原型鏈是null,即Object.prototype.__proto__ === null
  9. 另外要注意的是Function.__proto__ === Function.prototype,這是由於JS中全部函數的原型都是Function.prototype,也就是說全部函數都是Function的實例。Function自己也是能夠做爲函數使用的----Function(),因此他也是Function的一個實例。相似的還有ObjectArray等,他們也能夠做爲函數使用:Object(), Array()。因此他們自己的原型也是Function.prototype,即Object.__proto__ === Function.prototype。換句話說,這些能夠new的內置對象其實都是一個類,就像咱們的Puppy類同樣。
  10. ES6的class實際上是函數類的一種語法糖,書寫起來更清晰,但原理是同樣的。

再來看一下完整圖:

image-20200222160832782

文章的最後,感謝你花費寶貴的時間閱讀本文,若是本文給了你一點點幫助或者啓發,請不要吝嗇你的贊和GitHub小星星,你的支持是做者持續創做的動力。

做者博文GitHub項目地址: https://github.com/dennis-jiang/Front-End-Knowledges

相關文章
相關標籤/搜索