摘要:類的方法相當于之前我們定義在構造函數的原型上。的構造函數中調用其目的就是調用父類的構造函數。是先創建子類的實例,然后在子類實例的基礎上創建父類的屬性。
前言
首先歡迎大家關注我的Github博客,也算是對我的一點鼓勵,畢竟寫東西沒法獲得變現,能堅持下去也是靠的是自己的熱情和大家的鼓勵。
許久已經沒有寫東西了,因為雜七雜八的原因最近一直沒有抽出時間來把寫作堅持下來,感覺和跑步一樣,一旦松懈下來就很難再次撿起來。最近一直想重新靜下心來寫點什么,選題又成為一個讓我頭疼的問題,最近工作中偶爾會對JavaScript繼承的問題有時候會感覺恍惚,意識到很多知識即使是很基礎,也需要經常的回顧和練習,否則即使再熟悉的東西也會經常讓你感到陌生,所以就選擇這么一篇非常基礎的文章作為今年的開始吧。
JavaScript不像Java語言本身就具有類的概念,JavaScript作為一門基于原型(ProtoType)的語言,(推薦我之前寫的我所認識的JavaScript作用域鏈和原型鏈),時至今日,仍然有很多人不建議在JavaScript中大量使用面對對象的特性。但就目前而言,很多前端框架,例如React都有基于類的概念。首先明確一點,類存在的目的就是為了生成對象,而在JavaScript生成對象的過程并不不像其他語言那么繁瑣,我們可以通過對象字面量語法輕松的創建一個對象:
var person = { name: "MrErHu", sayName: function(){ alert(this.name); } };
一切看起來是這樣的完美,但是當我們希望創建無數個相似的對象時,我們就會發現對象字面量的方法就不能滿足了,當然聰明的你肯定會想到采用工廠模式去創建一系列的對象:
function createObject(name){ return { "name": name, "sayName": function(){ alert(this.name); } } }
但是這樣方式有一個顯著的問題,我們通過工廠模式生成的各個對象之間并沒有聯系,沒法識別對象的類型,這時候就出現了構造函數。在JavaScript中構造函數和普通的函數沒有任何的區別,僅僅是構造函數是通過new操作符調用的。
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引擎這種問題可以忽略。對于構造函數的原型修改,處理上述的方式,可能還存在:
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
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/93677.html
摘要:的類與繼承的類與一般的面向對象語言有很大的不同,類的標識是它的構造函數,下面先定義一個類顯然我們可以看出這兩個函數是不同的,雖然它們實現了相同的功能。利用構造函數來繼承上面的方法子類顯然無法繼承父類的原型函數,這樣不符合我們使用繼承的目的。 javascript的類與繼承 javascript的類與一般的面向對象語言有很大的不同,類的標識是它的構造函數,下面先定義一個類 var ...
摘要:定義類的種方法工廠方法構造函數方法原型方法大家可以看到這種方法有缺陷,類里屬性的值都是在原型里給定的。組合使用構造函數和原型方法使用最廣將構造函數方法和原型方法結合使用是目前最常用的定義類的方法。 JavaScript定義類的4種方法 工廠方法 function creatPerson(name, age) { var obj = new Object...
摘要:函數用于指定對象的行為。關于屬性只在構造器函數的原型上才有的屬性并指向該構造器,改寫了的原型對象默認是沒有屬性的。函數化工廠模式在偽類模式里,構造器函數不得不重復構造器已經完成的工作。 1.對象適合于收集和管理數據,容易形成樹型結構。Javascript包括一個原型鏈特性,允許對象繼承另一對象的屬性。正確的使用它能減少對象的初始化時間和內存消耗。2.函數它們是javascript的基礎...
摘要:在類內部的方法中使用時。類的私有方法兩個下劃線開頭,聲明該方法為私有方法,不能在類地外部調用。先在本類中查找調用的方法,找不到才去基類中找。如果在繼承元組中列了一個以上的類,那么它就被稱作多重繼承。 類定義 類對象:創建一個類之后,可以通過類名訪問、改變其屬性、方法 實例對象:類實例化后,可以使用其屬性,可以動態的為實例對象添加屬性(類似javascript)而不影響類對象。 類...
閱讀 1619·2021-11-11 10:59
閱讀 2624·2021-09-04 16:40
閱讀 3650·2021-09-04 16:40
閱讀 2979·2021-07-30 15:30
閱讀 1615·2021-07-26 22:03
閱讀 3164·2019-08-30 13:20
閱讀 2225·2019-08-29 18:31
閱讀 439·2019-08-29 12:21