1 理解對象
1-1 屬性的類型
屬性分兩種:數據屬性和訪問器屬性
-
數據屬性
數據屬性包含一個保存數據值的位置。值會從這個位置讀取,也會寫入到這個位置,數據屬性有4個特性來描述它們的行為。
-
[[Configurable]]
:表示屬性是否可以通過delete刪除并重新定義,是否可以修改它的特性,以及是否可以把它改為訪問器屬性。默認為true -
[[Enumerable]]
:表示屬性是否可枚舉(通過for-in循環返回),默認為true -
[[Writable]]
:表示屬性的值是否可修改,默認為true -
[[Value]]
:屬性實際的值,默認是undefined
注意點:雖然可以對同一個屬性多次調用
Object.defineProperty()
,但在把configurable
設為false之后就會受限制了 -
-
訪問器屬性
訪問器屬性不包含數據值。相反,它們包含一個獲取函數(getter)和一個設置函數(setter),訪問器屬性有4個特性來描述它們的行為
-
[[Configurable]]
:表示屬性是否可以通過delete刪除并重新定義,是否可以修改它的特性,以及是否可以把它改為數據屬性。默認為true -
[[Enumberable]]
:表示屬性是否可枚舉(通過for-in循環返回),默認為true -
[[Get]]
:獲取函數,在讀取屬性時調用,默認為undefined -
[[Set]]
:設置函數,在寫入屬性時調用,默認為undefined
-
1-2 定義多個屬性
在一個對象上同時定義多個屬性時,使用Object.defineProperties()
方法,區別于Object.definedProperty()
方法一次只能定義或修改多個屬性,具體看MDN文檔
1-3 讀取屬性的特性
使用Object.getOwnPropertyDescription(obj, prop)
方法可以獲取指定屬性的屬性描述符,也就是屬性的特性。接收兩個參數:屬性所在的對象和要取得其描述符的屬性名。
Object.getOwnPropertyDescriptions(obj)
方法可用來獲取一個對象的所有屬性的屬性描述符。接收一個參數:需要獲取的對象
1-4 合并對象
ES6
中使用Object.assign(target, source)
方法進行對象的合并,返回值是目標對象。這個方法實際上是對每個源對象執行的是淺復制。
1-5 對象標識及相等判定
為了解決 === 操作符判定特殊情況帶來的問題,ES6
新增了Object.is()
// ===
console.log(+0 === -0) // true
console.log(+0 === 0) // true
console.log(-0 === 0) // true
console.log(NaN === NaN) // false
// Object.is()
console.log(Object.is(+0 === -0)) // false
console.log(Object.is(+0 === 0)) // true
console.log(Object.is(-0 === 0)) // false
console.log(Object.is(NaN, NaN)) // true
2 創建對象
2-1 工廠模式
function createPerson(name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name)
}
return o;
}
let person1 = createPerson('xiaoming', 10, 'student');
let person2 = createPerson('zhangsan', 20, 'doctor');
console.log(person1 instanceof Person); // false 不能識別對象的類型
弊端:這里,函數每次調用都會返回一個新的對象, 這種方法可以解決創建多個類似對象的問題,但是沒有解決對象標識問題(即新創建的對象是什么類型),構造函數模式可以解決這個問題。
2-2 構造函數模式
ECMAScript
中的構造函數是用于創建特定類型對象的。
前面的例子使用構造函數可以這么寫:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name)
}
}
let person1 = new Person('xiaoming', 10, 'student');
let person2 = new Person('zhangsan', 20, 'doctor');
這里的代碼和前面使用工廠函數創建的例子基本是一樣的,只是有以下區別:
- 沒有顯示地創建對象
- 屬性和方法直接賦值給this
- 沒有返回值
為什么?可以看到我們在創建實例地時候使用了new操作符。那使用new時,內部執行了以下的操作:
- 創建一個新的空對象
- 這個對象內部的
__proto__
屬性(這里應該是[[Prototype]]特性,具體為什么__proto__
可以訪問到?下面會解釋到)指向構造函數(即Person)的prototype屬性- 構造函數內部的this指向這個新創建的空對象
- 執行構造函數內部的代碼,也就是不斷地給this賦值,不斷給this添加屬性
- 返回this對象(即新創建的對象)
instanceof
操作符是用來確定對象類型最可靠的方式。相比于工廠模式,可識別對象的類型是一個很大的好處。
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
弊端:構造函數內部定義的方法會在每個實例上都創建一遍,上面的例子中,
person1
和person2
中都有名為sayName()
的方法,因為是做同一件事,所以沒必要創建兩次。這個問題可以通過原型模式來解決。
2-3 原型模式
每個函數都會創建一個prototype
屬性,這個屬性是一個對象。
使用原型對象的好處是:在它上面定義的屬性和方法可以被實例共享。原來在構造函數中直接賦值給對象實例的值,可以直接賦值給他們的原型,如下:
function Person() {}
Person.prototype.name = 'xiaoming';
Person.prototype.age = 10;
Person.prototype.job = 'student';
Person.prototype.sayName = function() {
console.log(this.name);
}
let person1 = new Person();
let person2 = new Person();
console.log(person1.name) // xiaoming
console.log(person1.sayName == person2.sayName) // true
雖然構造函數中什么都沒有,但是卻可以訪問得到相應得屬性和方法,而且使用定義在原型上的屬性和方法是共享給所有的實例的(即所有實例都可以訪問得到,也不會存在重復創建的問題)
- 理解原型
無論何時,只要創建一個函數,這個函數就存在一個
prototype
屬性(指向原型對象)。默認情況下,所有原型對象都有一個名為constructor
的屬性,指回對應的構造函數。比如上面的例子Person.prototype.constructor
指回Person
每次調用構造函數創建一個新實例,這個實例內部存在一個
[[Prototype]]
特性,會指向構造函數的原型對象。由于腳本中沒有訪問這個[[Prototype]]
特性的標準方式,但Firefox
,Safari
,Chrome
中會在每個對象上暴露__proto__
屬性,通過這個屬性可以訪問實例對象的原型。(這也為上面將new操作符時說為什么可以通過__proto__
訪問的到原型做了解釋)關鍵在于理解這一點:實例與構造函數原型之間有直接的聯系,但實例與構造函數之間沒有
看圖:
- 原型層級
在通過對象訪問屬性時,會按照這個屬性的名稱開始搜索。搜索開始于對象實例本身。如果在這個實例上發現了給定的名稱,則返回該名稱對應的值。如果沒有找到這個屬性,則搜索會沿著指針進入原型對象,然后在原型對象上找到屬性后,再返回對應的值。
雖然可以通過實例讀取原型對象上的值,但不可能通過實例重寫這些值。如果在實例上添加了一個與原型對象中同名的屬性,那就會在實例上創建這個屬性,這個屬性會遮住原型對象上的屬性。即使在實例上把這個屬性設置為null,也不會恢復它和原型的聯系,不過,使用delete操作符可以完全刪除實例上的這個屬性,從而讓標識符解析過程能夠繼續搜索原型對象。
hasOwnProperty()方法
用于確定某個屬性是在實例上還是在原型對象上。會在屬性存在于調用它的對象實例上時返回true,即如果該屬性是存在于實例上時,返回true,反之返回false。function Person() {} Person.prototype.name = 'xiaoming'; let person1 = new Person() person1.name = 'lucy' console.log(person1.hasOwnProperty('name')) // true delete person1.name // 刪除實例上的name屬性 console.log(person1.hasOwnProperty('name')) // false
- 原型和in操作符
有兩種方式使用in操作符:
for-in循環中使用
- for-in中使用in操作符時,遍歷對象的所有可枚舉屬性
- 要想獲得對象上所有可枚舉的實例屬性,可以使用
Object.keys()
方法。(接收一個對象作為參數,返回所有可枚舉屬性組成的字符串數組)Object.getOwnPropertyNames()
方法返回的是所有實例屬性,無論是否可枚舉;Object.getOwnPropertySymbols()
類似;單獨使用時,in操作符會在可以通過對象訪問指定屬性時返回true,無論該屬性是在實例上還是在原型上。
如果要確定某個屬性是否存在于原型上,則可以像這樣同時使用
hasOwnProperty
和in
操作符function hasPrototypeProperty(object, name) { return !Object.hasOwnProperty(name) && (name in Object) }
- 屬性枚舉順序
- 順序不確定:
for-in
循環,Object.keys()
,取決于JavaScript
引擎,可能因瀏覽器而異- 順序確定:
Object.getOwnPropertyNames()
、Object.getOwnPropertySymbols()
、Object.assign()
,先以升序枚舉數值鍵,再按定義的順序插入枚舉字符串和符號鍵。(數字鍵優先,并且升序排列,和定義屬性的順序無關,次之是字符串和符號鍵,這兩種就按照定義屬性的順序來插入)
2-4 對象迭代
ESMAScript2017
新增了兩個靜態方法。用于迭代對象,這兩個方法執行對象的淺復制,都會忽略符號屬性。
-
Object.values()
:返回的是對象 值的數組 -
Object.entries()
:返回的是 鍵/值對的數組
- 其他原型語法
function Person() {} Person.prototype = { name: 'xiaoming', sayName() { console.log(this.name); } }
看上面的代碼,在直接通過一個包含所有屬性和方法的對象來重寫原型時,要注意,這樣重寫后,
Person.prototype
的constructor
屬性就不指向Person
了,而是指向Object。如果我們想依靠constructor
屬性來識別類型,那怎么辦?那就重新指定一下function Person() {} Person.prototype = { constructor: Person, name: 'xiaoming', sayName() { console.log(this.name) } }
好了,但是有個問題,以這種方式恢復
constructor
屬性它是一個[[Enumerable]]
為true
的屬性,而原生的constructor屬性
默認是不可枚舉的。因此我們得用Object.definedProperty()
方法來定義constructor
屬性:function Person() {} Person.prototype = { name: 'xiaoming', sayName() { console.log(this.name) } } // 恢復constructor屬性 Object.defineProperty(Person.prototype, 'constructor', { enumerable: false, value: Person })
這樣就可以完美恢復
constructor
屬性了。
- 原型的動態性
注意給原型添加屬性和方法和重寫整個原型是完全兩回事。
先看個例子:給原型添加屬性和方法
let friend = new Person() Person.prototype.sayHi = function() { console.log('Hi') } friend.sayHi() // Hi
雖然我們是在實例化之后才給原型添加
sayHi()
方法的,為什么實例可以直接訪問到該方法?這是因為new的時候實例的
[[Prototype]]
指針就已經指向Person.prototype
了,所以無論我們后面怎么給原型對象添加屬性,實例都能夠訪問得到。再看看這個例子:重寫整個原型
let friend = new Person() Person.prototype = { constructor: Person, name: 'xiaoming', sayName() { console.log(this.name) } } friend.sayName(); // 報錯
為什么?
這也是剛剛上面說的,實例的
[[Prototype]]
指針是在new的時候被賦值為Person.prototype
的,而上面的代碼因為重寫了原型,相當于又創建了一個新的對象, 而這時實例指向的還是最初的原型對象,上面并沒有sayName()
方法,所以報錯重寫構造函數上的原型之后再創建的實例才會引用新的原型。而在此之前創建的實例仍然會引用最 初的原型。
- 原生對象原型
盡管可以像修改自定義對象原型一樣修改原生對象原型,隨時添加方法,但不推薦在產品環境中修改原生對象原型。這樣做很可能造成誤會,而且可能引發命名沖突(比如一個名稱在某個瀏覽器實現中不存在,在另一個實現中卻存在)。另外還有可能意外重寫原生的方法。推薦的做法是創建一個自定義的類,繼承原生類型。
- 原型的問題
存在的問題:
- 弱化了向構造函數傳遞初始化參數的能力,會導致所有的實例默認都取得相同的屬性值。
- 原型上的方法和屬性都是所有實例共享的,這對于方法來說比較合適,但是對于屬性來說就不是特別好。如果屬性是原始類型,那還好,可以通過實例上添加同名屬性來覆蓋原型上地屬性。但是,如果屬性是引用類型,那么當我們修改了某個實例上的該屬性,(由于指針指向是相同的)那么這樣就影響了其他實例上的屬性,這是不合理的。
所以實際開發中通常不單獨使用原型模式。
3 繼承
繼承分為接口繼承和**實現繼承**,實現繼承是`ECMAScript`唯一支持的繼承方式,而這主要是通過原型鏈實現的。
3-1 原型鏈繼承
原型鏈繼承就是 **使子類的原型指向父類的構造出來的實例對象**
SubType.prototype = new SuperType()
- 默認原型
任何函數的默認原型都是
Object
的實例,這意味著這個實例有一個內部指針指向Object.prototype
,所以自定義類型能夠繼承如toString()
,valueOf()
這些方法。
- 原型與繼承的關系
原型與實例的關系可以通過兩種方式來確定:
instanceof
操作符:(實例 instanceof 構造函數)
如果一個實例的原型鏈中出現過相應的構造函數,則返回true
isPrototypeOf()方法
:(構造函數.prototype.isPrototypeOf(需要檢測的實例對象))
原型鏈中的每個原型都可以調用這個方法,用于檢測實例對象是否存在于另一個對象的原型鏈上,是則返回true
弊端:如果父類構造函數中存在引用值會導致子類的原型中也存在著引用值(因為子類的原型是被賦值為父類的一個實例對象),所以子類的所有實例都會共享存在的引用值。
3-2 盜用(借用)構造函數繼承
為了解決原型包含引用值導致的繼承問題,我們可以使用“盜用構造函數繼承”
基本思路:在子類構造函數中調用父類構造函數,可以使用
call()
和apply()
方法以新創建的對象為上下文執行構造函數。function SuperType(name) { this.name = name this.colors = ['red', 'green'] this.sayName = function() { console.log(this.name) } } function SubType() { SuperType.call(this, 'xiaoming') // 繼承SuperType并傳參 } let instance1 = new SubType() instance1.colors.push('blue') console.log(instance1.colors) // ['red', 'green', 'blue'] let instance2 = new SubType() console.log(instance2.colors) // ['red', 'green'] // 通過使用call()/apply()方法, SuperType構造函數在SubType的實例創建的新對象的上下文中執行了,相當于新的SubType對象上運行了SuperType函數中所有初始化代碼。結果就是每個實例都會有自己的colors和name屬性。
優點:可以在子類構造函數中向父類構造函數傳參
缺點:也是構造函數模式的缺點:就是必須在構造函數中定義方法,因此函數不能重用
3-3 組合繼承
組合繼承綜合了原型鏈和盜用(借用)構造函數繼承,將兩者的優點集中了起來。
基本思路:使用原型鏈繼承原型上的屬性和方法,而通過盜用構造函數繼承實例屬性。
function SuperType(name) { this.name = name this.colors = ['red', 'blue'] } SuperType.prototype.sayName = function() { console.log(this.name) } function SubType(name, age) { SuperType.call(this, name) // 借用構造函數繼承 讓SubType的每個實例都擁有name 和 colors屬性,相互之間不受影響 this.age = age } SubType.prototype = new SuperType() // 原型鏈繼承父類 SubType.prototype.sayAge = function() { console.log(this.age) } let instance1 = new SubType('xiaoming', 10) instance1.colors.push('yellow') console.log(instance1.colors) // ['red', 'blue', 'yellow'] instance1.sayName() // xiaoming instance1.sayAge() // 10 let instance2 = new SubType('lucy', 20) console.log(instance2.colors) // ['red', 'blue'] instance2.sayName() // lucy instance2.sayAge() // 20
優點:組合繼承彌補了原型鏈和盜用構造函數的不足,是
JavaScript
中使用最多的繼承模式,而且組合繼承也保留了instanceof
操作符和isPrototypeOf()
方法識別合成對象的能力弊端:存在效率問題,就是父類構造函數始終會被調用兩次:一次是在賦值給子類原型時調用,另一次是在子類構造函數中調用。
3-4 原型式繼承
基本思路:即使不自定義類型也可以通過原型實現對象之間的信息共享
function object(o) { function F() {} // 創建一個臨時構造函數F F.prototype = o // 構造函數F的原型指向o,說明F的實例對象能夠訪問到o的屬性和方法 return new F() // 返回構造函數F的實例對象 } let person = { name: 'xiaoming', friends: ['xxx', 'yyy']} let anotherPerson = object(person) // 返回一個對象,這個對象的[[Prototype]]指針指向o anotherPerson.friends.push('zzz') let yetAnotherPerson = object(person) yetANotherPerson.friends.push('hhh') console.log(person) // ['xxx', 'yyy', 'zzz', 'hhh'] // 實際上,object()是對傳入的對象執行了一次淺復制
適用場景:
- 你有一個對象,想在它的基礎上再創建一個新對象。你需要先把這個對象傳入
object()
,然后再對返回的對象做相應的修改- 適合不需要單獨創建構造函數,但仍然需要在對象間共享信息的場合。
Object.create()
方法將原型式繼承的概念規范化了。這個方法接收兩個參數:第一個參數:作為新對象原型的對象;第二個參數(可選):給新對象定義額外屬性的對象。當只有一個參數時,Objcet.create()
和object()
方法效果相同
Object.create()
的第二個參數與Object.definedProperties()
的第二個參數一樣:每個新增的屬性都通過各自的描述符來描述。以這種方式添加的屬性會遮蔽原型對象上的同名屬性。弊端:屬性中包含的引用值類型始終會在各個實例之間共享,跟適用原型模式是一樣的。
3-5 寄生式繼承
寄生式繼承與原型式繼承比較接近。
基本思路:創建一個實現繼承的函數,以某種方式增強對象,然后返回這個對象
function object(o) { function F() {} F.prototype = o return new F() } function createAnother(original) { let clone = object(original) // 通過調用函數創建一個新對象 clone.sayHi = function() { // 以某種方式增強這個對象 console.log('hi') // 返回這個對象 } return clone } let person = { name: 'xiaoming', friends: ['xxx', 'yyy'] } let anotherPerson = createAnother(person) anotherPerson.sayHi() // 'hi'
弊端:通過寄生式繼承給對象添加函數會導致函數難以復用,與構造函數模式類似。(即每次創建實例都要重復創建方法)
3-6 寄生式組合繼承
前面說到組合繼承其實存在性能問題:父類構造函數最終會被調用兩次。(第一次是在給子類原型賦值時調用;第二次是在子類構造函數里面調用)寄生式組合繼承可以解決這個問題。
繼承方法:組合繼承(原型鏈繼承+借用構造函數繼承)+ 寄生式繼承
基本思路:不通過調用父類構造函數來給子類原型賦值,而是通過取得父類原型的一個副本
function object(o) { function F() {} F.prototype = o // 這里由于直接用對象賦值的形式重寫原型對象,所以constructor的指向發生改變,指向該對象o return new F() } function inheritPrototype(subType, superType) { let prototype = object(superType.prototype) // 返回父類構造函數的一個副本 prototype.constructor = subType // 修改constructor的指向 subType.prototype = prototype // } function SuperType(name) { this.name = name this.colors = ['red', 'yellow'] } SuperType.prototype.sayName = function() { console.log(this.name) } function SubType(name, age) { SuperType.call(this, name) this.age = age } // SubType.prototype = new SuperType() inheritPrototype(SubType, SuperType) SubType.prototype.sayAge = function() { console.log(this.age) }
這樣的話就只調用一次父類構造函數,這樣效率更高。而且原型鏈保持不變,因此
instanceof
操作符和isPrototypeOf()
方法有效,所以寄生式組合繼承可以算是引用類型繼承的最佳方式。
4 類
ES6
引入一個class
關鍵字具有定義類的能力,是一個語法糖。class
背后使用的仍然是原型和構造函數的概念。
4-1 類定義
定義類有兩種主要方式:類聲明和類表達式
類聲明:
Class Person {}
類表達式:
const Animal = class {}
類聲明不能提升
4-2 類構造函數
constructor
關鍵字用于在類定義塊的內部創建類的構造函數。方法名constructor
會告訴解釋器在使用new
操作符創建類的新實例時,應該調用這個函數。
- 實例化
調用類構造函數時必須使用
new
操作符,否則會報錯。而普通構造函數如果不使用new
,那就會以全局的this
(通常是window
)作為內部對象類構造函數實例化之后,它會變為普通的實例方法(但是它作為類構造函數,仍然需要使用new調用)
class Person { // Person:類標識符 constructor() {} // constructor:類構造函數 } let p1 = new Person() let p2 = new p1.constructor()
4-3 實例、原型和類成員
類的語法可以非常方便地定義應該存在于實例上的成員、應該存在于原型上的成員,以及應該存在于類本身的成員
- 實例成員
每次通過
new
調用類標識符時,都會執行類構造函數。可以為新創建的實例(this)
添加“自有”屬性。沒有限制是什么屬性。構造函數執行完畢后,仍然可以給實例繼續添加新成員。
每個實例都對應一個唯一的成員對象,這意味著所有成員都不會在原型上共享。
- 原型方法與訪問器
為了在實例間共享方法,類定義語法把在類塊中定義的方法作為原型方法。
class Person { constructor(name) { // 添加到this上面的所有內容都會存在于不同的實例上面 this.name = name } // 在類塊中定義的所有內容都會定義在類的原型上 locate() { console.log('prototype') } } let p1 = new Person('Jack') let p2 = new Person('May') console.log(p1.name) // Jack console.log(p2.name) // May p1.locate() // prototype p2.locate() // prototype
類方法等同于對象屬性,因此可以使用字符串,符號或者計算的值作為鍵。
類定義也支持獲取和設置訪問器。語法與行為跟普通對象一樣
class Person() { set name(newName) { this.name_ = newName } get name() { return this.name_ } }
- 靜態類方法
可以在類上定義靜態方法,與原型成員類似,靜態成員每個類上只能有一個。使用
static
關鍵字作為前綴,this
引用類自身。class Person () { ... 省略代碼 // 定義在類本身上 static locate() { console.log('class') } }
- 非函數原型和類成員的添加
雖然類定義不顯示支持在原型上或類上添加成員數據,但在類定義的外部,可以通過手動來添加。
- 迭代器與生成器方法
4-4 繼承
- 繼承基礎
ES6
類支持單繼承。使用extends
關鍵字,不僅可以繼承一個類,也可以繼承普通的構造函數派生類都會通過原型鏈訪問到類和原型上定義的方法。
this
的值會反映調用相應方法的實例或者類。
- 構造函數,
HomeObject
,super()
super
關鍵字只能在派生類中使用,而且僅限于構造函數,實例方法和靜態方法內部。
在構造函數中使用
super
可以調用繼承的父類的構造函數class Vehicle { constructor() { this.hasEngine = true } } class Bus extends Vehicle { constructor() { super() // 相當于super.constructor() console.log(this.hasEngine) // true console.log(this) // Bus { hasEngine: true } } } new Bus()
在靜態方法中使用
super
可以調用繼承的父類上定義的靜態方法class Vehicle { static identify() { console.log('vehicle') } } class Bus extends Vehicle { static identify() { super.identify() } } Bus.identify() // vehicle
使用
super
注意事項:
super
只能在派生類構造函數和靜態方法中使用。不能單獨引用
super
關鍵字,要么用它調用構造函數,要么用它引用靜態方法調用
super()
會調用父類構造函數,并將父類構造函數中返回的實例賦值給子類中的this
class Father { constructor() { this.name = 'xiaoming' } } class Child extends Father { constructor() { super() console.log(this) // Child { name: 'xiaoming' } } } let c1 = new Child() console.log(c1) // Child { name: 'xiaoming' }
super()
的行為如同調用構造函數,如果需要給父類構造函數傳參,則需要手動傳入。class Father { constructor(name) { this.name = name } } class Child extends Father { constructor(name) { super(name) } } let c1 = new Child('Jack')
- 如果沒有派生類中沒有定義類構造函數,在實例化派生類時會自動調用
super()
,而且會自動傳入所有傳給派生類的參數class Father { constructor(name) { this.name = name } } class Child extends Father {} let c1 = new Child('Jack')
在派生類構造函數中,不能在調用
super()
之前引用this
如果在派生類中顯式定義了構造函數,則要么必須在其中調用
super()
,要么必須在其中返回一個對象class Father { constructor() { this.name = 'Jack' } } class Child extends Father { constructor() { // 顯示定義了構造函數 super(); } } // 或者 class Child extends Father { constructor() { return {} } }
3.抽象基類
可供其他類繼承, 但本身不會被實例化。可以通過
new.target
來實現。另外可以通過抽象基類構造函數中進行檢查,可以要求派生類必須定義某個方法。// 抽象基類 class Father { constructor() { if (new.target === Father) { throw new Error('Father cannot be directly instantiated') } if (!this.foo) { throw new Error('Inheriting class must define foo()') } console.log("success") } } // 派生類 class Child extends Father { foo() {} } new Child() // class Child {} // success new Father() // class Father {} // Error: Father cannot be directly instantiated