"Code tailor",爲前端開發者提供技術相關資訊以及系列基礎文章,微信關注「小和山的菜鳥們」公衆號,及時獲取最新文章。javascript
在開始學習以前,咱們想要告訴您的是,本文章是對 JavaScript
語言知識中 "對象、類與面向對象編程" 部分的總結,若是您已掌握下面知識事項,則可跳過此環節直接進入題目練習前端
若是您對某些部分有些遺忘,👇🏻 已經爲您準備好了!java
ECMA-262
將對象定義爲一組屬性的無序集合。嚴格來講,這意味着對象就是一組沒有特定順序的值。對象的每一個屬性或方法都由一個名稱來標識,這個名稱映射到一個值。正由於如此(以及其餘還未討論的緣由),能夠把ECMAScript
的對象想象成一張散列表,其中的內容就是一組名/值對,值能夠是數據或者函數。web
建立自定義對象的一般方式是建立 Object 的一個新實例,而後再給它添加屬性和方法,以下例 所示:編程
let person = new Object()
person.name = 'XHS-rookies'
person.age = 18
person.job = 'Software Engineer'
person.sayName = function () {
console.log(this.name)
}
複製代碼
這個例子建立了一個名爲 person
的對象,並且有三個屬性(name
、age
和 job
)和一個方法(sayName()
)。sayName()
方法會顯示 this.name
的值,這個屬性會解析爲 person.name
。早期JavaScript
開發者頻繁使用這種方式建立新對象。幾年後,對象字面量變成了更流行的方式。前面的例子若是使用對象字面量則能夠這樣寫:設計模式
let person = {
name: 'XHS-rookies',
age: 18,
job: 'Software Engineer',
sayName() {
console.log(this.name)
},
}
複製代碼
這個例子中的 person
對象跟前面例子中的 person
對象是等價的,它們的屬性和方法都同樣。這些屬性都有本身的特徵,而這些特徵決定了它們在 JavaScript
中的行爲。瀏覽器
綜觀 ECMAScript
規範的歷次發佈,每一個版本的特性彷佛都出人意料。ECMAScript 5.1
並無正式 支持面向對象的結構,好比類或繼承。可是,正如接下來幾節會介紹的,巧妙地運用原型式繼承能夠成 功地模擬一樣的行爲。ECMAScript 6
開始正式支持類和繼承。ES6
的類旨在徹底涵蓋以前規範設計的基於原型的繼承模式。不過,不管從哪方面看,ES6
的類都僅僅是封裝了ES5.1
構造函數加原型繼承的語法糖而已。微信
工廠模式是一種衆所周知的設計模式,普遍應用於軟件工程領域,用於抽象建立特定對象的過程。下面的例子展現了一種按照特定接口建立對象的方式:markdown
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('XHS-rookies', 18, 'Software Engineer')
let person2 = createPerson('XHS-boos', 18, 'Teacher')
複製代碼
這裏,函數 createPerson()
接收 3 個參數,根據這幾個參數構建了一個包含 Person
信息的對象。能夠用不一樣的參數屢次調用這個函數,每次都會返回包含 3 個屬性和 1 個方法的對象。這種工廠模式雖然能夠解決建立多個相似對象的問題,但沒有解決對象標識問題(即新建立的對象是什麼類型)。app
ECMAScript
中的構造函數是用於建立特定類型對象的。像 Object
和 Array
這 樣的原生構造函數,運行時能夠直接在執行環境中使用。固然也能夠自定義構造函數,以函數的形式爲 本身的對象類型定義屬性和方法。 好比,前面的例子使用構造函數模式能夠這樣寫:
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('XHS-rookies', 18, 'Software Engineer')
let person2 = new Person('XHS-boos', 18, 'Teacher')
person1.sayName() // XHS-rookies
person2.sayName() // XHS-boos
複製代碼
在這個例子中,Person()
構造函數代替了createPerson()
工廠函數。實際上,Person()
內部 的代碼跟 createPerson()
基本是同樣的,只是有以下區別。
沒有顯式地建立對象。
屬性和方法直接賦值給了 this
。
沒有 return
。
另外,要注意函數名 Person
的首字母大寫了。按照慣例,構造函數名稱的首字母都是要大寫的, 非構造函數則以小寫字母開頭。這是從面向對象編程語言那裏借鑑的,有助於在 ECMAScript
中區分構 造函數和普通函數。畢竟 ECMAScript
的構造函數就是能建立對象的函數。
要建立 Person
的實例,應使用 new
操做符。以這種方式調用構造函數會執行以下操做。
(1)在內存中建立一個新對象。
(2)這個新對象內部的 [[Prototype]]
特性被賦值爲構造函數的 prototype
屬性。
(3)構造函數內部的 this
被賦值爲這個新對象(即 this
指向新對象)。
(4)執行構造函數內部的代碼(給新對象添加屬性)。
(5)若是構造函數返回非空對象,則返回該對象;不然,返回剛建立的新對象。
上一個例子的最後,person1
和 person2
分別保存着 Person
的不一樣實例。這兩個對象都有一個 constructor
屬性指向 Person
,以下所示:
console.log(person1.constructor == Person) // true
console.log(person2.constructor == Person) // true
複製代碼
constructor
原本是用於標識對象類型的。不過,通常認爲 instanceof
操做符是肯定對象類型更可靠的方式。前面例子中的每一個對象都是 Object
的實例,同時也是 Person
的實例,以下面調用 instanceof
操做符的結果所示:
console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true
console.log(person2 instanceof Object) // true
console.log(person2 instanceof Person) // true
複製代碼
定義自定義構造函數能夠確保實例被標識爲特定類型,相比於工廠模式,這是一個很大的好處。在 這個例子中,person1
和 person2
之因此也被認爲是 Object
的實例,是由於全部自定義對象都繼承自 Object
(後面再詳細討論這一點)。構造函數不必定要寫成函數聲明的形式。賦值給變量的函數表達式也能夠表示構造函數:
let Person = function (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
console.log(this.name)
}
}
let person1 = new Person('XHS-rookies', 18, 'Software Engineer')
let person2 = new Person('XHS-boos', 18, 'Teacher')
person1.sayName() // XHS-rookies
person2.sayName() // XHS-boos
console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true
console.log(person2 instanceof Object) // true
console.log(person2 instanceof Person) // true
複製代碼
在實例化時,若是不想傳參數,那麼構造函數後面的括號可加可不加。只要有 new
操做符,就能夠調用相應的構造函數:
function Person() {
this.name = 'rookies'
this.sayName = function () {
console.log(this.name)
}
}
let person1 = new Person()
let person2 = new Person()
person1.sayName() // rookies
person2.sayName() // rookies
console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true
console.log(person2 instanceof Object) // true
console.log(person2 instanceof Person) // true
複製代碼
1. 構造函數也是函數
構造函數與普通函數惟一的區別就是調用方式不一樣。除此以外,構造函數也是函數。並無把某個函數定義爲構造函數的特殊語法。任何函數只要使用 new
操做符調用就是構造函數,而不使用 new
操做符調用的函數就是普通函數。好比,前面的例子中定義的 Person()
能夠像下面這樣調用:
// 做爲構造函數
let person = new Person('XHS-rookies', 18, 'Software Engineer')
person.sayName() // "XHS-rookies"
// 做爲函數調用
Person('XHS-boos', 18, 'Teacher') // 添加到 window 對象
window.sayName() // "XHS-boos"
// 在另外一個對象的做用域中調用
let o = new Object()
Person.call(o, 'XHS-sunshineboy', 25, 'Nurse')
o.sayName() // "XHS-sunshineboy"
複製代碼
這個例子一開始展現了典型的構造函數調用方式,即便用 new
操做符建立一個新對象。而後是普通函數的調用方式,這時候沒有使用 new
操做符調用 Person()
,結果會將屬性和方法添加到 window
對象。這裏要記住,在調用一個函數而沒有明確設置 this
值的狀況下(即沒有做爲對象的方法調用,或 者沒有使用 call()/apply()
調用),this
始終指向 Global
對象(在瀏覽器中就是 window
對象)。 所以在上面的調用以後,window
對象上就有了一個 sayName()
方法,調用它會返回 "Greg"
。最後展現的調用方式是經過 call()
(或apply()
)調用函數,同時將特定對象指定爲做用域。這裏的調用將 對象 o
指定爲 Person()
內部的 this
值,所以執行完函數代碼後,全部屬性和 sayName()
方法都會添加到對象 o
上面。
2. 構造函數的問題
構造函數雖然有用,但也不是沒有問題。構造函數的主要問題在於,其定義的方法會在每一個實例上都建立一遍。所以對前面的例子而言,person1
和 person2
爲 sayName()
的方法,但這兩個方法不是同一個 Function
實例。咱們知道,ECMAScript
中的函數是對象,所以每次定義函數時,都會初始化一個對象。邏輯上講,這個構造函數其實是這樣的:
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = new Function('console.log(this.name)') // 邏輯等價
}
複製代碼
這樣理解這個構造函數能夠更清楚地知道,每一個 Person
實例都會有本身的 Function
實例用於顯 示 name
屬性。固然了,以這種方式建立函數會帶來不一樣的做用域鏈和標識符解析。但建立新 Function
實例的機制是同樣的。所以不一樣實例上的函數雖然同名卻不相等,以下所示:
console.log(person1.sayName == person2.sayName) // false
複製代碼
由於都是作同樣的事,因此不必定義兩個不一樣的 Function
實例。何況,this
對象能夠把函數 與對象的綁定推遲到運行時。 要解決這個問題,能夠把函數定義轉移到構造函數外部:
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = sayName
}
function sayName() {
console.log(this.name)
}
let person1 = new Person('XHS-rookies', 18, 'Software Engineer')
let person2 = new Person('XHS-boos', 18, 'Teacher')
person1.sayName() // XHS-rookies
person2.sayName() // XHS-boos
複製代碼
在這裏,sayName()
被定義在了構造函數外部。在構造函數內部,sayName
屬性等於全局 sayName()
函數。由於這一次 sayName
屬性中包含的只是一個指向外部函數的指針,因此 person1
和 person2
共享了定義在全局做用域上的 sayName()
函數。這樣雖然解決了相同邏輯的函數重複定義的問題,但全局做用域也所以被搞亂了,由於那個函數實際上只能在一個對象上調用。若是這個對象須要多個方法, 那麼就要在全局做用域中定義多個函數。這會致使自定義類型引用的代碼不能很好地彙集一塊兒。這個新問題能夠經過原型模式來解決。
每一個函數都會建立一個 prototype
屬性,這個屬性是一個對象,包含應該由特定引用類型的實例 共享的屬性和方法。實際上,這個對象就是經過調用構造函數建立的對象的原型。使用原型對象的好處是,在它上面定義的屬性和方法能夠被對象實例共享。原來在構造函數中直接賦給對象實例的值,能夠直接賦值給它們的原型,以下所示:
function Person() {}
Person.prototype.name = 'XHS-rookies'
Person.prototype.age = 18
Person.prototype.job = 'Software Engineer'
Person.prototype.sayName = function () {
console.log(this.name)
}
let person1 = new Person()
person1.sayName() // "XHS-rookies"
let person2 = new Person()
person2.sayName() // "XHS-rookies"
console.log(person1.sayName == person2.sayName) // true
複製代碼
使用函數表達式也能夠:
let Person = function () {}
Person.prototype.name = 'XHS-rookies'
Person.prototype.age = 18
Person.prototype.job = 'Software Engineer'
Person.prototype.sayName = function () {
console.log(this.name)
}
let person1 = new Person()
person1.sayName() // "XHS-rookies"
let person2 = new Person()
person2.sayName() // "XHS-rookies"
console.log(person1.sayName == person2.sayName) // true
複製代碼
這裏,全部屬性和 sayName()
方法都直接添加到了Person
的 prototype
屬性上,構造函數體中什麼也沒有。但這樣定義以後,調用構造函數建立的新對象仍然擁有相應的屬性和方法。與構造函數模式不一樣,使用這種原型模式定義的屬性和方法是由全部實例共享的。所以 person1
和 person2
訪問的都是相同的屬性和相同的 sayName()
函數。要理解這個過程,就必須理解 ECMAScript
中原型的本質。(詳細學習 ECMAScript
中的原型請見:對象原型)
有讀者可能注意到了,在前面的例子中,每次定義一個屬性或方法都會把 Person.prototype
重寫一遍。爲了減小代碼冗餘,也爲了從視覺上更好地封裝原型功能,直接經過一個包含全部屬性和方法 的對象字面量來重寫原型成爲了一種常見的作法,以下面的例子所示:
function Person() {}
Person.prototype = {
name: 'XHS-rookies',
age: 18,
job: 'Software Engineer',
sayName() {
console.log(this.name)
},
}
複製代碼
在這個例子中,Person.prototype
被設置爲等於一個經過對象字面量建立的新對象。最終結果是同樣的,只有一個問題:這樣重寫以後,Person.prototype
的 constructor
屬性就不指向 Person
了。在建立函數時,也會建立它的prototype
對象,同時會自動給這個原型的 constructor
屬性賦值。而上面的寫法徹底重寫了默認的prototype
對象,所以其 constructor
屬性也指向了徹底不一樣的新對象(Object
構造函數),再也不指向原來的構造函數。雖然 instanceof
操做符還能可靠地返回值,但咱們不能再依靠 constructor
屬性來識別類型了,以下面的例子所示:
let 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
複製代碼
這裏,instanceof
仍然對 Object
和 Person
都返回 true
。但 constructor
屬性如今等於 Object
而不是 Person
了。若是constructor
的值很重要,則能夠像下面這樣在重寫原型對象時專門設置一 下它的值:
function Person() {}
Person.prototype = {
constructor: Person,
name: 'XHS-rookies',
age: 18,
job: 'Software Engineer',
sayName() {
console.log(this.name)
},
}
複製代碼
此次的代碼中特地包含了 constructor
屬性,並將它設置爲 Person
,保證了這個屬性仍然包含恰當的值。 但要注意,以這種方式恢復 constructor
屬性會建立一個 [[Enumerable]]
爲 true
的屬性。而原生 constructor
屬性默認是不可枚舉的。所以,若是你使用的是兼容 ECMAScript
的 JavaScript
引擎, 那可能會改成使用 Object.defineProperty()
方法來定義 constructor
屬性:
function Person() {}
Person.prototype = {
name: 'XHS-rookies',
age: 18,
job: 'Software Engineer',
sayName() {
console.log(this.name)
},
}
// 恢復 constructor 屬性
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person,
})
複製代碼
前幾節深刻講解了如何只使用 ECMAScript 5
的特性來模擬相似於類(class-like
)的行爲。不難看出,各類策略都有本身的問題,也有相應的妥協。正由於如此,實現繼承的代碼也顯得很是冗長和混亂。
爲解決這些問題,ECMAScript 6
新引入的class
關鍵字具備正式定義類的能力。類(class
)是 ECMAScript
中新的基礎性語法糖結構,所以剛開始接觸時可能會不太習慣。雖然 ECMAScript 6
類表面 上看起來能夠支持正式的面向對象編程,但實際上它背後使用的仍然是原型和構造函數的概念。
與函數類型類似,定義類也有兩種主要方式:類聲明和類表達式。這兩種方式都使用 class
關鍵 字加大括號:
// 類聲明
class Person {}
// 類表達式
const Animal = class {}
複製代碼
與函數表達式相似,類表達式在它們被求值前也不能引用。不過,與函數定義不一樣的是,雖然函數聲明能夠提高,但類定義不能:
console.log(FunctionExpression) // undefined
var FunctionExpression = function () {}
console.log(FunctionExpression) // function() {}
console.log(FunctionDeclaration) // FunctionDeclaration() {}
function FunctionDeclaration() {}
console.log(FunctionDeclaration) // FunctionDeclaration() {}
console.log(ClassExpression) // undefined
var ClassExpression = class {}
console.log(ClassExpression) // class {}
console.log(ClassDeclaration) // ReferenceError: ClassDeclaration is not defined
class ClassDeclaration {}
console.log(ClassDeclaration) // class ClassDeclaration {}
複製代碼
另外一個跟函數聲明不一樣的地方是,函數受函數做用域限制,而類受塊做用域限制:
{
function FunctionDeclaration() {}
class ClassDeclaration {}
}
console.log(FunctionDeclaration) // FunctionDeclaration() {}
console.log(ClassDeclaration) // ReferenceError: ClassDeclaration is not defined
複製代碼
類能夠包含構造函數方法、實例方法、獲取函數、設置函數和靜態類方法,但這些都不是必需的。 空的類定義照樣有效。默認狀況下,類定義中的代碼都在嚴格模式下執行。
與函數構造函數同樣,多數編程風格都建議類名的首字母要大寫,以區別於經過它建立的實例(好比,經過 class Foo {}
建立實例 foo
):
// 空類定義,有效
class Foo {}
// 有構造函數的類,有效
class Bar {
constructor() {}
}
// 有獲取函數的類,有效
class Baz {
get myBaz() {}
}
// 有靜態方法的類,有效
class Qux {
static myQux() {}
}
複製代碼
類表達式的名稱是可選的。在把類表達式賦值給變量後,能夠經過 name
屬性取得類表達式的名稱字符串。但不能在類表達式做用域外部訪問這個標識符。
let Person = class PersonName {
identify() {
console.log(Person.name, PersonName.name)
}
}
let p = new Person()
p.identify() // PersonName PersonName
console.log(Person.name) // PersonName
console.log(PersonName) // ReferenceError: PersonName is not defined
複製代碼
constructor
關鍵字用於在類定義塊內部建立類的構造函數。方法名 constructor
會告訴解釋器 在使用 new
操做符建立類的新實例時,應該調用這個函數。構造函數的定義不是必需的,不定義構造函 數至關於將構造函數定義爲空函數。
實例化
使用 new
操做符實例化 Person 的操做等於使用 new
調用其構造函數。惟一可感知的不一樣之處就 是,JavaScript
解釋器知道使用 new
和類意味着應該使用 constructor
函數進行實例化。 使用 new
調用類的構造函數會執行以下操做。
(1)在內存中建立一個新對象。
(2)這個新對象內部的 [[Prototype]]
指針被賦值爲構造函數的 prototype
屬性。
(3)構造函數內部的 this
被賦值爲這個新對象(即 this
指向新對象)。
(4)執行構造函數內部的代碼(給新對象添加屬性)。
(5)若是構造函數返回非空對象,則返回該對象;不然,返回剛建立的新對象。
來看下面的例子:
class Animal {}
class Person {
constructor() {
console.log('person ctor')
}
}
class Vegetable {
constructor() {
this.color = 'orange'
}
}
let a = new Animal()
let p = new Person() // person ctor
let v = new Vegetable()
console.log(v.color) // orange
複製代碼
類實例化時傳入的參數會用做構造函數的參數。若是不須要參數,則類名後面的括號也是可選的:
class Person {
constructor(name) {
console.log(arguments.length)
this.name = name || null
}
}
let p1 = new Person() // 0
console.log(p1.name) // null
let p2 = new Person() // 0
console.log(p2.name) // null
let p3 = new Person('Jake') // 1
console.log(p3.name) // Jake
複製代碼
默認狀況下,類構造函數會在執行以後返回 this
對象。構造函數返回的對象會被用做實例化的對 象,若是沒有什麼引用新建立的 this
對象,那麼這個對象會被銷燬。不過,若是返回的不是 this
對 象,而是其餘對象,那麼這個對象不會經過 instanceof
操做符檢測出跟類有關聯,由於這個對象的原型指針並無被修改。
class Person {
constructor(override) {
this.foo = 'foo'
if (override) {
return {
bar: 'bar',
}
}
}
}
let p1 = new Person(),
p2 = new Person(true)
console.log(p1) // Person{ foo: 'foo' }
console.log(p1 instanceof Person) // true
console.log(p2) // { bar: 'bar' }
console.log(p2 instanceof Person) // false
複製代碼
類構造函數與構造函數的主要區別是,調用類構造函數必須使用 new
操做符。而普通構造函數若是不使用 new
調用,那麼就會以全局的 this
(一般是 window
)做爲內部對象。調用類構造函數時若是 忘了使用 new
則會拋出錯誤:
function Person() {}
class Animal {}
// 把 window 做爲 this 來構建實例
let p = Person()
let a = Animal()
// TypeError: class constructor Animal cannot be invoked without 'new'
複製代碼
類構造函數沒有什麼特殊之處,實例化以後,它會成爲普通的實例方法(但做爲類構造函數,仍然要使用 new
調用)。所以,實例化以後能夠在實例上引用它:
class Person {}
// 使用類建立一個新實例
let p1 = new Person()
p1.constructor()
// TypeError: Class constructor Person cannot be invoked without 'new'
// 使用對類構造函數的引用建立一個新實例
let p2 = new p1.constructor()
複製代碼
類的語法能夠很是方便地定義應該存在於實例上的成員、應該存在於原型上的成員,以及應該存在 於類自己的成員。
1. 實例成員
每次經過 new
調用類標識符時,都會執行類構造函數。在這個函數內部,能夠爲新建立的實例(this
) 添加「自有」屬性。至於添加什麼樣的屬性,則沒有限制。另外,在構造函數執行完畢後,仍然能夠給 實例繼續添加新成員。
每一個實例都對應一個惟一的成員對象,這意味着全部成員都不會在原型上共享:
class Person {
constructor() {
// 這個例子先使用對象包裝類型定義一個字符串
// 爲的是在下面測試兩個對象的相等性
this.name = new String('xhs-rookies')
this.sayName = () => console.log(this.name)
this.nicknames = ['xhs-rookies', 'J-Dog']
}
}
let p1 = new Person(),
p2 = new Person()
p1.sayName() // xhs-rookies
p2.sayName() // xhs-rookies
console.log(p1.name === p2.name) // false
console.log(p1.sayName === p2.sayName) // false
console.log(p1.nicknames === p2.nicknames) // false
p1.name = p1.nicknames[0]
p2.name = p2.nicknames[1]
p1.sayName() // xhs-rookies
p2.sayName() // J-Dog
複製代碼
2. 原型方法與訪問器
爲了在實例間共享方法,類定義語法把在類塊中定義的方法做爲原型方法。
class Person {
constructor() {
// 添加到 this 的全部內容都會存在於不一樣的實例上
this.locate = () => console.log('instance')
}
// 在類塊中定義的全部內容都會定義在類的原型上
locate() {
console.log('prototype')
}
}
let p = new Person()
p.locate() // instance
Person.prototype.locate() // prototype
複製代碼
能夠把方法定義在類構造函數中或者類塊中,但不能在類塊中給原型添加原始值或對象做爲成員數據:
class Person {
name: 'xhs-rookies'
}
// Uncaught SyntaxError: Unexpected token
複製代碼
類方法等同於對象屬性,所以可使用字符串、符號或計算的值做爲鍵:
const symbolKey = Symbol('symbolKey')
class Person {
stringKey() {
console.log('invoked stringKey')
}
[symbolKey]() {
console.log('invoked symbolKey')
}
['computed' + 'Key']() {
console.log('invoked computedKey')
}
}
let p = new Person()
p.stringKey() // invoked stringKey
p[symbolKey]() // invoked symbolKey
p.computedKey() // invoked computedKey
複製代碼
類定義也支持獲取和設置訪問器。語法與行爲跟普通對象同樣:
class Person {
set name(newName) {
this.name_ = newName
}
get name() {
return this.name_
}
}
let p = new Person()
p.name = 'xhs-rookies'
console.log(p.name) // xhs-rookies
複製代碼
3. 靜態類方法
能夠在類上定義靜態方法。這些方法一般用於執行不特定於實例的操做,也不要求存在類的實例。與原型成員相似,靜態成員每一個類上只能有一個。 靜態類成員在類定義中使用 static
關鍵字做爲前綴。在靜態成員中,this
引用類自身。其餘所 有約定跟原型成員同樣:
class Person {
constructor() {
// 添加到 this 的全部內容都會存在於不一樣的實例上
this.locate = () => console.log('instance', this)
}
// 定義在類的原型對象上
locate() {
console.log('prototype', this)
}
// 定義在類自己上
static locate() {
console.log('class', this)
}
}
let p = new Person()
p.locate() // instance, Person {}
Person.prototype.locate() // prototype, {constructor: ... }
Person.locate() // class, class Person {}
複製代碼
靜態類方法很是適合做爲實例工廠:
class Person {
constructor(age) {
this.age_ = age
}
sayAge() {
console.log(this.age_)
}
static create() {
// 使用隨機年齡建立並返回一個 Person 實例
return new Person(Math.floor(Math.random() * 100))
}
}
console.log(Person.create()) // Person { age_: ... }
複製代碼
4. 非函數原型和類成員
雖然類定義並不顯式支持在原型或類上添加成員數據,但在類定義外部,能夠手動添加:
class Person {
sayName() {
console.log(`${Person.greeting} ${this.name}`)
}
}
// 在類上定義數據成員
Person.greeting = 'My name is'
// 在原型上定義數據成員
Person.prototype.name = 'xhs-rookies'
let p = new Person()
p.sayName() // My name is xhs-rookies
複製代碼
注意 類定義中之因此沒有顯式支持添加數據成員,是由於在共享目標(原型和類)上添 加可變(可修改)數據成員是一種反模式。通常來講,對象實例應該獨自擁有經過
this
引用的數據(注意在不一樣狀況下使用this
的狀況會略有些不一樣,詳細this
學習請見this-MDN)。
5. 迭代器與生成器方法
類定義語法支持在原型和類自己上定義生成器方法:
class Person {
// 在原型上定義生成器方法
*createNicknameIterator() {
yield 'xhs-Jack'
yield 'xhs-Jake'
yield 'xhs-J-Dog'
}
// 在類上定義生成器方法
static *createJobIterator() {
yield 'xhs-Butcher'
yield 'xhs-Baker'
yield 'xhs-Candlestick maker'
}
}
let jobIter = Person.createJobIterator()
console.log(jobIter.next().value) // xhs-Butcher
console.log(jobIter.next().value) // xhs-Baker
console.log(jobIter.next().value) // xhs-Candlestick maker
let p = new Person()
let nicknameIter = p.createNicknameIterator()
console.log(nicknameIter.next().value) // xhs-Jack
console.log(nicknameIter.next().value) // xhs-Jake
console.log(nicknameIter.next().value) // xhs-J-Dog
複製代碼
由於支持生成器方法,因此能夠經過添加一個默認的迭代器,把類實例變成可迭代對象:
class Person {
constructor() {
this.nicknames = ['xhs-Jack', 'xhs-Jake', 'xhs-J-Dog']
}
*[Symbol.iterator]() {
yield* this.nicknames.entries()
}
}
let p = new Person()
for (let [idx, nickname] of p) {
console.log(nickname)
}
// xhs-Jack
// xhs-Jake
// xhs-J-Dog
//也能夠只返回迭代器實例:
class Person {
constructor() {
this.nicknames = ['xhs-Jack', 'xhs-Jake', 'xhs-J-Dog']
}
[Symbol.iterator]() {
return this.nicknames.entries()
}
}
let p = new Person()
for (let [idx, nickname] of p) {
console.log(nickname)
}
// xhs-Jack
// xhs-Jake
// xhs-J-Dog
複製代碼
ECMAScript 6
新增了對象解構語法,能夠在一條語句中使用嵌套數據實現一個或多個賦值操做。簡單地說,對象解構就是使用與對象匹配的結構來實現對象屬性賦值。 下面的例子展現了兩段等價的代碼,首先是不使用對象解構的:
// 不使用對象解構
let person = {
name: 'xhs-Matt',
age: 18,
}
let personName = person.name,
personAge = person.age
console.log(personName) // xhs-Matt
console.log(personAge) // 18
複製代碼
而後,是使用對象解構的:
// 使用對象解構
let person = {
name: 'xhs-Matt',
age: 18,
}
let { name: personName, age: personAge } = person
console.log(personName) // xhs-Matt
console.log(personAge) // 18
複製代碼
使用解構,能夠在一個相似對象字面量的結構中,聲明多個變量,同時執行多個賦值操做。若是想讓變量直接使用屬性的名稱,那麼可使用簡寫語法,好比:
let person = {
name: 'xhs-Matt',
age: 18,
}
let { name, age } = person
console.log(name) // xhs-Matt
console.log(age) // 18
複製代碼
解構不成功以及對象解構能夠指定一些默認值的狀況,這些詳細內容能夠見咱們的解構賦值文章,在對象中咱們不過多贅述。
本章前面花了大量篇幅討論如何使用 ES5
的機制實現繼承。ECMAScript 6
新增特性中最出色的一 個就是原生支持了類繼承機制。雖然類繼承使用的是新語法,但背後依舊使用的是原型鏈。
ES6
類支持單繼承。使用 extends
關鍵字,就能夠繼承任何擁有 [[Construct]]
和原型的對象。 很大程度上,這意味着不只能夠繼承一個類,也能夠繼承普通的構造函數(保持向後兼容):
class Vehicle {}
// 繼承類
class Bus extends Vehicle {}
let b = new Bus()
console.log(b instanceof Bus) // true
console.log(b instanceof Vehicle) // true
function Person() {}
// 繼承普通構造函數
class Engineer extends Person {}
let e = new Engineer()
console.log(e instanceof Engineer) // true
console.log(e instanceof Person) // true
複製代碼
派生類都會經過原型鏈訪問到類和原型上定義的方法。this
的值會反映調用相應方法的實例或者類:
class Vehicle {
identifyPrototype(id) {
console.log(id, this)
}
static identifyClass(id) {
console.log(id, this)
}
}
class Bus extends Vehicle {}
let v = new Vehicle()
let b = new Bus()
b.identifyPrototype('bus') // bus, Bus {}
v.identifyPrototype('vehicle') // vehicle, Vehicle {}
Bus.identifyClass('bus') // bus, class Bus {}
Vehicle.identifyClass('vehicle') // vehicle, class Vehicle {}
複製代碼
注意: extends
關鍵字也能夠在類表達式中使用,所以 let Bar = class extends Foo {}
是有效的語法。
派生類的方法能夠經過 super
關鍵字引用它們的原型。這個關鍵字只能在派生類中使用,並且僅限於類構造函數、實例方法和靜態方法內部。在類構造函數中使用 super
能夠調用父類構造函數。
class Vehicle {
constructor() {
this.hasEngine = true
}
}
class Bus extends Vehicle {
constructor() {
// 不要在調用 super()以前引用 this,不然會拋出 ReferenceError
super() // 至關於 super.constructor()
console.log(this instanceof Vehicle) // 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
複製代碼
注意: ES6
給類構造函數和靜態方法添加了內部特性 [[HomeObject]]
,這個特性是一個指針,指向定義該方法的對象。這個指針是自動賦值的,並且只能在 JavaScript 引擎內部訪問。super
始終會定義爲[[HomeObject]]
的原型。
super
只能在派生類構造函數和靜態方法中使用。class Vehicle {
constructor() {
super()
// SyntaxError: 'super' keyword unexpected
}
}
複製代碼
super
關鍵字,要麼用它調用構造函數,要麼用它引用靜態方法。class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(super)
// SyntaxError: 'super' keyword unexpected here
}
}
複製代碼
super()
會調用父類構造函數,並將返回的實例賦值給 this
。class Vehicle {}
class Bus extends Vehicle {
constructor() {
super()
console.log(this instanceof Vehicle)
}
}
new Bus() // true
複製代碼
super()
的行爲如同調用構造函數,若是須要給父類構造函數傳參,則須要手動傳入。class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate
}
}
class Bus extends Vehicle {
constructor(licensePlate) {
super(licensePlate)
}
}
console.log(new Bus('1337H4X')) // Bus { licensePlate: '1337H4X' }
複製代碼
super()
,並且會傳入全部傳給派生類的 參數。class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate
}
}
class Bus extends Vehicle {}
console.log(new Bus('1337H4X')) // Bus { licensePlate: '1337H4X' }
複製代碼
super()
以前引用this
。class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(this)
}
}
new Bus()
// ReferenceError: Must call super constructor in derived class
// before accessing 'this' or returning from derived constructor
複製代碼
super()
,要麼必須在其中返回 一個對象。class Vehicle {}
class Car extends Vehicle {}
class Bus extends Vehicle {
constructor() {
super()
}
}
class Van extends Vehicle {
constructor() {
return {}
}
}
console.log(new Car()) // Car {}
console.log(new Bus()) // Bus {}
console.log(new Van()) // {}
複製代碼
爲了方便操做原始值,ECMAScript
提供了 3 種特殊的引用類型:Boolean
、Number
和 String
。 這些類型具備本章介紹的其餘引用類型同樣的特色,但也具備與各自原始類型對應的特殊行爲。每當用到某個原始值的方法或屬性時,後臺都會建立一個相應原始包裝類型的對象,從而暴露出操做原始值的 各類方法。來看下面的例子:
let s1 = 'xhs-rookies'
let s2 = s1.substring(2)
複製代碼
在這裏,s1
是一個包含字符串的變量,它是一個原始值。第二行緊接着在 s1
上調用了 substring()
方法,並把結果保存在 s2
中。咱們知道,原始值自己不是對象,所以邏輯上不該該有方法。而實際上 這個例子又確實按照預期運行了。這是由於後臺進行了不少處理,從而實現了上述操做。具體來講,當 第二行訪問 s1
時,是以讀模式訪問的,也就是要從內存中讀取變量保存的值。在以讀模式訪問字符串 值的任什麼時候候,後臺都會執行如下 3 步:
(1)建立一個 String
類型的實例;
(2)調用實例上的特定方法;
(3)銷燬實例。
能夠把這 3 步想象成執行了以下 3 行 ECMAScript
代碼:
let s1 = new String('xhs-rookies')
let s2 = s1.substring(2)
s1 = null
複製代碼
這種行爲可讓原始值擁有對象的行爲。對布爾值和數值而言,以上 3 步也會在後臺發生,只不過 使用的是 Boolean
和 Number
包裝類型而已。 引用類型與原始值包裝類型的主要區別在於對象的生命週期。在經過 new
實例化引用類型後,獲得 的實例會在離開做用域時被銷燬,而自動建立的原始值包裝對象則只存在於訪問它的那行代碼執行期 間。這意味着不能在運行時給原始值添加屬性和方法。好比下面的例子:
let s1 = 'xhs-rookies'
s1.color = 'red'
console.log(s1.color) // undefined
複製代碼
這裏的第二行代碼嘗試給字符串 s1 添加了一個 color
屬性。但是,第三行代碼訪問 color
屬性時, 它卻不見了。緣由就是第二行代碼運行時會臨時建立一個 String
對象,而當第三行代碼執行時,這個對象已經被銷燬了。實際上,第三行代碼在這裏建立了本身的 String
對象,但這個對象沒有 color
屬性。
能夠顯式地使用 Boolean
、Number
和String
構造函數建立原始值包裝對象。不過應該在確實必 要時再這麼作,不然容易讓開發者疑惑,分不清它們究竟是原始值仍是引用值。在原始值包裝類型的實 例上調用 typeof
會返回 "object"
,全部原始值包裝對象都會轉換爲布爾值true
。
另外,Object
構造函數做爲一個工廠方法,可以根據傳入值的類型返回相應原始值包裝類型的實 例。好比:
let obj = new Object('xhs-rookies')
console.log(obj instanceof String) // true
複製代碼
若是傳給 Object
的是字符串,則會建立一個 String
的實例。若是是數值,則會建立 Number
的 實例。布爾值則會獲得 Boolean
的實例。
注意,使用 new
調用原始值包裝類型的構造函數,與調用同名的轉型函數並不同。例如:
let value = '18'
let number = Number(value) // 轉型函數
console.log(typeof number) // "number"
let obj = new Number(value) // 構造函數
console.log(typeof obj) // "object"
複製代碼
在這個例子中,變量 number
中保存的是一個值爲 25 的原始數值,而變量 obj
中保存的是一個 Number
的實例。
雖然不推薦顯式建立原始值包裝類型的實例,但它們對於操做原始值的功能是很重要的。每一個原始值包裝類型都有相應的一套方法來方便數據操做。
一:全部對象都有原型。
除了基本對象(base object
),全部對象都有原型。基本對象能夠訪問一些方法和屬性,好比 .toString
。這就是爲何你可使用內置的 JavaScript
方法!全部這些方法在原型上都是可用的。雖然JavaScript
不能直接在對象上找到這些方法,但 JavaScript
會沿着原型鏈找到它們,以便於你使用。
二:如下哪一項會對對象 person 有反作用?
const person = {
name: 'Lydia Hallie',
address: {
street: '100 Main St',
},
}
Object.freeze(person)
複製代碼
person.name = "Evan Bacon"
delete person.address
person.address.street = "101 Main St"
person.pet = { name: "Mara" }
Answer:C
使用方法 Object.freeze
對一個對象進行 凍結。不能對屬性進行添加,修改,刪除。
然而,它僅對對象進行淺凍結,意味着只有 對象中的 直接 屬性被凍結。若是屬性是另外一個 object
,像案例中的 address
,address
中的屬性沒有被凍結,仍然能夠被修改。
三:使用哪一個構造函數能夠成功繼承Dog
類?
class Dog {
constructor(name) {
this.name = name
}
}
class Labrador extends Dog {
// 1
constructor(name, size) {
this.size = size
}
// 2
constructor(name, size) {
super(name)
this.size = size
}
// 3
constructor(size) {
super(name)
this.size = size
}
// 4
constructor(name, size) {
this.name = name
this.size = size
}
}
複製代碼
Answer:B
在子類中,在調用 super
以前不能訪問到 this
關鍵字。 若是這樣作,它將拋出一個 ReferenceError:1
和 4 將引起一個引用錯誤。
使用 super
關鍵字,須要用給定的參數來調用父類的構造函數。 父類的構造函數接收 name
參數,所以咱們須要將 name
傳遞給 super
。
Labrador
類接收兩個參數,name
參數是因爲它繼承了 Dog
,size
做爲 Labrador
類的額外屬性,它們都須要傳遞給 Labrador
的構造函數,所以使用構造函數 2 正確完成。
JavaScript 系列的對象,咱們到這裏結束啦,謝謝各位對做者的支持!大家的關注和點贊,將會是咱們前進的最強動力!謝謝你們!