instanceof 运算符.__proto__ 与 .prototypePerson.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 {
#firstName; // (A)
constructor(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');
assert.equal(
tarzan.describe(),
'Person named Tarzan'
);
assert.deepEqual(
Person.extractNames([tarzan, new Person('Cheeta')]),
['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');
assert.equal(
jane.title,
'CTO'
);
assert.equal(
jane.describe(),
'Person named Jane (CTO)'
);备注
.#firstName 是一个 私有字段,必须先声明(第 A 行),然后才能初始化(第 B 行)。.title 是一个属性,可以在没有事先声明的情况下初始化(第 C 行)。JavaScript 相对经常将实例数据公开(与例如 Java 更喜欢隐藏它形成对比)。类基本上是用于设置原型链的简洁语法(在 上一章 中有解释)。在底层,JavaScript 的类并不传统。但这在使用它们时我们很少会看到。对于使用过其他面向对象编程语言的人来说,它们通常应该很熟悉。
请注意,我们不需要类来创建对象。我们也可以通过 对象字面量 来创建对象。这就是为什么在 JavaScript 中不需要单例模式,并且类比在许多其他具有类的语言中使用得更少的原因。
我们之前使用过 jane 和 tarzan,它们是表示人的单个对象。让我们使用 类声明 来实现此类对象的工厂
class Person {
#firstName; // (A)
constructor(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 是一个 实例私有字段:此类字段存储在实例中。它们的访问方式与属性类似,但它们的名称是独立的——它们总是以井号 (#) 开头。并且它们对类外部的世界是不可见的
assert.deepEqual(
Reflect.ownKeys(jane),
[]
);在我们可以在构造函数中初始化 .#firstName 之前(第 B 行),我们需要通过在类主体中提及它来声明它(第 A 行)。
.describe() 是一个方法。如果我们通过 obj.describe() 调用它,则 this 在 .describe() 的主体内部指的是 obj。
assert.equal(
jane.describe(), 'Person named Jane'
);
assert.equal(
tarzan.describe(), 'Person named Tarzan'
);.extractName() 是一个 静态 方法。“静态”意味着它属于类,而不是实例
assert.deepEqual(
Person.extractNames([jane, tarzan]),
['Jane', 'Tarzan']
);我们还可以在构造函数中创建实例属性(公共字段)
class Container {
constructor(value) {
this.value = value;
}
}
const abcContainer = new Container('abc');
assert.equal(
abcContainer.value, 'abc'
);与实例私有字段不同,实例属性不必在类主体中声明。
有两种 类定义(定义类的方式)
类表达式可以是匿名的,也可以是命名的
// Anonymous class expression
const Person = class { ··· };
// Named class expression
const Person = class MyClass { ··· };命名类表达式的名称的工作方式类似于 命名函数表达式的名称:它只能在类的主体内部访问,并且无论类被分配给什么,它都保持不变。
instanceof 运算符instanceof 运算符告诉我们一个值是否是给定类的实例
> new Person('Jane') instanceof Person
true
> {} instanceof Person
false
> {} instanceof Object
true
> [] instanceof Array
true我们将在 后面 更详细地探讨 instanceof 运算符,在我们了解了子类化之后。
在 JavaScript 语言中,对象可以有两种“槽”。
这些是我们需要了解的关于属性和私有槽的最重要的规则
static 以及其他因素。 有关属性和私有槽的更多信息
本章没有涵盖属性和私有槽的所有细节(仅涵盖了基本内容)。如果您想深入了解,可以在这里进行
[[PrivateElements]]”。以下类演示了两种槽。它的每个实例都有一个私有字段和一个属性
class MyClass {
#instancePrivateField = 1;
instanceProperty = 2;
getInstanceValues() {
return [
this.#instancePrivateField,
this.instanceProperty,
];
}
}
const inst = new MyClass();
assert.deepEqual(
inst.getInstanceValues(), [1, 2]
);正如预期的那样,在 MyClass 之外,我们只能看到该属性
assert.deepEqual(
Reflect.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 {
#instancePrivateField = 1;
instanceProperty = 2;
getInstanceValues() {
return [
this.#instancePrivateField,
this.instanceProperty,
];
}
}在内部,MyClass 的私有字段的处理方式大致如下
let MyClass;
{ // Scope of the body of the class
const instancePrivateFieldKey = Symbol();
MyClass = class {
// Very loose approximation of how this
// works in the language specification
__PrivateElements__ = new Map([
[instancePrivateFieldKey, 1],
]);
instanceProperty = 2;
getInstanceValues() {
return [
this.__PrivateElements__.get(instancePrivateFieldKey),
this.instanceProperty,
];
}
}
}instancePrivateFieldKey 的值称为 私有名称。我们不能在 JavaScript 中直接使用私有名称,我们只能通过私有字段、私有方法和私有访问器的固定标识符间接使用它们。公共槽的固定标识符(例如 getInstanceValues)被解释为字符串键,而私有槽的固定标识符(例如 #instancePrivateField)则引用私有名称(类似于变量名称如何引用值)。
因为私有槽的标识符不用作键,所以在不同的类中使用相同的标识符会产生不同的槽(第 A 行和第 C 行)
class Color {
#name; // (A)
constructor(name) {
this.#name = name; // (B)
}
static getName(obj) {
return obj.#name;
}
}
class Person {
#name; // (C)
constructor(name) {
this.#name = name;
}
}
assert.equal(
Color.getName(new Color('green')), 'green'
);
// We can’t access the private slot #name of a Person in line B:
assert.throws(
() => 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 {
#privateField = 'super';
getSuperPrivateField() {
return this.#privateField;
}
}
class SubClass extends SuperClass {
#privateField = 'sub';
getSubPrivateField() {
return this.#privateField;
}
}
const inst = new SubClass();
assert.equal(
inst.getSuperPrivateField(), 'super'
);
assert.equal(
inst.getSubPrivateField(), 'sub'
);本章稍后将解释通过 extends 进行子类化。
in 检查对象是否具有给定的私有槽in 运算符可用于检查私有槽是否存在(A 行)
class Color {
#name;
constructor(name) {
this.#name = name;
}
static check(obj) {
return #name in obj; // (A)
}
}让我们看看更多将 in 应用于私有槽的示例。
私有方法。以下代码显示私有方法在实例中创建私有槽
class C1 {
#priv() {}
static check(obj) {
return #priv in obj;
}
}
assert.equal(C1.check(new C1()), true);静态私有字段。我们也可以对静态私有字段使用 in
class C2 {
static #priv = 1;
static check(obj) {
return #priv in obj;
}
}
assert.equal(C2.check(C2), true);
assert.equal(C2.check(new C2()), false);静态私有方法。我们可以检查静态私有方法的槽
class C3 {
static #priv() {}
static check(obj) {
return #priv in obj;
}
}
assert.equal(C3.check(C3), true);在不同的类中使用相同的私有标识符。在下一个示例中,Color 和 Person 这两个类都有一个标识符为 #name 的槽。in 运算符可以正确区分它们
class Color {
#name;
constructor(name) {
this.#name = name;
}
static check(obj) {
return #name in obj;
}
}
class Person {
#name;
constructor(name) {
this.#name = name;
}
static check(obj) {
return #name in obj;
}
}
// Detecting Color’s #name
assert.equal(
Color.check(new Color()), true
);
assert.equal(
Color.check(new Person()), false
);
// Detecting Person’s #name
assert.equal(
Person.check(new Person()), true
);
assert.equal(
Person.check(new Color()), false
);我建议使用类,原因如下
类是用于对象创建和继承的通用标准,现在已在各种库和框架中得到广泛支持。与以前相比,这是一个进步,以前几乎每个框架都有自己的继承库。
它们可以帮助 IDE 和类型检查器等工具完成工作,并在其中启用新功能。
如果您是从其他语言转向 JavaScript 并习惯于使用类,则可以更快地上手。
JavaScript 引擎会对其进行优化。也就是说,使用类的代码几乎总是比使用自定义继承库的代码快。
我们可以对内置构造函数(如 Error)进行子类化。
这并不意味着类是完美的
存在过度使用继承的风险。
存在在类中放置太多功能的风险(而其中一些功能通常最好放在函数中)。
类对于来自其他语言的程序员来说看起来很熟悉,但它们的工作方式和使用方式都不同(请参阅下一小节)。因此,这些程序员有可能编写出感觉不像 JavaScript 的代码。
类表面上的工作方式与实际工作方式大相径庭。换句话说,语法和语义之间存在脱节。以下是两个例子
C 中的方法定义会在对象 C.prototype 中创建一个方法。造成这种脱节的原因是为了向后兼容。值得庆幸的是,这种脱节在实践中很少引起问题;如果我们按照类的表面意思去做,通常不会有问题。
这是对类的初步了解。我们很快就会探索更多功能。
练习:编写一个类
exercises/classes/point_class_test.mjs
在底层,一个类会变成两个连接的对象。让我们重新审视 Person 类,看看它是如何工作的
class Person {
#firstName;
constructor(firstName) {
this.#firstName = firstName;
}
describe() {
return `Person named ${this.#firstName}`;
}
static extractNames(persons) {
return persons.map(person => person.#firstName);
}
}类创建的第一个对象存储在 Person 中。它有四个属性
assert.deepEqual(
Reflect.ownKeys(Person),
['length', 'name', 'prototype', 'extractNames']
);
// The number of parameters of the constructor
assert.equal(
Person.length, 1
);
// The name of the class
assert.equal(
Person.name, 'Person'
);其余两个属性是
Person.extractNames 是我们之前已经见过的静态方法。Person.prototype 指向类定义创建的第二个对象。以下是 Person.prototype 的内容
assert.deepEqual(
Reflect.ownKeys(Person.prototype),
['constructor', 'describe']
);有两个属性
Person.prototype.constructor 指向构造函数。Person.prototype.describe 是我们已经使用过的方法。对象 Person.prototype 是所有实例的原型
const jane = new Person('Jane');
assert.equal(
Object.getPrototypeOf(jane), Person.prototype
);
const tarzan = new Person('Tarzan');
assert.equal(
Object.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 === Person
true这种设置的存在是为了向后兼容。但它还有两个额外的好处。
首先,类的每个实例都继承了属性 .constructor。因此,给定一个实例,我们可以通过它创建“类似”的对象
const jane = new Person('Jane');
const cheeta = new jane.constructor('Cheeta');
// cheeta is also an instance of Person
assert.equal(cheeta instanceof Person, true);其次,我们可以获取创建给定实例的类的名称
const tarzan = new Person('Tarzan');
assert.equal(tarzan.constructor.name, 'Person');在本小节中,我们将学习两种不同的方法调用方式
了解这两种方法将使我们对方法的工作原理有重要的了解。
我们还将在本章稍后需要第二种方法:它将允许我们借用 Object.prototype 中的有用方法。
让我们来看看方法调用是如何与类一起工作的。我们正在重新审视之前的 jane
class Person {
#firstName;
constructor(firstName) {
this.#firstName = firstName;
}
describe() {
return 'Person named '+this.#firstName;
}
}
const jane = new Person('Jane');图 14 是一个包含 jane 的原型链的图表。
普通方法调用是_调度_的 - 方法调用
jane.describe()分两步进行
调度:JavaScript 遍历从 jane 开始的原型链,找到第一个具有键 'describe' 的自身属性的对象:它首先查看 jane,但没有找到自身属性 .describe。它继续查看 jane 的原型 Person.prototype,并找到一个自身属性 describe,并返回其值。
const func = jane.describe;调用:方法调用值与函数调用值的不同之处在于,它不仅使用括号内的参数调用括号前的值,而且还将 this 设置为方法调用的接收者(在本例中为 jane)
func.call(jane);这种动态查找和调用方法的方式称为_动态调度_。
我们也可以_直接_进行方法调用,而无需调度
Person.prototype.describe.call(jane)这一次,我们通过 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()
assert.throws(
() => obj.toString(),
/^TypeError: obj.toString is not a function$/
);
assert.equal(
Object.prototype.toString.call(obj),
'[object Object]'
);在 ECMAScript 6 之前,JavaScript 没有类。相反,普通函数被用作_构造函数_
function StringBuilderConstr(initialString) {
this.string = initialString;
}
StringBuilderConstr.prototype.add = function (str) {
this.string += str;
return this;
};
const sb = new StringBuilderConstr('¡');
sb.add('Hola').add('!');
assert.equal(
sb.string, '¡Hola!'
);类为这种方法提供了更好的语法
class StringBuilderClass {
constructor(initialString) {
this.string = initialString;
}
add(str) {
this.string += str;
return this;
}
}
const sb = new StringBuilderClass('¡');
sb.add('Hola').add('!');
assert.equal(
sb.string, '¡Hola!'
);使用构造函数进行子类化尤其棘手。类还提供了超出更方便的语法之外的优势
Error 等内置构造函数进行子类化。super 访问被覆盖的属性。new 调用,并且没有 .prototype 属性。类与构造函数的兼容性 настолько высока, что они могут даже расширять их
function SuperConstructor() {}
class SubClass extends SuperConstructor {}
assert.equal(
new SubClass() instanceof SuperConstructor, true
);extends 和子类化将在本章稍后解释。
这让我们有了一个有趣的发现。一方面,StringBuilderClass 通过 StringBuilderClass.prototype.constructor 引用其构造函数。
另一方面,类_就是_构造函数(一个函数)
> StringBuilderClass.prototype.constructor === StringBuilderClass
true
> typeof StringBuilderClass
'function' 构造函数与类
由于它们非常相似,因此我将_构造函数_和_类_这两个术语互换使用。
以下类声明主体中的所有成员都创建 PublicProtoClass.prototype 的属性。
class PublicProtoClass {
constructor(args) {
// (Do something with `args` here.)
}
publicProtoMethod() {
return 'publicProtoMethod';
}
get publicProtoAccessor() {
return 'publicProtoGetter';
}
set publicProtoAccessor(value) {
assert.equal(value, 'publicProtoSetter');
}
}
assert.deepEqual(
Reflect.ownKeys(PublicProtoClass.prototype),
['constructor', 'publicProtoMethod', 'publicProtoAccessor']
);
const inst = new PublicProtoClass('arg1', 'arg2');
assert.equal(
inst.publicProtoMethod(), 'publicProtoMethod'
);
assert.equal(
inst.publicProtoAccessor, 'publicProtoGetter'
);
inst.publicProtoAccessor = 'publicProtoSetter';const accessorKey = Symbol('accessorKey');
const syncMethodKey = Symbol('syncMethodKey');
const syncGenMethodKey = Symbol('syncGenMethodKey');
const asyncMethodKey = Symbol('asyncMethodKey');
const asyncGenMethodKey = Symbol('asyncGenMethodKey');
class PublicProtoClass2 {
// Identifier keys
get accessor() {}
set accessor(value) {}
syncMethod() {}
* syncGeneratorMethod() {}
async asyncMethod() {}
async * asyncGeneratorMethod() {}
// Quoted keys
get 'an accessor'() {}
set 'an accessor'(value) {}
'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();
inst['sync method']();
inst[syncMethodKey]();带引号的键和计算出的键也可以在对象字面量中使用
有关访问器(通过 getter 和/或 setter 定义)、生成器、异步方法和异步生成器方法的更多信息
私有方法(和访问器)是原型成员和实例成员的有趣组合。
一方面,私有方法存储在实例的槽中(A 行)
class MyClass {
#privateMethod() {}
static check() {
const inst = new MyClass();
assert.equal(
#privateMethod in inst, true // (A)
);
assert.equal(
#privateMethod in MyClass.prototype, false
);
assert.equal(
#privateMethod in MyClass, false
);
}
}
MyClass.check();为什么它们不存储在 .prototype 对象中?私有槽不会被继承,只有属性才会被继承。
另一方面,私有方法在实例之间共享 - 就像原型公共方法一样
class MyClass {
#privateMethod() {}
static check() {
const inst1 = new MyClass();
const inst2 = new MyClass();
assert.equal(
inst1.#privateMethod,
inst2.#privateMethod
);
}
}由于这一点,并且由于它们的语法与原型公共方法相似,因此我们将在这里介绍它们。
以下代码演示了私有方法和访问器的工作原理
class PrivateMethodClass {
#privateMethod() {
return 'privateMethod';
}
get #privateAccessor() {
return 'privateGetter';
}
set #privateAccessor(value) {
assert.equal(value, 'privateSetter');
}
callPrivateMembers() {
assert.equal(this.#privateMethod(), 'privateMethod');
assert.equal(this.#privateAccessor, 'privateGetter');
this.#privateAccessor = 'privateSetter';
}
}
assert.deepEqual(
Reflect.ownKeys(new PrivateMethodClass()), []
);对于私有槽,键始终是标识符
class PrivateMethodClass2 {
get #accessor() {}
set #accessor(value) {}
#syncMethod() {}
* #syncGeneratorMethod() {}
async #asyncMethod() {}
async * #asyncGeneratorMethod() {}
}有关访问器(通过 getter 和/或 setter 定义)、生成器、异步方法和异步生成器方法的更多信息
以下类的实例有两个实例属性(在 A 行和 B 行中创建)
class InstPublicClass {
// Instance public field
instancePublicField = 0; // (A)
constructor(value) {
// We don’t need to mention .property elsewhere!
this.property = value; // (B)
}
}
const inst = new InstPublicClass('constrArg');
assert.deepEqual(
Reflect.ownKeys(inst),
['instancePublicField', 'property']
);
assert.equal(
inst.instancePublicField, 0
);
assert.equal(
inst.property, 'constrArg'
);如果我们在构造函数中创建实例属性(B 行),则无需在其他地方“声明”它。正如我们已经看到的,这与实例私有字段不同。
请注意,实例属性在 JavaScript 中相对常见;比在例如 Java 中更常见,在 Java 中,大多数实例状态都是私有的。
const computedFieldKey = Symbol('computedFieldKey');
class InstPublicClass2 {
'quoted field key' = 1;
[computedFieldKey] = 2;
}
const inst = new InstPublicClass2();
assert.equal(inst['quoted field key'], 1);
assert.equal(inst[computedFieldKey], 2);this 的值是什么?(高级)在实例公共字段的初始化器中,this 指的是新创建的实例
class MyClass {
instancePublicField = this;
}
const inst = new MyClass();
assert.equal(
inst.instancePublicField, inst
);实例公共字段的执行大致遵循以下两条规则
super() 时设置其实例槽。super() 之后立即执行。以下示例演示了这些规则
class SuperClass {
superProp = console.log('superProp');
constructor() {
console.log('super-constructor');
}
}
class SubClass extends SuperClass {
subProp = console.log('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 {
#privateField1 = 'private field 1'; // (A)
#privateField2; // (B) required!
constructor(value) {
this.#privateField2 = value; // (C)
}
/**
* Private fields are not accessible outside the class body.
*/
checkPrivateValues() {
assert.equal(
this.#privateField1, 'private field 1'
);
assert.equal(
this.#privateField2, 'constructor argument'
);
}
}
const inst = new InstPrivateClass('constructor argument');
inst.checkPrivateValues();
// No instance properties were created
assert.deepEqual(
Reflect.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:
assert.deepEqual(
Object.keys(new Countdown()),
['_counter', '_action']);使用这种技术,我们无法获得任何保护,并且私有名称可能会发生冲突。从好的方面来说,它易于使用。
私有方法的工作原理类似:它们是名称以下划线开头的普通方法。
我们还可以通过 WeakMap 管理私有实例数据
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
// The two pseudo-properties are truly private:
assert.deepEqual(
Object.keys(new Countdown()),
[]);有关其工作原理的详细说明,请参阅WeakMap 章节。
这种技术为我们提供了相当大的外部访问保护,并且不会发生任何名称冲突。但使用起来也更加复杂。
我们通过控制谁可以访问伪属性 _superProp 来控制其可见性,例如:如果变量存在于模块内部并且未导出,则模块内部的每个人都可以访问它,而模块外部的任何人都无法访问它。换句话说:在这种情况下,隐私范围不是类,而是模块。不过,我们可以缩小范围
let Countdown;
{ // class scope
const _counter = new WeakMap();
const _action = new WeakMap();
Countdown = class {
// ···
}
}这种技术实际上不支持私有方法。但是,可以访问 _superProp 的模块本地函数是次优选择
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
privateDec(this);
}
}
function privateDec(_this) { // (A)
let counter = _counter.get(_this);
counter--;
_counter.set(_this, counter);
if (counter === 0) {
_action.get(_this)();
}
}请注意,this 变成了显式函数参数 _this(A 行)。
如前所述,实例私有字段仅在其类内部可见,甚至在子类中也不可见。因此,没有内置的方法来获取
在上一小节中,我们通过 WeakMap 模拟了“模块可见性”(模块内部的每个人都可以访问一部分实例数据)。因此
下一个示例演示了受保护的可见性
const _superProp = new WeakMap();
class SuperClass {
constructor() {
_superProp.set(this, 'superProp');
}
}
class SubClass extends SuperClass {
getSuperProp() {
return _superProp.get(this);
}
}
assert.equal(
new SubClass().getSuperProp(),
'superProp'
);本章稍后将解释通过 extends 进行子类化。
以下类声明体中的所有成员都创建了所谓的*静态*属性——StaticClass 本身的属性。
class StaticPublicMethodsClass {
static staticMethod() {
return 'staticMethod';
}
static get staticAccessor() {
return 'staticGetter';
}
static set staticAccessor(value) {
assert.equal(value, 'staticSetter');
}
}
assert.equal(
StaticPublicMethodsClass.staticMethod(), 'staticMethod'
);
assert.equal(
StaticPublicMethodsClass.staticAccessor, 'staticGetter'
);
StaticPublicMethodsClass.staticAccessor = 'staticSetter';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:
StaticPublicMethodsClass2['sync method']();
StaticPublicMethodsClass2[syncMethodKey]();带引号的键和计算出的键也可以在对象字面量中使用
有关访问器(通过 getter 和/或 setter 定义)、生成器、异步方法和异步生成器方法的更多信息
以下代码演示了静态公共字段。StaticPublicFieldClass 有三个这样的字段
const computedFieldKey = Symbol('computedFieldKey');
class StaticPublicFieldClass {
static identifierFieldKey = 1;
static 'quoted field key' = 2;
static [computedFieldKey] = 3;
}
assert.deepEqual(
Reflect.ownKeys(StaticPublicFieldClass),
[
'length', // number of constructor parameters
'name', // 'StaticPublicFieldClass'
'prototype',
'identifierFieldKey',
'quoted field key',
computedFieldKey,
],
);
assert.equal(StaticPublicFieldClass.identifierFieldKey, 1);
assert.equal(StaticPublicFieldClass['quoted field key'], 2);
assert.equal(StaticPublicFieldClass[computedFieldKey], 3);以下类有两个静态私有槽(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();
}
}
assert.deepEqual(
Reflect.ownKeys(StaticPrivateClass),
[
'length', // number of constructor parameters
'name', // 'StaticPublicFieldClass'
'prototype',
'getResultOfTwice',
],
);
assert.equal(
StaticPrivateClass.getResultOfTwice(),
'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 {
assert.equal(this, SuperClass);
console.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 {
assert.equal(this, SubClass);
console.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 进行子类化。
静态公共字段是属性。如果我们进行方法调用
assert.equal(SuperClass.getPublicViaThis(), 1);则 this 指向 SuperClass,并且一切按预期工作。我们还可以通过子类调用 .getPublicViaThis()
assert.equal(SubClass.getPublicViaThis(), 1);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
assert.equal(SuperClass.getPrivateDataViaThis(), 2);但是,通过 SubClass 调用 .getPrivateDataViaThis() 无法正常工作,因为 this 现在指向 SubClass,而 SubClass 没有静态私有字段 .#privateData(原型链中的私有槽不会被继承)
assert.throws(
() => SubClass.getPrivateDataViaThis(),
{
name: 'TypeError',
message: 'Cannot read private member #privateData from'
+ ' an object whose class did not declare it',
}
);解决方法是通过 SuperClass 直接访问 .#privateData
assert.equal(SubClass.getPrivateDataViaClassName(), 2);对于静态私有方法,我们也面临着同样的问题。
类中的每个成员都可以访问该类中的所有其他成员——包括公共成员和私有成员
class DemoClass {
static #staticPrivateField = 1;
#instPrivField = 2;
static staticMethod(inst) {
// A static method can access static private fields
// and instance private fields
assert.equal(DemoClass.#staticPrivateField, 1);
assert.equal(inst.#instPrivField, 2);
}
protoMethod() {
// A prototype method can access instance private fields
// and static private fields
assert.equal(this.#instPrivField, 2);
assert.equal(DemoClass.#staticPrivateField, 1);
}
}相反,外部任何人都无法访问私有成员
// Accessing private fields outside their classes triggers
// syntax errors (before the code is even executed).
assert.throws(
() => 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).
assert.throws(
() => 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;
}
}
assert.deepEqual(
Point.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
assert.throws(
() => new Point(3, 4),
TypeError
);类也可以扩展现有类。例如,以下类 Employee 扩展了 Person
class Person {
#firstName;
constructor(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');
assert.equal(
jane.title,
'CTO'
);
assert.equal(
jane.describe(),
'Person named Jane (CTO)'
);与扩展相关的术语
Person 是 Employee 的超类。Employee 是 Person 的子类。在派生类的 .constructor() 内部,我们必须在访问 this 之前通过 super() 调用超构造函数。为什么呢?
让我们考虑一个类链
AB 扩展了 A。C 扩展了 B。如果我们调用 new C(),C 的构造函数会超级调用 B 的构造函数,后者会超级调用 A 的构造函数。实例总是在基类中创建,然后子类的构造函数才会添加它们的槽。因此,在我们调用 super() 之前,实例并不存在,我们还不能通过 this 访问它。
请注意,静态公共槽会被继承。例如,Employee 继承了静态方法 .extractNames()
> 'extractNames' in Employee
true 练习:子类化
exercises/classes/color_point_class_test.mjs
上一节中的类 Person 和 Employee 由多个对象组成(图 15)。理解这些对象如何关联的一个关键见解是,存在两条原型链
实例原型链从 jane 开始,继续到 Employee.prototype 和 Person.prototype。原则上,原型链在这一点结束,但我们还有一个对象:Object.prototype。此原型为几乎所有对象提供服务,这也是它也包含在此处的原因
> Object.getPrototypeOf(Person.prototype) === Object.prototype
true在类原型链中,Employee 排在第一位,Person 排在第二位。之后,链条继续到 Function.prototype,它之所以存在,是因为 Person 是一个函数,而函数需要 Function.prototype 的服务。
> Object.getPrototypeOf(Person) === Function.prototype
trueinstanceof 和子类化(高级)我们还没有学习 instanceof 的真正工作原理。instanceof 如何确定值 x 是否是类 C 的实例(它可以是 C 的直接实例,也可以是 C 的子类的直接实例)?它检查 C.prototype 是否在 x 的原型链中。也就是说,以下两个表达式是等效的
x instanceof C
C.prototype.isPrototypeOf(x)如果我们回到图 15,我们可以确认原型链确实引导我们得出以下正确答案
> jane instanceof Employee
true
> jane instanceof Person
true
> jane instanceof Object
true请注意,如果 instanceof 的左侧是原始值,则它始终返回 false
> 'abc' instanceof String
false
> 123 instanceof Number
falseObject 的实例(高级)对象(非原始值)仅当 Object.prototype 在其原型链中时才是 Object 的实例(参见上一小节)。几乎所有对象都是 Object 的实例,例如
assert.equal(
{a: 1} instanceof Object, true
);
assert.equal(
['a'] instanceof Object, true
);
assert.equal(
/abc/g instanceof Object, true
);
assert.equal(
new Map() instanceof Object, true
);
class C {}
assert.equal(
new C() instanceof Object, true
);在下一个示例中,obj1 和 obj2 都是对象(A 行和 C 行),但它们不是 Object 的实例(B 行和 D 行):Object.prototype 不在它们的原型链中,因为它们没有任何原型。
const obj1 = {__proto__: null};
assert.equal(
typeof obj1, 'object' // (A)
);
assert.equal(
obj1 instanceof Object, false // (B)
);
const obj2 = Object.create(null);
assert.equal(
typeof obj2, 'object' // (C)
);
assert.equal(
obj2 instanceof Object, false // (D)
);Object.prototype 是结束大多数原型链的对象。它的原型是 null,这意味着它也不是 Object 的实例
> typeof Object.prototype
'object'
> Object.getPrototypeOf(Object.prototype)
null
> Object.prototype instanceof Object
false接下来,我们将使用我们对子类化的知识来理解一些内置对象的原型链。以下工具函数 p() 有助于我们进行探索。
const p = Object.getPrototypeOf.bind(Object);我们提取了 Object 的方法 .getPrototypeOf() 并将其分配给 p。
{} 的原型链让我们从检查普通对象开始
> p({}) === Object.prototype
true
> p(p({})) === null
true图 16 展示了此原型链的图表。我们可以看到 {} 确实是 Object 的一个实例 – Object.prototype 在其原型链中。
[] 的原型链数组的原型链是什么样的?
> p([]) === Array.prototype
true
> p(p([])) === Object.prototype
true
> p(p(p([]))) === null
true此原型链(在图 17 中可视化)告诉我们,数组对象是 Array 和 Object 的实例。
function () {} 的原型链最后,普通函数的原型链告诉我们所有函数都是对象
> p(function () {}) === Function.prototype
true
> p(p(function () {})) === Object.prototype
true基类的原型是 Function.prototype,这意味着它是一个函数(Function 的实例)
class A {}
assert.equal(
Object.getPrototypeOf(A),
Function.prototype
);
assert.equal(
Object.getPrototypeOf(class {}),
Function.prototype
);派生类的原型是其超类
class B extends A {}
assert.equal(
Object.getPrototypeOf(B),
A
);
assert.equal(
Object.getPrototypeOf(class extends Object {}),
Object
);有趣的是,Object、Array 和 Function 都是基类
> Object.getPrototypeOf(Object) === Function.prototype
true
> Object.getPrototypeOf(Array) === Function.prototype
true
> Object.getPrototypeOf(Function) === Function.prototype
true但是,正如我们所见,即使是基类的实例在其原型链中也有 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 {
name = '(Unnamed)';
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');
assert.equal(
paris.name, 'Paris'
);
assert.equal(
paris.toString(), 'City named 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);
assert.equal(obj instanceof Object, false);
assert.throws(
() => obj.hasOwnProperty('prop'),
{
name: 'TypeError',
message: 'obj.hasOwnProperty is not a function',
}
);另一方面,如果对象使用自有属性覆盖它,我们就不能使用 .hasOwnProperty()(第 A 行)
const obj = {
hasOwnProperty: 'yes' // (A)
};
assert.throws(
() => 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)
}
assert.equal(
hasOwnProp(Object.create(null), 'prop'), false
);
assert.equal(
hasOwnProp({hasOwnProperty: 'yes'}, 'prop'), false
);
assert.equal(
hasOwnProp({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({})
NaNObject.prototype.isPrototypeOf()如果 proto 在 obj 的原型链中,则 proto.isPrototypeOf(obj) 返回 true,否则返回 false。
const a = {};
const b = {__proto__: a};
const c = {__proto__: b};
assert.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);
assert.equal(a.isPrototypeOf(a), false);
assert.equal(c.isPrototypeOf(a), false);以下是安全使用此方法的方法(有关详细信息,请参阅 §29.8.1 “安全地使用 Object.prototype 方法”)
const obj = {
// Overrides Object.prototype.isPrototypeOf
isPrototypeOf: true,
};
// Doesn’t work in this case:
assert.throws(
() => obj.isPrototypeOf(Object.prototype),
{
name: 'TypeError',
message: 'obj.isPrototypeOf is not a function',
}
);
// Safe way of using .isPrototypeOf():
assert.equal(
Object.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(
obj, 'nonEnumObjProp',
{
enumerable: false,
}
);
assert.equal(
obj.propertyIsEnumerable('enumerableProtoProp'),
false // not an own property
);
assert.equal(
obj.propertyIsEnumerable('enumerableObjProp'),
true
);
assert.equal(
obj.propertyIsEnumerable('nonEnumObjProp'),
false // not enumerable
);
assert.equal(
obj.propertyIsEnumerable('unknownProp'),
false // not a property
);以下是安全使用此方法的方法(有关详细信息,请参阅 §29.8.1 “安全地使用 Object.prototype 方法”)
const obj = {
// Overrides Object.prototype.propertyIsEnumerable
propertyIsEnumerable: true,
enumerableProp: 'yes',
};
// Doesn’t work in this case:
assert.throws(
() => obj.propertyIsEnumerable('enumerableProp'),
{
name: 'TypeError',
message: 'obj.propertyIsEnumerable is not a function',
}
);
// Safe way of using .propertyIsEnumerable():
assert.equal(
Object.prototype.propertyIsEnumerable.call(obj, 'enumerableProp'),
true
);另一个安全的替代方法是使用 属性描述符
assert.deepEqual(
Object.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 {
get __proto__() {
return Object.getPrototypeOf(this);
}
set __proto__(other) {
Object.setPrototypeOf(this, other);
}
// ···
}由于 __proto__ 是从 Object.prototype 继承的,因此我们可以通过创建一个在其原型链中没有 Object.prototype 的对象来删除此功能(请参阅 §29.7.3 “并非所有对象都是 Object 的实例”)
> '__proto__' in {}
true
> '__proto__' in Object.create(null)
falseObject.prototype.hasOwnProperty()
.hasOwnProperty() 的更好替代方案:Object.hasOwn() [ES2022]
如果 obj 具有一个自有(非继承)属性,其键为 propKey,则 obj.hasOwnProperty(propKey) 返回 true,否则返回 false。
const obj = { ownProp: true };
assert.equal(
obj.hasOwnProperty('ownProp'), true // own
);
assert.equal(
'toString' in obj, true // inherited
);
assert.equal(
obj.hasOwnProperty('toString'), false
);以下是安全使用此方法的方法(有关详细信息,请参阅 §29.8.1 “安全地使用 Object.prototype 方法”)
const obj = {
// Overrides Object.prototype.hasOwnProperty
hasOwnProperty: true,
};
// Doesn’t work in this case:
assert.throws(
() => obj.hasOwnProperty('anyPropKey'),
{
name: 'TypeError',
message: 'obj.hasOwnProperty is not a function',
}
);
// Safe way of using .hasOwnProperty():
assert.equal(
Object.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 视为私有或公共。
测验
请参阅 测验应用程序。