js 深入理解类-class

news2024/9/21 9:19:44

目录

  • 概述
  • 1. 类的定义
  • 2. 类构造函数
    • 2.1. 实例化
      • 2.1.1 实例化流程
      • 2.1.2 带参实例化
      • 2.1.3 执行构造函数返回的两种对象
      • 2.1.4 类构造函数和普通构造函数的区别
    • 2.2 把类当成特殊函数
    • 2.2.1 辨别是不是函数,使用 typeof
    • 2.2.2 辨别是不是函数,是否有prototype
    • 2.2.3 检查对象是否为某个类的实例 -instanceof
    • 2.2.4 类还能当参数传递
  • 3. 实例、原型和类成员
    • 3.1 实例成员
    • 3.2 原型方法与访问器
    • 3.3 静态类方法
    • 3.4 非函数原型和类成员
    • 3.5 迭代器与生成器方法
  • 4. 继承
    • 4.1 继承基础 --关键字 extends
    • 4.2 构造函数、 HomeObject 和 super()
    • 4.3 抽象类 --通过 new.target实现
    • 4.4 继承内置类型(js原生类型)
  • 4.5 多重继承(通过类混入的方式模拟)
  • 总结

概述

终于到了类的阶段了。ES 6 终于有了class 关键字了,继承可以节省了很多冗长的代码。 那么有了类 这个语法糖,以前学习的原型、对象、构造函数的知识是不是都没用了。恰恰相反,ES 6的类 其实都是在使用 原型、构造函数的概念。所以学习本章的内容最好学习前面几章的知识。

1. 类的定义

  与函数类型相似,定义类也有两种主要方式:类声明 类表达式。这两种方式都使用 class 关键字加大括号:


// 类声明
class Person {}

// 类表达式
const Animal = class {};

  与函数表达式类似,类表达式在它们被求值前也不能引用。不过,与函数定义不同的是,虽然函数声明可以提升,但类声明不能

console.log(FunctionExpression); // undefined , 函数表达式不可提升
var FunctionExpression = function() {};				//函数表达式`在这里插入代码片`
console.log(FunctionExpression); // function() {}  
console.log(FunctionDeclaration); // FunctionDeclaration() {} //函数声明可以提升  
function FunctionDeclaration() {} 						//函数声明
console.log(FunctionDeclaration); // FunctionDeclaration() {} 
console.log(ClassExpression); // undefined , 类表达式不可提升
var ClassExpression = class {};					 	   //类表达式
console.log(ClassExpression); // class {}			
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined 类声明不可提升
class ClassDeclaration {}								//类声明
console.log(ClassDeclaration); // class ClassDeclaration {}

  另一个跟函数声明不同的地方是,函数受函数作用域限制,而类受块作用域限制:


{
	function FunctionDeclaration() {}
	class ClassDeclaration {}
}
console.log(FunctionDeclaration); // FunctionDeclaration() {} 可以访问函数
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined 不可以访问类

类的构成
  类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,但这些都不是必需的。
空的类定义照样有效。默认情况下,类定义中的代码都在严格模式下执行。
  与函数构造函数一样,多数编程风格都建议类名的首字母要大写,以区别于通过它创建的实例(比
如,通过 class Foo {} 创建实例 foo ):


// 空类定义,有效
class Foo {}

// 有构造函数的类,有效
class Bar {
	constructor() {}
}

// 有获取函数的类,有效
class Baz {
	get myBaz() {}
}

// 有静态方法的类,有效
class Qux {
	static myQux() {}
}

  类表达式的名称是可选的。在把类表达式赋值给变量后,可以通过 name 属性取得类表达式的名称字符串。但不能在类表达式作用域外部访问这个标识符


let Person = class PersonName {
	identify() {
		console.log(Person.name, PersonName.name);
	}
}
let p = new Person();
p.identify(); // PersonName PersonName
console.log(Person.name); // PersonName
console.log(PersonName); // ReferenceError: PersonName is not defined 。

2. 类构造函数

  constructor 关键字用于在类定义块内部创建类的构造函数。方法名 constructor 会告诉解释器在使用 new 操作符创建类的新实例时,应该调用这个函数。构造函数的定义不是必需的,不定义构造函数相当于将构造函数定义为空函数。

2.1. 实例化

2.1.1 实例化流程

  使用 new 操作符实例化 Person 的操作等于使用 new 调用其构造函数。唯一可感知的不同之处就是,JavaScript 解释器知道使用 new 和类意味着应该使用 constructor 函数进行实例化。
使用 new 调用类的构造函数会执行如下操作。

(1) 在内存中创建一个新对象。
(2) 这个新对象内部的 [[Prototype]] 指针被赋值为类的 prototype 属性。
(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

  发现上面5个步骤,可以分类为两大类:1. 内存中创建对象。2. 执行构造函数
  来看下面的例子:

class Animal {}
class Person {
	constructor() { //其实这个大括号里面的部分相当于 传统的构造函数的一部分,主要用来初始化对象。这里可以定义属性,实例对象也会创建这些属性
		console.log('person ctor');
	}
}
class Vegetable {
	constructor() { //这里创建了一个color属性,实例对象也会创建这个属性
		this.color = 'orange';
	}
}
let a = new Animal();
let p = new Person(); // person ctor 
let v = new Vegetable();
console.log(v.color); // orange

2.1.2 带参实例化

  类实例化时传入的参数会用作构造函数的参数。如果不需要参数,则类名后面的括号也是可选的:

class Person {
	constructor(name) {
		console.log(arguments.length);
		this.name = name || null;
	}
}
let p1 = new Person; // 0
console.log(p1.name); // null
let p2 = new Person(); // 0
console.log(p2.name); // null
let p3 = new Person('Jake'); // 1
console.log(p3.name); // Jake

2.1.3 执行构造函数返回的两种对象

  默认情况下,类构造函数会在执行之后返回 this 对象。构造函数返回的对象会被用作实例化的对象,如果没有什么引用新创建的 this 对象,那么这个对象会被销毁。不过,如果返回的不是 this 对象,而是其他对象,那么这个对象不会通过 instanceof 操作符检测出跟类有关联,因为这个对象的原型指针并没有被修改。

class Person {
	constructor(override) {
		this.foo = 'foo';
		if (override) {
			return { bar: 'bar'};
		}
	}
}
let p1 = new Person(),
	p2 = new Person(true);
console.log(p1); // Person{ foo: 'foo' }
console.log(p1 instanceof Person); // true
console.log(p2); // Object{ bar: 'bar' }
console.log(p2 instanceof Person); // false ,发现和 Person没有关联

2.1.4 类构造函数和普通构造函数的区别

  类构造函数与构造函数的主要区别是,调用类构造函数必须使用 new 操作符。而普通构造函数如果不使用 new 调用,那么就会以全局的 this (通常是 window )作为内部对象。调用类构造函数时如果忘了使用 new 则会抛出错误:

function Person() {}
class Animal {}
// 把 window 作为 this 来构建实例
let p = Person();
let a = Animal(); // TypeError: class constructor Animal cannot be invoked without 'new' ,提示必须加 new 

  类构造函数没有什么特殊之处,实例化之后,它会成为普通的实例方法(但作为类构造函数,仍然要使用 new 调用)。因此,实例化之后可以在实例上引用它:

class Person {}
// 使用类创建一个新实例
let p1 = new Person();
p1.constructor();
// TypeError: Class constructor Person cannot be invoked without 'new'
// 使用对类构造函数的引用创建一个新实例
let p2 = new p1.constructor();

2.2 把类当成特殊函数

2.2.1 辨别是不是函数,使用 typeof

  ECMAScript 中没有正式的类这个类型。从各方面来看,ECMAScript 类就是一种特殊函数。声明一个类之后,通过 typeof 操作符检测类标识符,表明它是一个函数:

class Person {}
console.log(Person); // class Person {}
console.log(typeof Person); // function  , 是函数吧,诚不我欺

2.2.2 辨别是不是函数,是否有prototype

  类标识符有 prototype 属性,而这个原型也有一个 constructor 属性指向类自身(这不妥妥的函数嘛):

class Person{}
console.log(Person.prototype); // { constructor: f() }
console.log(Person === Person.prototype.constructor); // true

2.2.3 检查对象是否为某个类的实例 -instanceof

  与普通构造函数一样,可以使用 instanceof 操作符检查构造函数原型是否存在于实例的原型链中:

class Person {}
let p = new Person();
console.log(p instanceof Person); // true

  由此可知,可以使用 instanceof 操作符检查一个对象与类构造函数,以确定这个对象是不是类的实例。只不过此时的类构造函数要使用类标识符,比如,在前面的例子中要检查 p 和 Person 。
  如前所述,类本身具有与普通构造函数一样的行为。在类的上下文中,类本身在使用 new 调用时就会被当成构造函数。重点在于,类中定义的 constructor 方法不会被当成构造函数,在对它使用instanceof 操作符时会返回 false 。但是,如果在创建实例时直接将类构造函数当成普通构造函数来使用,那么 instanceof 操作符的返回值会反转:

class Person {}
let p1 = new Person();
console.log(p1.constructor === Person); // true
console.log(p1 instanceof Person); // true
console.log(p1 instanceof Person.constructor); // false


let p2 = new Person.constructor();  //类构造函数当做普通的构造函数
console.log(p2.constructor === Person); // false
console.log(p2 instanceof Person); // false
console.log(p2 instanceof Person.constructor); // true

2.2.4 类还能当参数传递

  类是 JavaScript 的一等公民,因此可以像其他对象或函数引用一样把类作为参数传递

// 类可以像函数一样在任何地方定义,比如在数组中
let classList = [
	class {
		constructor(id) {
			this.id_ = id;
			console.log(`instance ${this.id_}`);
		}
	}
];
function createInstance(classDefinition, id) {
	return new classDefinition(id);
}
let foo = createInstance(classList[0], 3141); // instance 3141

  与立即调用函数表达式相似,类也可以立即实例化1

// 因为是一个类表达式,所以类名是可选的
let p = new class Foo {
	constructor(x) {
		console.log(x);
	}
}('bar'); // bar
console.log(p); // Foo {}

3. 实例、原型和类成员

  类的语法可以非常方便地定义应该存在于实例上的成员、应该存在于原型上的成员,以及应该存在于类本身的成员(静态成员)

3.1 实例成员

  每次通过 new 调用类标识符时,都会执行类构造函数。在这个函数内部,可以为新创建的实例( this )添加“自有”属性。至于添加什么样的属性,则没有限制。另外,在构造函数执行完毕后,仍然可以给实例继续添加新成员。
  每个实例都对应一个唯一的成员对象,这意味着所有成员都不会在原型上共享:

class Person {
	constructor() {
		// 这个例子先使用对象包装类型定义一个字符串
		// 为的是在下面测试两个对象的相等性
		this.name = new String('Jack');
		this.sayName = () => console.log(this.name);
		this.nicknames = ['Jake', 'J-Dog']
	}
}
let p1 = new Person(),
p2 = new Person();
p1.sayName(); // Jack
p2.sayName(); // Jack
console.log(p1.name === p2.name); // false
console.log(p1.sayName === p2.sayName); // false
console.log(p1.nicknames === p2.nicknames); // false
p1.name = p1.nicknames[0];
p2.name = p2.nicknames[1];
p1.sayName(); // Jake
p2.sayName(); // J-Dog

3.2 原型方法与访问器

  为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法

class Person {
	constructor() {
	// 添加到 this 的所有内容都会存在于不同的实例上
		this.locate = () => console.log('instance');
	}
	//  在类块中定义的所有内容都会定义在类的原型上
	locate() {
		console.log('prototype');
	}
}
let p = new Person();
p.locate(); // instance		调用的实例对象的方法
Person.prototype.locate(); // prototype 调用的原型的方法

  可以把方法定义在类构造函数中或者类块中,但 不能在类块中给原型添加原始值或对象作为成员数据(可以定义变量)

class Person {
	name: 'Jake'
}
// Uncaught SyntaxError: Unexpected token

class Person {
	age = 39;				//新建对象时,变量age 会变成实例属性
}
let p1 =new Person();
console.log(p1.hasOwnProperty("age"));// true ,说明在类的主体中定义变量,会自动的变成示例对象的属性。

  类方法等同于对象属性,因此可以使用字符串、符号或计算的值作为键:

const symbolKey = Symbol('symbolKey');
class Person {
	stringKey() {
		console.log('invoked stringKey');
	}
	[symbolKey]() {
		console.log('invoked symbolKey');
	}
	['computed' + 'Key']() {
		console.log('invoked computedKey');
	}
}
let p = new Person();
p.stringKey(); // invoked stringKey
p[symbolKey](); // invoked symbolKey
p.computedKey(); // invoked computedKey

  类定义也支持获取和设置访问器。语法与行为跟普通对象一样:

class Person {
	set name(newName) {
		this.name_ = newName;
	}
	get name() {
		return this.name_;
	}
}
let p = new Person();
p.name = 'Jake';
console.log(p.name); // Jake

3.3 静态类方法

  可以在类上定义静态方法。这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例。与原型成员类似,静态成员每个类上只能有一个
  静态类成员在类定义中使用 static 关键字作为前缀。在静态成员中, this 引用类自身。其他所有约定跟原型成员一样:

class Person {
	constructor() {
		// 添加到 this 的所有内容都会存在于不同的实例上
		this.locate = () => console.log('instance', this);
	}
	
	// 定义在类的原型对象上
	locate() {
		console.log('prototype', this);
	}
	
	//  定义在类本身上
	static locate() {
		console.log('class', this);
	}
}
let p = new Person();
p.locate(); // instance, Person {}
Person.prototype.locate(); // prototype, {constructor: ... }
Person.locate(); // class, class Person {}

  静态类方法非常适合作为实例工厂:

class Person {
	constructor(age) {
		this.age_ = age;
	}
	sayAge() {
		console.log(this.age_);
	}
	static create() {
		// 使用随机年龄创建并返回一个 Person 实例
		return new Person(Math.floor(Math.random()*100));
	}
}
console.log(Person.create()); // Person { age_: ... }

3.4 非函数原型和类成员

  类定义外部,可以手动添加属性:

class Person {
	sayName() {
		console.log(`${Person.greeting} ${this.name}`);
	}
}
//  在类上定义数据成员
Person.greeting = 'My name is';
//  在原型上定义数据成员
Person.prototype.name = 'Jake';
let p = new Person();
p.sayName(); // My name is Jake

注意 类定义中之所以没有显式支持添加数据成员,是因为在共享目标(原型和类)上添加可变(可修改)数据成员是一种反模式。一般来说,对象实例应该独自拥有通过 this引用的数据。

3.5 迭代器与生成器方法

  类定义语法支持在原型和类本身上定义生成器方法:

class Person {
	// 在原型上定义生成器方法
	*createNicknameIterator() {
		yield 'Jack';
		yield 'Jake';
		yield 'J-Dog';
	}
	// 在类上定义生成器方法
	static *createJobIterator() {
		yield 'Butcher';
		yield 'Baker';
		yield 'Candlestick maker';
	}
}
let jobIter = Person.createJobIterator();
console.log(jobIter.next().value); // Butcher
console.log(jobIter.next().value); // Baker
console.log(jobIter.next().value); // Candlestick maker
let p = new Person();
let nicknameIter = p.createNicknameIterator();
console.log(nicknameIter.next().value); // Jack
console.log(nicknameIter.next().value); // Jake
console.log(nicknameIter.next().value); // J-Dog

  因为支持生成器方法,所以可以通过添加一个默认的迭代器,把类实例变成可迭代对象:

class Person {
	constructor() {
		this.nicknames = ['Jack', 'Jake', 'J-Dog'];
	}
	*[Symbol.iterator]() {
		yield *this.nicknames.entries();
	}
}
let p = new Person();
for (let [idx, nickname] of p) {
	console.log(nickname);
}
// Jack
// Jake
// J-Dog

也可以只返回迭代器实例:

class Person {
	constructor() {
		this.nicknames = ['Jack', 'Jake', 'J-Dog'];
	}
	[Symbol.iterator]() {
		return this.nicknames.entries();
	}
}
let p = new Person();
for (let [idx, nickname] of p) {
	console.log(nickname);
}
// Jack
// Jake
// J-Dog

4. 继承

  在《深入学习各种继承方法使用场景》介绍了ES5的各种继承方法,ECMAScript 6 新增特性中最出色的一个就是原生支持了类继承机制。虽然类继承使用的是新语法,但底层依旧使用的是原型链

4.1 继承基础 --关键字 extends

  ES6 类支持单继承。使用 extends 关键字(这和java一样),就可以继承任何拥有 [[Construct]] 和原型的对象。很大程度上,这意味着不仅可以继承一个类,也可以继承普通的构造函数(保持向后兼容):

class Vehicle {}
// 继承类
class Bus extends Vehicle {}
let b = new Bus();
console.log(b instanceof Bus); // true
console.log(b instanceof Vehicle); // true
function Person() {}
// 继承普通构造函数
class Engineer extends Person {}
let e = new Engineer();
console.log(e instanceof Engineer); // true
console.log(e instanceof Person); // true

   派生类都会通过原型链访问到类和原型上定义的方法。 this 的值会反映调用相应方法的实例或者类:

class Vehicle {
	identifyPrototype(id) {
		console.log(id, this);
	}
	static identifyClass(id) {
		console.log(id, this);
	}
}
class Bus extends Vehicle {}
let v = new Vehicle();
let b = new Bus();
b.identifyPrototype('bus'); // bus, Bus {}
v.identifyPrototype('vehicle'); // vehicle, Vehicle {}
Bus.identifyClass('bus'); // bus, class Bus {}
Vehicle.identifyClass('vehicle'); // vehicle, class Vehicle {}

注意 extends 关键字也可以在类表达式中使用,因此 let Bar = class extends Foo {} 是有效的语法。

4.2 构造函数、 HomeObject 和 super()

  派生类的方法可以通过 super 关键字引用它们的原型(这也和java一样)。这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部

  • 在类构造函数中使用 super 可以调用父类构造函数。
class Vehicle {
	constructor() {
		this.hasEngine = true;
	}
}
class Bus extends Vehicle {
	constructor() {
		// 不要在调用 super()之前引用 this,否则会抛出 ReferenceError
		super(); // 相当于 super.constructor()
		console.log(this instanceof Vehicle); // true
		console.log(this); // Bus { hasEngine: true }
	}
}
new Bus();
  • 在静态方法中可以通过 super 调用继承的类上定义的静态方法:
class Vehicle {
	static identify() {
		console.log('vehicle');
	}
}
class Bus extends Vehicle {
	static identify() {
		super.identify();
	}
}
Bus.identify(); // vehicle

注意 ES6 给类构造函数和静态方法添加了内部特性 [[HomeObject]] ,这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在 JavaScript 引擎内部访问。 super 始终会定义为 [[HomeObject]] 的原型。

   在使用 super 时要注意几个问题 :

  • super 只能在派生类构造函数和静态方法中使用。
class Vehicle {
	constructor() {
		super();
		// SyntaxError: 'super' keyword unexpected
	}
}
  • 不能单独引用 super 关键字,要么用它调用构造函数,要么用它引用静态方法。
class Vehicle {}
class Bus extends Vehicle {
   constructor() {
   	console.log(super);
   	// SyntaxError: 'super' keyword unexpected here
   }
}
  • 调用 super() 会调用父类构造函数,并将返回的实例赋值给 this 。
class Vehicle {}
class Bus extends Vehicle {
   constructor() {
   	super();
   	console.log(this instanceof Vehicle);
   }
}
new Bus(); // true
  • super() 的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入。
class Vehicle {
	constructor(licensePlate) {
		this.licensePlate = licensePlate;
	}
}
class Bus extends Vehicle {
	constructor(licensePlate) {
		super(licensePlate);
	}
}
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }
  • 如果没有定义类构造函数,在实例化派生类时会默认调用 super() ,而且会传入所有传给派生类的参数。
class Vehicle {
	constructor(licensePlate) {
		this.licensePlate = licensePlate;
	}
}
class Bus extends Vehicle {}
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }
  • 在类构造函数中,不能在调用 super() 之前引用 this 。
class Vehicle {}
class Bus extends Vehicle {
	constructor() {
		console.log(this);
	}
}
new Bus();
// ReferenceError: Must call super constructor in derived class(派生类) before accessing 'this' or returning from derived constructor
  • 如果在派生类中显式定义了构造函数,则要么必须在其中调用 super() ,要么必须在其中返回一个对象。
class Vehicle {}
class Car extends Vehicle {}
class Bus extends Vehicle {
	constructor() {
		super();	
	}
}
class Van extends Vehicle {
	constructor() {
		return {};		//返回一个对象
	}
}
console.log(new Car()); // Car {}
console.log(new Bus()); // Bus {}
console.log(new Van()); // {}

4.3 抽象类 --通过 new.target实现

   有时候可能需要定义这样一个类,它可供其他类继承,但本身不会被实例化。虽然 ECMAScript 没有专门支持这种类的语法 ,但通过 new.target 也很容易实现。 new.target 保存通过 new 关键字调用的类或函数。通过在实例化时检测 new.target 是不是抽象基类,可以阻止对抽象基类的实例化:

// 抽象基类
class Vehicle {
	constructor() {
		console.log(new.target);
		if (new.target === Vehicle) {
			throw new Error('Vehicle cannot be directly instantiated');
		}
	}
}
// 派生类
class Bus extends Vehicle {}
new Bus(); // class Bus {}
new Vehicle(); // class Vehicle {}
// Error: Vehicle cannot be directly instantiated

  另外,通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在调用类构造函数之前就已经存在了,所以可以通过 this 关键字来检查相应的方法:

// 抽象基类
class Vehicle {
	constructor() {
		if (new.target === Vehicle) {
			throw new Error('Vehicle cannot be directly instantiated');
		}
		if (!this.foo) {
			throw new Error('Inheriting class must define foo()');
		}
		console.log('success!');
	}
}
// 派生类
class Bus extends Vehicle {
	foo() {}
}
// 派生类
class Van extends Vehicle {}
new Bus(); // success!
new Van(); // Error: Inheriting class must define foo()

   啥时候也能像 Java 一样,直接搞个关键字 abstract 。

4.4 继承内置类型(js原生类型)

  ES6 类为继承内置引用类型提供了顺畅的机制,开发者可以方便地扩展内置类型:


class SuperArray extends Array {
  shuffle() {
  // 洗牌算法
    for (let i = this.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));  //随机生成一个1到5的数字
      [this[i], this[j]] = [this[j], this[i]];			//更换下标为 i 和 j的数值
    }
  }
}
let a = new SuperArray(1, 2, 3, 4, 5);
console.log(a instanceof Array); // true
console.log(a instanceof SuperArray); // true
console.log(a); // [1, 2, 3, 4, 5]
a.shuffle();
console.log(a); // [3, 1, 4, 5, 2]

  有些内置类型的方法会返回新实例。默认情况下,返回实例的类型与原始实例的类型是一致的:

class SuperArray extends Array {}
let a1 = new SuperArray(1, 2, 3, 4, 5);
let a2 = a1.filter(x => !!(x%2))     //这里的 两个感叹号 !! 通常用于将一个表达式强制转换为布尔类型
console.log(a1); // [1, 2, 3, 4, 5]
console.log(a2); // [1, 3, 5]
console.log(a1 instanceof SuperArray); // true
console.log(a2 instanceof SuperArray); // true

  如果想覆盖这个默认行为,则可以覆盖 Symbol.species 访问器,这个访问器决定在创建返回的实例时使用的类:

class SuperArray extends Array {
	static get [Symbol.species]() {
		return Array;
	}
}
let a1 = new SuperArray(1, 2, 3, 4, 5);
let a2 = a1.filter(x => !!(x%2))
console.log(a1); // [1, 2, 3, 4, 5]
console.log(a2); // [1, 3, 5]
console.log(a1 instanceof SuperArray); // true
console.log(a2 instanceof SuperArray); // false  ,这里被指定为Array类型了

4.5 多重继承(通过类混入的方式模拟)

  把不同类的行为集中到一个类是一种常见的 JavaScript 模式。虽然 ES6 没有显式支持多类继承,但通过现有特性可以轻松地模拟这种行为。

注意 Object.assign() 方法是为了混入对象行为而设计的。只有在需要混入类的行为时才有必要自己实现混入表达式。如果只是需要混入多个对象的属性,那么使用 Object.assign() 就可以了。

  在下面的代码片段中, extends 关键字后面是一个 JavaScript 表达式。任何可以解析为一个类或一个构造函数的表达式都是有效的。这个表达式会在求值类定义时被求值:

class Vehicle {}
function getParentClass() {
	console.log('evaluated expression');
	return Vehicle;
}
class Bus extends getParentClass() {}
// 可求值的表达式

  混入模式可以通过在一个表达式中连缀多个混入元素来实现,这个表达式最终会解析为一个可以被继承的类。如果 Person 类需要组合 A、B、C,则需要某种机制实现 B 继承 A,C 继承 B,而 Person 再继承 C,从而把 A、B、C 组合到这个超类中。实现这种模式有不同的策略。
  一个策略是定义一组“可嵌套”的函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类。这些组合函数可以连缀调用,最终组合成超类表达式:

class Vehicle {}
let FooMixin = (Superclass) => class extends Superclass {    //拉姆达表达式也叫匿名函数,所以本质上也是函数。	
	foo() {
		console.log('foo');
	}
};
let BarMixin = (Superclass) => class extends Superclass {
	bar() {
		console.log('bar');
	}
};
let BazMixin = (Superclass) => class extends Superclass {
	baz() {
		console.log('baz');
	}
};
class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz

  通过写一个辅助函数,可以把嵌套调用展开:

class Vehicle {}
let FooMixin = (Superclass) => class extends Superclass {
	foo() {
		console.log('foo');
	}
};
let BarMixin = (Superclass) => class extends Superclass {
	bar() {
		console.log('bar');
	}
};
let BazMixin = (Superclass) => class extends Superclass {
	baz() {
		console.log('baz');
	}
};
function mix(BaseClass, ...Mixins) {
	return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);
}
class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz

注意 很多 JavaScript 框架(特别是 React)已经抛弃混入模式,转向了组合模式(把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不使用继承)。这反映了那个众所周知的软件设计原则:“组合胜过继承(composition over inheritance)。”这个设计原则被很多人遵循,在代码设计中能提供极大的灵活性。

总结

  对象在代码执行过程中的任何时候都可以被创建和增强,具有极大的动态性,并不是严格定义的实体。下面的模式适用于创建对象。

  • 工厂模式就是一个简单的函数,这个函数可以创建对象,为它添加属性和方法,然后返回这个对象。这个模式在构造函数模式出现后就很少用了。
  • 使用构造函数模式可以自定义引用类型,可以使用 new 关键字像创建内置类型实例一样创建自定义类型的实例。不过,构造函数模式也有不足,主要是其成员无法重用,包括函数。考虑到函数本身是松散的、弱类型的,没有理由让函数不能在多个对象实例间共享。
  • 原型模式解决了成员共享的问题,只要是添加到构造函数 prototype 上的属性和方法就可以共享。而组合构造函数和原型模式通过构造函数定义实例属性,通过原型定义共享的属性和方法。

  JavaScript 的继承主要通过原型链来实现。原型链涉及把构造函数的原型赋值为另一个类型的实例。这样一来,子类就可以访问父类的所有属性和方法,就像基于类的继承那样。原型链的问题是所有继承的属性和方法都会在对象实例间共享,无法做到实例私有。盗用构造函数模式通过在子类构造函数中调用父类构造函数,可以避免这个问题。这样可以让每个实例继承的属性都是私有的,但要求类型只能通过构造函数模式来定义(因为子类不能访问父类原型上的方法)。目前最流行的继承模式是组合继承,即通过原型链继承共享的属性和方法,通过盗用构造函数继承实例属性。
  除上述模式之外,还有以下几种继承模式。

  • 原型式继承可以无须明确定义构造函数而实现继承,本质上是对给定对象执行浅复制。这种操 作的结果之后还可以再进一步增强。
  • 与原型式继承紧密相关的是寄生式继承,即先基于一个对象创建一个新对象,然后再增强这个 新对象,最后返回新对象。这个模式也被用在组合继承中,用于避免重复调用父类构造函数导 致的浪费。
  • 寄生组合继承被认为是实现基于类型继承的最有效方式。

  ECMAScript 6 新增的类很大程度上是基于既有原型机制的语法糖。类的语法让开发者可以优雅地定义向后兼容的类,既可以继承内置类型,也可以继承自定义类型。类有效地跨越了对象实例、对象原型和对象类之间的鸿沟。


  1. 实际开发中,如果只有一个实例化对象,直接字面量就行了。用不着使用立即实例化 ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2151934.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

我与Linux的爱恋:命令行参数|环境变量

​ ​ 🔥个人主页:guoguoqiang. 🔥专栏:Linux的学习 文章目录 一.命令行参数二.环境变量1.环境变量的基本概念2.查看环境变量的方法3.环境变量相关命令 一.命令行参数 【示例1】main函数也是函数,main函数可以带参…

Paragon NTFS for Mac和Tuxera NTFS for Mac,那么两种工具有什么区别呢?

我们在使用Mac系统读取U盘的过程中往往会遇到一个问题,那就是U盘插进电脑无法显示,或者只能读取不能编辑。出现这种情况的原因就一般是格式错误。 很多小伙伴在解决这种问题的时候会选择使用U盘读写工具,那么哪一种读写工具比较好呢&#xf…

Windows下利用MSYS2和VS的nmake编译nginx源码

目录 一、使用说明 二、安装软件 2.1 下载依赖库 2.3 下载并安装 StrawberryPerl 2.4 下载并安装 MSYS 2 2.5 nginx源代码下载 三、编译配置 3.1 设置NGX_MSVC_VER 3.2 配置 Makefile 3.3 编译代码 3.4 整理Nginx发布环境 四、错误处理 一、使用说明 本文章主要记…

Hash入门

unordered_set void test_unordered_set() {unordered_set<int> us;us.insert(4);us.insert(2);us.insert(1);us.insert(5);us.insert(6);us.insert(2);us.insert(2);//去重unordered_set<int>::iterator it us.begin();while (it ! us.end()){cout << *it…

​OpenAI最强模型o1系列:开启人工智能推理新时代

前不久OpenAI发布全新模型——o1模型&#xff0c;也就是业界说的“草莓模型”&#xff0c;包含三款型号&#xff1a;OpenAI o1、OpenAI o1-preview和OpenAI o1-mini。 其中&#xff0c;OpenAI o1-mini和 o1-preview已经对用户开放使用&#xff1a; OpenAI o1&#xff1a;高级推…

mysql笔记—sql性能分析

1.查看数据库各个语句的执行频次 show global/session status like ‘com__’ 2.慢查询 默认没有开启&#xff0c;需要手动开启&#xff08;在/etc/my.cnf中开启&#xff09; 开启后在localhost-slow.log中可以查询到慢查询的语句的相关信息&#xff1a; 3.explain 用法&…

<<编码>> 第 16 章 存储器组织(1)--比特锁存器 示例电路

1 比特锁存器 info::操作说明 鼠标单击逻辑输入切换 0|1 状态 就是前面的电平触发的 D 型锁存器. 写入(Write) 就是时钟信号 primary::在线交互操作链接 https://cc.xiaogd.net/?startCircuitLinkhttps://book.xiaogd.net/code-hlchs-examples/assets/circuit/code-hlchs-ch16…

2025年最新大数据毕业设计选题-Hadoop综合项目

选题思路 回忆学过的知识(Python、Java、Hadoop、Hive、Sqoop、Spark、算法等等。。。) 结合学过的知识确定大的方向 a. 确定技术方向&#xff0c;比如基于Hadoop、基于Hive、基于Spark 等等。。。 b. 确定业务方向&#xff0c;比如民宿分析、电商行为分析、天气分析等等。。。…

OpenCV特征检测(6)对初步检测到的角点位置进行亚像素级别的精炼函数cornerSubPix()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 细化角点的位置。 该函数迭代以找到角点或径向鞍点的亚像素级准确位置&#xff0c;如 93中所述&#xff0c;并如下图所示。 亚像素级准确的角点…

TryHackMe 第2天 | Pre Security (上)

该学习路径讲解了网络安全入门的必备技术知识&#xff0c;比如计算机网络、网络协议、Linux命令、Windows设置等内容。本篇博客将记录第一项&#xff1a;计算机网络。 Network Fundamentals What is networking? 网络就是相互连接的事物&#xff0c;我们的人际关系也可以抽…

Liveweb视频汇聚平台支持GB28181转RTMP、HLS、RTSP、FLV格式播放方案

GB28181协议凭借其在安防流媒体行业独有的大统一地位&#xff0c;目前已经在各种安防项目上使用。雪亮工程、幼儿园监控、智慧工地、物流监控等等项目上目前都需要接入安防摄像头或平台进行直播、回放。而GB28181协议作为国家推荐标准&#xff0c;目前基本所有厂家的安防摄像头…

[Unity Demo]从零开始制作空洞骑士Hollow Knight第六集:制作小骑士完整的跳跃落地行为

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、制作一个完整的小骑士跳跃落地行为 1.制作动画以及UNITY编辑器编辑2.使用代码实现完整的跳跃落地行为控制3.更多要考虑到的点总结 前言 大家好久不见&…

【CSS Tricks】如何做一个粒子效果的logo

效果展示 代码展示 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>粒子效果Logo</title>…

VUE面试题(单页应用及其首屏加载速度慢的问题)

目录 一、单页应用 1.概念 2.单页面应用的优缺点 二、多页面应用&#xff1a; 1.概念 2.区别 三、SPA的实现 1.原理 2.方式&#xff1a; 3.Hash与History模式有什么区别 四、首屏加载速度慢如何优化 1.什么是首屏加载&#xff1f; 2.首屏加载慢的原因 3.如何解决…

OpenCV特征检测(2)边缘检测函数Canny()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 使用 Canny 算法 48在图像中查找边缘。 该函数使用 Canny 算法在输入图像中查找边缘&#xff0c;并在输出地图 edges 中标记它们。在 threshold1…

【0332】Postgres内核 start background worker(s)

0. 相关联文件 postmaster.cilist.h1. 启动 background worker(s) Postgres内核在 PostmasterMain() 函数中初始化 postmaster 守护进程时候,通过 maybe_start_bgworkers() 函数开始尝试启动 background worker(s)。 若时机恰当,则启动 background worker(s)。 作为一种附带…

基于云的补丁管理

什么是云补丁 云补丁或基于云的补丁管理是指扫描和检测缺失补丁、测试补丁并将它们部署到所需系统的过程&#xff0c;所有这些都通过基于云的控制台或软件完成。虽然补丁管理工作流程通常保持不变&#xff0c;但基于云的补丁管理的主要区别在于&#xff0c;整个过程仅通过基于…

iOS 18 适配 Xcode 16 问题

在适配 iOS 18 xcode 16时遇到的问题&#xff0c;记录一下。 1. 使用xcode 16 iOS 18 运行App时遇到&#xff0c;APP 的icon 出现空白现象。 原先APP icon 设置方案。 暂时解决方案&#xff1a; 2、

Python 低层多线程接口_thread的用法

_thread是python标准库中的一个低层多线程API&#xff0c;可以在进程中启动线程来处理任务&#xff0c;并且提供了简单的锁机制来控制共享资源的同步访问。本文就_thread模块的用法和特性做个简单的演示。 文章目录 一、进程和线程的区别二、_thread模块的用法2.1 派生线程2.2…

Percona发布开源DBaaS平台;阿里云RDS发布全球多活数据库(GAD);Redshift支持自然语言生成SQL

重要更新 1. 云栖大会于本周四/五在杭州举行&#xff0c;周五上午云栖主论坛阿里云数据库负责人李飞飞将发表《从数据到智能&#xff1a;DataAI驱动的云原生数据库》演讲&#xff0c;另外&#xff0c;还有多场次的数据库专场&#xff0c;感兴趣的可以现场或在线观看&#xff1a…