一篇文章带你彻底掌握JS面向对象

140 阅读5分钟

什么是面向对象

  • 面向对象是一种编程思想,JS 本身就是基于面向对象构建出来的(例如:JS 中有很多内置类,Promise 就是 ES6 中新增的一个内置类,我们可以基于 new Promise 来创建一个实例来管理异步编程,在开发中经常会使用到 Promise),平时开发用到的 Vue/React 也是基于面向对象构建出来的,他们都是类,使用时都是创建出他们的实例来操作的
  • JS 中的面向对象,和其他编程语言有不一样的地方,JS 中类和实例是基于原型和原型链机制来处理的。JS 中关于类的重载、重写、继承也和其他语言不太一样

特性

  • 封装:低耦合高内聚
  • 多态:重载和重写
    • 重载:方法名相同,形参个数或者类型不一样,当调用时,根据传参的不同走不同的方法(JS 有变量提升,同名函数会覆盖,所以 JS 不存在真正意义的重载,JS 的重载指的是同一个方法,根据传参不同实现不同的逻辑)
    • 重写:在类的继承中,子类可以重写父类中的方法
  • 继承:子类继承父类中的属性和方法,目的是让子类的实例能够调用父类中的属性和方法

JS 继承的方案

  1. 原型继承
    • 原理:利用原型和原型链的机制,让父类中的属性和方法在子类实例的原型链上
    • 特点:
      1. 不像其他语言的继承(其他语言的继承一般是拷贝继承,子类继承父类,会把父类中的属性和方法拷贝一份到子类中,供其子类的实例调用),JS 是把父类的原型放在子类实例的原型链上,实例想调用这些方法,是基于__proto__原型链查找机制完成的
      2. 子类可以重写父类上的方法(这样会导致父类的其他实例也受到影响)
      3. 父类中私有或共有的属性方法,最后都会变为子类的公有的属性和方法
function A(x) {
    this.x = x;
}
A.prototype.getX = function () {
    console.log(this.x);
};

function B(y) {
    this.y = y;
}
B.prototype = new A(200);
B.prototype.getY = function () {
    console.log(this.y);
};
// 保证原型重定向后的完整性
B.prototype.constructor = B;
let b1 = new B(100);
b1.getY();
b1.getX();
  1. call 继承

    • 原理:在子类的将父类当作普通函数执行,通过 call 改变 this 指向,让父类中的 this 指向子类的实例,相当于给子类的实例设置私有的属性和方法
    • 特点:
      1. 只能继承父类私有的属性和方法(原型链上的共有属性方法无法继承)
      2. 父类私有的变为子类私有的
function A(x) {
    this.x = x;
}
A.prototype.getX = function () {
    console.log(this.x);
};

function B(y) {
    // this是B的实例b1
    A.call(this, 200); // b1.x = 200
    this.y = y;
}
B.prototype.getY = function () {
    console.log(this.y);
};
let b1 = new B(100);
console.info(b1.x);
  1. 组合继承

    • 原理:利用 call 和原型继承,改变原型指向实现
    • 特点:
      1. 父类的私有属性方法变为子类的私有属性和方法,父类的公有属性方法变为子类的公有属性方法,但是调用了两次父类
      2. 父类的私有属性方法既是子类的私有属性方法,也是子类的公有属性方法
      3. 子类可以重写父类上的方法(这样会导致父类的其他实例也受到影响)
function A(x) {
    this.x = x;
}
A.prototype.getX = function () {
    console.log(this.x);
};

function B(y) {
    // this是B的实例b1
    A.call(this, 200); // b1.x = 200
    this.y = y;
}
B.prototype = new A(200);
B.prototype.constructor = B;
B.prototype.getY = function () {
    console.log(this.y);
};
let b1 = new B(100);
b1.getX();
  1. 寄生组合继承

    • 原理:利用 call 和类似原型继承的方式,改变原型指向实现(通过 Object.create 创建空对象的方式只继承父类的公有属性和方法)

    • 特点:

      1. 父类的私有属性方法变为子类的私有属性和方法,父类的公有属性方法变为子类的公有属性方法,且不用调用两次父类(推荐使用的方法)
      2. 子类可以重写父类上的方法(这样会导致父类的其他实例也受到影响)
    • 扩展:重写 Object.create

      // 重写Object.create
      Object.create = function (obj) {
          // IE浏览器不允许操作__proto__
          // let oo = {};
          // oo.__proto__ = obj;
          // return oo;
      
          function Fn() {}
          fn.prototype = obj;
          return new Fn();
      };
      
function A(x) {
    this.x = x;
}
A.prototype.getX = function () {
    console.log(this.x);
};

function B(y) {
    A.call(this, 200);
    this.y = y;
}
// Object.create(obj) 创建一个空对象,让空对象的__proto__指向obj
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;
B.prototype.getY = function () {
    console.log(this.y);
};
let b1 = new B(100);
console.info(b1);
  1. ES6 中的继承

    • 特点:
      1. ES6 中基于class关键词创造出来的类不能当作普通函数执行
      2. call 继承就不能使用了,会报错
      3. 不允许重定向原型的指向,原型继承不能使用
      4. 组合继承和寄生组合继承都不能使用
      5. ES6 中增加公有属性要在 prototype 上加
        A.prototype.name = "张三";
        
      6. ES6 中使用 extends 和 super 来实现继承),原理还是寄生组合继承的方式,只是语法不一样
        • extends => B.prototype.__proto__ = A.prototype
        • super(200) => A.call(this, 200) 把父类当作普通函数执行,把this指向变为子类的实例
        class A {
            constructor(x) {
                this.x = x;
            }
            getX() {
                console.info(this.x);
            }
        }
        // ES6中增加公有属性要通过prototype来加
        A.prototype.name = "张三";
        class B extends A {
            constructor(y) {
                super(200);
                this.y = y;
            }
            getY() {
                console.info(this.y);
            }
        }
        let b1 = new B(100);
        console.info(b1);
        
      7. 子类只要继承父类,可以不写 constructor,一旦写了,则 constructor 的第一句必须是 super(),否则会报错
        // 不写 constructor,默认会创建constructor
        constructor(...args) {
            super(...args)
        }