摘要:構造函數創建對象為了能夠判斷實例與對象的關系,我們就使用構造函數來搞定。像和這樣的原生構造函數,在運行時自動出現在執行環境中。
大綱:
一、理解對象
1.1 屬性類型
1.2 屬性方法
二、創建對象
2.1 簡單方式創建
2.2 工廠模式
2.3 構造函數
2.4 原型
三、繼承
3.1 原型鏈
3.2 借用構造函數
3.3 組合繼承(原型鏈+借用構造函數)
3.4 原型式繼承
3.5 寄生式繼承
3.6 寄生組合繼承
3.6 總結
四、ES6繼承
Class關鍵字
extends繼承
super關鍵字
原生構造函數拓展
Mixin模式的實現
一、理解對象ECMAScript中沒有類的概念,因此它的對象也與基于類的語言的對象有所不同。
ECMA-262把對象定義為“無序屬性的集合,其屬性可以包含基本值,對象或者函數”。對象的每個屬性或方法都有一個名字,而每個名字映射到一個值。我們可以把ECMAScript的對象想象成散列表:無非就是一組鍵值對,其值可以是數據或函數。
每個對象都是基于一個引用類型創建的,這個引用類型可以是原生類型,也可以是開發人員定義的類型。
ECMAScript中有兩種屬性:數據屬性和訪問器屬性。
1.1.1數據屬性數據屬性包含一個數據值的位置,在這個位置可以讀取和寫入值。數據屬性有4個描述其行為的特征。
[[Configurable]]:表示能否通過delete刪除屬性從而重新定義屬性。能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。直接在對象上定義的屬性,它們的這個特性默認值為true。
[[Enumerable]]:表示能否通過for-in循環返回屬性。直接在對象上定義的屬性,它們的這個特性默認值為true。
[[Writable]]:表示能否修改屬性的值。直接在對象上定義的屬性,它們的這個特性默認為true。
[[Value]]:包含這個屬性的數據值。讀取屬性值的時候,從這個位置讀;寫入屬性值的時候,把新值保存在這個位置。這個特性的默認值為undefined。
要修改屬性默認的特性,必須通過ES5的Object.defineProperty()方法。這個方法接收三個參數:屬性所在的對象、屬性的名字和一個描述符對象。其中,描述符(descriptor)對象的屬性必須是:configurable、enumerable、writable和value。設置其中的一或多個值,可以更改對應的特征值。
var person = {}; Object.defineProperty(person, "name", { writable: false, //不能修改屬性的值.... configurable: false, //不能通過delete刪除屬性..... value: "Jason" //寫入屬性值 }); console.log(person.name); //Jason person.name = "Cor"; console.log(person.name); //Jason delete person.name; console.log(person.name); //Jason
注意,一旦把屬性設置為不可配置的,就不能再把它更改為可配置的了。此時再調用
Object.defineProperty()方法修改除writable之外的特性就會導致錯誤。 var person = {}; Object.defineProperty(person, "name", { configurable: false, value: "Jason" }); //拋出錯誤 Object.defineProperty(person, "name", { comfogirable: true, //這行代碼修改了特性導致報錯 value: "Cor" });
在調用Object.defineProperty()方法時,如果不指定configurable、enumerable和writable特性的默認值都是false。
1.1.2訪問器屬性訪問器屬性不包含數據值,它包含一對getter和setter函數(不過,這兩個函數都不是必須的)。在讀取訪問器屬性時回調去getter函數,這個函數負責返回有效的值;在寫入訪問器屬性時,會調用setter函數并傳入新值,這個函數負責決定如何處理數據。訪問器屬性有如下4個特性:
[[Configurable]]:表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為數據屬性。對于直接在對象上定義的屬性,這個特性默認值為true。
[[Enumerable]]:表示能否通過for-in循環返回屬性。對于直接在對象上定義的屬性,這個特性的默認值為true。
[[Get]]:在讀取屬性時調用的函數。默認值為undefined。
[[Set]]:在寫入屬性時調用的函數。默認值為undefined。
訪問器屬性不能直接定義,必須使用Object.defineProperty()方法來定義。
注意,一旦定義了取值函數get(或存值函數set),就不能將writable屬性設為true,或者同時定義value屬性,否則會報錯。
var book = { _year: 2004, edition: 1 }; Object.defineProperty(book, "year", { get: function() { return this._year; }, set: function(newValue) { //接受新值的參數 if(newValue > 2004) { this._year = newValue; this.edition += newValue - 2004; } } }); book.year = 2005; //寫入訪問器,會調用setter并傳入新值 console.log(book.edition); //2
var obj = {}; Object.defineProperty(obj, "p", { value: 123, get: function() { return 456; } }); // TypeError: Invalid property. // A property cannot both have accessors and be writable or have a value Object.defineProperty(obj, "p", { writable: true, get: function() { return 456; } }); // TypeError: Invalid property descriptor. // Cannot both specify accessors and a value or writable attribute1.2屬性方法
Object.getOwnPropertyDescriptor()
該方法可以獲取屬性描述對象。它的第一個參數是一個對象,第二個參數是一個字符串,對應該對象的某個屬性名。注意,該方法只能用于對象自身的屬性,不能用于繼承的屬性。
var obj = { p: "a" }; Object.getOwnPropertyDescriptor(obj, "p") // Object { value: "a", // writable: true, // enumerable: true, // configurable: true // }
Object.getOwnPropertyNames()
該方法返回一個數組,成員是參數對象自身的全部屬性的屬性名,不管該屬性是否可遍歷。下面例子中,obj.p1是可遍歷的,obj.p2是不可遍歷的。但是Object.getOwnPropertyNames會將它們都返回。
var obj = Object.defineProperties({}, { p1: { value: 1, enumerable: true }, p2: { value: 2, enumerable: false } }); Object.getOwnPropertyNames(obj) // ["p1", "p2"]
與Object.keys的行為不同,Object.keys只返回對象自身的可遍歷屬性的全部屬性名。下面代碼中,數組自身的length屬性是不可遍歷的,Object.keys不會返回該屬性。第二個例子的Object.prototype也是一個對象,所以實例對對象都會繼承它,它自身的屬性都是不可遍歷的。
Object.keys([]) // [] Object.getOwnPropertyNames([]) // [ "length" ] Object.keys(Object.prototype) // [] Object.getOwnPropertyNames(Object.prototype) // ["hasOwnProperty", // "valueOf", // "constructor", // "toLocaleString", // "isPrototypeOf", // "propertyIsEnumerable", // "toString"]
Object.defineProperty(),Object.defineProperties()
Object.defineProperty()方法允許通過屬性描述對象,定義或修改一個屬性,然后返回修改后的對象。實例上面已經介紹。
如果一次性定義或修改多個屬性,可以使用Object.defineProperties方法。
var obj = Object.defineProperties({}, { p1: { value: 123, enumerable: true }, p2: { value: "abc", enumerable: true }, p3: { get: function () { return this.p1 + this.p2 }, enumerable:true, configurable:true } }); obj.p1 // 123 obj.p2 // "abc" obj.p3 // "123abc"
Object.prototype.propertyIsEnumerable()
實例對象的propertyIsEnumerable方法返回一個布爾值,用來判斷某個屬性是否可遍歷。
var obj = {}; obj.p = 123; obj.propertyIsEnumerable("p") // true obj.propertyIsEnumerable("toString") // false二、創建對象 2.1 簡單方式創建
我們可以通過new的方式創建一個對象,也可以通過字面量的形式創建一個簡單的對象。
var obj = new Object(); 或 var obj = {}; //為對象添加方法,屬性 var person = {}; person.name = "TOM"; person.getName = function() { return this.name; } // 也可以這樣 var person = { name: "TOM", getName: function() { return this.name; } }
這種方式創建對象簡單,但也存在一些問題:創建出來的對象無法實現對象的重復利用,并且沒有一種固定的約束,操作起來可能會出現這樣或者那樣意想不到的問題。如下面這種情況。
var a = new Object;
var b = new Object;
var c = new Object;
c[a]=a;
c[b]=b;
console.log(c[a]===a); //輸出什么 false
該題的詳細解析請參考文章一條面試題
當我們需要創建一系列相似對象時,顯然上面簡單的對象創建方式已經不可以了,這會使代碼中出現很對重復的編碼,造成代碼冗余難維護。就以person對象為例,假如我們在實際開發中,需要一個名字叫做TOM的person對象,同時還需要另外一個名為Jake的person對象,雖然它們有很多相似之處,但是我們不得不重復寫兩次。沒增加一個新的person對象,就重復一遍代碼,聽起來就是很崩潰的。
var perTom = { name: "TOM", age: 20, getName: function() { return this.name } }; var perJake = { name: "Jake", age: 22, getName: function() { return this.name } }
我們可以使用工廠模式的方式解決這個問題。顧名思義,工廠模式就是我們提供一個模子,然后通過這個模子復制出我們需要的對象。需要多少,就復制多少。
var createPerson = function(name, age) { // 聲明一個中間對象,該對象就是工廠模式的模子 var o = new Object(); // 依次添加我們需要的屬性與方法 o.name = name; o.age = age; o.getName = function() { return this.name; } return o; } // 創建兩個實例 var perTom = createPerson("TOM", 20); var PerJake = createPerson("Jake", 22);
工廠模式幫助我們解決了重復代碼的問題,可以快速的創建對象。但是這種方式仍然存在兩個問題:沒有辦法識別對象實例的類型
var obj = {}; var foo = function() {} console.log(obj instanceof Object); // true console.log(foo instanceof Function); // true console.log(perTom instancceof (類名??)); //發現好像并不存在一個Person類
因此,在工廠模式的基礎上,我們需要使用構造函數的方式來解決這個問題。
2.3 構造函數 2.3.1 new關鍵字在Javascript中,new關鍵字十分神奇,可以讓一個函數變的與眾不同。看下面這個例子。
function demo() { console.log(this); } demo(); // window,嚴格模式下this指向undefined new demo(); // demo
從這個例子我們可以看到,使用new之后,函數內部發生了一些變化,this指向發生了改變。那么new關鍵字到底都做了什么事情呢?
// 先一本正經的創建一個構造函數,其實該函數與普通函數并無區別 var Person = function(name, age) { this.name = name; this.age = age; this.getName = function() { return this.name; } } // 將構造函數以參數形式傳入 function New(func) { // 聲明一個中間對象,該對象為最終返回的實例 var res = {}; if (func.prototype !== null) { // 將實例的原型指向構造函數的原型 res.__proto__ = func.prototype; } // ret為構造函數執行的結果,這里通過apply,將構造函數內部的this指向修改為指向實例對象res var ret = func.apply(res, Array.prototype.slice.call(arguments, 1)); // 當我們在構造函數中明確指定了返回對象時,那么new的執行結果就是該返回對象(即在構造函數中明確寫了return this;) if ((typeof ret === "object" || typeof ret === "function") && ret !== null) { return ret; } // 如果沒有明確指定返回對象,則默認返回res,這個res就是實例對象 return res; } // 通過new聲明創建實例,這里的p1,實際接收的正是new中返回的res var person1 = New(Person, "tom", 20); console.log(person1.getName()); // 當然,這里也可以判斷出實例的類型了 console.log(p1 instanceof Person); // true
JavaScript內部會通過一些特殊處理,將var p1 = New(Person, ’tom’, 20);等效于var person1 = new Person(’tom’, 20); 我們熟悉的這種形式。具體是怎么處理的,暫時沒法作出解釋,需要更深入的了解原理。
當構造函數顯示的return,會出現什么情況?我們先來列出幾種返回的情況,看一下返回什么結果:
//直接 return function A(){ return; } //返回 數字類型 function B(){ return 123; } //返回 string類型 function C(){ return "abcdef"; } //返回 數組 function D(){ return ["aaa", "bbb"]; } //返回 對象 function E(){ return {a: 2}; } //返回 包裝類型 function F(){ return new Number(123); } //結果是: A {} B {} C {} ["aaa", "bbb"] Object {a: 2} Number {[[PrimitiveValue]]: 123} A {}
結合構造函數我們來看一下結果:
function Super (a) { this.a = a; return 123; } Super.prototype.sayHello = function() { alert("hello world"); } function Super_ (a) { this.a = a; return {a: 2}; } Super_.prototype.sayHello = function() { alert("hello world"); } new Super(1); new Super_(1); //結果 Super {a: 1} 具有原型方法sayHello Object {a: 2}
總結一下:在構造函數中 return 基本類型不會影響構造函數的值,而 return 對象類型 則會替代構造函數返回該對象。
2.3.2 構造函數創建對象為了能夠判斷實例與對象的關系,我們就使用構造函數來搞定。
像Object和Array這樣的原生構造函數,在運行時自動出現在執行環境中。我們也可以創建自定義的構造函數,從而定義對象類型的屬性和方法。例如,
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.getName = function() { console.log(this.name); } } var person1 = new Person("Jason", 18, "WEB”); var person2 = new Person("Cor", 19, "WEB"); console.log(person1.getName()); //Jason console.log(person1 instanceof Person); //true
構造函數模式和工廠模式存在一下不同之處:
沒有顯示的創建對象(new Object() 或者 var a = {})
直接將屬性和方法賦給this對象
沒有return語句
關于構造函數,如果你暫時不能夠理解new的具體實現,就先記住下面這幾個結論:
與普通函數相比,構造函數并沒有任何特別的地方,首字母大寫只是我們開發中的約定規定,用于區分普通函數
new關鍵字讓構造函數擁有了與普通函數不同的許多特點,new的過程中,執行了下面的過程:
聲明一個中間對象,即實例對象
將該中間對象的原型指向構造函數原型(res.__proto__ = func.prototype)
將構造函數this,指向該中間對象
返回該中間對象,及返回實例對象
2.3.3 把構造函數當普通函數//當作構造函數使用 var person = new Person("Jason", 18, "web"); person.getName(); //“Jason" //作為普通函數調用 Person("Cor", 19, "web"); //添加到window window.getName(); //“cor" //在另一個對象的作用域中調用 var o = new Object(); Person.call(o, "Kristen", 22, "web"); o.getName(); //"kriten"
當在全局作用域中調用一個函數時,this對象總是指向Global對象(在瀏覽器中的window對象),最后使用了call() ( 或者apply() )在某個特殊對象的作用域中調用Person()函數。這里是在對象o的作用域調用的,因此調用后o就擁有了所有屬性和方法。
2.3.4 構造函數的問題構造函數的主要問題:上述例子中,每一個getName方法實現的功能其實是一模一樣的,但是由于分別屬于不同的實例,就不得不一直不停的為getName分配空間。
person1.getName == person2.getName; //false
我們對構造函數稍加修改,在構造函數內部我們把getName屬性設置成等于全局的getName函數。由于構造函數的getName屬性包含的是一個指向函數的指針,因此person1和person2對象就共享了在全局作用域中定義的同一個getName()函數。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.getName = getName; } function getName() { console.log(this.name); } var person1 = new Person("Jason", 18, "WEB"); var person2 = new Person("Cor", 19, "WEB”); person1.getName == person2.getName; //true2.4 原型
我們創建的每一個函數,都可以有一個prototype屬性,該屬性指向一個對象,這個對象就是我們說的原型對象。原型對象的用途是:包含所有可以由構造函數實例共享的屬性和方法。按照字面理解就是,prototype就是由構造函數創建的實例對象的原型對象,使用原型對象的好處就是可以讓所有實例共享原型對象所包含的方法,屬性。
2.4.1 理解原型對象上面說了,每一個函數創建的時候,都會依據某些規則為該函數創建一個prototype屬性,這個屬性指向函數的原型對象。在默認情況下,所有原型的對象都會自動獲得一個constructor(構造函數)屬性,這個屬性包含一個指向prototype屬性所在函數的指針。以上面的例子來說,也就是Person.prototype.constructor指向Person。
創建了自定義構造函數之后,其原型對象默認只會取得constructor屬性;至于其它方法,則都是從Object繼承而來的。當調用構造函數new一個新實例后(person1) ,實例都有一個__proto__屬性,該屬性指向構造函數的原型對象(Person.prototype),通過這個屬性,讓實例對象也能夠訪問原型對象上的方法。因此,當多有的實例都能夠通過__proto__訪問到原型對象時,原型對象的方法與屬性就變成了共有方法與屬性。
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.getName = function() { console.log(this.name); } var person1 = new Person("Jason", 20); var person2 = new Person("Tim", 40); console.log(person1.getName == person2.getName); //true
ECMA-262第五版中管這個指針叫[[Prototype]],雖然在腳本中沒有標準的方式訪問[[Prototype]],單Firefox,Safari和Chrome在每個對象上都支持一個屬性__proto__;而在其他實現中,這個屬性對腳本則是完全不可見的。不過需要明確的真正重要一點就是,這個連接存在于實例與構造函數的原型對象之間,而不是存在于實例與構造函數之間。
雖然所有的實現中都無法訪問到[[Prototype]],但是可以通過isPrototypeOf()方法來確定對象之間是否存在這種關系。從本質上講,如果[[Prototype]]指向調用isPrototypeOf()方法的對象(Person.prototype),那么這個方法就會返回true。
console.log(Person.prototype.isPrototypeOf(person1)) //true console.log(Person.prototype.isPrototypeOf(person2)) //true
ES5增加了一個新方法,叫Object.getPrototypeOf(),在所有支持的實現中,這個方法返回[[Prototype]]的值,可以方便的獲取一個對象的原型。
console.log(Object.getPrototypeOf(person1) == Person.prototype); //true console.log(Object.getPrototypeOf(person1).getName()); //"Jason"
當我們訪問對象的屬性或者方法時,會優先訪問實例對象自身的屬性和方法。當代碼執行到讀取對象的屬性a時,都會執行一次搜索。搜索首先從對象的實例本身開始。如果在實例中找到屬性a,則返回該屬性的值;如果沒找到,則繼續搜索之震驚指向的原型對象,在原型對象中查找屬性a,如果在原型中找到這個屬性,則返回該屬性。簡單的說,就是會一層層搜索,若搜索到則返回,沒搜索到則繼續下層搜索。
雖然可以通過實例訪問原型的值,但是卻不能通過對象實例重寫原型的值。如果我們為水添加了一個屬性,并且該屬性名和實例原型中的一個屬性同名,就會在實例中創建該屬性,該屬性戶屏蔽原型中的相同屬性。因為搜索的時候,首先在實例本身搜索,查找到后直接返回實例中屬性的值,不會搜索到原型中。
function Person() { } Person.prototype.name = "Jason"; Person.prototype.age = 29; Person.prototype.job = "Web"; Person.prototype.getName = function() { console.log(this.name); } var person1 = new Person(); var person2 = new Person(); person1.name = "Cor"; person1.getName(); //"Cor" person2.getName(); //"Jason"
若想能夠訪問原型中的屬性,只要用delete操作符刪掉實例中的屬性即可。
delete person1.name; person1.getName(); //"Jason"hasOwnProperty()
該方法可以檢測一個屬性是存在在實例中,還是存在于原型中。這個方法繼承于Object,只有在給定屬性存在于對象實例中時,才會返回true
console.log(person1.hasOwnProperty("name")); //false person1.name = "Cor"; console.log(person1.hasOwnProperty("name")); //true;2.4.2 in操作符
in操作符有兩種使用方式:多帶帶使用和在for-in循環中使用。
在多帶帶使用時,in操作符在通過對象能訪問到給定屬性時就會返回true,無論屬性存在于實例還是原型中。
console.log("name" in person1); true
in的這種特殊性做常用的場景之一,就是判斷當前頁面是否在移動端打開。
isMobile = "ontouchstart" in document; // 很多人喜歡用瀏覽器UA的方式來判斷,但并不是很好的方式2.4.3 更簡單的原型語法
可以用一個包含所有屬性和方法的對象字面量來重寫整個原型對象。
function Person(){} Person.prototype = { name : "Jason", age : 29, job : "Web", getName : function() { console.log(this.name) } };
用對象字面的方法和原來的方法會有區別:constructor屬性不再指向Person了。因為這種寫法,本質上是修改了Person.prototype對象的引用,將引用從原來的默認值修改為了這個新對象{}的引用,constructor屬性也變成了新對象{}的constructor屬性(指向Object構造函數),不再指向Person。盡管instanceof操作符能返回正確的結果,但是constructor已經無法確定對象的類型了。
var friend = new Person(); console.log(friend instanceof Object); //true console.log(friend instanceof Person); //true console.log(friend.constructor == Person); //false console.log(friend.constructor == Object); //true
如果construct的值很重要,我們可以像下面這樣特意將它設置回適當的值。
function Person(){} Person.prototype = { constructor: Person, name : "Jason", age : 29, job : "Web", getName : function() { console.log(this.name) } };2.4.4 原型的動態性
由于在原型中查找值的過程是一次搜索,因此我們在原型對象上所做的任何修改都能夠立即從實例上反映出來——即使是先創建了實例后修改原型也一樣。下面這個例子中,friend實例是在添加sayHi方法之前創建的,但它仍然可以訪問新方法。這是因為實例與原型之間只不過是一個指針,而非一個副本,因此就可以在原型中找到新的sayHi屬性并返回值。
var friend = new Person(); Person.prototype.sayHi = function() { alert("hi"); }; friend.sayHi(); //"hi"
但是,如果是通過{}這種重寫原型對象的情況,就和上邊不一樣了。因為new實例時,實例中的__proto__屬性指向的是最初原型,而把原型修改為新的對象{}就等于切斷了構造函數與最初原型之間的聯系,同時實例中仍然保存的是最初原型的指針,因此無法訪問到構造函數的新原型中的屬性。請記住:實例只與原型有關,與構造函數無關。
function Person(){} var friend = new Person(); Person.prototype = { constructor: Person, name : "Jason", age : 29, job : "Web", sayName : function() { console.log(this.name) } }; friend.sayName(); //error,friend.sayName is not a function
如圖,重寫原型對象切斷了現有原型與任務之前已經存在的對象實例之間的聯系;friend實例引用的仍然是最初的原型,因此訪問不到sayName屬性。
注意,若想使用對象字面量重寫原型,要在創建實例之前完成。
原型創建對象的重要性不僅體現在創建自定義對象方面,就連所有原生的引用類型都采用這種模式創建。所有原生引用類型(Object、Array、String,等等)都在其構造函數的原型上定義了方法。
console.log(Array.prototype.sort); //function(){…} console.log(String.prototype.substring); //function(){...}
通過原生對象的原型,我們也可以自定義新的方法。
String.prototype.startsWith = function(text) { return this.indexOf(text) == 0; }; var msg = "Hello world!"; console.log(msg.startsWith("Hello")); //true
鞏固一下原型相關的知識點,我們以window這個對象為例,來查看一下各個屬性值
原型對象其實也是普通的對象。幾乎所有的對象都可能是原型對象,也可能是實例對象,而且還可以同時是原型對象與實例對象。這樣的一個對象,正是構成原型鏈的一個節點。
我們知道所有的函數都有一個叫做toString的方法,那么這個方法到底是從哪來的呢?先聲明一個函數:function add() {};,通過下圖來看一下這個函數的原型鏈情況。
其中add是Function對象的實例。而Function的原型對象同時又是Object原型的實例。這樣就構成了一條原型鏈。原型鏈的訪問,其實跟作用域鏈有很大的相似之處,他們都是一次單向的查找過程。因此實例對象能夠通過原型鏈,訪問到處于原型鏈上對象的所有屬性與方法。這也是foo最終能夠訪問到處于Object原型對象上的toString方法的原因。
我們再來看一個例子:
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subproperty = false; } //繼承了SuperType //SubType的原型對象等于SubperType的實例, //這樣SubType內部就會有一個指向SuperType的指針從而實現繼承 SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function(){ return this.subproperty; } var instance = new SubType(); console.log(instance.getSuperValue()); //true
SubType繼承了superType,而繼承是通過創建SuperType實例,并將該實例賦給SubType.prototype實現的。原來存在于SuperType的實例中的所有屬性和方法,現在也存在于SubType.prototype中了。這個例子中的實例、構造函數和原型之間的關系如圖:
注意,instance.constructor現在指向的是SuperType,是因為subtype的原型指向了另一個對象SuperType的原型,而這個原型對象的constructor屬性指向的是SuperType。
通過實現原型鏈,本質上擴展了前面說的原型搜索機制。在通過原型鏈實現繼承的情況下,搜索過程就得以沿著原型鏈繼續向上。那上面這個例子來說,調用instance.getSuperValue()會經歷三個搜索步驟:
搜索instance實例;未搜到;
搜索SubType.prototype,未搜索到;
搜索SuperType.prototype,找到該屬性,返回值。
3.1.1 默認原型所有函數的默認原型都是Object的實例,因此默認原型都會包含一個內布指針,指向Object.prototype。這也正是所有自定義類型都會繼承toString()、valueOf()等默認方法的根本原因。所以上面的例子展示的原型鏈中還應該包含另一個繼承層次。
有兩種方式可以確認:instanceof操作符以及isPrototypeOf()方法。
console.log(instance instanceof Object); //true console.log(instance instanceof SuperType); //true console.log(instance instanceof SubType); //true console.log(Object.prototype.isPrototypeOf(instance)); //true console.log(SuperType.prototype.isPrototypeOf(instance)); //true console.log(SubType.prototype.isPrototypeOf(instance)); //true
由于原型鏈的關系,可以說instance是Object,SuperType,SubType中任何一個類型的實例,因此都返回true。同樣的,只要是原型鏈中出現過的原型,都可以說是該原型鏈所派生實例的原型。
3.1.3 謹慎定義方法子類型(SubType)有時候需要重寫超類型(SuperType)中的某個方法,或者需要添加超類型中沒有的方法,但不管怎樣,給原型添加方法一定要在替換原型語句之后。避免出現2.4.4中出現的問題。
另外要注意,在通過原型鏈實現繼承時,不能使用對象字面量創建原型方法,這樣做會重寫原型。
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subproperty = false; } //繼承超類型 SubType.prototype = new SuperType(); //添加新方法 SubType.prototype.getSubValue = function(){ return this.subproperty; } //重寫超類型中的方法 SubType.prototype.getSuperValue = function(){ return false; } //使用字面量添加新方法,會導致上面代碼無效 SubType.prototype = { getSubValue : function(){ return this.subproperty; }, someOtherMethod : function(){ return false; } }3.1.4 原型鏈的問題
function SuperType(){ this.colors = ["red", "blue", "green"]; } function SubType(){ } SubType.prototype = new SuperType(); var instance1 = new SubType(); instance1.colors.push("black"); console.log(instance1.colors); //"red, blue, green, black" var instance2 = new SubType(); console.log(instance2.colors); //"red, blue, green, black"
這個例子中的SuperType構造函數定義了一個colors屬性,該屬性包含一個數組(引用類型值)。SuperType的每個實例都會有各自包含自己數組的colors屬性。當SubType通過原型鏈繼承了SuperType之后,SubType.prototype就變成了SuperType(),所以它也用用了一個colors屬性。就跟專門創建了一個SubType.prototype.colors屬性一樣。結果SubType得所有實例都會共享這一個colors屬性。
原型鏈的第二個問題是:在創建子類型的實例時,不能向超類型的構造函數傳遞參數。實際上,應該說是沒有辦法在不影響所有對象實例的情況下,給超類型的構造函數傳遞參數。所以在實際運用中很少會多帶帶使用原型鏈。
在子類型構造函數中調用超類型構造函數。函數只不過是在特定環境中執行代碼的對象,因此通過使用apply()和call()方法也可以在新創建的對象上執行構造函數。
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } function SubType(){ //繼承了SuperType,同時還傳遞了參數 SuperType.call(this, "Jason”); //執行上邊這個語句,相當于把SuperType構造函數的屬性在這里復制了一份 //this.name = "Jason”; //this.colors = ["red", "blue", "green"]; //實例屬性 this.age = 18; } var instance1 = new SubType(); instance1.colors. push("black"); console.log(instance1.colors); //"red,blue,green,black" console.log(instance1.name); //"Jason" console.log(instance1.age); //18 var instance2 = new SubType(); console.log(instance2.colors); //"red,blue,green"
為了確保SuperType構造函數不會重寫子類型屬性,可以在調用超類型構造函數之后,再添加需要在子類型的定義的私有屬性。
這個方式實現繼承仍然存在問題:
方法都在構造函數中定義,每個實例創建后都會為構造函數的屬性分配自己的內存,復用方法就無從談起。
而且,即使在超類型的原型中定義了公有屬性,但這些屬性對于子類型而言是不可見的,所以采用這種方式繼承,就要把所有屬性寫在構造函數中
所以這種方式在實際開發中是很少多帶帶這樣使用的。
3.3 組合繼承(原型鏈+借用構造函數)組合繼承(combination inheritance),有時候也叫做經典繼承,指的是將原型鏈和借用構造函數的繼承方式組合到一塊,從而發揮二者的長處的一種繼承方式。其背后的思路是使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。這樣既可以通過在原型上定義方法實現了函數復用,又可以在構造函數中定義方法保證每個實例都有自己的私有屬性。
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ console.log(this.name); }; function SubType(name, age){ //繼承屬性 SuperType.call(this, name); this.age = age; } SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ console.log(this.age); }; var instance1 = new SubType("Jason", 18); instance1.colors.push("black"); console.log(instance1.colors); //"red,blue,green,black" instance1.sayName(); //"Jason" instance1.sayAge(); //18 var instance2 = new SubType("Cor", 20); console.log(instance2.colors) //"red,blue,green" instance2.sayName(); //"Cor" instance2.sayAge(); //20組合繼承的問題
組合繼承雖然是現在javascript最常用的繼承模式,但是它也有不足。組合繼承最大的問題就是無論什么情況下,都會調用兩次超類型的構造函數:一次是在創建子類型原型的時候,另一次是在子類型構造函數的內部。
function SuperType(name){ this.name = name; this.colors = ["red", "bule", "grenn"]; } SuperType.prototype.sayName = function(){ console.log(this.name); } function SubType(name, age){ SuperType.call(this, name); //第二次調用SuperType this.age = age; } SubType.prototype = new SuperType(); //第一次調用SuperType SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ console.log(this.age); }
有注釋的兩行代碼是調用SuperType構造函數的代碼,第一次調用SuperType構造函數時,SubType.prototype會有SuperType的實例屬性。第二次調用SuperType的構造函數時SubType會在構造函數中添加了SuperType的實例屬性。當創建SubType的實例它的[[Prototype]]和自身上都有相同屬性。根據搜索機制自身的屬性就會屏蔽SubType原型對象上的屬性。等于原型對象上的屬性是多余的了。如圖所示,有兩組name和colors屬性:一組在實例上,一組在Subtype原型中。這就是調用兩次構造函數的結果。
基本思想是借助原型可以基于已有的對象創建新對象,同時還不必因此創建自定義類型。
function object(o){ function F(){} F.prototype = o; return new F(); }
在object()函數內部,先創建了一個臨時性的構造函數,然后傳入的對象作為這個構造函數的原型,最后返回了這個臨時構造函數的一個新實例。從本質上講,object()對傳入其中的對象執行了一次復制。
在ECMAScript5通過新增Object.create()方法規范了原型式繼承。這個方法接收兩個參數:第一個用于做新對象原型的對象;第二個參數可選,為新對象定義額外的屬性的對象。在傳入一個參數的情況下,
Object.create()和object()方法的行為相同。 var person = { name: "Jason", friends: ["Cor", "Court", "Sam"] }; var anotherPerson = Object.create(person); anotherPerson.friends.push("Rob"); var yetAnotherPerson = Object.create(person, { name: { value: "Greg" } }); yetAnotherPerson.friends.push("Barbie"); console.log(yetAnotherPerson.name); //"Greg" console.log(anotherPerson.name); //"Jason" console.log(person.friends); //"Cor,Court,Sam,Rob,Barbie" console.log(anotherPerson.__proto__); //"Cor,Court,Sam,Rob,Barbie"
這種原型式繼承,要求必須有一個對象可以作為另一個對象的基礎,把這個它傳遞給object()對象,然后再根據需求對得到的對象加以修改。在這個例子中,person對象可以作為另一個對象的基礎,把它傳入object()函數后返回一個新對象。這個新對象是將person作為原型,這意味著person.friend不僅屬于person,而且被anotherPerson和yetAnotherPerson共享。
3.5 寄生式繼承寄生式(parasitic)繼承是與原型式繼承緊密相關的一種思路。寄生式繼承的思路與寄生構造函數和工廠模式類似,即創建一個僅用于封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最后再像真的是它做了所有工作一樣返回對象。
function createAnother(original){ var clone = object(original); //通過調用函數創建一個新對象 clone.sayHi = function(){ //以某種方式來增強這個對象 alert("Hi"); }; return clone; //返回這個對象 } //使用createAnother var person = { name: "Jason", friends: ["Shelby", "Court", "Van"] }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); //"hi"
這個例子中的代碼基于person對象返回了一個新對象——anotherPerson。新對象不僅具有person的所有屬性和方法,而且還有自己的sayHi()方法。
3.6 寄生組合式繼承所謂寄生組合式繼承,即通過借用構造函數來繼承實例屬性,通過寄生式繼承方式來繼承原型屬性。其基本思路就是:不必為指定子類型的原型二調用超類型的構造函數,我們需要的只是超類型原型的一個副本而已。本質上就是,使用寄生式繼承超類型的原型,然后將結果指定給子類型的原型。寄生組合式繼承的基本模式如下:
function inheritPrototype(subType, superType){ var prototype = object(superType.prototype); //創建對象 prototype.constructor = subType; //增強對象 subType.prototype = prototype //指定對象 }
這個實例的inheritPrototype()函數實現了寄生組合式繼承的最簡單形式。這個函數接受兩個參數:子類型構造函數和超類型構造函數。在函數內部,第一步是創建超類型原型的一個副本。第二步是為創建的副本添加constructor屬性,從而彌補因重寫原型而失去的默認的constructor屬性。最后一步,將新創建的對象(即副本)賦值給子類型的原型。
function object(o){ function F(){} //創建個臨時構造函數 F.prototype = o; //superType.prototype return new F(); //返回實例 } function inheritPrototype(subType, superType){ /* 創建對象 傳入超類型的原型,通過臨時函數進行淺復制,F.prototype的指針就指向superType.prototype,在返回new F() */ var prototype = object(superType.prototype); prototype.constructor = subType; //增強對象 /* 指定對象 子類型的原型等于F類型的實例,當調用構造函數創建一個新實例后,該實例會包含一個[[prototype]]的指針指向構造函數的原型對象,所以subType.prototype指向了超類型的原型對象這樣實現了繼承,因為構造函數F沒有屬性和方法這樣就子類型的原型中就不會存在超類型構造函數的屬性和方法了。 */ subType.prototype = prototype //new F(); } function SuperType(name){ this.name = name; this.colors = ["red", "bule", "grenn"]; } SuperType.prototype.sayName = function(){ console.log(this.name); } function SubType(name, age){ SuperType.call(this, name); this.age = age; } inheritPrototype(SubType, SuperType); //即等價于: SubType.prototype = Object.create(Super.Prototype); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ console.log(this.age); } var ins1 = new SubType("Jason", 18);3.7 總結
這里我們對六種繼承方式的基本思想,具體實現,優缺點做一個簡單的總結,鞏固一下我們上面學到的知識。
繼承方式:原型鏈繼承
基本思想:利用原型鏈來實現繼承,超類的一個實例作為子類的原型
具體實現:
// 子類 function Sub(){ this.property = ‘Sub Property’; } Sub.prototype = new Super(); // 注意這里new Super()生成的超類對象并沒有constructor屬性,故需添加上 Sub.prototype.constructor = Sub;
優缺點:
優點:
簡單明了,容易實現
實例是子類的實例,實際上也是父類的一個實例
父類新增原型方法/原型屬性,子類都能訪問到
缺點:
所有子類的實例的原型都共享同一個超類實例的屬性和方法
在創建子類型的實例時,不能向超類型的構造函數傳遞參數。實際上,應該說是沒有辦法在不影響所有對象實例的情況下,給超類型的構造函數傳遞參數
繼承方式:借用構造函數
基本思想:通過使用call、apply方法可以在新創建的對象上執行構造函數,用父類的構造函數來增加子類的實例
具體實現:
// 子類 function Sub(){ Super.call(this); this.property = "Sub Property’; }
優缺點:
優點:
簡單明了,直接繼承超類構造函數的屬性和方法
可以傳遞參數
缺點:
無法繼承原型鏈上的屬性和方法
實例只是子類的實例,不是父類的實例
繼承方式:組合繼承
基本思想:利用構造繼承和原型鏈組合。使用原型鏈實現對原型屬性和方法的繼承,用借用構造函數模式實現對實例屬性的繼承。這樣既通過在原型上定義方法實現了函數復用,又能保證每個實例都有自己的屬性
具體實現:
// 子類 function Sub(){ Super.call(this); this.property = "Sub Property’; } Sub.prototype = new Super(); // 注意這里new Super()生成的超類對象并沒有constructor屬性,故需添加上 Sub.prototype.constructor = Sub;
優缺點:
優點:
解決了構造函數的兩個問題
既是父類實例,也是子類實例
缺點:
調用兩次超類型的構造函數,導致子類上擁有兩份超類屬性:一份在子類實例中,一份在子類原型上,且搜索時實例中屬性屏蔽了原型中的同名屬性
繼承方式:原型式繼承
基本思想:采用原型式繼承并不需要定義一個類,傳入參數obj,生成一個繼承obj對象的對象
具體實現:
function object(obj){ function F(){}; F.prototype = obj; return new F(); }
優缺點:
優點:
直接通過對象生成一個繼承該對象的對象
缺點:
不是類式繼承,而是原型式繼承,缺少了類的概念
繼承方式:寄生式繼承
基本思想:創建一個僅僅用于封裝繼承過程的函數,然后在內部以某種方式增強對象,最后返回對象
具體實現:
function object(obj){ function F(){} F.prototype = obj; return new F(); } function createSubObj(superInstance){ var clone = object(superInstance); clone.property = "Sub Property’; return clone; }
優缺點:
優點:
原型式繼承的一種拓展
缺點:
依舊沒有類的概念
繼承方式:寄生組合式繼承
基本思想:通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法,不必為了指定子類型的原型而調用超類型的構造函數,只需要超類型的一個副本。本質上,就是使用寄生式繼承來繼承超類型的原型,然后再將結果指定給子類型的原型
具體實現:
function inheritPrototype(Super,Sub){ var superProtoClone = Object.Create(Super.prototype); superProtoClone.constructor = Sub; Sub.prototype = superProtoClone; } function Sub(){ Super.call(this); Sub.property = "Sub Property’; } inheritPrototype(Super,Sub);
優缺點:
優點:
完美實現繼承,解決了組合式繼承帶兩份屬性的問題
缺點:
過于繁瑣,故不如組合繼承
四、ES6繼承 4.1 Class關鍵字ES6中通過class關鍵字定義類。
class Parent { constructor(name,age){ this.name = name; this.age = age; } speakSomething(){ console.log("I can speek chinese"); } } // 經babel轉碼之后,代碼是: "use strict"; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Parent = function () { function Parent(name, age) { _classCallCheck(this, Parent); this.name = name; this.age = age; } _createClass(Parent, [{ key: "speakSomething", value: function speakSomething() { console.log("I can speek chinese"); } }]); return Parent; }();
可以看出類的底層還是通過構造函數去創建的。
注意一點,通過ES6創建的類,是不允許直接調用的。即在ES5中,可以直接運行構造函數Parent()。但是在ES6中就不行,在轉碼的構造函數中有 _classCallCheck(this, Parent);語句,防止通過構造函數直接運行。直接在ES6運行Parent(),報錯Class constructor Parent cannot be invoked without ‘new’,轉碼后報錯Cannot call a class as a function。
轉碼中_createClass方法,它調用Object.defineProperty方法去給新創建的Parent添加各種屬性。defineProperties(Constructor.prototype, protoProps)是給原型添加屬性。如果你有靜態屬性,會直接添加到構造函數上defineProperties(Constructor, staticProps)。
Class可以通過extends關鍵字實現繼承,這比ES5的通過修改原型鏈實現繼承,要清晰和方便很多。
class Parent { static height = 12 constructor(name,age){ this.name = name; this.age = age; } speakSomething(){ console.log("I can speek chinese"); } } Parent.prototype.color = "yellow" //定義子類,繼承父類 class Child extends Parent { static width = 18 constructor(name,age){ super(name,age); } coding(){ console.log("I can code JS"); } } var c = new Child("job",30); c.coding()
轉碼之后的代碼變成了這樣
"use strict"; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn"t been initialised - super() hasn"t been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Parent = function () { function Parent(name, age) { _classCallCheck(this, Parent); this.name = name; this.age = age; } _createClass(Parent, [{ key: "speakSomething", value: function speakSomething() { console.log("I can speek chinese"); } }]); return Parent; }(); Parent.height = 12; //注意,該方法并不在轉碼后的構造函數function Parent中, Parent.prototype.color = "yellow"; //定義子類,繼承父類 var Child = function (_Parent) { _inherits(Child, _Parent); function Child(name, age) { _classCallCheck(this, Child); return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name, age)); } _createClass(Child, [{ key: "coding", value: function coding() { console.log("I can code JS"); } }]); return Child; }(Parent); Child.width = 18; var c = new Child("job", 30); c.coding();
可以看到,構造類的方法沒變,只是添加了_inherits核心方法來實現繼承,我們來重點分析一個這個方法做了什么。
首先判斷父類的實例
然后執行
subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); //這段代碼翻譯一下就是 function F(){} F.prototype = superClass.prototype subClass.prototype = new F() subClass.prototype.constructor = subClass
最后,subClass.__proto__ = superClass。
_inherits方法的核心思想,總結一下就是下面這兩句話:
subClass.prototype.__proto__ = superClass.prototype subClass.__proto__ = superClass
那為什么這樣一倒騰,它就實現了繼承了呢?
首先 subClass.prototype.__proto__ = superClass.prototype保證了c instanceof Parent是true,Child的實例可以訪問到父類的屬性,包括內部屬性,以及原型屬性。其次,subClass.__proto__ = superClass,保證了Child.height也能訪問到,也就是靜態方法。
class Parent {} class Child extends Parent {} // for static propertites and methods alert(Child.__proto__ === Parent); // true // and the next step is Function.prototype alert(Parent.__proto__ === Function.prototype); // true // that"s in addition to the "normal" prototype chain for object methods alert(Child.prototype.__proto__ === Parent);在內置對象中沒有靜態繼承
請注意,內置類沒有靜態 [[Prototype]] 引用。例如,Object 具有 Object.defineProperty,Object.keys等方法,但 Array,Date 不會繼承它們。
Date 和 Object 之間毫無關聯,他們獨立存在,不過 Date.prototype 繼承于 Object.prototype,僅此而已。
造成這個情況是因為 JavaScript 在設計初期沒有考慮使用 class 語法和繼承靜態方法。
super這個關鍵字,既可以當函數使用,也可以當對象使用。在這兩種情況下,它的用法完全不同。
第一種情況,super作為函數調用時代表父類的構造函數。ES6要求,子類的構造函數必須執行一次super函數。
子類B的構造函數之中的super()代表調用父類的構造函數,必須執行,否則會報錯。
class A {} class B extends A { constructor() { super(); } }
注意,super雖然代表了父類A的構造函數,但是返回的是子類B的實例,即super內部的this指的是B的實例,因此super()在這里相當于A.prototype.constructor.call(this)。
new.target指向當前正在執行的函數。可以看到,在super()執行時,它指向的是子類B的構造函數,而不是父類A的構造函數。也就是說,super()內部的this指向的是B。
class A { constructor() { console.log(new.target.name); } } class B extends A { constructor() { super(); } } new A() // A new B() // B
注意,作為函數時,super()只能用在子類的構造函數之中,用在其他地方會報錯。
class A {} class B extends A { m() { super(); // 報錯 } }
第二種情況,super作為對象時,在普通方法中,指向父類的原型對象;在靜態方法中,指向父類。
class A { constructor() { this.x = 2; this.y = 8; } p() { console.log(this.x); }, } A.prototype.z = 10; class B extends A { constructor() { super(); this.x = 5; console.log(super.p()); //5; } getY() { return super.y; } getZ() { return super.z; } } let b = new B(); b.getY(); //undefined b.getZ(); //10
在普通方法中,super指向A.prototype,所以super.p()就相當于A.prototype.p()。但是這里需要注意兩點:
super指向的是父類原型,所以定義在父類實例上的方法或屬性,無法通過super調用。所以在B類的getY()方法中調用super.y獲取不到值。但是定義在父類原型上的方法就可以獲取到,如getZ()方法中。
ES6規定,在子類普通方法中通過super調用父類的方法時,方法內部的this指向當前子類的實例。在B類中調用super.p(),this指向B類實例,輸出的結果為5。super.p()實際上執行的是super.p.call(this)。
由于this指向子類實例,所以如果通過super對某個屬性賦值,這時super就是this,賦值的屬性會變成子類實例的屬性。
class A { constructor() { this.x = 1; } } class B extends A { constructor() { super(); this.x = 2; super.x = 3; console.log(super.x); // undefined,super獲取不到父類的實例屬性 console.log(this.x); // 3 } }
在靜態方法中,super作為對象指向父類,而不是父類的原型。另外,在子類的靜態方法中通過super調用父類的方法時,方法內部的this指向當前的子類,而不是子類的實例。
class A { constructor() { this.x = 1; } static print() { console.log(this.x); } } class B extends A { constructor() { super(); this.x = 2; } static m() { super.print(); } } B.x = 3; B.m() // 3
注意,
使用super的時候,必須顯式指定是作為函數、還是作為對象使用,否則會報錯。
class A {} class B extends A { constructor() { super(); console.log(super); // 報錯 } }
由于對象總是繼承其他對象的,所以可以在任意一個對象中,使用super關鍵字。
var obj = { toString() { return "MyObject: " + super.toString(); } }; obj.toString(); // MyObject: [object Object]在內置對象中沒有靜態繼承
內置類沒有靜態 __proto__引用。例如,Object 具有 Object.defineProperty,Object.keys等方法,但 Array,Date 不會繼承它們。
Date 和 Object 之間毫無關聯,他們獨立存在,不過 Date.prototype 繼承于 Object.prototype,僅此而已。
造成這個情況是因為 JavaScript 在設計初期沒有考慮使用 class 語法和繼承靜態方法。
原生構造函數是指語言內置的構造函數,通常用來生成數據結構。ECMAScript的原生構造函數大致有下面這些。
Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()
以前,原生構造函數是無法繼承的。比如,不能自己定義一個Array的子類。
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類。但是,我們看到,這個類的行為與Array完全不一致。
之所以會發生這種情況,是因為子類無法獲得原生構造函數的內部屬性,通過Array.apply()或者分配給原型對象都不行。原生構造函數會忽略apply方法傳入的this,也就是說,原生構造函數的this無法綁定,導致拿不到內部屬性。
ES5 是先新建子類的實例對象this,再將父類的屬性添加到子類上,由于父類的內部屬性無法獲取,導致無法繼承原生的構造函數。比如,Array構造函數有一個內部屬性[[DefineOwnProperty]],用來定義新屬性時,更新length屬性,這個內部屬性無法在子類獲取,導致子類的length屬性行為不正常。
ES6 允許繼承原生構造函數定義子類,因為 ES6 是先新建父類的實例對象this,然后再用子類的構造函數修飾this,使得父類的所有行為都可以繼承。下面是一個繼承Array的例子。
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關鍵字不僅可以用來繼承類,還可以用來繼承原生的構造函數。 Array,Map 等內置類也可以擴展。
class MyArray extends Array { constructor(...args) { super(...args); } } var arr = new MyArray(); arr[0] = 12; arr.length // 1 arr.length = 0; arr[0] // undefined
注意,繼承Object的子類,有一個行為差異。
class NewObj extends Object{ constructor(){ super(...arguments); } } var o = new NewObj({attr: true}); o.attr === true // false
上面代碼中,NewObj繼承了Object,但是無法通過super方法向父類Object傳參。這是因為 ES6 改變了Object構造函數的行為,一旦發現Object方法不是通過new Object()這種形式調用,ES6 規定Object構造函數會忽略參數。
4.5 Mixin模式的實現Mixin 指的是多個對象合成一個新的對象,新對象具有各個組成成員的接口。它的最簡單實現如下。
const a = { a: "a" }; const b = { b: "b" }; const c = {...a, ...b}; // {a: "a", b: "b’}
上面代碼中,c對象是a對象和b對象的合成,具有兩者的接口。
下面是一個更完備的實現,將多個類的接口“混入”(mix in)另一個類。
function mix(...mixins) { class Mix { constructor() { for (let mixin of mixins) { copyProperties(this, new mixin()); // 拷貝實例屬性 } } } for (let mixin of mixins) { copyProperties(Mix, mixin); // 拷貝靜態屬性 copyProperties(Mix.prototype, mixin.prototype); // 拷貝原型屬性 } return Mix; } function copyProperties(target, source) { for (let key of Reflect.ownKeys(source)) { if ( key !== "constructor" && key !== "prototype" && key !== "name" ) { let desc = Object.getOwnPropertyDescriptor(source, key); Object.defineProperty(target, key, desc); } } }
上面代碼的mix函數,可以將多個對象合
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/109718.html
摘要:我們通過一個簡單的例子與圖示,來了解構造函數,實例與原型三者之間的關系。而原型對象的指向構造函數。于是根據構造函數與原型的特性,我們就可以將在構造函數中,通過聲明的屬性與方法稱為私有變量與方法,它們被當前被某一個實例對象所獨有。 showImg(https://segmentfault.com/img/remote/1460000008593382); 如果要我總結一下學習前端以來我遇...
摘要:原文地址詳解的類博主博客地址的個人博客從當初的一個彈窗語言,一步步發展成為現在前后端通吃的龐然大物。那么,的類又該怎么定義呢在面向對象編程中,類是對象的模板,定義了同一組對象又稱實例共有的屬性和方法。這個等同于的屬性現已棄用。。 前言 生活有度,人生添壽。 原文地址:詳解javascript的類 博主博客地址:Damonare的個人博客 ??Javascript從當初的一個彈窗語言,一...
摘要:原文地址詳解的類博主博客地址的個人博客從當初的一個彈窗語言,一步步發展成為現在前后端通吃的龐然大物。那么,的類又該怎么定義呢在面向對象編程中,類是對象的模板,定義了同一組對象又稱實例共有的屬性和方法。這個等同于的屬性現已棄用。。 前言 生活有度,人生添壽。 原文地址:詳解javascript的類 博主博客地址:Damonare的個人博客 ??Javascript從當初的一個彈窗語言,一...
摘要:很多情況下,通常一個人類,即創建了一個具體的對象。對象就是數據,對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍圖或原型。在中,對象通過對類的實體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實例化來的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 馬上就要到七夕了,離年底老媽老爸...
摘要:很多情況下,通常一個人類,即創建了一個具體的對象。對象就是數據,對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍圖或原型。在中,對象通過對類的實體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實例化來的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 馬上就要到七夕了,離年底老媽老爸...
閱讀 2570·2021-09-06 15:02
閱讀 3199·2021-09-02 10:18
閱讀 2820·2019-08-30 15:44
閱讀 684·2019-08-30 15:43
閱讀 1947·2019-08-30 14:08
閱讀 2757·2019-08-30 13:16
閱讀 1397·2019-08-26 13:52
閱讀 930·2019-08-26 12:21