• 封装(Encapsulation)
    • 将对数据的操作细节隐藏起来只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象“低耦合,高内聚”
    • 类也是一个函数,把实现一个功能的代码进行封装,以此实现“低耦合,高内聚”
  • 继承(Inheritance):
    • 子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
  • 多态(Polymorphism):
    • 也叫重载:方法名相同,参数类型或个数不同,这样会认为是多个方法)JS 是不存在重载的
    • 继承而产生了相关的不同的类,传入参数不同,有不同的功能 【子类重写父类的方法】

多态:重载、重写

  1. 重写:子类重写父类上的方法(伴随继承运行的)
  2. 重载:相同的方法,由于参数/返回值不一样,具备不同功能JS不具备严格意义上的重载
  3. js中的重载:同一个方法内,根据传参不同实现不同功能

    多态背后的思想就是想‘做什么’和‘谁去做’分离开来,也就是将‘不变的事物’和‘可变的事物’分离开来 以上的例子中可以说明,动物都会叫,这是不变的。而不同的动物怎么叫是可变的 把不变的隔离开来,把可变的封装起来,这给予了我们扩展代码的能力,不管有多少只动物,都可以通过增加代码的方式来实现,而无需改变产生行为的代码

一、原型链继承—-父所有变子公有

核⼼:将⽗类实例作为⼦类原型
优点:⽅法复⽤
由于⽅法定义在⽗类的原型上,复⽤了⽗类构造函数的⽅法。⽐如say⽅法。
缺点:

  1. 创建⼦类实例的时候,不能传⽗类的参数(⽐如name)。
  2. ⼦类实例共享了⽗类构造函数的引⽤属性,⽐如arr属性。(如果一个实例更改了原型属性,其它实例的原型也会被修改)
  3. 继承单一,⽆法实现多继承。
  1. function Parent(name) {
  2. this.name = name || '⽗亲'; // 实例基本属性 (该属性,强调私有,不共享)
  3. this.arr = [1]; // (该属性,强调私有)
  4. }
  5. Parent.prototype.say = function() { // -- 将需要复⽤、共享的⽅法定义在⽗类原型上
  6. console.log('hello')
  7. }
  8. function Child(like) {
  9. this.like = like;
  10. }
  11. Child.prototype = new Parent() // 核⼼,但此时Child.prototype.constructor==Parent
  12. Child.prototype.constructor = Child // 修正constructor指向
  13. let boy1 = new Child()
  14. let boy2 = new Child()
  15. // 优点:共享了⽗类构造函数的say⽅法
  16. console.log(boy1.say(), boy2.say(), boy1.say === boy2.say); // hello , hello , true
  17. // 缺点1:不能向⽗类构造函数传参
  18. console.log(boy1.name, boy2.name, boy1.name===boy2.name); // ⽗亲,⽗亲,true
  19. // 缺点2: ⼦类实例共享了⽗类构造函数的引⽤属性,⽐如arr属性
  20. boy1.arr.push(2); //改变原数组的方法
  21. // 修改了boy1的arr属性,boy2的arr属性,也会变化,因为两个实例的原型上(Child.prototype)有了⽗类构造函数的实例属性arr;
  22. 所以只要修改了boy1.arrboy2.arr的属性也会变化。
  23. console.log(boy2.arr); // [1,2]
  24. 注意1:修改boy1name属性,是不会影响到boy2.name。因为设置boy1.name相当于在⼦类实例新增了name属性。
  25. 注意2
  26. console.log(boy1.constructor); // Parent 你会发现实例的构造函数居然是Parent。
  27. ⽽实际上,我们希望⼦类实例的构造函数是Child,所以要记得修复构造函数指向。
  28. 修复如下:Child.prototype.constructor = Child;

二、构造函数继承

核⼼:借⽤⽗类的构造函数来增强⼦类实例,等于是复制⽗类的实例属性给⼦类。
优点:实例之间独⽴。

  1. 创建⼦类实例,可以向⽗类构造函数传参数。
  2. ⼦类实例不共享⽗类构造函数的引⽤属性。如arr属性
  3. 可实现多继承(通过多个call或者apply继承多个⽗类)

缺点:

  1. ⽗类的⽅法不能复⽤
  2. 由于⽅法在⽗构造函数中定义,导致⽅法不能复⽤(因为每次创建⼦类实例都要创建⼀遍⽅法)。⽐如say⽅法。(⽅法应该要复⽤、共享)
  3. ⼦类实例,继承不了⽗类原型上的属性。(因为没有⽤到原型)
  1. function Parent(name) {
  2. this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
  3. this.arr = [1]; // (该属性,强调私有)
  4. this.say = function() { // 实例引⽤属性 (该属性,强调复⽤,需要共享)
  5. console.log('hello')
  6. }
  7. }
  8. function Child(name,like) {
  9. Parent.call(this,name); // 核⼼ 拷⻉了⽗类的实例属性和⽅法
  10. this.like = like;
  11. }
  12. let boy1 = new Child('⼩红','apple');
  13. let boy2 = new Child('⼩明', 'orange ');
  14. // 优点1:可向⽗类构造函数传参
  15. console.log(boy1.name, boy2.name); // ⼩红, ⼩明
  16. // 优点2:不共享⽗类构造函数的引⽤属性
  17. boy1.arr.push(2);
  18. console.log(boy1.arr,boy2.arr);// [1,2] [1]详解js继承 4
  19. // 缺点1:⽅法不能复⽤
  20. console.log(boy1.say === boy2.say) // false (说明,boy1和boy2的say⽅法是独⽴,不是共享的)
  21. // 缺点2:不能继承⽗类原型上的⽅法
  22. Parent.prototype.walk = function () { // 在⽗类的原型对象上定义⼀个walk⽅法。
  23. console.log('我会⾛路')
  24. }
  25. boy1.walk; // undefined (说明实例,不能获得⽗类原型上的⽅法)

三、组合继承(组合原型链和构造函数继承)

核⼼:通过调⽤⽗类构造函数,继承⽗类的属性并保留传参的优点;然后通过将⽗类实例作为
⼦类原型,实现函数复⽤。
优点:

  1. 保留构造函数的优点:创建⼦类实例,可以向⽗类构造函数传参数。
  2. 保留原型链的优点:⽗类的⽅法定义在⽗类的原型对象上,可以实现⽅法复⽤。
  3. 不共享⽗类的引⽤属性。⽐如arr属性

缺点:

  1. 由于调⽤了2次⽗类的构造⽅法(耗内存),会存在⼀份多余的⽗类实例属性,具体原因⻅⽂末。

    注意:’组合继承’这种⽅式,要记得修复Child.prototype.constructor指向 第⼀次Parent.call(this);从⽗类拷⻉⼀份⽗类实例属性,作为⼦类的实例属性,第⼆次Child.prototype = new Parent();创建⽗类实例作为⼦类原型,Child.protype中的⽗类属性和⽅法 会被第⼀次拷⻉来的实例属性屏蔽掉,所以多余。

  1. function Parent(name) {
  2. this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
  3. this.arr = [1]; // (该属性,强调私有)
  4. }
  5. Parent.prototype.say = function() { // --- 将需要复⽤、共享的⽅法定义在⽗类原型上
  6. console.log('hello')
  7. }
  8. function Child(name,like) {
  9. Parent.call(this,name,like) // 核⼼ 第⼆次
  10. this.like = like;
  11. }
  12. Child.prototype = new Parent() // 核⼼ 第⼀次
  13. Child.prototype.constructor = Child // 修正constructor指向
  14. let boy1 = new Child('⼩红','apple')
  15. let boy2 = new Child('⼩明','orange')
  16. // 优点1:可以向⽗类构造函数传参数
  17. console.log(boy1.name,boy1.like); // ⼩红,apple
  18. // 优点2:可复⽤⽗类原型上的⽅法
  19. console.log(boy1.say === boy2.say) // true详解js继承 5
  20. // 优点3:不共享⽗类的引⽤属性,如arr属性
  21. boy1.arr.push(2)
  22. console.log(boy1.arr,boy2.arr); // [1,2] [1] 可以看出没有共享arr属性。
  23. // 缺点1:由于调⽤了2次⽗类的构造⽅法,会存在⼀份多余的⽗类实例属性
  24. 其实Child.prototype = new Parent()
  25. console.log(Child.prototype.__proto__ === Parent.prototype); // true
  26. 因为Child.prototype等于Parent的实例,所以__proto__指向Parent.prototype

四、组合继承优化1

核⼼:
通过这种⽅式,砍掉⽗类的实例属性,这样在调⽤⽗类的构造函数的时候,就不会初始化两次实
例,避免组合继承的缺点。
优点:

  1. 只调⽤⼀次⽗类构造函数。
  2. 保留构造函数的优点:创建⼦类实例,可以向⽗类构造函数传参数。
  3. 保留原型链的优点:⽗类的实例⽅法定义在⽗类的原型对象上,可以实现⽅法复⽤。

缺点:
修正构造函数的指向之后,⽗类实例的构造函数指向,同时也发⽣变化(这是我们不希望的)
注意:’组合继承优化1’这种⽅式,要记得修复Child.prototype.constructor指向
原因是:不能判断⼦类实例的直接构造函数,到底是⼦类构造函数还是⽗类构造函数。

  1. function Parent(name) {
  2. this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
  3. this.arr = [1]; // (该属性,强调私有)
  4. }
  5. Parent.prototype.say = function() { // --- 将需要复⽤、共享的⽅法定义在⽗类原型上
  6. console.log('hello')
  7. }
  8. function Child(name,like) {
  9. Parent.call(this,name,like) // 核⼼
  10. this.like = like;
  11. }
  12. Child.prototype = Parent.prototype // 核⼼ ⼦类原型和⽗类原型,实质上是同⼀个
  13. <!--这⾥是修复构造函数指向的代码-->
  14. Child.prototype.constructor = Child详解js继承 6
  15. let boy1 = new Child('⼩红','apple')
  16. let boy2 = new Child('⼩明','orange')
  17. let p1 = new Parent('⼩爸爸')
  18. // 优点1:可以向⽗类构造函数传参数
  19. console.log(boy1.name,boy1.like); // ⼩红,apple
  20. // 优点2:可复⽤⽗类原型上的⽅法
  21. console.log(boy1.say === boy2.say) // true
  22. // 缺点1:当修复⼦类构造函数的指向后,⽗类实例的构造函数指向也会跟着变了。
  23. 没修复之前:console.log(boy1.constructor); // Parent
  24. 修复代码:Child.prototype.constructor = Child
  25. 修复之后:console.log(boy1.constructor); // Child
  26. console.log(p1.constructor);// Child 这⾥就是存在的问题(我们希望是Parent)
  27. 具体原因:因为是通过原型来实现继承的,Child.prototype的上⾯是没有constructor属性的,
  28. 就会往上找,这样就找到了Parent.prototype上⾯的constructor属性;当你修改了⼦类实例的
  29. construtor属性,所有的constructor的指向都会发⽣变化。

五、寄⽣组合继承 —- 完美⽅式

  1. function Parent(name) {
  2. this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
  3. this.arr = [1]; // (该属性,强调私有)
  4. }
  5. Parent.prototype.say = function() { // --- 将需要复⽤、共享的⽅法定义在⽗类原型上
  6. console.log('hello')
  7. }
  8. function Child(name,like) {
  9. Parent.call(this,name,like) // 核⼼ call方法继承作用是将父级的私有的方法都复制到子级中 (因为this已经是子级boy1/boy2,所以 this.name = boy.name)
  10. this.like = like;
  11. }
  12. // 核⼼ 通过创建中间对象,⼦类原型和⽗类原型,就会隔离开。不是同⼀个啦,有效避免了⽅式4的缺点。
  13. Child.prototype = Object.create(Parent.prototype) // Object.create创建一个新对象 作为原型 这样就不会公用父级的原型
  14. // 这⾥是修复构造函数指向的代码
  15. Child.prototype.constructor = Child
  16. let boy1 = new Child('⼩红','apple')
  17. let boy2 = new Child('⼩明','orange')
  18. let p1 = new Parent('⼩爸爸')
  19. 注意:这种⽅法也要修复构造函数的
  20. 修复代码:Child.prototype.constructor = Child