1. 程式人生 > 實用技巧 >JS面向物件與prototype,proto,constructor

JS面向物件與prototype,proto,constructor

JS面向物件與prototype,proto,constructor

一、Java中的面向物件與繼承

  1. 下面程式碼中,我們定義了一個小狗類,在類中定義了一個屬性和兩個方法,一個構造方法用於初始化小狗的年齡 age,一個公有方法 say 用於列印。
public class Puppy{
    int puppyAge;
    
    public Puppy(age){
      puppyAge = age;
    }
    
    public void say() {
      System.out.println("汪汪汪"); 
    }
}

  1. 這是一個通用的類,當我們需要一個兩歲的小狗的例項是這樣寫的,這個例項同時具有父類的方法。
Puppy myPuppy = new Puppy(2);
muPuppy.say(); // 汪汪汪

  1. 以上的類和例項的實現均基於 java 的語法來的,但是相比於相對完善的 java 語法來說,早期的 js 沒有 class 關鍵字啊(以下說 js 沒有 class 關鍵字都是指 ES6 之前的 js ,主要幫助大家理解概念)。JS為了支援面向物件,使用了一種比較曲折的方式,具體如下。

二、JS中的面向物件與繼承

  1. 沒有 class,用函式代替 :早期的 js 沒有 class 關鍵字,是怎麼辦的呢?對,是用函式來代替,函式不僅能執行普通功能,還能當 class 使用,栗子如下。
function Puppy() {}

  1. 以上程式碼實現了一個函式。下面我們就可以生成以上函式的例項了。

建構函式本身就是一個函式,與普通函式沒有任何區別,不過為了規範一般將其首字母大寫。建構函式和普通函式的區別在於,使用 new 生成例項的函式就是建構函式,直接呼叫的就是普通函式。

let myPuppy = new Puppy()

  1. 函式本身就是建構函式 :雖然我們有了小狗的例項,但是不像 java 語法似的可以在類中定義建構函式來不能設定小狗的年齡啊。不慌,其實,充當類使用的函式本身就是建構函式,而且它就是預設的建構函式,下面我們重寫以上程式碼,讓建構函式接收函式來初始化小狗的年齡 age 。
// 建構函式:可接收引數來初始化屬性值
function Puppy(age) {
  this.puppyAge = age
}

// 例項化時可以傳年齡引數了
let myPuppy = new Puppy(2)

  1. 建構函式中的 this 指向例項化物件 :建構函式中的 this 指向需要注意:被作為類使用的函式裡面 this 總是指向例項化物件,也就是 myPuppy 。這麼設計的目的就是讓使用者可以通過建構函式給例項物件設定屬性,這時候打印出來看 myPuppy.puppyAge 就是 2 。
console.log(myPuppy.puppyAge)   // 2

  1. prototype 上定義例項方法 :以上 4 點,我們實現了建構函式定義以及例項化。java 語法可以直接在類中定義公共方法來讓例項小狗汪汪汪,js 如何辦呢?對此,js 給出的解決方案是給構造方法新增一個 prototype 屬性,掛載在這上面的方法,例項化時就會給到例項物件。
// 在建構函式的 prototype 上新增方法
Puppy.prototype.say = function() {
  console.log("汪汪汪")
}

// 例項物件呼叫相應方法
myPuppy.say()    // 汪汪汪

  1. 例項方法的查詢用 proto :以上可能有的同學就會有疑問了,方法在建構函式的 prototype 上,例項物件 myPuppy 怎麼會找到 say 方法了呢?我們來列印 myPuppy 。

(1)當你訪問一個物件上沒有的屬性時,比如 myPuppy.say,物件會去 __proto__ 查詢。 __proto__ 的值就等於父類的 prototype, myPuppy.__proto__ 指向了 Puppy.prototype。

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


  1. constructor

(1)每個例項都有一個 constructor(建構函式)屬性,該屬性指向物件本身。

(2)prototype.constructor 是 prototype 上的一個保留屬性,這個屬性就指向類函式本身,用於指示當前類的建構函式。

(3)既然 prototype.constructor 是指向建構函式的一個指標,那我們是不是可以通過它來修改建構函式呢?我們來試試就知道了。我們先修改下這個函式,然後新建一個例項看看效果

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

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

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

上例說明,我們修改 prototype.constructor 只是修改了這個指標而已,並沒有修改真正的建構函式。

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


  1. 靜態方法 :我們知道很多面向物件有靜態方法這個概念,比如 java 直接是加一個 static 關鍵字就能將一個方法定義為靜態方法。js 中定義一個靜態方法更簡單,直接將它作為類函式的屬性就行。
// 在建構函式上定義靜態方法 statciFunc
Puppy.statciFunc = function() {
  console.log('我是靜態方法,this拿不到例項物件')
}      

// 直接通過類名呼叫
Puppy.statciFunc(); 

  1. 繼承:面向物件怎麼能沒有繼承呢,根據前面所講的知識,我們其實已經能夠自己寫一個繼承了。所謂繼承不就是子類能夠繼承父類的屬性和方法嗎?換句話說就是子類能夠找到父類的 prototype ,最簡單的方法就是子類原型的 __proto__ 指向父類原型就行了。

(1)以下繼承方法只是讓 Child 訪問到了 Parent 原型鏈,但是沒有執行 Parent 的建構函式

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

(2)為了解決上述問題,我們不能單純的修改 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

(3)上述方法會多一個 __proto__ 層級,可以換成修改 Child.prototype 的指向來解決,注意將 Child.prototype.constructor 重置回來。

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

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

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

  1. 自己實現一個new:結合上面講的,我們知道 new 其實就是生成了一個物件,這個物件能夠訪問類的原型,知道了原理,我們就可以自己實現一個 new 了。
function myNew(func, ...args) {
    // 新建一個空物件
  const obj = {};     
  // 執行建構函式
  func.call(obj, ...args);  
  // 設定原型鏈
  obj.__proto__ = func.prototype;    

  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());     // 汪汪汪

  1. 自己實現一個 new:知道了原理,其實我們也知道了 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 就是前面說的函式類的語法糖,比如我們的 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.call 和 Function.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 的一個例項。類似的還有Object,Array等,他們也可以作為函式使用: Object(), Array() 。所以他們本身的原型也是Function.prototype,即 Object.__proto__ === null Function.prototype 。換句話說,這些可以 new 的內建物件其實都是一個類,就像我們的 Puppy 類一樣。

  10. ES6 的 class 其實是函式類的一種語法糖,書寫起來更清晰,但原理是一樣的。




來源參考:輕鬆理解JS中的面向物件,順便搞懂prototype和__proto__
JS 系列二:深入 constructor、prototype、proto、[[Prototype]] 及 原型鏈