出现的目的

JS - 面向对象编程

image.png
image.png

=============

1、类定义

声明 *

  1. // 类声明
  2. class Person {}
  3. // 类表达式
  4. const Animal = class {};

特点:
1、函数声明可以提升(在定义前提前使用),但类定义不能
2、函数受函数作用域限制,而类受块作用域限制

类和构造函数的异同 *

image.png image.png
image.png true
发现,和构造函数是一样的,因此class只是构造函数的语法糖

==============

2、构造函数 constructor

构造函数 *

constructor 关键字用于在类定义块内部创建类的构造函数,告诉解释器在使用 new 操作符创建类的新实例时,应该调用这个函数
image.png
这个属性不是必须的

下面代码实现了,一般情况通过函数创建对象,以及通过class的方式创建对象,对比,实际上两个方式是一模一样的。

  1. // ============ 一般的函数 ============
  2. function Person1(name,age){
  3. // 1、定义属性
  4. this.name = name
  5. this.age = age
  6. }
  7. // 2、定义方法
  8. Person1.prototype.running = function(){
  9. console.log(this.name + "正在跑步")
  10. }
  11. var p1 = new Person1("yjl",18)
  12. p1.running()
  13. // ============ 类的构造方法 ============
  14. class Person2 {
  15. // 1、定义属性
  16. constructor(name,age) { // 类构造函数,这个名字是固定的,而且不能写多个;new 创建一个实例时,会执行里面的代码
  17. this.name = name
  18. this.age = age
  19. }
  20. // 2、定义方法
  21. // 实际也是给原型链添加方法,和上面一样的
  22. running(){
  23. console.log(this.name + "正在跑步")
  24. }
  25. }
  26. var p2 = new Person2("yjl",18)
  27. p2.running()

new 操作符

类构造函数与构造函数的主要区别是,调用类构造函数必须使用 new 操作符。

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

  1. function Person() {}
  2. class Animal {}
  3. // 把 window 作为 this 来构建实例
  4. let p = Person();
  5. let a = Animal();
  6. // TypeError: class constructor Animal cannot be invoked without 'new'
  7. p.constructor();
  8. // TypeError: Class constructor Person cannot be invoked without 'new'
  9. // 使用对类构造函数的引用创建一个新实例
  10. let p2 = new p.constructor();

任意地方定义

  1. // 类可以像函数一样在任何地方定义,比如在数组中
  2. let classList = [
  3. class {
  4. constructor(id) {
  5. this.id_ = id;
  6. console.log(`instance ${this.id_}`);
  7. }
  8. }
  9. ];
  10. function createInstance(classDefinition, id) {
  11. return new classDefinition(id);
  12. }
  13. let foo = createInstance(classList[0], 3141); // instance 3141

立刻实例化

  1. let p = new class Foo {
  2. constructor(x) {
  3. console.log(x);
  4. }
  5. }('bar'); // bar
  6. console.log(p); // Foo {}

==============

3、实例

每次通过new调用类标识符时,都会执行类构造函数。

在这个函数内部,可以为新创建的实例(this)添加“自有”属性。

至于添加什么样的属性,则没有限制。

而且在每个实例的属性都不共享,都是属于他们自己的

属性和方法

  1. class Person {
  2. constructor(name) {
  3. // 添加到 this 的所有内容都会存在于不同的实例上
  4. // 通过 实例.属性 访问
  5. this.name = name
  6. // 通过 实例.方法() 访问
  7. this.locate = () => console.log('instance');
  8. }
  9. // 在类块中定义的所有内容都会定义在类的原型上
  10. //共享属性
  11. // 1、通过 类名.prototype.属性 访问
  12. // 2、通过 实例名.__proto__.属性 访问
  13. otherName = 'human'
  14. //共享方法
  15. // 1、通过 类名.prototype.方法() 访问
  16. // 2、通过 实例名.__proto__.方法() 访问
  17. locate() {
  18. console.log('prototype');
  19. }
  20. }
  21. p.locate(); // instance
  22. Person.prototype.locate() // prototype

访问器 get set *

可以给一个属性设置get和set方法,表示这个属性被读取时(get)执行什么,以及被设置(set)时执行什么。

类似于给对象的属性设置特性,可以查看: https://www.yuque.com/yejielin/mypn47/mkkfc6#y0Swp

  1. class Person2 {
  2. constructor(name,age) {
  3. this.name = name
  4. this.age = age
  5. this._city = "深圳"
  6. }
  7. running(){
  8. console.log(this.name + "正在跑步")
  9. }
  10. // 访问器(可以拦截对象属性的读取和写入的操作)
  11. get city(){ // 访问这个属性时执行
  12. console.log("读取了我所在的城市")
  13. return this._city
  14. }
  15. set city(newCity){ // 重新设置这个属性时执行
  16. console.log("重新设置了我所在的城市")
  17. this._city = newCity
  18. }
  19. }
  20. var p2 = new Person2("yjl",18)
  21. p2.city = "广州" // 输出:重新设置了我所在的城市

类的静态属性方法 static *

类里面加上static,表示这个属性或方法是类私有的,只能通过 类名.属性 或 类名.方法 访问。

比如 Promise.resole( ) 、 Promise.reject( )、 Promise.all( )这些就是静态方法。

  1. // 定义人类
  2. class Person {
  3. constructor(name,age) {
  4. // 添加到 this 的所有内容都会存在于不同的实例上
  5. this.locate = () => console.log('这个是实例上的');
  6. this.name = name;
  7. this.age = age;
  8. }
  9. // 定义在类的原型对象上
  10. // 1、通过 实例.属性 或 实例.方法() 访问和在类外部创建
  11. // 2、通过 类名.prototype.属性 或 类名.prototype.方法() 访问和在类外部创建
  12. locate1() {
  13. console.log('这个是原型上的');
  14. }
  15. // 定义在类本身上私有方法
  16. // 通过 类名.方法() 访问和在类外部创建
  17. static locate() {
  18. console.log('这个是类的静态/私有方法');
  19. }
  20. // 定义在类本身上私有属性
  21. // 通过 类名.属性 访问和在类外部创建
  22. static name = 'human'
  23. // 静态方法非常适合用于特定方法构造对象实例
  24. static create(name) {
  25. // 使用随机年龄创建并返回一个 Person 实例
  26. return new Person(name,Math.floor(Math.random()*100));
  27. }
  28. }
  29. let T = new Person('Tom');
  30. T.locate() // 这个是实例上的
  31. T.locate1() // 这个是原型上的
  32. Person.locate() // 这个是类的静态/私有方法
  33. // Person.locate1() // 报错
  34. Person.prototype.locate1() // 这个是原型上的
  35. console.log('Person.name:',Person.name) // Person.name: human
  36. console.log('T.name:',T.name) // T.name: Tom
  37. let J = Person.create('Jerry')
  38. console.log('J.age:',J.age) // 15 随机数

迭代器与生成器

  1. // 类定义语法支持在原型和类本身上定义生成器方法:
  2. class Person {
  3. // 在原型上定义生成器方法
  4. *createNicknameIterator() {
  5. yield 'Jack';
  6. yield 'Jake';
  7. yield 'J-Dog';
  8. }
  9. // 在类上定义生成器方法
  10. static *createJobIterator() {
  11. yield 'Butcher';
  12. yield 'Baker';
  13. yield 'Candlestick maker';
  14. }
  15. }
  16. let jobIter = Person.createJobIterator();
  17. console.log(jobIter.next().value); // Butcher
  18. console.log(jobIter.next().value); // Baker
  19. console.log(jobIter.next().value); // Candlestick maker
  20. let p = new Person();
  21. let nicknameIter = p.createNicknameIterator();
  22. console.log(nicknameIter.next().value); // Jack
  23. console.log(nicknameIter.next().value); // Jake
  24. console.log(nicknameIter.next().value); // J-Dog
  25. // 因为支持生成器方法,所以可以通过添加一个默认的迭代器,把类实例变成可迭代对象:
  26. class Person {
  27. constructor() {
  28. this.nicknames = ['Jack', 'Jake', 'J-Dog'];
  29. }
  30. *[Symbol.iterator]() {
  31. yield *this.nicknames.entries();
  32. }
  33. }
  34. let p = new Person();
  35. for (let [idx, nickname] of p) {
  36. console.log(nickname);
  37. }
  38. // Jack
  39. // Jake
  40. // J-Dog
  41. //也可以只返回迭代器实例:
  42. class Person {
  43. constructor() {
  44. this.nicknames = ['Jack', 'Jake', 'J-Dog'];
  45. }
  46. [Symbol.iterator]() {
  47. return this.nicknames.entries();
  48. }
  49. }
  50. let p = new Person();
  51. for (let [idx, nickname] of p) {
  52. console.log(nickname);
  53. }
  54. // Jack
  55. // Jake
  56. // J-Dog

类私有属性

这个类特有的属性,外部都无法进行访问
image.png
image.pngimage.png
image.png 报错,对象的外部得不到私有属性
image.png
image.png 只有对象内部可以访问

==============

4、继承

面向对象编程里面说到ES5的继承
JS - 面向对象编程

ES6新增类的继承。

虽然类继承使用的是新语法,但背后依旧使用的是原型链

继承基础 extends

继承任何拥有[[Construct]]和原型的对象。

这意味着不仅可以继承一个类,也可以继承普通的构造函数

  1. class Vehicle {}
  2. // 继承类
  3. class Bus extends Vehicle {
  4. // Bus类的属性和方法
  5. }
  6. // 或 let Bus = class extends Vehicle { // Bus类的属性和方法 }
  7. let b = new Bus();
  8. console.log(b instanceof Bus); // true
  9. console.log(b instanceof Vehicle); // true
  10. function Person() {}
  11. // 继承普通构造函数
  12. class Engineer extends Person {
  13. // Engineer类的属性和方法
  14. }
  15. let e = new Engineer();
  16. console.log(e instanceof Engineer); // true
  17. console.log(e instanceof Person); // true

继承父类的 this

指向调用相应方法的实例或者类

  1. class Vehicle {
  2. identifyPrototype(id) {
  3. console.log(id, this);
  4. }
  5. static identifyClass(id) {
  6. console.log(id, this);
  7. }
  8. }
  9. class Bus extends Vehicle {}
  10. let v = new Vehicle();
  11. let b = new Bus();
  12. b.identifyPrototype('bus'); // bus, Bus {}
  13. v.identifyPrototype('vehicle'); // vehicle, Vehicle {}
  14. Bus.identifyClass('bus'); // bus, class Bus {}
  15. Vehicle.identifyClass('vehicle'); // vehicle, class Vehicle {}

引用父类super *

image.png
image.png image.png
image.png
引用错误,必须在子类里,访问子类的this之前,通过super( )调用父类的构造方法;或者在return 之前,通过super( )调用父类的构造方法

image.png(正确)

重新实现父类方法 *

如果对父类的方法不满意,可以在子类里面重新实现一个同名的。

因为父类的方法是在父类的原型对象里面,如果自己没有就去找父类的,如果自己有,就用自己的。

(1)直接重写
image.png image.png

(2)引用父类方法再重写
当自己重写的方法,部分逻辑和父类的相同,也可以通过super来引用父类的方法,同时可以传入参数
image.png image.png

(3)引用静态方法重写
同样是直接通过super就可以调用父类的静态方法
image.png image.png

抽象基类

基类:就是被其他类继承的父类,或者父类的父类…
抽象:无法实例化

JS没有专门支持这种类的语法 ,但通过 new.target 也很容易实现,表示当前创建实例时的类名

  1. // 抽象基类
  2. class Vehicle {
  3. constructor() {
  4. console.log(new.target);
  5. if (new.target === Vehicle) { // 当前创建实例时的类名
  6. throw new Error('Vehicle 不允许实例化');
  7. }
  8. // 可以要求派生类必须定义某个方法,这里用if做检测,没有就报错
  9. if (!this.foo) {
  10. throw new Error('继承的类必须有属性foo()');
  11. }
  12. console.log('success!');
  13. }
  14. }
  15. // 派生类
  16. class Bus extends Vehicle {}
  17. new Bus(); // class Bus {} 报错,检查到foo属性不存在,因此抛出异常
  18. new Vehicle(); // class Vehicle {} 报错,new.target === Vehicle,因此抛出异常

内置对象扩展

JS内部提供了很多内置对象,比如Array,String等。

如果我想给数组加一个我通用的方法,我可以直接给Array加,但是这样就会产生问题,可能会意外重写了原本内部已经有了的方法,这样可能会导致其他问题,而且后面维护报错时完全没办法查看是哪里的问题。

因此可以创建一个自己的类,继承内置对象,然后再扩展。

但是实际开发中这样操作比较少。Java等面向对象语言用的比较多。

继承内置对象

this指的是调用new 的那个对象
image.png image.png

类混入(理解)

JS中,类只有一个父类。
image.png
如果说我想继承多个父类,JS中只能一个一个继承。

可以写一个函数,在函数中实现继承,然后返回出去
image.png
用的不是特别多。

  1. class Vehicle {}
  2. let FooMixin = (Superclass) => class extends Superclass {
  3. foo() {
  4. console.log('foo');
  5. }
  6. };
  7. let BarMixin = (Superclass) => class extends Superclass {
  8. bar() {
  9. console.log('bar');
  10. }
  11. };
  12. let BazMixin = (Superclass) => class extends Superclass {
  13. baz() {
  14. console.log('baz');
  15. }
  16. };
  17. class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {}
  18. // 或者
  19. function mix(BaseClass, ...Mixins) {
  20. return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);
  21. }
  22. class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}
  23. let b = new Bus();
  24. b.foo(); // foo
  25. b.bar(); // bar
  26. b.baz(); // baz

很多 JavaScript 框架(特别是 React)已经抛弃混入模式,转向了组合模式(把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不使用继承)。

这反映了那个众所周知的软件设计原则:“组合胜过继承(composition over inheritance)。”

这个设计原则被很多人遵循,在代码设计中能提供极大的灵活性。

==============

类的模块化

文件1:

  1. class a {
  2. // 类a 的属性和方法
  3. }
  4. export {a}

文件2

  1. import {a} from '路径/文件1.js'
  2. let b = new a()

因此可以把类专门写在一个js文件中,通过es6模块化的方式引入到业务文件中,直接new 创建实例来使用,避免业务文件臃肿,也利于其他业务复用。

模块化可以查看
JS-模块化

babel转换成ES5代码

有的工具如webpack的babel,会把ES6的类转成ES5代码,为了可以适配IE10和更低的浏览器
image.png
这里多写了一个函数 _classCallCheck,目的是检查这个类class,不能让他可以直接被调用 Person( ),调用的话如果this不是她自己的实例(直接调用this就是window),就会抛出错误。

除去则函数,可以看到class 就是构造函数的简写