js面向对象/封装/继承/多态理解

3,005 阅读15分钟

面向对象

面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。

面向对象和面向过程的区别

面向对象和面向过程是两种不同的编程思想,我们经常会听到两者的比较,刚开始编程的时候,大部分应该都是使用的面向过程的编程,但是随着我们的成长,还是面向对象的编程思想比较好一点~

其实面向对象和面向过程并不是完全相对的,也并不是完全独立的。我认为面向对象和面向过程的主要区别是面向过程主要是以动词为主,解决问题的方式是按照顺序一步一步调用不同的函数。而面向对象主要是以名词为主,将问题抽象出具体的对象,而这个对象有自己的属性和方法,在解决问题的时候是将不同的对象组合在一起使用。所以说面向对象的好处就是可扩展性更强一些,解决了代码重用性的问题。

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了,函数式编程就是典型的面向过程编程。 面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。

封装

面向对象有三大特性,封装、继承和多态。对于ES5来说,没有class的概念,并且由于js的函数级作用域(在函数内部的变量在函数外访问不到),所以我们就可以模拟 class的概念,在es5中,类其实就是保存了一个函数的变量,这个函数有自己的属性和方法。将属性和方法组成一个类的过程就是封装

封装:把客观事物封装成抽象的类,隐藏属性和方法的实现细节,仅对外公开接口。

通过构造函数添加

javascript提供了一个构造函数(Constructor)模式,用来在创建对象时初始化对象。构造函数其实就是普通的函数,只不过有以下的特点

  • 首字母大写(建议构造函数首字母大写,即使用大驼峰命名,非构造函数首字母小写)
  • 内部使用this
  • 使用 new生成实例

通过构造函数添加属性和方法实际上也就是通过this添加的属性和方法。因为this总是指向当前对象的,所以通过this添加的属性和方法只在当前对象上添加,是该对象自身拥有的。所以我们实例化一个新对象的时候,this指向的属性和方法都会得到相应的创建,也就是会在内存中复制一份,这样就造成了内存的浪费。

function Cat(name,color){
        this.name = name;
        this.color = color;
        this.eat = function () {
            alert('吃老鼠')
        }
    }

生成实例:

var cat1 = new Cat('tom','red')

通过this定义的属性和方法,我们实例化对象的时候都会重新复制一份

通过原型prototype

在类上通过 this的方式添加属性和对象会导致内存浪费的问题,我们就考虑,有什么方法可以让实例化的类所使用的方法直接使用指针指向同一个方法。于是,就想到了原型的方式。

Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。 也就是说,对于那些不变的属性和方法,我们可以直接将其添加在类的prototype 对象上。

   function Cat(name,color){
    this.name = name;
    this.color = color;
  }
  Cat.prototype.type = "猫科动物";
  Cat.prototype.eat = function(){alert("吃老鼠")};

然后生成实例

    var cat1 = new Cat("大毛","黄色");
  var cat2 = new Cat("二毛","黑色");
  alert(cat1.type); // 猫科动物
  cat1.eat(); // 吃老鼠

这时所有实例的type属性和eat()方法,其实都是同一个内存地址,指向prototype对象,因此就提高了运行效率。

在类的外部通过.语法添加

我们还可以在类的外部通过. 语法进行添加,因为在实例化对象的时候,并不会执行到在类外部通过. 语法添加的属性,所以实例化之后的对象是不能访问到. 语法所添加的对象和属性的,只能通过该类访问。

三者的区别

通过构造函数、原型和. 语法三者都可以在类上添加属性和方法。但是三者是有一定的区别的。

  • 构造函数:通过this添加的属性和方法总是指向当前对象的,所以在实例化的时候,通过this添加的属性和方法都会在内存中复制一份,这样就会造成内存的浪费。但是这样创建的好处是即使改变了某一个对象的属性或方法,不会影响其他的对象(因为每一个对象都是复制的一份)。
  • 原型:通过原型继承的方法并不是自身的,我们要在原型链上一层一层的查找,这样创建的好处是只在内存中创建一次,实例化的对象都会指向这个prototype 对象,但是这样做也有弊端,因为实例化的对象的原型都是指向同一内存地址,改动其中的一个对象的属性可能会影响到其他的对象
  • . 语法:在类的外部通过. 语法创建的属性和方法只会创建一次,但是这样创建的实例化的对象是访问不到的,只能通过类的自身访问

javascript也有private public protected

对于java程序员来说private public protected这三个关键字应该是很熟悉的哈,但是在js中,并没有类似于private public protected这样的关键字,但是我们又希望我们定义的属性和方法有一定的访问限制,于是我们就可以模拟private public protected这些访问权限。不熟悉java的小伙伴可能不太清楚private public protected概念(其他语言我也不清楚有没有哈,但是应该都是类似的~),先来科普一下小知识点~

  • public:public表明该数据成员、成员函数是对所有用户开放的,所有用户都可以直接进行调用
  • private:private表示私有,私有的意思就是除了class自己之外,任何人都不可以直接使用,私有财产神圣不可侵犯嘛,即便是子女,朋友,都不可以使用。
  • protected:protected对于子女、朋友来说,就是public的,可以自由使用,没有任何限制,而对于其他的外部class,protected就变成private。

js中的private

因为javascript函数级作用域的特性(在函数中定义的属性和方法外界访问不到),所以我们在函数内部直接定义的属性和方法都是私有的。

js中的public

通过new关键词实例化时,this定义的属性和变量都会被复制一遍,所以通过this定义的属性和方法就是公有的。通过prototype创建的属性在类的实例化之后类的实例化对象也是可以访问到的,所以也是公有的。

js中的protected

在函数的内部,我们可以通过this定义的方法访问到一些类的私有属性和方法,在实例化的时候就可以初始化对象的一些属性了。

new的实质

var o = new Object() // 新建一个对象o

o. proto = Object.prototype

将新创建的对象的__proto__属性指向构造函数的prototype

将this指向新创建的对象

返回新对象,但是这里需要看构造函数有没有返回值,如果构造函数的返回值为基本数据类型string,boolean,number,null,undefined,那么就返回新对象,如果构造函数的返回值为对象类型,那么就返回这个对象类型。

  • 示例
var Book = function (id, name, price) {
        //private(在函数内部定义,函数外部访问不到,实例化之后实例化的对象访问不到)
        var num = 1;
        var id = id;
        function checkId() {
            console.log('private')
        }
        //protected(可以访问到函数内部的私有属性和私有方法,在实例化之后就可以对实例化的类进行初始化拿到函数的私有属性)
        this.getName = function () {
            console.log(id)
        }
        this.getPrice = function () {
            console.log(price)
        }

        //public(实例化的之后,实例化的对象就可以访问到了~)
        this.name = name;
        this.copy = function () {
            console.log('this is public')
        }

    }

    //在Book的原型上添加的方法实例化之后可以被实例化对象继承
    Book.prototype.proFunction = function () {
        console.log('this is proFunction')
    }

    //在函数外部通过.语法创建的属性和方法,只能通过该类访问,实例化对象访问不到
    Book.setTime = function () {
        console.log('this is new time')
    }
    var book1 = new Book('111','悲惨世界','$99')
    book1.getName();        // 111 getName是protected,可以访问到类的私有属性,所以实例化之后也可以访问到函数的私有属性
    book1.checkId();        //报错book1.checkId is not a function
    console.log(book1.id)   // undefined id是在函数内部通过定义的,是私有属性,所以实例化对象访问不到
    console.log(book1.name) //name 是通过this创建的,所以在实例化的时候会在book1中复制一遍name属性,所以可以访问到
    book1.copy()            //this is public
    book1.proFunction();    //this is proFunction
    Book.setTime();         //this is new time
    book1.setTime();        //报错book1.setTime is not a function

继承

继承:子类可以使用父类的所有功能,并且对这些功能进行扩展。继承的过程,就是从一般到特殊的过程。

其实继承都是基于以上封装方法的三个特性来实现的。

类式继承

所谓的类式继承就是使用的原型的方式,将方法添加在父类的原型上,然后子类的原型是父类的一个实例化对象。

//声明父类
    var SuperClass = function () {
        var id = 1;
        this.name = ['javascript'];
        this.superValue = function () {
            console.log('superValue is true');
            console.log(id)
        }
    };

    //为父类添加共有方法
    SuperClass.prototype.getSuperValue = function () {
        return this.superValue();
    };

    //声明子类
    var SubClass = function () {
        this.subValue = function () {
            console.log('this is subValue ')
        }
    };

    //继承父类
    SubClass.prototype = new SuperClass() ;

    //为子类添加共有方法
    SubClass.prototype.getSubValue= function () {
        return this.subValue()
    };

    var sub = new SubClass();
    var sub2 =  new  SubClass();

    sub.getSuperValue();   //superValue is true
    sub.getSubValue();     //this is subValue

    console.log(sub.id);    //undefined
    console.log(sub.name);  //javascript

    sub.name.push('java');  //["javascript"]
    console.log(sub2.name)  //["javascript", "java"]

构造函数继承

正式因为有了上述的缺点,才有了构造函数继承,构造函数继承的核心思想就是SuperClass.call(this,id),直接改变this的指向,使通过this创建的属性和方法在子类中复制一份,因为是单独复制的,所以各个实例化的子类互不影响。但是会造成内存浪费的问题

//构造函数继承
    //声明父类
    function SuperClass(id) {
        var name = 'javascript'
        this.books=['javascript','html','css'];
        this.id = id
    }

    //声明父类原型方法
    SuperClass.prototype.showBooks = function () {
        console.log(this.books)
    }

    //声明子类
    function SubClass(id) {
        SuperClass.call(this,id)
    }

    //创建第一个子类实例
    var subclass1 = new SubClass(10);
    var subclass2 = new SubClass(11);

    console.log(subclass1.books);
    console.log(subclass2.id);
    console.log(subclass1.name);   //undefined
    subclass2.showBooks();

组合式继承

我们先来总结一下类继承和构造函数继承的优缺点

--类继承构造函数继承
核心思想子类的原型是父类实例化的对象SuperClass.call(this,id)
优点子类实例化对象的属性和方法都指向父类的原型每个实例化的子类互不影响
缺点子类之间可能会互相影响内存浪费
所以组合式继承就是汲取两者的优点,即避免了内存浪费,又使得每个实例化的子类互不影响。
//组合式继承
    //声明父类
    var SuperClass = function (name) {
        this.name = name;
        this.books=['javascript','html','css']
    };
    //声明父类原型上的方法
    SuperClass.prototype.showBooks = function () {
        console.log(this.books)
    };

    //声明子类
    var SubClass = function (name) {
        SuperClass.call(this, name)

    };

    //子类继承父类(链式继承)
    SubClass.prototype = new SuperClass();

    //实例化子类
    var subclass1 = new SubClass('java');
    var subclass2 = new SubClass('php');
    subclass2.showBooks();
    subclass1.books.push('ios');    //["javascript", "html", "css"]
    console.log(subclass1.books);  //["javascript", "html", "css", "ios"]
    console.log(subclass2.books);   //["javascript", "html", "css"]

寄生组合继承

那么问题又来了组合式继承的方法固然好,但是会导致一个问题,父类的构造函数会被创建两次(call()的时候一遍,new的时候又一遍),所以为了解决这个问题,又出现了寄生组合继承。刚刚问题的关键是父类的构造函数在类继承和构造函数继承的组合形式中被创建了两遍,但是在类继承中我们并不需要创建父类的构造函数,我们只是要子类继承父类的原型即可。所以说我们先给父类的原型创建一个副本,然后修改子类constructor属性,最后在设置子类的原型就可以了

//原型式继承
    //原型式继承其实就是类式继承的封装,实现的功能是返回一个实例,改实例的原型继承了传入的o对象
    function inheritObject(o) {
        //声明一个过渡函数对象
        function F() {}
        //过渡对象的原型继承父对象
        F.prototype = o;
        //返回一个过渡对象的实例,该实例的原型继承了父对象
        return new F();
    }
    //寄生式继承
    //寄生式继承就是对原型继承的第二次封装,使得子类的原型等于父类的原型。并且在第二次封装的过程中对继承的对象进行了扩展
    function inheritPrototype(subClass, superClass){
        //复制一份父类的原型保存在变量中,使得p的原型等于父类的原型
        var p = inheritObject(superClass.prototype);
        //修正因为重写子类原型导致子类constructor属性被修改
        p.constructor = subClass;
        //设置子类的原型
        subClass.prototype = p;
    }
    //定义父类
    var SuperClass = function (name) {
        this.name = name;
        this.books = ['javascript','html','css']
    };
    //定义父类原型方法
    SuperClass.prototype.getBooks = function () {
        console.log(this.books)
    };

    //定义子类
    var SubClass = function (name) {
        SuperClass.call(this,name)
    }

    inheritPrototype(SubClass,SuperClass);

    var subclass1 = new SubClass('php')

es6的继承方式

es6引入了class、extends、super、static(部分为ES2016标准)

class Person{
  //static sCount=0 //1
  constructor(name){
     this.name=name; 
     this.sCount++;
  }
  //实例方法 //2
  getName(){
   console.log(this.name)
  }
  static sTest(){
    console.log("static method test")
  }
}

class Man extends Person{
  constructor(name){
    super(name)//3
    this.sex="male"
  }
}
var man=new Man("Davin")
man.getName()
//man.sTest()
Man.sTest()//4
输出结果:
Davin
static method test

ES6明确规定,Class内部只有静态方法,没有静态属性,所以1处是有问题的,ES7有一个静态属性的提案,目前Babel转码器支持。熟悉java的可能对上面的代码感觉很亲切,几乎是自解释的。我们大概解释一下,按照代码中标号对应:

  1. constructor为构造函数,一个类有一个,相当于es5中构造函数标准化,负责一些初始化工作,如果没有定义,js vm会定义一个空的默认的构造函数。
  2. 实例方法,es6中可以不加"function"关键字,class内定义的所有函数都会置于该类的原型当中,所以,class本身只是一个语法糖。
  3. 构造函数中通过super()调用父类构造函数,如果有super方法,需要时构造函数中第一个执行的语句,this关键字在调用super之后才可用。
  4. 静态方法,在类定义的外部只能通过类名调用,内部可以通过this调用,并且静态函数是会被继承的。如示例中:sTest是在Person中定义的静函数,可以通过Man.sTest()直接调用。

es6和es5继承的区别

解题思路

  • ES5 的继承使用借助构造函数实现,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面。ES6 的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

  • ES6 在继承的语法上不仅继承了类的原型对象,还继承了类的静态属性和静态方法

大多数浏览器的ES5实现之中,每一个对象都有__pro‘

to__属性,指向对应的构造函数的prototype属性。Class作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

上面代码中,子类B的__proto__属性指向父类A,子类B的prototype属性的__proto__属性指向父类A的prototype属性。

这样的结果是因为,类的继承是按照下面的模式实现的:

class A {
}

class B {
}

// B的实例继承A的实例
Object.setPrototypeOf(B.prototype, A.prototype);

// B继承A的静态属性
Object.setPrototypeOf(B, A);

Object.setPrototypeOf的简单实现如下:

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

因此,就得到了上面的结果。

Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;

Object.setPrototypeOf(B, A);
// 等同于
B.__proto__ = A;

这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(__proto__属性)是父类(A);作为一个构造函数,子类(B)的原型(prototype属性)是父类的实例。

Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;

es6继承的不足

  1. 不支持静态属性(除函数)。
  2. class中不能定义私有变量和函数。class中定义的所有函数都会被放倒原型当中,都会被子类继承,而属性都会作为实例属性挂到this上。如果子类想定义一个私有的方法或定义一个private 变量,便不能直接在class花括号内定义,这真的很不方便!

总结一下,和es5相比,es6在语言层面上提供了面向对象的部分支持,虽然大多数时候只是一个语法糖,但使用起来更方便,语意化更强、更直观,同时也给javascript继承提供一个标准的方式。还有很重要的一点就是-es6支持原生对象继承。

多态

多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态。

非多态代码示例

var makeSound = function(animal) {
    if(animal instanceof Duck) {
        console.log('嘎嘎嘎');
    } else if (animal instanceof Chicken) {
        console.log('咯咯咯');
    }
}
var Duck = function(){}
var Chiken = function() {};
makeSound(new Chiken());
makeSound(new Duck());

多态的代码示例

var makeSound = function(animal) {
    animal.sound();
}

var Duck = function(){}
Duck.prototype.sound = function() {
    console.log('嘎嘎嘎')
}
var Chiken = function() {};
Chiken.prototype.sound = function() {
    console.log('咯咯咯')
}

makeSound(new Chicken());
makeSound(new Duck());