在GitHub上看到的關於JavaScript原型與繼承的講解,感覺很有用,為方便以後閱讀,copy到自己的隨筆中。 原文地址:https://github.com/dreamapplehappy/blog/blob/master/2018/12/30/README.md 首先我們需要知道的是,Ja... ...
在GitHub上看到的關於JavaScript原型與繼承的講解,感覺很有用,為方便以後閱讀,copy到自己的隨筆中。
原文地址:https://github.com/dreamapplehappy/blog/blob/master/2018/12/30/README.md
首先我們需要知道的是,JavaScript是一種動態語言,本質上說它是沒有Class
(類)的;但是它也需要一種繼承的方式, 那就是原型繼承;JavaScript對象的一些屬性和方法都是繼承自別的對象。
很多同學對JavaScript的原型和繼承不是很理解,一個重要的原因就是大家沒有理解__proto__
和prototype
這兩個屬性的意思。 接下來我們先來好好梳理一下這兩個屬性,看看它們存在哪裡,代表了什麼意義,又有什麼作用。
首先來說一下__proto__
這個屬性吧,我們需要知道的是,除了null
和undefined
,JavaScript中的所有數據類型都有這個屬性; 它表示的意義是:當我們訪問一個對象的某個屬性的時候,如果這個對象自身不存在這個屬性, 那麼就從這個對象的__proto__
(為了方便下麵描述,這裡暫且把這個屬性稱作p0
)屬性上面 繼續查找這個屬性,如果p0
上面還存在__proto__
(p1)屬性的話,那麼就會繼續在p1
上面查找響應的屬性, 直到查找到這個屬性,或者沒有__proto__
屬性為止。
我們可以用下麵這兩幅圖來表示:
上面這幅圖表示在obj
的原型鏈
上面找到了屬性名字是a
的值
上面這幅圖表示在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__
屬性做一個總結:
- 存在哪裡? 除了
null
和undefined
所有其他的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
表示一個對象的構造函數,除了null
和undefined
以外,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的指向修改為Animal.prototype
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