首先歡迎你們關注個人Github博客,也算是對個人一點鼓勵,畢竟寫東西無法得到變現,能堅持下去也是靠的是本身的熱情和你們的鼓勵。javascript
許久已經沒有寫東西了,由於雜七雜八的緣由最近一直沒有抽出時間來把寫做堅持下來,感受和跑步同樣,一旦鬆懈下來就很難再次撿起來。最近一直想從新靜下心來寫點什麼,選題又成爲一個讓我頭疼的問題,最近工做中偶爾會對JavaScript繼承的問題有時候會感受恍惚,意識到不少知識即便是很基礎,也須要常常的回顧和練習,不然即便再熟悉的東西也會常常讓你感到陌生,因此就選擇這麼一篇很是基礎的文章做爲今年的開始吧。 前端
JavaScript不像Java語言自己就具備類的概念,JavaScript做爲一門基於原型(ProtoType
)的語言,(推薦我以前寫的我所認識的JavaScript做用域鏈和原型鏈),時至今日,仍然有不少人不建議在JavaScript中大量使用面對對象的特性。但就目前而言,不少前端框架,例如React都有基於類的概念。首先明確一點,類存在的目的就是爲了生成對象,而在JavaScript生成對象的過程並不不像其餘語言那麼繁瑣,咱們能夠經過對象字面量語法輕鬆的建立一個對象:java
var person = {
name: "MrErHu",
sayName: function(){
alert(this.name);
}
};
複製代碼
一切看起來是這樣的完美,可是當咱們但願建立無數個類似的對象時,咱們就會發現對象字面量的方法就不能知足了,固然聰明的你確定會想到採用工廠模式去建立一系列的對象: git
function createObject(name){
return {
"name": name,
"sayName": function(){
alert(this.name);
}
}
}
複製代碼
可是這樣方式有一個顯著的問題,咱們經過工廠模式生成的各個對象之間並無聯繫,無法識別對象的類型,這時候就出現了構造函數。在JavaScript中構造函數和普通的函數沒有任何的區別,僅僅是構造函數是經過new
操做符調用的。 github
function Person(name, age, job){
this.name = name;
this.sayName = function(){
alert(this.name);
};
}
var obj = new Person();
obj.sayName();
複製代碼
咱們知道new
操做符會作如下四個步驟的操做: 數組
[[Prototype]]
(非正式屬性__proto__
)鏈接到構造函數的原型this
會綁定新的對象new
表達式中的函數調用會自動返回這個新對象 這樣咱們經過構造函數的方式生成的對象就能夠進行類型判斷。可是單純的構造函數模式會存在一個問題,就是每一個對象的方法都是相互獨立的,而函數本質上就是一種對象,所以就會形成大量的內存浪費。回顧new
操做符的第三個步驟,咱們新生成對象的內部屬性[[Prototype]]
會鏈接到構造函數的原型上,所以利用這個特性,咱們能夠混合構造函數模式和原型模式,解決上面的問題。前端框架
function Person(name, age, job){
this.name = name;
}
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
}
var obj = new Person();
obj.sayName();
複製代碼
咱們經過將sayName
函數放到構造函數的原型中,這樣生成的對象在使用sayName
函數經過查找原型鏈就能夠找到對應的方法,全部對象共用一個方法就解決了上述問題,即便你可能認爲原型鏈查找可能會耽誤一點時間,實際上對於如今的JavaScript引擎這種問題能夠忽略。對於構造函數的原型修改,處理上述的方式,可能還存在: app
Person.prototype.sayName = function(){
alert(this.name);
}
複製代碼
咱們知道函數的原型中的constructor
屬性是執行函數自己,若是你是將原來的原型替換成新的對象而且constructor
對你又比較重要記得手動添加,所以第一種並不許確,由於constructor
是不可枚舉的,所以更準確的寫法應該是:框架
Object.defineProperty(Person, "constructor", {
configurable: false,
enumerable: false,
writable: true,
value: Person
});
複製代碼
到如今爲止,咱們會以爲在JavaScript中建立個類也太麻煩了,其實遠遠不止如此,好比咱們建立的類可能會被直接調用,形成全局環境的污染,好比: 函數
Person('MrErHu');
console.log(window.name); //MrErHu
複製代碼
不過咱們迎來了ES6的時代,事情正在其變化,ES6爲咱們在JavaScript中實現了類的概念,上面的的代碼均可以用簡介的類(class)實現。
class Person {
constructor(name){
this.name = name;
}
sayName(){
alert(this.name);
}
}
複製代碼
經過上面咱們就定義了一個類,使用的時候同以前同樣:
let person = new Person('MrErHu');
person.sayName(); //MrErHu
複製代碼
咱們能夠看到,類中的constructor
函數負擔起了以前的構造函數的功能,類中的實例屬性均可以在這裏初始化。類的方法sayName
至關於以前咱們定義在構造函數的原型上。其實在ES6中類僅僅只是函數的語法糖:
typeof Person //"function"
複製代碼
相比於上面本身建立的類方式,ES6中的類有幾個方面是與咱們自定義的類不相同的。首先類是不存在變量提高的,所以不能先使用後定義:
let person = new Person('MrErHu')
class Person { //...... }
複製代碼
上面的使用方式是錯誤的。所以類更像一個函數表達式。
其次,類聲明中的全部代碼都是自動運行在嚴格模式下,而且不能讓類脫離嚴格模式。至關於類聲明中的全部代碼都運行在"use strict"中。
再者,類中的全部方法都是都是不可枚舉的。
最後,類是不能直接調用的,必須經過new
操做符調用。其實對於函數有內部屬性[[Constructor]]
和[[Call]]
,固然這兩個方法咱們在外部是無法訪問到的,僅存在於JavaScript引擎。當咱們直接調用函數時,其實就是調用了內部屬性[[Call]]
,所作的就是直接執行了函數體。當咱們經過new
操做符調用時,其實就是調用了內部屬性[[Constructor]]
,所作的就是建立新的實例對象,並在實例對象上執行函數(綁定this
),最後返回新的實例對象。由於類中不含有內部屬性[[Call]]
,所以是無法直接調用的。順即可以提一句ES6中的元屬性 new.target
所謂的元屬性指的就是非對象的屬性,能夠提供給咱們一些補充信息。new.target
就是其中一個元屬性,當調用的是[[Constructor]]
屬性時,new.target
就是new
操做符的目標,若是調用的是[[Call]]
屬性,new.target
就是undefined
。其實這個屬性是很是有用的,好比咱們能夠定義一個僅能夠經過new
操做符調用的函數:
function Person(){
if(new.target === undefined){
throw('該函數必須經過new操做符調用');
}
}
複製代碼
或者咱們能夠用JavaScript建立一個相似於C++中的虛函數的函數:
class Person {
constructor() {
if (new.target === Person) {
throw new Error('本類不能實例化');
}
}
}
複製代碼
在沒有ES6的時代,想要實現繼承是一個不小的工做。一方面咱們要在派生類中建立父類的屬性,另外一方面咱們須要繼承父類的方法,例以下面的實現方法:
function Rectangle(width, height){
this.width = width;
this.height = height;
}
Rectangle.prototype.getArea = function(){
return this.width * this.height;
}
function Square(length){
Rectangle.call(this, length, length);
}
Square.prototype = Object.create(Rectangle.prototype, {
constructor: {
value: Square,
enumerable: false,
writable: false,
configurable: false
}
});
var square = new Square(3);
console.log(square.getArea());
console.log(square instanceof Square);
console.log(square instanceof Rectangle);
複製代碼
首先子類Square
爲了建立父類Rectangle
的屬性,咱們在Square
函數中以Rectangle.call(this, length, length)
的方式進行了調用,其目的就是在子類中建立父類的屬性,爲了繼承父類的方法,咱們給Square
賦值了新的原型。除了經過Object.create
方式,你應該也見過如下方式:
Square.prototype = new Rectangle();
Object.defineProperty(Square.prototype, "constructor", {
value: Square,
enumerable: false,
writable: false,
configurable: false
});
複製代碼
Object.create
是ES5新增的方法,用於建立一個新對象。被建立的對象會繼承另外一個對象的原型,在建立新對象時還能夠指定一些屬性。Object.create
指定屬性的方式與Object.defineProperty
相同,都是採用屬性描述符的方式。所以能夠看出,經過Object.create
與new
方式實現的繼承其本質上並無什麼區別。 可是ES6能夠大大簡化繼承的步驟:
class Rectangle{
constructor(width, height){
this.width = width;
this.height = height;
}
getArea(){
return this.width * this.height;
}
}
class Square extends Rectangle{
construct(length){
super(length, length);
}
}
複製代碼
咱們能夠看到經過ES6的方式實現類的繼承是很是容易的。Square
的構造函數中調用super
其目的就是調用父類的構造函數。固然調用super
函數並非必須的,若是你默認缺省了構造函數,則會自動調用super
函數,並傳入全部的參數。 不只如此,ES6的類繼承賦予了更多新的特性,首先extends
能夠繼承任何類型的表達式,只要該表達式最終返回的是一個可繼承的函數(也就是講extends
能夠繼承具備[[Constructor]]
的內部屬性的函數,好比null
和生成器函數、箭頭函數都不具備該屬性,所以不能夠被繼承)。好比:
class A{}
class B{}
function getParentClass(type){
if(//...){
return A;
}
if(//...){
return B;
}
}
class C extends getParentClass(//...){
}
複製代碼
能夠看到咱們經過上面的代碼實現了動態繼承,能夠根據不一樣的判斷條件繼承不一樣的類。 ES6的繼承與ES5實現的類繼承,還有一點不一樣。ES5是先建立子類的實例,而後在子類實例的基礎上建立父類的屬性。而ES6正好是相反的,是先建立父類的實例,而後在父類實例的基礎上擴展子類屬性。利用這個屬性咱們能夠作到一些ES5沒法實現的功能:繼承原生對象。
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});
var colors = new MyArray();
colors[0] = "red";
colors.length // 0
colors.length = 0;
colors[0] // "red"
複製代碼
能夠看到,繼承自原生對象Array
的MyArray
的實例中的length
並不能如同原生Array
類的實例 同樣能夠動態反應數組中元素數量或者經過改變length
屬性從而改變數組中的數據。究其緣由就是由於傳統方式實現的數組繼承是先建立子類,而後在子類基礎上擴展父類的屬性和方法,因此並無繼承的相關方法,但ES6卻能夠輕鬆實現這一點:
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined
複製代碼
咱們能夠看見經過extends
實現的MyArray
類建立的數組就能夠同原生數組同樣,使用length
屬性反應數組變化和改變數組元素。不只如此,在ES6中,咱們可使用Symbol.species
屬性使得當咱們繼承原生對象時,改變繼承自原生對象的方法的返回實例類型。例如,Array.prototype.slice
原本返回的是Array
類型的實例,經過設置Symbol.species
屬性,咱們可讓其返回自定義的對象類型:
class MyArray extends Array {
static get [Symbol.species](){
return MyArray;
}
constructor(...args) {
super(...args);
}
}
let items = new MyArray(1,2,3,4);
subitems = items.slice(1,3);
subitems instanceof MyArray; // true
複製代碼
最後須要注意的一點,extends
實現的繼承方式能夠繼承父類的靜態成員函數,例如:
class Rectangle{
// ......
static create(width, height){
return new Rectangle(width, height);
}
}
class Square extends Rectangle{
//......
}
let rect = Square.create(3,4);
rect instanceof Square; // true
複製代碼