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 行)。JavaScript 相对经常将实例数据公开(与例如 Java 更喜欢隐藏它形成对比)。类基本上是用于设置原型链的简洁语法(在 上一章 中有解释)。在底层,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 “通过 WeakMap 模拟受保护的可见性和友元可见性” 中解释。
私有槽具有类似于 符号 的唯一键。考虑前面提到的以下类
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
与 SubClass
中的 .#privateField
不会冲突,即使这两个槽都直接存储在 inst
中
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'
构造函数与类
由于它们非常相似,因此我将_构造函数_和_类_这两个术语互换使用。
以下类声明主体中的所有成员都创建 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']); [
使用这种技术,我们无法获得任何保护,并且私有名称可能会发生冲突。从好的方面来说,它易于使用。
私有方法的工作原理类似:它们是名称以下划线开头的普通方法。
我们还可以通过 WeakMap 管理私有实例数据
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()),
; [])
有关其工作原理的详细说明,请参阅WeakMap 章节。
这种技术为我们提供了相当大的外部访问保护,并且不会发生任何名称冲突。但使用起来也更加复杂。
我们通过控制谁可以访问伪属性 _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 行)。
如前所述,实例私有字段仅在其类内部可见,甚至在子类中也不可见。因此,没有内置的方法来获取
在上一小节中,我们通过 WeakMap 模拟了“模块可见性”(模块内部的每个人都可以访问一部分实例数据)。因此
下一个示例演示了受保护的可见性
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
的构造函数,后者会超级调用 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 的类系统仅支持_单继承_。也就是说,每个类最多只能有一个超类。解决此限制的一种方法是使用一种称为_混入类_(简称:_混入_)的技术。
其思路如下:假设我们希望类 C
从两个超类 S1
和 S2
继承。那就是_多重继承_,JavaScript 不支持。
我们的解决方法是将 S1
和 S2
转换为_混入_,即子类的工厂
const S1 = (Sup) => class extends Sup { /*···*/ };
const S2 = (Sup) => class extends Sup { /*···*/ };
这两个函数中的每一个都返回一个扩展了给定超类 Sup
的类。我们创建类 C
如下
class C extends S2(S1(Object)) {
/*···*/
}
我们现在有一个类 C
,它扩展了 S2()
返回的类,该类扩展了 S1()
返回的类,该类扩展了 Object
。
我们实现了一个混入 Branded
,它具有用于设置和获取对象品牌的辅助方法
const Named = (Sup) => class extends Sup {
= '(Unnamed)';
name toString() {
const className = this.constructor.name;
return `${className} named ${this.name}`;
}; }
我们使用此混入来实现一个具有名称的类 City
class City extends Named(Object) {
constructor(name) {
super();
this.name = name;
} }
以下代码确认混入有效
const paris = new City('Paris');
.equal(
assert.name, 'Paris'
paris;
).equal(
assert.toString(), 'City named Paris'
paris; )
混入使我们摆脱了单继承的限制
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()
(第 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);
这是如何工作的?当我们像上一个示例中的第 A 行那样调用 .call()
时,它会完全按照 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
在 obj
的原型链中,则 proto.isPrototypeOf(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
具有一个自有可枚举属性,其键为 propKey
,则 obj.propertyIsEnumerable(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
具有一个自有(非继承)属性,其键为 propKey
,则 obj.hasOwnProperty(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
视为私有或公共。
测验
请参阅 测验应用程序。