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詳解》