出现的目的
=============
1、类定义
声明 *
// 类声明
class Person {}
// 类表达式
const Animal = class {};
特点:
1、函数声明可以提升(在定义前提前使用),但类定义不能
2、函数受函数作用域限制,而类受块作用域限制
类和构造函数的异同 *
true
发现,和构造函数是一样的,因此class只是构造函数的语法糖
==============
2、构造函数 constructor
构造函数 *
constructor 关键字用于在类定义块内部创建类的构造函数,告诉解释器在使用 new 操作符创建类的新实例时,应该调用这个函数
这个属性不是必须的
下面代码实现了,一般情况通过函数创建对象,以及通过class的方式创建对象,对比,实际上两个方式是一模一样的。
// ============ 一般的函数 ============
function Person1(name,age){
// 1、定义属性
this.name = name
this.age = age
}
// 2、定义方法
Person1.prototype.running = function(){
console.log(this.name + "正在跑步")
}
var p1 = new Person1("yjl",18)
p1.running()
// ============ 类的构造方法 ============
class Person2 {
// 1、定义属性
constructor(name,age) { // 类构造函数,这个名字是固定的,而且不能写多个;new 创建一个实例时,会执行里面的代码
this.name = name
this.age = age
}
// 2、定义方法
// 实际也是给原型链添加方法,和上面一样的
running(){
console.log(this.name + "正在跑步")
}
}
var p2 = new Person2("yjl",18)
p2.running()
new 操作符
类构造函数与构造函数的主要区别是,调用类构造函数必须使用 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'
p.constructor();
// TypeError: Class constructor Person cannot be invoked without 'new'
// 使用对类构造函数的引用创建一个新实例
let p2 = new p.constructor();
任意地方定义
// 类可以像函数一样在任何地方定义,比如在数组中
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
立刻实例化
let p = new class Foo {
constructor(x) {
console.log(x);
}
}('bar'); // bar
console.log(p); // Foo {}
==============
3、实例
每次通过new调用类标识符时,都会执行类构造函数。
在这个函数内部,可以为新创建的实例(this)添加“自有”属性。
至于添加什么样的属性,则没有限制。
而且在每个实例的属性都不共享,都是属于他们自己的
属性和方法
class Person {
constructor(name) {
// 添加到 this 的所有内容都会存在于不同的实例上
// 通过 实例.属性 访问
this.name = name
// 通过 实例.方法() 访问
this.locate = () => console.log('instance');
}
// 在类块中定义的所有内容都会定义在类的原型上
//共享属性
// 1、通过 类名.prototype.属性 访问
// 2、通过 实例名.__proto__.属性 访问
otherName = 'human'
//共享方法
// 1、通过 类名.prototype.方法() 访问
// 2、通过 实例名.__proto__.方法() 访问
locate() {
console.log('prototype');
}
}
p.locate(); // instance
Person.prototype.locate() // prototype
访问器 get set *
可以给一个属性设置get和set方法,表示这个属性被读取时(get)执行什么,以及被设置(set)时执行什么。
类似于给对象的属性设置特性,可以查看: https://www.yuque.com/yejielin/mypn47/mkkfc6#y0Swp
class Person2 {
constructor(name,age) {
this.name = name
this.age = age
this._city = "深圳"
}
running(){
console.log(this.name + "正在跑步")
}
// 访问器(可以拦截对象属性的读取和写入的操作)
get city(){ // 访问这个属性时执行
console.log("读取了我所在的城市")
return this._city
}
set city(newCity){ // 重新设置这个属性时执行
console.log("重新设置了我所在的城市")
this._city = newCity
}
}
var p2 = new Person2("yjl",18)
p2.city = "广州" // 输出:重新设置了我所在的城市
类的静态属性方法 static *
类里面加上static,表示这个属性或方法是类私有的,只能通过 类名.属性 或 类名.方法 访问。
比如 Promise.resole( ) 、 Promise.reject( )、 Promise.all( )这些就是静态方法。
// 定义人类
class Person {
constructor(name,age) {
// 添加到 this 的所有内容都会存在于不同的实例上
this.locate = () => console.log('这个是实例上的');
this.name = name;
this.age = age;
}
// 定义在类的原型对象上
// 1、通过 实例.属性 或 实例.方法() 访问和在类外部创建
// 2、通过 类名.prototype.属性 或 类名.prototype.方法() 访问和在类外部创建
locate1() {
console.log('这个是原型上的');
}
// 定义在类本身上私有方法
// 通过 类名.方法() 访问和在类外部创建
static locate() {
console.log('这个是类的静态/私有方法');
}
// 定义在类本身上私有属性
// 通过 类名.属性 访问和在类外部创建
static name = 'human'
// 静态方法非常适合用于特定方法构造对象实例
static create(name) {
// 使用随机年龄创建并返回一个 Person 实例
return new Person(name,Math.floor(Math.random()*100));
}
}
let T = new Person('Tom');
T.locate() // 这个是实例上的
T.locate1() // 这个是原型上的
Person.locate() // 这个是类的静态/私有方法
// Person.locate1() // 报错
Person.prototype.locate1() // 这个是原型上的
console.log('Person.name:',Person.name) // Person.name: human
console.log('T.name:',T.name) // T.name: Tom
let J = Person.create('Jerry')
console.log('J.age:',J.age) // 15 随机数
迭代器与生成器
// 类定义语法支持在原型和类本身上定义生成器方法:
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的继承
JS - 面向对象编程
ES6新增类的继承。
虽然类继承使用的是新语法,但背后依旧使用的是原型链
继承基础 extends
继承任何拥有[[Construct]]和原型的对象。
这意味着不仅可以继承一个类,也可以继承普通的构造函数
class Vehicle {}
// 继承类
class Bus extends Vehicle {
// Bus类的属性和方法
}
// 或 let Bus = class extends Vehicle { // Bus类的属性和方法 }
let b = new Bus();
console.log(b instanceof Bus); // true
console.log(b instanceof Vehicle); // true
function Person() {}
// 继承普通构造函数
class Engineer extends Person {
// Engineer类的属性和方法
}
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 {}
引用父类super *
引用错误,必须在子类里,访问子类的this之前,通过super( )调用父类的构造方法;或者在return 之前,通过super( )调用父类的构造方法
(正确)
重新实现父类方法 *
如果对父类的方法不满意,可以在子类里面重新实现一个同名的。
因为父类的方法是在父类的原型对象里面,如果自己没有就去找父类的,如果自己有,就用自己的。
(1)直接重写
(2)引用父类方法再重写
当自己重写的方法,部分逻辑和父类的相同,也可以通过super来引用父类的方法,同时可以传入参数
(3)引用静态方法重写
同样是直接通过super就可以调用父类的静态方法
抽象基类
基类:就是被其他类继承的父类,或者父类的父类…
抽象:无法实例化
JS没有专门支持这种类的语法 ,但通过 new.target 也很容易实现,表示当前创建实例时的类名
// 抽象基类
class Vehicle {
constructor() {
console.log(new.target);
if (new.target === Vehicle) { // 当前创建实例时的类名
throw new Error('Vehicle 不允许实例化');
}
// 可以要求派生类必须定义某个方法,这里用if做检测,没有就报错
if (!this.foo) {
throw new Error('继承的类必须有属性foo()');
}
console.log('success!');
}
}
// 派生类
class Bus extends Vehicle {}
new Bus(); // class Bus {} 报错,检查到foo属性不存在,因此抛出异常
new Vehicle(); // class Vehicle {} 报错,new.target === Vehicle,因此抛出异常
内置对象扩展
JS内部提供了很多内置对象,比如Array,String等。
如果我想给数组加一个我通用的方法,我可以直接给Array加,但是这样就会产生问题,可能会意外重写了原本内部已经有了的方法,这样可能会导致其他问题,而且后面维护报错时完全没办法查看是哪里的问题。
因此可以创建一个自己的类,继承内置对象,然后再扩展。
但是实际开发中这样操作比较少。Java等面向对象语言用的比较多。
继承内置对象
this指的是调用new 的那个对象
类混入(理解)
JS中,类只有一个父类。
如果说我想继承多个父类,JS中只能一个一个继承。
可以写一个函数,在函数中实现继承,然后返回出去
用的不是特别多。
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))) {}
// 或者
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)。”
这个设计原则被很多人遵循,在代码设计中能提供极大的灵活性。
==============
类的模块化
文件1:
class a {
// 类a 的属性和方法
}
export {a}
文件2
import {a} from '路径/文件1.js'
let b = new a()
因此可以把类专门写在一个js文件中,通过es6模块化的方式引入到业务文件中,直接new 创建实例来使用,避免业务文件臃肿,也利于其他业务复用。
模块化可以查看
JS-模块化
babel转换成ES5代码
有的工具如webpack的babel,会把ES6的类转成ES5代码,为了可以适配IE10和更低的浏览器
这里多写了一个函数 _classCallCheck,目的是检查这个类class,不能让他可以直接被调用 Person( ),调用的话如果this不是她自己的实例(直接调用this就是window),就会抛出错误。
除去则函数,可以看到class 就是构造函数的简写