JavaScript物件導向名詞詳解

Accumulate_HangZhou發表於2019-01-28

JavaScript是一門基於原型設計的語言。
這句話其實描述了JavaScript這門語言關於物件導向設計的一個最重要的特性。區別於常見的面嚮物件語言,JavaScript對於OOP的實現,有自己的一套設計邏輯和實現方式。正是因為有別於傳統常見的方式,只有掌握了它相關的基礎概念,才能徹底掌握和理解JavaScript關於物件導向的內容。這篇文章致力於全面梳理JavaScript中關於物件導向和繼承相關的名詞,徹底理解它們,是至關重要的一步。

原型

JavaScript是基於原型設計的語言,那第一步,當然是先搞清楚到底什麼是原型?要搞清楚什麼是原型,先了解下JavaScript中的資料型別。
JavaScript中的資料型別有兩種:基本資料型別和引用資料型別。

  • 基本資料型別包括:數字(Number),字串(String),布林值(Boolean),null,undefined,Symbol
  • 引用資料型別包括:物件(Object),陣列(Array),函式(Function)
    基本型別的資料暫且不說,引用型別的資料都有什麼特性呢?引用型別的資料都可以有自己的屬性,來看一下。
let arr = [1,2,3];
arr.width = 5;

let obj = {};
obj.width = 7;

let fn = function() {};
fn.width = 9;

console.log(arr.width);     // 5
console.log(obj.width);     // 7
console.log(fn.width);      // 9
複製程式碼

這是JavaScript裡最基本的概念,引用型別的資料可以有自己的屬性,甚至方法,但是這跟原型有什麼關係?
原型,是JavaScript中函式型別資料才有的屬性。
只有函式才有原型,原型只是函式的一個特殊屬性,僅此而已。那麼原型是用來幹什麼的?其實也很簡單,原型就是用來"傳承"一個函式的屬性和方法的,也就是說,如果一個函式的屬性和方法掛載到它的prototype屬性上,那麼,通過new這個函式所建立的物件,都可以使用prototype屬性上的屬性和方法。

let fn = function() {};
fn.prototype.name = 'abc';
fn.prototype.say = function() {
    console.log(this.name);
}

let f1 = new fn();
f1.name;    // abc
f1.say();   // abc
複製程式碼

這就是JavaScript裡繼承的原理,把屬性和方法掛載到函式的原型(prototype)上,這樣通過new這個函式建立的例項,就能使用這些屬性和方法了。但是有一點需要注意,例項可以使用這些屬性和方法,但不是說這些屬性和方法是例項的,這就牽涉到另一個概念:原型鏈。

原型鏈 && __proto__

希望我已經說清楚了什麼是原型,原型只是函式這種型別資料所擁有的一種特殊屬性而已,掛載到原型上的屬性和方法能夠被例項所使用。而且我們說,這些屬性和方法並不是例項的,而是函式的,那例項是通過什麼方式獲取和使用這些屬性和方法的呢?這就是原型鏈的作用了。
我們知道,JavaScript有許多內建的函式(也叫建構函式),比如:String,Array,Date,RegExp,Functioin,Number等等。除了undefined和null這兩個另類,JavaScript中所有的資料,都是可以由這些內建的函式建立的。我們都知道,每種型別的資料都會有自己的方法,比如字串有splice,split等方法,函式有call,bind等方法,數字有toFixed等方法。
通過原型的概念,我們也瞭解到,這些方法都是通過掛載到建構函式的prototype屬性上,他們才能獲取使用的。那麼每一種型別的資料,是通過什麼找到自己對應的建構函式的原型上的屬性和方法的呢?這需要通過一個屬性:__proto__。
原型是函式才有的屬性,__proto__是所有資料都有的屬性(除了null和undefined)。
字串有自己的__proto__,數字有自己的__proto__,函式有自己的__proto__,原型,也有自己的__proto__。通過__proto__,大家都可以找到自己建構函式的原型(也就是說資料的__proto__是指向它建構函式的原型的)。我們來看下。

'abc'.__proto__ === String.prototype;       // true
567..__proto__ === Number.prototype;        // true(數字的寫法需要注意一下,要兩個".")
[1,2,3].__proto__  === Array.prototype;     // true
複製程式碼

建構函式又可以通過它自己的__proto__屬性,往上查詢它自己的建構函式的原型。比如。

Array.__proto__ === Function.prototype;     // true
Date.__proto__ === Function.prototype;      // true
Error.__proto__ === Function.prototype;     // true
Function.__proto__ === Function.prototype;  // true
複製程式碼

連Function的__proto__都指向它自己的原型,我們不禁好奇,Function.prototype的__proto__又指向誰?(我們說過了,除了null和undefined,所有資料都是有__proto__屬性的)。

Function.prototype.__proto__ === Object.prototype;      // true
複製程式碼

繼續,看看Object.prototype的__proto__又指向誰。

Object.prototype.__proto__;     // null
複製程式碼

竟然是個null,一個沒有__proto__屬性的東西。至此,這個鏈條就走到了它的盡頭了。這就是原型鏈,我們從一個最具體的資料,然後通過它的__proto__屬性,一層一層往上找,一直到Object.prototype,一直到null。這就是“毅種迴圈”,也就是JavaScript中的原型鏈。我們再通過一個數字的原型鏈之旅來感受一下。

let n = 123;

n.__proto__ === Number.prototype;
Number.__proto__ === Function.prototype;
Function.__proto__ === Function.prototype;
Function.prototype.__proto__ === Object.prototype;
Object.prototype.__proto__ === null;
複製程式碼

把n替換成任何null和undefined之外的資料,都是一樣的。在這個鏈條上,所有掛載在prototype上的方法,n都可以使用,但是n自己身上並沒有這些方法。這就是原型鏈的作用。

constructor

constructor這個單詞的字面意思就是構造器。凡是能夠通過函式建立的資料,都有constructor屬性。也就是說,資料的constructor屬性指向它的建構函式。看幾個例子就明白了。

// 陣列:Array
let arr = [1,2,3];
arr.constructor === Array;      // true

// 數字:Number
let num = 123;
num.constructor === Number;     // true

// 建構函式
Array.constructor === Function;         // true
Object.constructor === Function;        // true
Function.constrcutor === undefined;     // true
複製程式碼

到了Function這裡constructor就斷了,這裡就是盡頭(constructor總是指向函式的,更確切的說是建構函式)。稍微有些不同的是,prototype物件也有constructor屬性,它指向函式本身。

Array.prototype.constructor === Array;          // true
Number.prototype.constructor === Number         // true
Function.prototype.constructor === Function;    // true
複製程式碼

所以,關於constructor這個屬性,我們記住兩點就可以了:
1.資料的constructor屬性,指向它的建構函式。
2.原型(prototype)物件的constructor屬性,指向函式本身。

建構函式

JavaScript中建構函式其實就是函式,有特定用途的函式。這種特定用途是什麼?一般來說,是用於建立特定的物件,而這種建立物件的方式,就是通過new關鍵字來呼叫建構函式。

let f1 = function() {};
let f2 = function() {
    this.name = 'jack';
}

let a = new f1();
let b = new f2();
console.log(a);     // {}
console.log(b);     // {name: "jack"}
複製程式碼

在這個例子中,f1,f2都是函式,f1,f2也都是建構函式。但是我們一般不會把f1叫作建構函式,因為沒有意義--f1內部無論程式碼如何龐大複雜,只要沒有出現一個關鍵字,通過new方式呼叫f1,都是沒有意義的,這個關鍵字就是:this。
我們一般會把具有這兩樣特徵的函式,稱為建構函式:
1.函式內部會有this能夠設定一些屬性和方法;
2.函式的原型(prototype)上會掛載一些屬性和方法。
只有這樣,我們通過new去呼叫這個函式的時候,才能生成一些特定的物件,這樣才有意義嘛,就像上面例子中的f2函式一樣,可以通過new呼叫生成一個具有name屬性的物件。
現在我們知道,建構函式就是函式,沒有區別。只是通常建構函式會有許多屬性和方法,無論是在函式內部,還是在函式的原型上,這樣就能用於生成特定的物件了。建構函式的首字母通常會採用大寫字母,用於區別普通的函式。

關於JavaScript中的物件導向,還有另外重要的一環,就是對this的理解,這個至關重要。關於這方面的解釋,有興趣可以參考本人的另外一篇文章:《JavaScript中的this詳解》

相關文章