JavaScript原型與繼承的祕密

原文發佈在dreamapplehappy/blog,本文如如有更新,都會在個人博客進行更新。javascript

咱們最想誇耀的事物,就是咱們所未擁有的事物 《羅生門》- 芥川龍之介java

JavaScript的原型與繼承是每個學習JavaScript的同窗都會面對的一個問題,也是不少面試的必考題目; 可是常常會有一些同窗對此只知其一;不知其二,或者是淺嘗輒止;這是由於不少講解原型與繼承的文章寫的不是那麼通俗易懂, 而本文的目的就是一次性的幫助你們把這一系列的知識點梳理清楚;但願我此次可以作一個好的投球手。git

首先咱們須要知道的是,JavaScript是一種動態語言,本質上說它是沒有Class(類)的;可是它也須要一種繼承的方式, 那就是原型繼承;JavaScript對象的一些屬性和方法都是繼承自別的對象。github

不少同窗對JavaScript的原型和繼承不是很理解,一個重要的緣由就是你們沒有理解__proto__prototype這兩個屬性的意思。 接下來咱們先來好好梳理一下這兩個屬性,看看它們存在哪裏,表明了什麼意義,又有什麼做用。面試

首先來講一下__proto__這個屬性吧,咱們須要知道的是,除了nullundefined,JavaScript中的全部數據類型都有這個屬性; 它表示的意義是:當咱們訪問一個對象的某個屬性的時候,若是這個對象自身不存在這個屬性, 那麼就從這個對象的__proto__(爲了方便下面描述,這裏暫且把這個屬性稱做p0)屬性上面 繼續查找這個屬性,若是p0上面還存在__proto__(p1)屬性的話,那麼就會繼續在p1上面查找響應的屬性, 直到查找到這個屬性,或者沒有__proto__屬性爲止。 咱們能夠用下面這兩幅圖來表示:瀏覽器

__proto__

上面這幅圖表示在obj原型鏈上面找到了屬性名字是a的值app

__proto__

上面這幅圖表示在obj原型鏈上面沒有找到屬性名字是a的值函數

咱們把一個對象的__proto__屬性所指向的對象,叫作這個對象的原型;咱們能夠修改一個對象的原型來讓這個對象擁有某種屬性,或者某個方法。性能

// 修改一個Number類型的值的原型
const num = 1;
num.__proto__.name = "My name is 1";
console.log(num.name); // My name is 1

// 修改一個對象的原型
const obj = {};
obj.__proto__.name = "dreamapple";
console.log(obj.name); // dreamapple
複製代碼

這裏須要特別注意的是,__proto__這個屬性雖然被大多數的瀏覽器支持,可是其實它僅在ECMAScript 2015 規範中被準確的定義, 目的是爲了給這個傳統的功能定製一個標準,以確保瀏覽器之間的兼容性。經過使用__proto__屬性來修改一個對象的原型是很是慢且影響性能的一種操做。 因此,如今若是咱們想要獲取一個對象的原型,推薦使用Object.getPrototypeOf 或者Reflect.getPrototypeOf,設置一個對象的原型推薦使用Object.setPrototypeOf或者是Reflect.setPrototypeOf學習

到這裏爲止,咱們來對__proto__屬性作一個總結:

  • 存在哪裏? 除了nullundefined全部其餘的JavaScript對象或者原始類型都有這個屬性
  • 表明了什麼? 表示了一個對象的原型
  • 有什麼做用? 能夠獲取和修改一個對象的原型

說完__proto__屬性,接下來咱們就要好好的來理解一下prototype屬性了;首先咱們須要記住的是,這個屬性通常只存在於函數對象上面; 只要是可以做爲構造器的函數,他們都包含這個屬性。也就是說,只要這個函數可以經過使用new操做符來生成一個新的對象, 那麼這個函數確定具備prototype屬性。由於咱們自定義的函數均可以經過new操做符生成一個對象,因此咱們自定義的函數都有prototype 這個屬性。

// 函數字面量
console.log((function(){}).prototype); // {constructor: ƒ}

// Date構造器
console.log(Date.prototype); // {constructor: ƒ, toString: ƒ, toDateString: ƒ, toTimeString: ƒ, toISOString: ƒ, …}

// Math.abs 不是構造器,不能經過new操做符生成一個新的對象,因此不含有prototype屬性
console.log(Math.abs.prototype); // undefined
複製代碼

那這個prototype屬性有什麼做用呢?這個prototype屬性的做用就是:函數經過使用new操做符生成的一個對象, 這個對象的原型(也就是__proto__)指向該函數的prototype屬性。 那麼一個比較簡潔的表示__proto__prototype 屬性之間關係的等式也就出來了,以下所示:

// 其中F表示一個自定義的函數或者是含有prototype屬性的內置函數
new F().__proto__ === F.prototype // true
複製代碼

咱們可使用下面這張圖來更加形象的表示上面這種關係:

看到上面等式,我想你們對於__proto__prototype之間關係的理解應該會更深一層了。

好,接下來咱們對prototype屬性也作一個總結:

  • 存在哪裏? 自定義的函數,或者可以經過new操做符生成一個對象的內置函數
  • 表明了什麼? 它表示了某個函數經過new操做符生成的對象的原型
  • 有什麼做用? 可讓一個函數經過new操做符生成的許多對象共享一些方法和屬性

其實到這裏爲止,關於JavaScript的原型和繼承已經講得差很少了;下面的內容是一些基於上面的一些拓展, 可讓你更好地理解咱們上面所說的。

當咱們理解了上面的知識點以後,咱們就能夠對下面的表達式作一個判斷了:

// 由於Object是一個函數,函數的構造器都是Function
Object.__proto__ === Function.prototype // true

// 經過函數字面量定義的函數的__proto__屬性都指向Function.prototype
(function(){}).__proto__ === Function.prototype // true

// 經過對象字面量定義的對象的__proto__屬性都是指向Object.prototype
({}).__proto__ === Object.prototype // true

// Object函數的原型的__proto__屬性指向null
Object.prototype.__proto__ === null // true

// 由於Function自己也是一個函數,因此Function函數的__proto__屬性指向它自身的prototype
Function.__proto__ === Function.prototype // true

// 由於Function的prototype是一個對象,因此Function.prototype的__proto__屬性指向Object.prototype
Function.prototype.__proto__ === Object.prototype // true
複製代碼

若是你可以把上面的表達式都梳理清楚的話,那麼說明你對這部分知識掌握的仍是不錯的。

談及JavaScript的原型和繼承,那麼咱們還須要知道另外一個概念;那就是constructor,那什麼是constructor呢? constructor表示一個對象的構造函數,除了nullundefined之外,JavaScript中的全部數據類型都有這個屬性; 咱們能夠經過下面的代碼來驗證一下:

null.constructor // Uncaught TypeError: Cannot read property 'constructor' of null ...
undefined.constructor // Uncaught TypeError: Cannot read property 'constructor' of undefined ...

(true).constructor // ƒ Boolean() { [native code] }
(1).constructor // ƒ Number() { [native code] }
"hello".constructor // ƒ String() { [native code] }
複製代碼

咱們還可使用下面的圖來更加具體的表現:

可是其實上面這張圖的表示並不算準確,由於一個對象的constructor屬性確切地說並非存在這個對象上面的; 而是存在這個對象的原型上面的(若是是多級繼承須要手動修改原型的constructor屬性,見文章末尾的代碼),咱們可使用下面的代碼來解釋一下:

const F = function() {};
// 當咱們定義一個函數的時候,這個函數的prototype屬性上面的constructor屬性指向本身自己
F.prototype.constructor === F; // true
複製代碼

下面的圖片形象的展現了上面的代碼所表示的內容:

關於constructor還有一些須要注意的問題,對與JavaScript的原始類型來講,它們的constructor屬性是隻讀的,不能夠修改。 咱們能夠經過下面的代碼來驗證一下:

(1).constructor = "something";
console.log((1).constructor); // 輸出 ƒ Number() { [native code] }
複製代碼

固然,若是你真的想更改這些原始類型的constructor屬性的話,也不是不能夠,你能夠經過下面的方式來進行修改:

Number.prototype.constructor = "number constructor";
(1).constructor = 1;
console.log((1).constructor); // 輸出 number constructor
複製代碼

固然上面的方式咱們是不推薦你在真實的開發中去使用的,若是你想要了解更多關於constructor的內容,能夠看看Object.prototype.constructor

接下來,我會使用一些代碼來把今天講解的知識再大體的回顧一下:

function Animal(name) {
  this.name = name;
}

Animal.prototype.setName = function(name) {
  this.name = name;
};
Animal.prototype.getName = function(name) {
  return this.name;
};

function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);

// 由於上面的語句將咱們原來的prototype的指向修改了,因此咱們要從新定義Dog的prototype屬性的constructor屬性
Reflect.defineProperty(Dog.prototype, "constructor", {
  value: Dog,
  enumerable: false, // 不可枚舉
  writable: true
});

const animal = new Animal("potato");
console.log(animal.__proto__ === Animal.prototype); // true
console.log(animal.constructor === Animal); // true
console.log(animal.name); // potato

const dog = new Dog("potato", "labrador");
console.log(dog.name); // potato
console.log(dog.breed); // labrador
console.log(dog.__proto__ === Dog.prototype); // true
console.log(dog.constructor === Dog); // true
複製代碼

這篇文章到這裏基本上能夠告一段落了,可是其實關於JavaScript的原型與繼承還有許多內容,也還有許多能夠研究的地方;可是這篇文章到這裏就算是結束了。

我後面還會寫一些關於JavaScript原型與繼承的內容,若是你們有興趣的話,能夠關注一下;也能夠點擊這裏發表留言。

相關文章
相關標籤/搜索