instanceof
營運子.__proto__
與 .prototype
Person.prototype.constructor
(進階)this
存取靜態私有欄位instanceof
和子類別化(進階)Object
的實例(進階)Object.prototype
的方法和存取器(進階)
Object.prototype
方法Object.prototype.toString()
Object.prototype.toLocaleString()
Object.prototype.valueOf()
Object.prototype.isPrototypeOf()
Object.prototype.propertyIsEnumerable()
Object.prototype.__proto__
(存取器)Object.prototype.hasOwnProperty()
本書分四個步驟介紹 JavaScript 的物件導向程式 (OOP) 風格。本章涵蓋步驟 3 和 4,上一章涵蓋步驟 1 和 2。這些步驟如下(圖 12)
超類別
class Person {
; // (A)
#firstNameconstructor(firstName) {
this.#firstName = firstName; // (B)
}describe() {
return `Person named ${this.#firstName}`;
}static extractNames(persons) {
return persons.map(person => person.#firstName);
}
}const tarzan = new Person('Tarzan');
.equal(
assert.describe(),
tarzan'Person named Tarzan'
;
).deepEqual(
assert.extractNames([tarzan, new Person('Cheeta')]),
Person'Tarzan', 'Cheeta']
[; )
子類別
class Employee extends Person {
constructor(firstName, title) {
super(firstName);
this.title = title; // (C)
}describe() {
return super.describe() +
` (${this.title})`;
}
}
const jane = new Employee('Jane', 'CTO');
.equal(
assert.title,
jane'CTO'
;
).equal(
assert.describe(),
jane'Person named Jane (CTO)'
; )
備註
.#firstName
是私有欄位,必須先宣告(A 行)才能初始化(B 行)。
.title
是屬性,可以在沒有事先宣告的情況下初始化(C 行)。相較於偏好隱藏資料的 Java 等語言,JavaScript 相對常讓實例資料公開。類別基本上是設定原型鏈的簡潔語法(在上一章中說明)。在底層,JavaScript 的類別是不尋常的。但這是我們在使用它們時很少見到的情況。對於使用過其他物件導向程式設計語言的人來說,它們通常應該感覺很熟悉。
請注意,我們不需要類別來建立物件。我們也可以透過物件文字來建立。這就是為什麼 JavaScript 中不需要單例模式,而且類別的使用次數比許多其他有類別的語言少。
我們之前使用過jane
和tarzan
,它們是代表人物的單一物件。我們使用類別宣告來實作此類物件的工廠
class Person {
; // (A)
#firstNameconstructor(firstName) {
this.#firstName = firstName; // (B)
}describe() {
return `Person named ${this.#firstName}`;
}static extractNames(persons) {
return persons.map(person => person.#firstName);
} }
現在可以使用new Person()
建立jane
和tarzan
const jane = new Person('Jane');
const tarzan = new Person('Tarzan');
讓我們檢查Person
類別主體內部有什麼。
.constructor()
是一個在建立新實例後呼叫的特殊方法。在其中,this
是指那個實例。
[ES2022] .#firstName
是實例私有欄位:此類欄位儲存在實例中。它們的存取方式類似於屬性,但它們的名稱是分開的,它們總是從井號符號(#
)開始。而且它們對類別外部的世界是不可見的
.deepEqual(
assertReflect.ownKeys(jane),
[]; )
在我們可以在建構函式中初始化.#firstName
(B 行)之前,我們需要透過在類別主體中提到它來宣告它(A 行)。
.describe()
是一個方法。如果我們透過 obj.describe()
呼叫它,則 this
會在 .describe()
的主體內參考 obj
。
.equal(
assert.describe(), 'Person named Jane'
jane;
).equal(
assert.describe(), 'Person named Tarzan'
tarzan; )
.extractName()
是一個靜態方法。「靜態」表示它屬於類別,不屬於實例
.deepEqual(
assert.extractNames([jane, tarzan]),
Person'Jane', 'Tarzan']
[; )
我們也可以在建構函式中建立實例屬性(公開欄位)
class Container {
constructor(value) {
this.value = value;
}
}const abcContainer = new Container('abc');
.equal(
assert.value, 'abc'
abcContainer; )
與實例私有欄位相反,實例屬性不必在類別主體中宣告。
有兩種類別定義(定義類別的方法)
類別表達式可以是匿名的,也可以是有名稱的
// Anonymous class expression
const Person = class { ··· };
// Named class expression
const Person = class MyClass { ··· };
命名類別表達式的名稱運作方式類似於 命名函式表達式的名稱:它只能在類別主體內存取,並且保持不變,無論類別如何指派。
instanceof
算子instanceof
算子告訴我們一個值是否為給定類別的實例
> new Person('Jane') instanceof Persontrue
> {} instanceof Personfalse
> {} instanceof Objecttrue
> [] instanceof Arraytrue
我們將在 稍後更詳細地探討 instanceof
算子,在我們看過子類別化之後。
在 JavaScript 語言中,物件可以有兩種「槽」。
以下是我們需要知道的關於屬性和私有槽最重要的規則
static
和其他因素。 關於屬性和私有槽的更多資訊
本章未涵蓋屬性和私有槽的所有詳細資訊(僅涵蓋要點)。如果您想要更深入地探討,可以按這裡
[[PrivateElements]]
」。以下類別展示了兩種槽。每個實例都有私有欄位和屬性
class MyClass {
= 1;
#instancePrivateField = 2;
instanceProperty getInstanceValues() {
return [
this.#instancePrivateField,
this.instanceProperty,
;
]
}
}const inst = new MyClass();
.deepEqual(
assert.getInstanceValues(), [1, 2]
inst; )
正如預期,在 MyClass
外部,我們只能看到屬性
.deepEqual(
assertReflect.ownKeys(inst),
'instanceProperty']
[; )
接下來,我們將探討私有槽的一些細節。
私有槽只能在其類別主體內部存取。我們甚至無法從子類別存取它
class SuperClass {
= 'superProp';
#superProp
}class SubClass extends SuperClass {
getSuperProp() {
return this.#superProp;
}
}// SyntaxError: Private field '#superProp'
// must be declared in an enclosing class
透過 extends
進行子類別化 將在本章稍後說明。如何在 §29.5.4「透過 WeakMaps 模擬受保護可見性和友元可見性」 中說明如何解決此限制。
私有槽有唯一的金鑰,類似於 符號。考慮以下類別
class MyClass {
= 1;
#instancePrivateField = 2;
instanceProperty getInstanceValues() {
return [
this.#instancePrivateField,
this.instanceProperty,
;
]
} }
在內部,MyClass
的私有欄位大致處理如下
let MyClass;
// Scope of the body of the class
{ const instancePrivateFieldKey = Symbol();
= class {
MyClass // Very loose approximation of how this
// works in the language specification
= new Map([
__PrivateElements__ , 1],
[instancePrivateFieldKey;
])= 2;
instanceProperty getInstanceValues() {
return [
this.__PrivateElements__.get(instancePrivateFieldKey),
this.instanceProperty,
;
]
}
} }
instancePrivateFieldKey
的值稱為私有名稱。我們無法在 JavaScript 中直接使用私有名稱,只能透過私有欄位、私有方法和私有存取器的固定識別碼間接使用。其中,公有槽的固定識別碼(例如 getInstanceValues
)被解釋為字串金鑰,私有槽的固定識別碼(例如 #instancePrivateField
)則是指私有名稱(類似於變數名稱指稱值)。
由於私有槽的識別碼不用作金鑰,因此在不同的類別中使用相同的識別碼會產生不同的槽(A 行和 C 行)
class Color {
; // (A)
#nameconstructor(name) {
this.#name = name; // (B)
}static getName(obj) {
return obj.#name;
}
}class Person {
; // (C)
#nameconstructor(name) {
this.#name = name;
}
}
.equal(
assert.getName(new Color('green')), 'green'
Color;
)
// We can’t access the private slot #name of a Person in line B:
.throws(
assert=> Color.getName(new Person('Jane')),
()
{name: 'TypeError',
message: 'Cannot read private member #name from'
+ ' an object whose class did not declare it',
}; )
即使子類別對私有欄位使用相同名稱,兩個名稱也不會衝突,因為它們指的是私有名稱(總是唯一的)。在以下範例中,SuperClass
中的 .#privateField
即使兩個插槽都直接儲存在 inst
中,也不會與 SubClass
中的 .#privateField
衝突
class SuperClass {
= 'super';
#privateField getSuperPrivateField() {
return this.#privateField;
}
}class SubClass extends SuperClass {
= 'sub';
#privateField getSubPrivateField() {
return this.#privateField;
}
}const inst = new SubClass();
.equal(
assert.getSuperPrivateField(), 'super'
inst;
).equal(
assert.getSubPrivateField(), 'sub'
inst; )
透過 extends
進行子類別化會在本章節稍後說明。
in
檢查物件是否有指定的私有插槽in
營運子可用於檢查私有插槽是否存在(A 行)
class Color {
;
#nameconstructor(name) {
this.#name = name;
}static check(obj) {
return #name in obj; // (A)
} }
讓我們看看應用於私有插槽的更多 in
範例。
私有方法。以下程式碼顯示私有方法會在執行個體中建立私有插槽
class C1 {
priv() {}
#static check(obj) {
return #priv in obj;
}
}.equal(C1.check(new C1()), true); assert
靜態私有欄位。我們也可以對靜態私有欄位使用 in
class C2 {
static #priv = 1;
static check(obj) {
return #priv in obj;
}
}.equal(C2.check(C2), true);
assert.equal(C2.check(new C2()), false); assert
靜態私有方法。我們也可以檢查靜態私有方法的插槽
class C3 {
static #priv() {}
static check(obj) {
return #priv in obj;
}
}.equal(C3.check(C3), true); assert
在不同的類別中使用相同的私有識別碼。在以下範例中,兩個類別 Color
和 Person
都有一個識別碼為 #name
的插槽。in
營運子正確區分它們
class Color {
;
#nameconstructor(name) {
this.#name = name;
}static check(obj) {
return #name in obj;
}
}class Person {
;
#nameconstructor(name) {
this.#name = name;
}static check(obj) {
return #name in obj;
}
}
// Detecting Color’s #name
.equal(
assert.check(new Color()), true
Color;
).equal(
assert.check(new Person()), false
Color;
)
// Detecting Person’s #name
.equal(
assert.check(new Person()), true
Person;
).equal(
assert.check(new Color()), false
Person; )
我建議使用類別,原因如下
類別是物件建立和繼承的常見標準,現在廣泛支援於各個函式庫和架構中。與之前的情況相比,這是一個進步,當時幾乎每個架構都有自己的繼承函式庫。
它們有助於 IDE 和類型檢查器等工具進行工作,並在那裡啟用新功能。
如果您從另一種語言轉到 JavaScript,並且習慣於類別,那麼您可以更快地開始。
JavaScript 引擎會最佳化它們。也就是說,使用類別的程式碼幾乎總是比使用自訂繼承函式庫的程式碼更快。
我們可以對內建建構函式(例如 Error
)進行子類別化。
這並不表示類別是完美的
有過度繼承的風險。
有將過多功能放入類別的風險(而其中一些功能通常最好放入函式中)。
類別看起來很熟悉,對於來自其他語言的程式設計師來說,但它們的工作方式不同,使用方式也不同(請參閱下一個小節)。因此,這些程式設計師有編寫不像是 JavaScript 程式碼的風險。
類別表面上看起來如何運作與實際運作方式有很大的不同。換句話說,語法和語意之間存在差距。兩個範例是
C
內的方法定義會在物件 C.prototype
中建立一個方法。不連接的動機是向後相容性。謝天謝地,在實務上不連接只會造成少數問題;如果我們順著類別假裝的樣子,通常沒問題。
這是類別的第一眼。我們很快就會探討更多功能。
練習:撰寫類別
exercises/classes/point_class_test.mjs
在幕後,類別會變成兩個連接的物件。讓我們重新檢視類別 Person
,看看它是如何運作的
class Person {
;
#firstNameconstructor(firstName) {
this.#firstName = firstName;
}describe() {
return `Person named ${this.#firstName}`;
}static extractNames(persons) {
return persons.map(person => person.#firstName);
} }
類別建立的第一個物件儲存在 Person
中。它有四個屬性
.deepEqual(
assertReflect.ownKeys(Person),
'length', 'name', 'prototype', 'extractNames']
[;
)
// The number of parameters of the constructor
.equal(
assert.length, 1
Person;
)
// The name of the class
.equal(
assert.name, 'Person'
Person; )
剩下的兩個屬性是
Person.extractNames
是我們已經看過實際運作的靜態方法。Person.prototype
指向類別定義建立的第二個物件。以下是 Person.prototype
的內容
.deepEqual(
assertReflect.ownKeys(Person.prototype),
'constructor', 'describe']
[; )
有兩個屬性
Person.prototype.constructor
指向建構函式。Person.prototype.describe
是我們已經使用過的方法。物件 Person.prototype
是所有實例的原型
const jane = new Person('Jane');
.equal(
assertObject.getPrototypeOf(jane), Person.prototype
;
)
const tarzan = new Person('Tarzan');
.equal(
assertObject.getPrototypeOf(tarzan), Person.prototype
; )
這說明了實例如何取得它們的方法:它們從物件 Person.prototype
繼承這些方法。
圖 13 視覺化了所有東西是如何連接的。
.__proto__
與 .prototype
很容易將 .__proto__
和 .prototype
搞混。希望圖 13 能清楚說明它們的差異
.__proto__
是類別 Object
的存取器,讓我們取得並設定其實例的原型。
.prototype
是個正常的屬性,就像其他屬性一樣。它之所以特別,是因為 new
算子會使用它的值作為實例的原型。它的名稱並不理想。使用其他名稱,例如 .instancePrototype
會更貼切。
Person.prototype.constructor
(進階)圖 13 中有一個細節我們尚未探討:Person.prototype.constructor
指回 Person
> Person.prototype.constructor === Persontrue
這種設定是為了向後相容性而存在。但它還有兩個額外的優點。
首先,類別的每個實例都會繼承 .constructor
屬性。因此,給定一個實例,我們可以透過它建立「類似的」物件
const jane = new Person('Jane');
const cheeta = new jane.constructor('Cheeta');
// cheeta is also an instance of Person
.equal(cheeta instanceof Person, true); assert
其次,我們可以取得建立給定實例的類別名稱
const tarzan = new Person('Tarzan');
.equal(tarzan.constructor.name, 'Person'); assert
在這個小節中,我們將瞭解呼叫方法的兩種不同方式
了解這兩種方式,將讓我們深入瞭解方法運作的方式。
我們也需要第二種方式,稍後在本章中:它將允許我們從 Object.prototype
借用有用的方法。
讓我們探討方法呼叫如何與類別一起運作。我們重新檢視早先的 jane
class Person {
;
#firstNameconstructor(firstName) {
this.#firstName = firstName;
}describe() {
return 'Person named '+this.#firstName;
}
}const jane = new Person('Jane');
圖 14 有個圖表,顯示 jane
的原型鏈。
正常的呼叫方法是派送的 – 方法呼叫
.describe() jane
分為兩個步驟
派送:JavaScript 從 jane
開始,遍歷原型鏈,以找到第一個擁有鍵值為 'describe'
的自有屬性的物件:它首先查看 jane
,並未找到自有屬性 .describe
。它繼續查看 jane
的原型 Person.prototype
,並找到一個自有屬性 describe
,並傳回其值。
const func = jane.describe;
呼叫:呼叫方法的值與呼叫函數的值不同,在於它不僅呼叫括號前的內容,並在括號內傳入參數,還會將 this
設定為方法呼叫的接收者(在本例中為 jane
)
.call(jane); func
這種動態尋找方法並呼叫它的方式稱為動態派送。
我們也可以直接呼叫方法,而不用派送
.prototype.describe.call(jane) Person
這次,我們透過 Person.prototype.describe
直接指向方法,而不會在原型鏈中搜尋它。我們也透過 .call()
以不同的方式指定 this
。
this
永遠指向實例
不論實例的原型鏈中的方法位於何處,this
永遠指向實例(原型鏈的開頭)。這使得 .describe()
能夠在範例中存取 .#firstName
。
何時直接方法呼叫是有用的?當我們想要從其他地方借用一個給定物件所沒有的方法時,例如
const obj = Object.create(null);
// `obj` is not an instance of Object and doesn’t inherit
// its prototype method .toString()
.throws(
assert=> obj.toString(),
() /^TypeError: obj.toString is not a function$/
;
).equal(
assertObject.prototype.toString.call(obj),
'[object Object]'
; )
在 ECMAScript 6 之前,JavaScript 沒有類別。取而代之的是,一般函式被用作建構函式
function StringBuilderConstr(initialString) {
this.string = initialString;
}.prototype.add = function (str) {
StringBuilderConstrthis.string += str;
return this;
;
}
const sb = new StringBuilderConstr('¡');
.add('Hola').add('!');
sb.equal(
assert.string, '¡Hola!'
sb; )
類別為此方法提供了更好的語法
class StringBuilderClass {
constructor(initialString) {
this.string = initialString;
}add(str) {
this.string += str;
return this;
}
}const sb = new StringBuilderClass('¡');
.add('Hola').add('!');
sb.equal(
assert.string, '¡Hola!'
sb; )
使用建構函式進行子類別化特別棘手。類別也提供了超越更方便語法的好處
Error
,可以被子類別化。super
存取覆寫的屬性。new
呼叫,也沒有 .prototype
屬性。類別與建構函式非常相容,甚至可以延伸它們
function SuperConstructor() {}
class SubClass extends SuperConstructor {}
.equal(
assertnew SubClass() instanceof SuperConstructor, true
; )
extends
和子類別化會在 本章節稍後 說明。
這讓我們獲得了一個有趣的見解。一方面,StringBuilderClass
透過 StringBuilderClass.prototype.constructor
參照其建構函式。
另一方面,類別就是建構函式(一個函式)
> StringBuilderClass.prototype.constructor === StringBuilderClasstrue
> typeof StringBuilderClass'function'
建構函式(函式)vs. 類別
由於它們非常相似,我交替使用建構函式(函式)和類別這兩個術語。
下列類別宣告主體中的所有成員都會建立 PublicProtoClass.prototype
的屬性。
class PublicProtoClass {
constructor(args) {
// (Do something with `args` here.)
}publicProtoMethod() {
return 'publicProtoMethod';
}publicProtoAccessor() {
get return 'publicProtoGetter';
}publicProtoAccessor(value) {
set .equal(value, 'publicProtoSetter');
assert
}
}
.deepEqual(
assertReflect.ownKeys(PublicProtoClass.prototype),
'constructor', 'publicProtoMethod', 'publicProtoAccessor']
[;
)
const inst = new PublicProtoClass('arg1', 'arg2');
.equal(
assert.publicProtoMethod(), 'publicProtoMethod'
inst;
).equal(
assert.publicProtoAccessor, 'publicProtoGetter'
inst;
).publicProtoAccessor = 'publicProtoSetter'; inst
const accessorKey = Symbol('accessorKey');
const syncMethodKey = Symbol('syncMethodKey');
const syncGenMethodKey = Symbol('syncGenMethodKey');
const asyncMethodKey = Symbol('asyncMethodKey');
const asyncGenMethodKey = Symbol('asyncGenMethodKey');
class PublicProtoClass2 {
// Identifier keys
accessor() {}
get accessor(value) {}
set syncMethod() {}
* syncGeneratorMethod() {}
async asyncMethod() {}
async * asyncGeneratorMethod() {}
// Quoted keys
'an accessor'() {}
get 'an accessor'(value) {}
set 'sync method'() {}
* 'sync generator method'() {}
async 'async method'() {}
async * 'async generator method'() {}
// Computed keys
get [accessorKey]() {}
set [accessorKey](value) {}
[syncMethodKey]() {}* [syncGenMethodKey]() {}
async [asyncMethodKey]() {}
async * [asyncGenMethodKey]() {}
}
// Quoted and computed keys are accessed via square brackets:
const inst = new PublicProtoClass2();
'sync method']();
inst[; inst[syncMethodKey]()
引號鍵和計算鍵也可以用在物件字面上
更多有關存取器(透過 getter 和/或 setter 定義)、產生器、非同步方法和非同步產生器方法的資訊
私有方法(和存取器)是原型成員和實例成員的有趣組合。
一方面,私有方法儲存在實例中的槽位(A 行)
class MyClass {
privateMethod() {}
#static check() {
const inst = new MyClass();
.equal(
assertin inst, true // (A)
#privateMethod ;
).equal(
assertin MyClass.prototype, false
#privateMethod ;
).equal(
assertin MyClass, false
#privateMethod ;
)
}
}.check(); MyClass
為什麼它們沒有儲存在 .prototype
物件中?私有槽位不會被繼承,只有屬性會被繼承。
另一方面,私有方法在實例之間共用,就像原型公開方法一樣
class MyClass {
privateMethod() {}
#static check() {
const inst1 = new MyClass();
const inst2 = new MyClass();
.equal(
assert.#privateMethod,
inst1.#privateMethod
inst2;
)
} }
由於它們的語法類似於原型公共方法,因此在此介紹。
下列程式碼示範私有方法和存取器的運作方式
class PrivateMethodClass {
privateMethod() {
#return 'privateMethod';
}privateAccessor() {
get #return 'privateGetter';
}privateAccessor(value) {
set #.equal(value, 'privateSetter');
assert
}callPrivateMembers() {
.equal(this.#privateMethod(), 'privateMethod');
assert.equal(this.#privateAccessor, 'privateGetter');
assertthis.#privateAccessor = 'privateSetter';
}
}.deepEqual(
assertReflect.ownKeys(new PrivateMethodClass()), []
; )
使用私有欄位時,金鑰永遠是識別碼
class PrivateMethodClass2 {
accessor() {}
get #accessor(value) {}
set #syncMethod() {}
#* #syncGeneratorMethod() {}
async #asyncMethod() {}
async * #asyncGeneratorMethod() {}
}
更多有關存取器(透過 getter 和/或 setter 定義)、產生器、非同步方法和非同步產生器方法的資訊
下列類別的執行個體有兩個執行個體屬性(在 A 行和 B 行建立)
class InstPublicClass {
// Instance public field
= 0; // (A)
instancePublicField
constructor(value) {
// We don’t need to mention .property elsewhere!
this.property = value; // (B)
}
}
const inst = new InstPublicClass('constrArg');
.deepEqual(
assertReflect.ownKeys(inst),
'instancePublicField', 'property']
[;
).equal(
assert.instancePublicField, 0
inst;
).equal(
assert.property, 'constrArg'
inst; )
如果在建構函式內建立執行個體屬性(B 行),我們不需要在其他地方「宣告」它。正如我們已經看到的,執行個體私有欄位則不同。
請注意,執行個體屬性在 JavaScript 中相對常見;比方說在 Java 中,大部分執行個體狀態都是私有的,因此比 Java 常見得多。
const computedFieldKey = Symbol('computedFieldKey');
class InstPublicClass2 {
'quoted field key' = 1;
= 2;
[computedFieldKey]
}const inst = new InstPublicClass2();
.equal(inst['quoted field key'], 1);
assert.equal(inst[computedFieldKey], 2); assert
this
在執行個體公共欄位中的值為何?(進階)在執行個體公共欄位的初始化程式中,this
指的是新建立的執行個體
class MyClass {
= this;
instancePublicField
}const inst = new MyClass();
.equal(
assert.instancePublicField, inst
inst; )
執行個體公共欄位的執行大致遵循下列兩個規則
super()
時設定其執行個體欄位。super()
之後立即執行。下列範例示範這些規則
class SuperClass {
= console.log('superProp');
superProp constructor() {
console.log('super-constructor');
}
}class SubClass extends SuperClass {
= console.log('subProp');
subProp constructor() {
console.log('BEFORE super()');
super();
console.log('AFTER super()');
}
}new SubClass();
// Output:
// 'BEFORE super()'
// 'superProp'
// 'super-constructor'
// 'subProp'
// 'AFTER super()'
extends
和子類別化會在 本章節稍後 說明。
下列類別包含兩個執行個體私有欄位(A 行和 B 行)
class InstPrivateClass {
= 'private field 1'; // (A)
#privateField1 ; // (B) required!
#privateField2constructor(value) {
this.#privateField2 = value; // (C)
}/**
* Private fields are not accessible outside the class body.
*/
checkPrivateValues() {
.equal(
assertthis.#privateField1, 'private field 1'
;
).equal(
assertthis.#privateField2, 'constructor argument'
;
)
}
}
const inst = new InstPrivateClass('constructor argument');
.checkPrivateValues();
inst
// No instance properties were created
.deepEqual(
assertReflect.ownKeys(inst),
[]; )
請注意,我們只能在 C 行中使用 .#privateField2
,如果我們在類別主體中宣告它。
在本節中,我們將探討兩種保持執行個體資料私有的技術。由於它們不依賴類別,因此我們也可以將它們用於透過其他方式建立的物件,例如物件文字。
第一種技術透過在屬性名稱之前加上底線來設定私有屬性。這並不會以任何方式保護屬性;它只是向外部發出訊號:「你不需要知道這個屬性。」
在以下程式碼中,屬性 ._counter
和 ._action
是私有的。
class Countdown {
constructor(counter, action) {
this._counter = counter;
this._action = action;
}dec() {
this._counter--;
if (this._counter === 0) {
this._action();
}
}
}
// The two properties aren’t really private:
.deepEqual(
assertObject.keys(new Countdown()),
'_counter', '_action']); [
使用此技術,我們無法獲得任何保護,而且私有名稱可能會衝突。好處是,它很容易使用。
私有方法的工作方式類似:它們是名稱以底線開頭的常規方法。
我們也可以透過 WeakMaps 管理私有實例資料
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
.set(this, counter);
_counter.set(this, action);
_action
}dec() {
let counter = _counter.get(this);
--;
counter.set(this, counter);
_counterif (counter === 0) {
.get(this)();
_action
}
}
}
// The two pseudo-properties are truly private:
.deepEqual(
assertObject.keys(new Countdown()),
; [])
確切的運作方式說明於 WeakMaps 章節 中。
此技術為我們提供相當程度的外部存取保護,而且不會有任何名稱衝突。但它也比較複雜。
我們透過控制誰有權存取偽屬性 _superProp
來控制其可見性,例如:如果變數存在於模組內且未匯出,則模組內的每個人都可以存取,而模組外的任何人則無法存取。換句話說:在此情況下,私密性的範圍不是類別,而是模組。不過,我們可以縮小範圍
let Countdown;
// class scope
{ const _counter = new WeakMap();
const _action = new WeakMap();
= class {
Countdown // ···
} }
此技術實際上不支援私有方法。但可以存取 _superProp
的模組局部函式是次佳選擇
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
.set(this, counter);
_counter.set(this, action);
_action
}dec() {
privateDec(this);
}
}
function privateDec(_this) { // (A)
let counter = _counter.get(_this);
--;
counter.set(_this, counter);
_counterif (counter === 0) {
.get(_this)();
_action
} }
請注意,this
會變成明確的函式參數 _this
(A 行)。
如前所述,實例私有欄位僅在其類別內可見,甚至在子類別中也看不到。因此,沒有內建方式可以取得
在先前的子節中,我們透過 WeakMaps 模擬了「模組可見性」(模組內的每個人都可以存取實例資料)。因此
以下範例示範受保護的可見性
const _superProp = new WeakMap();
class SuperClass {
constructor() {
.set(this, 'superProp');
_superProp
}
}class SubClass extends SuperClass {
getSuperProp() {
return _superProp.get(this);
}
}.equal(
assertnew SubClass().getSuperProp(),
'superProp'
; )
透過 extends
進行子類別化會在本章節稍後說明。
以下類別宣告主體中的所有成員都會建立所謂的靜態屬性,也就是 StaticClass
本身的屬性。
class StaticPublicMethodsClass {
static staticMethod() {
return 'staticMethod';
}static get staticAccessor() {
return 'staticGetter';
}static set staticAccessor(value) {
.equal(value, 'staticSetter');
assert
}
}.equal(
assert.staticMethod(), 'staticMethod'
StaticPublicMethodsClass;
).equal(
assert.staticAccessor, 'staticGetter'
StaticPublicMethodsClass;
).staticAccessor = 'staticSetter'; StaticPublicMethodsClass
const accessorKey = Symbol('accessorKey');
const syncMethodKey = Symbol('syncMethodKey');
const syncGenMethodKey = Symbol('syncGenMethodKey');
const asyncMethodKey = Symbol('asyncMethodKey');
const asyncGenMethodKey = Symbol('asyncGenMethodKey');
class StaticPublicMethodsClass2 {
// Identifier keys
static get accessor() {}
static set accessor(value) {}
static syncMethod() {}
static * syncGeneratorMethod() {}
static async asyncMethod() {}
static async * asyncGeneratorMethod() {}
// Quoted keys
static get 'an accessor'() {}
static set 'an accessor'(value) {}
static 'sync method'() {}
static * 'sync generator method'() {}
static async 'async method'() {}
static async * 'async generator method'() {}
// Computed keys
static get [accessorKey]() {}
static set [accessorKey](value) {}
static [syncMethodKey]() {}
static * [syncGenMethodKey]() {}
static async [asyncMethodKey]() {}
static async * [asyncGenMethodKey]() {}
}
// Quoted and computed keys are accessed via square brackets:
'sync method']();
StaticPublicMethodsClass2[; StaticPublicMethodsClass2[syncMethodKey]()
引號鍵和計算鍵也可以用在物件字面上
更多有關存取器(透過 getter 和/或 setter 定義)、產生器、非同步方法和非同步產生器方法的資訊
以下程式碼示範靜態公用欄位。StaticPublicFieldClass
有三個
const computedFieldKey = Symbol('computedFieldKey');
class StaticPublicFieldClass {
static identifierFieldKey = 1;
static 'quoted field key' = 2;
static [computedFieldKey] = 3;
}
.deepEqual(
assertReflect.ownKeys(StaticPublicFieldClass),
['length', // number of constructor parameters
'name', // 'StaticPublicFieldClass'
'prototype',
'identifierFieldKey',
'quoted field key',
,
computedFieldKey,
];
)
.equal(StaticPublicFieldClass.identifierFieldKey, 1);
assert.equal(StaticPublicFieldClass['quoted field key'], 2);
assert.equal(StaticPublicFieldClass[computedFieldKey], 3); assert
以下類別有兩個靜態私有插槽(A 行和 B 行)
class StaticPrivateClass {
// Declare and initialize
static #staticPrivateField = 'hello'; // (A)
static #twice() { // (B)
const str = StaticPrivateClass.#staticPrivateField;
return str + ' ' + str;
}static getResultOfTwice() {
return StaticPrivateClass.#twice();
}
}
.deepEqual(
assertReflect.ownKeys(StaticPrivateClass),
['length', // number of constructor parameters
'name', // 'StaticPublicFieldClass'
'prototype',
'getResultOfTwice',
,
];
)
.equal(
assert.getResultOfTwice(),
StaticPrivateClass'hello hello'
; )
這是所有種類的靜態私有插槽的完整清單
class MyClass {
static #staticPrivateMethod() {}
static * #staticPrivateGeneratorMethod() {}
static async #staticPrivateAsyncMethod() {}
static async * #staticPrivateAsyncGeneratorMethod() {}
static get #staticPrivateAccessor() {}
static set #staticPrivateAccessor(value) {}
}
要透過類別設定執行個體資料,我們有兩個建構
對於靜態資料,我們有
以下程式碼示範靜態區塊(A 行)
class Translator {
static translations = {
yes: 'ja',
no: 'nein',
maybe: 'vielleicht',
;
}static englishWords = [];
static germanWords = [];
static { // (A)
for (const [english, german] of Object.entries(this.translations)) {
this.englishWords.push(english);
this.germanWords.push(german);
}
} }
我們也可以在類別(頂層)之後執行靜態區塊內的程式碼。不過,使用靜態區塊有兩個好處
靜態初始化區塊運作規則相對簡單
以下程式碼示範這些規則
class SuperClass {
static superField1 = console.log('superField1');
static {
.equal(this, SuperClass);
assertconsole.log('static block 1 SuperClass');
}static superField2 = console.log('superField2');
static {
console.log('static block 2 SuperClass');
}
}
class SubClass extends SuperClass {
static subField1 = console.log('subField1');
static {
.equal(this, SubClass);
assertconsole.log('static block 1 SubClass');
}static subField2 = console.log('subField2');
static {
console.log('static block 2 SubClass');
}
}
// Output:
// 'superField1'
// 'static block 1 SuperClass'
// 'superField2'
// 'static block 2 SuperClass'
// 'subField1'
// 'static block 1 SubClass'
// 'subField2'
// 'static block 2 SubClass'
透過 extends
進行子類別化會在本章節稍後說明。
this
存取靜態私有欄位在靜態公用成員中,我們可以使用 this
存取靜態公用插槽。遺憾的是,我們不應該使用它來存取靜態私有插槽。
this
和靜態公用欄位考慮以下程式碼
class SuperClass {
static publicData = 1;
static getPublicViaThis() {
return this.publicData;
}
}class SubClass extends SuperClass {
}
透過 extends
進行子類別化會在本章節稍後說明。
靜態公用欄位是屬性。如果我們執行方法呼叫
.equal(SuperClass.getPublicViaThis(), 1); assert
那麼 this
會指向 SuperClass
,一切都如預期運作。我們也可以透過子類別呼叫 .getPublicViaThis()
.equal(SubClass.getPublicViaThis(), 1); assert
SubClass
從其原型 SuperClass
繼承 .getPublicViaThis()
。this
指向 SubClass
,而且事情繼續進行,因為 SubClass
也繼承屬性 .publicData
。
順帶一提,如果我們在 getPublicViaThis()
中指派給 this.publicData
,並透過 SubClass.getPublicViaThis()
呼叫它,那麼我們將建立 SubClass
的新自有屬性,它會(非破壞性地)覆寫從 SuperClass
繼承的屬性。
this
和靜態私有欄位考慮以下程式碼
class SuperClass {
static #privateData = 2;
static getPrivateDataViaThis() {
return this.#privateData;
}static getPrivateDataViaClassName() {
return SuperClass.#privateData;
}
}class SubClass extends SuperClass {
}
透過 SuperClass
呼叫 .getPrivateDataViaThis()
會運作,因為 this
指向 SuperClass
.equal(SuperClass.getPrivateDataViaThis(), 2); assert
然而,透過 SubClass
呼叫 .getPrivateDataViaThis()
無法運作,因為 this
現在指向 SubClass
,而 SubClass
沒有靜態私有欄位 .#privateData
(原型鏈中的私有插槽不會被繼承)
.throws(
assert=> SubClass.getPrivateDataViaThis(),
()
{name: 'TypeError',
message: 'Cannot read private member #privateData from'
+ ' an object whose class did not declare it',
}; )
解決方法是透過 SuperClass
直接存取 .#privateData
.equal(SubClass.getPrivateDataViaClassName(), 2); assert
使用靜態私有方法時,我們會遇到相同的問題。
類別中的每個成員都可以存取該類別中的所有其他成員,包括公用和私有成員
class DemoClass {
static #staticPrivateField = 1;
= 2;
#instPrivField
static staticMethod(inst) {
// A static method can access static private fields
// and instance private fields
.equal(DemoClass.#staticPrivateField, 1);
assert.equal(inst.#instPrivField, 2);
assert
}
protoMethod() {
// A prototype method can access instance private fields
// and static private fields
.equal(this.#instPrivField, 2);
assert.equal(DemoClass.#staticPrivateField, 1);
assert
} }
相反地,外部沒有人可以存取私有成員
// Accessing private fields outside their classes triggers
// syntax errors (before the code is even executed).
.throws(
assert=> eval('DemoClass.#staticPrivateField'),
()
{name: 'SyntaxError',
message: "Private field '#staticPrivateField' must"
+ " be declared in an enclosing class",
};
)// Accessing private fields outside their classes triggers
// syntax errors (before the code is even executed).
.throws(
assert=> eval('new DemoClass().#instPrivField'),
()
{name: 'SyntaxError',
message: "Private field '#instPrivField' must"
+ " be declared in an enclosing class",
}; )
以下程式碼僅在 ES2022 中運作,因為每一行都包含雜湊符號 (#
)
class StaticClass {
static #secret = 'Rumpelstiltskin';
static #getSecretInParens() {
return `(${StaticClass.#secret})`;
}static callStaticPrivateMethod() {
return StaticClass.#getSecretInParens();
} }
由於私有插槽每個類別只存在一次,因此我們可以將 #secret
和 #getSecretInParens
移至圍繞類別的範圍,並使用模組將它們隱藏在模組外部的世界中。
const secret = 'Rumpelstiltskin';
function getSecretInParens() {
return `(${secret})`;
}
// Only the class is accessible outside the module
export class StaticClass {
static callStaticPrivateMethod() {
return getSecretInParens();
} }
有時類別有多種實例化方式。然後我們可以實作靜態工廠方法,例如 Point.fromPolar()
class Point {
static fromPolar(radius, angle) {
const x = radius * Math.cos(angle);
const y = radius * Math.sin(angle);
return new Point(x, y);
}constructor(x=0, y=0) {
this.x = x;
this.y = y;
}
}
.deepEqual(
assertPoint.fromPolar(13, 0.39479111969976155),
new Point(12, 5)
; )
我喜歡靜態工廠方法的描述性:fromPolar
描述如何建立實例。JavaScript 的標準函式庫也有這樣的工廠方法,例如
Array.from()
Object.create()
我比較喜歡沒有靜態工廠方法或只有靜態工廠方法。在後者的情況下需要考慮的事項
在以下程式碼中,我們使用一個秘密令牌(A 行)來防止建構函數從當前模組外部呼叫。
// Only accessible inside the current module
const secretToken = Symbol('secretToken'); // (A)
export class Point {
static create(x=0, y=0) {
return new Point(secretToken, x, y);
}static fromPolar(radius, angle) {
const x = radius * Math.cos(angle);
const y = radius * Math.sin(angle);
return new Point(secretToken, x, y);
}constructor(token, x, y) {
if (token !== secretToken) {
throw new TypeError('Must use static factory method');
}this.x = x;
this.y = y;
}
}Point.create(3, 4); // OK
.throws(
assert=> new Point(3, 4),
() TypeError
; )
類別也可以擴充現有類別。例如,以下類別 Employee
擴充 Person
class Person {
;
#firstNameconstructor(firstName) {
this.#firstName = firstName;
}describe() {
return `Person named ${this.#firstName}`;
}static extractNames(persons) {
return persons.map(person => person.#firstName);
}
}
class Employee extends Person {
constructor(firstName, title) {
super(firstName);
this.title = title;
}describe() {
return super.describe() +
` (${this.title})`;
}
}
const jane = new Employee('Jane', 'CTO');
.equal(
assert.title,
jane'CTO'
;
).equal(
assert.describe(),
jane'Person named Jane (CTO)'
; )
與擴充相關的術語
Person
是 Employee
的父類別。Employee
是 Person
的子類別。在衍生類別的 .constructor()
內,我們必須在存取 this
之前透過 super()
呼叫父建構函數。為什麼會這樣?
讓我們考慮一個類別鏈
A
B
擴充 A
。C
擴充 B
。如果我們呼叫 new C()
,C
的建構函數會呼叫父建構函數 B
,而 B
會呼叫父建構函數 A
。實例總是建立在基底類別中,在子類別的建構函數新增其插槽之前。因此,在我們呼叫 super()
之前,實例並不存在,而且我們還無法透過 this
存取它。
請注意,靜態公用插槽會被繼承。例如,Employee
繼承靜態方法 .extractNames()
> 'extractNames' in Employeetrue
練習:子類化
exercises/classes/color_point_class_test.mjs
前一節的類別 Person
和 Employee
由多個物件組成(圖 15)。了解這些物件之間關係的一個關鍵見解是,有兩個原型鏈
實例原型鏈從 jane
開始,並繼續使用 Employee.prototype
和 Person.prototype
。原則上,原型鏈在這裡結束,但我們會得到另一個物件:Object.prototype
。此原型提供服務給幾乎所有物件,這就是為什麼它也包含在這裡
> Object.getPrototypeOf(Person.prototype) === Object.prototypetrue
在類別原型鏈中,Employee
優先,接著是 Person
。之後,鏈會繼續使用 Function.prototype
,它只會存在,因為 Person
是函式,而函式需要 Function.prototype
的服務。
> Object.getPrototypeOf(Person) === Function.prototypetrue
instanceof
和子類別化 (進階)我們尚未了解 instanceof
的實際運作方式。instanceof
如何判斷值 x
是否為類別 C
的實例 (它可能是 C
的直接實例,或 C
子類別的直接實例)?它會檢查 C.prototype
是否在 x
的原型鏈中。也就是說,下列兩個表達式是等效的
instanceof C
x .prototype.isPrototypeOf(x) C
如果我們回到圖 15,我們可以確認原型鏈確實會引導我們得出下列正確答案
> jane instanceof Employeetrue
> jane instanceof Persontrue
> jane instanceof Objecttrue
請注意,如果 instanceof
的自我手邊側為基本值,它總是會傳回 false
> 'abc' instanceof Stringfalse
> 123 instanceof Numberfalse
Object
的實例 (進階)物件 (非基本值) 只有在 Object.prototype
在其原型鏈中的情況下,才會是 Object
的實例 (請參閱前一個小節)。幾乎所有物件都是 Object
的實例,例如
.equal(
asserta: 1} instanceof Object, true
{;
).equal(
assert'a'] instanceof Object, true
[;
).equal(
assert/abc/g instanceof Object, true
;
).equal(
assertnew Map() instanceof Object, true
;
)
class C {}
.equal(
assertnew C() instanceof Object, true
; )
在下列範例中,obj1
和 obj2
都是物件 (A 行和 C 行),但它們不是 Object
的實例 (B 行和 D 行):Object.prototype
不在它們的原型鏈中,因為它們沒有任何原型。
const obj1 = {__proto__: null};
.equal(
asserttypeof obj1, 'object' // (A)
;
).equal(
assertinstanceof Object, false // (B)
obj1 ;
)
const obj2 = Object.create(null);
.equal(
asserttypeof obj2, 'object' // (C)
;
).equal(
assertinstanceof Object, false // (D)
obj2 ; )
Object.prototype
是結束大多數原型鏈的物件。它的原型是 null
,這表示它也不是 Object
的實例
> typeof Object.prototype'object'
> Object.getPrototypeOf(Object.prototype)null
> Object.prototype instanceof Objectfalse
接下來,我們將運用對子類別化的了解,來理解幾個內建物件的原型鏈。下列工具函式 p()
可協助我們進行探索。
const p = Object.getPrototypeOf.bind(Object);
我們萃取了 Object
的方法 .getPrototypeOf()
,並將它指定給 p
。
{}
的原型鏈讓我們從檢查純粹物件開始
> p({}) === Object.prototypetrue
> p(p({})) === nulltrue
圖 16 顯示此原型鏈的圖表。我們可以看到 {}
確實是 Object
的實例,因為 Object.prototype
在其原型鏈中。
[]
的原型鏈陣列的原型鏈是什麼樣子?
> p([]) === Array.prototypetrue
> p(p([])) === Object.prototypetrue
> p(p(p([]))) === nulltrue
這個原型鏈(在圖 17 中視覺化)告訴我們,陣列物件是 Array
和 Object
的實例。
function () {}
的原型鏈最後,一般函式的原型鏈告訴我們,所有函式都是物件
> p(function () {}) === Function.prototypetrue
> p(p(function () {})) === Object.prototypetrue
基底類別的原型是 Function.prototype
,這表示它是一個函式(Function
的實例)
class A {}
.equal(
assertObject.getPrototypeOf(A),
Function.prototype
;
)
.equal(
assertObject.getPrototypeOf(class {}),
Function.prototype
; )
衍生類別的原型是其超類別
class B extends A {}
.equal(
assertObject.getPrototypeOf(B),
A;
)
.equal(
assertObject.getPrototypeOf(class extends Object {}),
Object
; )
有趣的是,Object
、Array
和 Function
都是基底類別
> Object.getPrototypeOf(Object) === Function.prototypetrue
> Object.getPrototypeOf(Array) === Function.prototypetrue
> Object.getPrototypeOf(Function) === Function.prototypetrue
然而,正如我們所見,即使是基底類別的實例,其原型鏈中也有 Object.prototype
,因為它提供了所有物件需要的服務。
為什麼
Array
和 Function
是基底類別?
基底類別是實際建立實例的地方。Array
和 Function
都需要建立自己的實例,因為它們有所謂的「內部插槽」,無法在 Object
建立的實例中後續加入。
JavaScript 的類別系統僅支援單一繼承。也就是說,每個類別最多只能有一個超類別。解決這個限制的一種方法是使用一種稱為Mixin 類別(簡稱:Mixins)的技術。
概念如下:假設我們希望類別 C
從兩個超類別 S1
和 S2
繼承。這將是多重繼承,而 JavaScript 不支援此功能。
我們的解決方法是將 S1
和 S2
變成Mixins,也就是子類別的工廠
const S1 = (Sup) => class extends Sup { /*···*/ };
const S2 = (Sup) => class extends Sup { /*···*/ };
這兩個函式中的每個函式都會傳回一個延伸給定超類別 Sup
的類別。我們建立類別 C
的方式如下
class C extends S2(S1(Object)) {
/*···*/
}
我們現在有一個類別 C
,它擴充了類別 S2()
所回傳的類別,而類別 S2()
擴充了類別 S1()
所回傳的類別,而類別 S1()
擴充了 Object
。
我們實作了一個 mixin Branded
,它有設定和取得物件品牌的輔助方法
const Named = (Sup) => class extends Sup {
= '(Unnamed)';
name toString() {
const className = this.constructor.name;
return `${className} named ${this.name}`;
}; }
我們使用這個 mixin 來實作一個類別 City
,它有一個名稱
class City extends Named(Object) {
constructor(name) {
super();
this.name = name;
} }
以下程式碼確認這個 mixin 有效
const paris = new City('Paris');
.equal(
assert.name, 'Paris'
paris;
).equal(
assert.toString(), 'City named Paris'
paris; )
mixin 讓我們擺脫單一繼承的限制
Object.prototype
的方法和存取器(進階)正如我們在 §29.7.3 “並非所有物件都是 Object
的實例” 中所見,幾乎所有的物件都是 Object
的實例。這個類別提供了幾個有用的方法和一個存取器給它的實例
+
運算子):以下方法有預設的實作,但經常在子類別或實例中被覆寫。
.toString()
:設定物件如何轉換成字串。.toLocaleString()
:.toString()
的一個版本,可以透過參數(語言、地區等)設定成各種方式。.valueOf()
:設定物件如何轉換成非字串的原始值(通常是數字)。.isPrototypeOf()
:接收者在給定物件的原型鏈中嗎?.propertyIsEnumerable()
:接收者有可列舉的擁有屬性,其鍵值為給定值嗎?.__proto__
:取得和設定接收者的原型。
Object.getPrototypeOf()
Object.setPrototypeOf()
.hasOwnProperty()
:接收者有擁有屬性,其鍵值為給定值嗎?
Object.hasOwn()
。在我們仔細檢視這些功能的每個功能之前,我們將瞭解一個重要的陷阱(以及如何解決它):我們無法對所有物件使用 Object.prototype
的功能。
Object.prototype
方法在任意物件上呼叫 Object.prototype
的方法並不總是可行的。為了說明原因,我們使用 Object.prototype.hasOwnProperty
方法,如果物件有擁有屬性,其鍵值為給定值,則回傳 true
> {ownProp: true}.hasOwnProperty('ownProp')true
> {ownProp: true}.hasOwnProperty('abc')false
呼叫任意物件上的 .hasOwnProperty()
可能會以兩種方式失敗。一方面,如果物件不是 Object
的執行個體,此方法不可用(請參閱 §29.7.3「並非所有物件都是 Object
的執行個體」)
const obj = Object.create(null);
.equal(obj instanceof Object, false);
assert.throws(
assert=> obj.hasOwnProperty('prop'),
()
{name: 'TypeError',
message: 'obj.hasOwnProperty is not a function',
}; )
另一方面,如果物件以自有屬性覆寫 .hasOwnProperty()
,我們無法使用 .hasOwnProperty()
(A 行)
const obj = {
hasOwnProperty: 'yes' // (A)
;
}.throws(
assert=> obj.hasOwnProperty('prop'),
()
{name: 'TypeError',
message: 'obj.hasOwnProperty is not a function',
}; )
不過,有一個安全的方法可以使用 .hasOwnProperty()
function hasOwnProp(obj, propName) {
return Object.prototype.hasOwnProperty.call(obj, propName); // (A)
}.equal(
asserthasOwnProp(Object.create(null), 'prop'), false
;
).equal(
asserthasOwnProp({hasOwnProperty: 'yes'}, 'prop'), false
;
).equal(
asserthasOwnProp({hasOwnProperty: 'yes'}, 'hasOwnProperty'), true
; )
A 行中的方法呼叫在 §29.3.5「派送方法呼叫與直接方法呼叫」 中說明。
我們也可以使用 .bind()
來實作 hasOwnProp()
const hasOwnProp = Object.prototype.hasOwnProperty.call
.bind(Object.prototype.hasOwnProperty);
這是如何運作的?當我們呼叫 .call()
,就像前一個範例中的 A 行,它會執行 hasOwnProp()
應該執行的所有動作,包括避免陷阱。不過,如果我們想要以函式呼叫它,我們不能只是萃取它,我們還必須確保它的 this
永遠有正確的值。這就是 .bind()
的作用。
是否永遠不適合透過動態派送使用
Object.prototype
方法?
在某些情況下,我們可以偷懶,呼叫 Object.prototype
方法,就像呼叫一般方法(沒有 .call()
或 .bind()
):如果我們知道接收者,而且它們是固定配置物件。
另一方面,如果我們不知道接收者和/或它們是字典物件,我們就需要採取預防措施。
Object.prototype.toString()
透過覆寫 .toString()
(在子類別或執行個體中),我們可以設定物件轉換為字串的方式
> String({toString() { return 'Hello!' }})'Hello!'
> String({})'[object Object]'
要將物件轉換為字串,最好使用 String()
,因為它也可以用於 undefined
和 null
> undefined.toString()TypeError: Cannot read properties of undefined (reading 'toString')
> null.toString()TypeError: Cannot read properties of null (reading 'toString')
> String(undefined)'undefined'
> String(null)'null'
Object.prototype.toLocaleString()
.toLocaleString()
是 .toString()
的版本,可以透過地區設定和通常的其他選項來設定。任何類別或執行個體都可以實作此方法。在標準函式庫中,下列類別會實作此方法
Array.prototype.toLocaleString()
Number.prototype.toLocaleString()
Date.prototype.toLocaleString()
TypedArray.prototype.toLocaleString()
BigInt.prototype.toLocaleString()
舉例來說,這是數字如何以不同的方式轉換為字串,視地區設定而定('fr'
是法文,'en'
是英文)
> 123.45.toLocaleString('fr')'123,45'
> 123.45.toLocaleString('en')'123.45'
Object.prototype.valueOf()
透過覆寫 .valueOf()
(在子類別或實例中),我們可以設定物件如何轉換為非字串值(通常是數字)
> Number({valueOf() { return 123 }})123
> Number({})NaN
Object.prototype.isPrototypeOf()
proto.isPrototypeOf(obj)
如果 proto
在 obj
的原型鏈中,則傳回 true
,否則傳回 false
。
const a = {};
const b = {__proto__: a};
const c = {__proto__: b};
.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);
assert
.equal(a.isPrototypeOf(a), false);
assert.equal(c.isPrototypeOf(a), false); assert
以下是安全使用此方法的方式(詳細資訊請參閱 §29.8.1「安全使用 Object.prototype
方法」)
const obj = {
// Overrides Object.prototype.isPrototypeOf
isPrototypeOf: true,
;
}// Doesn’t work in this case:
.throws(
assert=> obj.isPrototypeOf(Object.prototype),
()
{name: 'TypeError',
message: 'obj.isPrototypeOf is not a function',
};
)// Safe way of using .isPrototypeOf():
.equal(
assertObject.prototype.isPrototypeOf.call(obj, Object.prototype), false
; )
Object.prototype.propertyIsEnumerable()
obj.propertyIsEnumerable(propKey)
如果 obj
有自己的可列舉屬性,其鍵為 propKey
,則傳回 true
,否則傳回 false
。
const proto = {
enumerableProtoProp: true,
;
}const obj = {
__proto__: proto,
enumerableObjProp: true,
nonEnumObjProp: true,
;
}Object.defineProperty(
, 'nonEnumObjProp',
obj
{enumerable: false,
};
)
.equal(
assert.propertyIsEnumerable('enumerableProtoProp'),
objfalse // not an own property
;
).equal(
assert.propertyIsEnumerable('enumerableObjProp'),
objtrue
;
).equal(
assert.propertyIsEnumerable('nonEnumObjProp'),
objfalse // not enumerable
;
).equal(
assert.propertyIsEnumerable('unknownProp'),
objfalse // not a property
; )
以下是安全使用此方法的方式(詳細資訊請參閱 §29.8.1「安全使用 Object.prototype
方法」)
const obj = {
// Overrides Object.prototype.propertyIsEnumerable
propertyIsEnumerable: true,
enumerableProp: 'yes',
;
}// Doesn’t work in this case:
.throws(
assert=> obj.propertyIsEnumerable('enumerableProp'),
()
{name: 'TypeError',
message: 'obj.propertyIsEnumerable is not a function',
};
)// Safe way of using .propertyIsEnumerable():
.equal(
assertObject.prototype.propertyIsEnumerable.call(obj, 'enumerableProp'),
true
; )
另一個安全的替代方案是使用 屬性描述符
.deepEqual(
assertObject.getOwnPropertyDescriptor(obj, 'enumerableProp'),
{value: 'yes',
writable: true,
enumerable: true,
configurable: true,
}; )
Object.prototype.__proto__
(存取器)屬性 __proto__
有兩個版本
Object
實例都有的存取器。我建議避免使用前一個功能
Object.prototype
方法」 中所解釋的,它無法對所有物件運作。相反地,物件文字中的 __proto__
永遠都能運作,而且不建議使用。
如果您有興趣瞭解存取器 __proto__
如何運作,請繼續閱讀。
__proto__
是 Object.prototype
的存取器,由所有 Object
實例繼承。透過類別實作它看起來像這樣
class Object {
__proto__() {
get return Object.getPrototypeOf(this);
}__proto__(other) {
set Object.setPrototypeOf(this, other);
}// ···
}
由於 __proto__
是從 Object.prototype
繼承的,我們可以透過建立一個在原型鏈中沒有 Object.prototype
的物件來移除此功能(請參閱 §29.7.3「並非所有物件都是 Object
的實例」)
> '__proto__' in {}true
> '__proto__' in Object.create(null)false
Object.prototype.hasOwnProperty()
比
.hasOwnProperty()
更好的替代方案:Object.hasOwn()
[ES2022]
obj.hasOwnProperty(propKey)
如果 obj
有自己的(非繼承的)屬性,其鍵為 propKey
,則傳回 true
,否則傳回 false
。
const obj = { ownProp: true };
.equal(
assert.hasOwnProperty('ownProp'), true // own
obj;
).equal(
assert'toString' in obj, true // inherited
;
).equal(
assert.hasOwnProperty('toString'), false
obj; )
以下是安全使用此方法的方式(詳細資訊請參閱 §29.8.1「安全使用 Object.prototype
方法」)
const obj = {
// Overrides Object.prototype.hasOwnProperty
hasOwnProperty: true,
;
}// Doesn’t work in this case:
.throws(
assert=> obj.hasOwnProperty('anyPropKey'),
()
{name: 'TypeError',
message: 'obj.hasOwnProperty is not a function',
};
)// Safe way of using .hasOwnProperty():
.equal(
assertObject.prototype.hasOwnProperty.call(obj, 'anyPropKey'), false
; )
這樣做是為了強調不同的屬性(公用插槽)和私有插槽之間的差異:透過改變形容詞的順序,單字「公用」和「欄位」以及單字「私有」和「欄位」總是會一起被提及。
#
?為什麼不透過 private
宣告私有欄位?私有欄位是否可以透過 private
宣告並使用一般識別碼?讓我們來檢視如果可以這樣做會發生什麼事
class MyClass {
private value; // (A)
compare(other) {
return this.value === other.value;
} }
每當 MyClass
的主體中出現類似 other.value
的表達式時,JavaScript 必須決定
.value
是屬性嗎?.value
是私有欄位嗎?在編譯時間,JavaScript 不知道 A 行中的宣告是否適用於 other
(因為它是 MyClass
的執行個體)或不適用。這會留下兩個選項來做出決定
.value
總是被解譯為私有欄位。other
是 MyClass
的執行個體,則 .value
會被解譯為私有欄位。.value
會被解譯為屬性。這兩個選項都有缺點
.value
用作任何物件的屬性。這就是為什麼會引入名稱前綴 #
。現在可以輕鬆做出決定:如果我們使用 #
,我們想要存取私有欄位。如果我們不使用,我們想要存取屬性。
private
適用於靜態類型語言(例如 TypeScript),因為它們在編譯時間知道 other
是否是 MyClass
的執行個體,然後可以將 .value
視為私有或公用。
測驗
請參閱 測驗應用程式。