javascript 面向对象编程

JavaScript是不是一门严格意义上的面向对象的语言,它并没有提供类的方法。它是使用 原型继承 而不是类继承达到面向对象的效果。(在 ES2015/ES6 中引入了class关键字,但只是语法糖,JavaScript 仍然是基于原型的)。

什么是面向对象

面向对象语言的三大特性:

  • 封装(Encapsulation): 把相关的信息(无论数据或方法)存储在对象中的能力
  • 继承(Inheritance): 由另一个类(或多个类)得来类的属性和方法的能力
  • 多态(Polymorphism): 编写能以多种方法运行的函数或方法的能力

其他的一些内容:

  • 类(Class): 定义了一件事物的抽象特点, 用来构造对象
  • 对象(Object): 类的实例化
  • 属性(Property): 对象具有的数据
  • 方法(Method): 也成消息,用于对象之间传递数据

封装(encapsulation )

对于ES5来说,没有class的概念,并且由于js的函数级作用域(在函数内部的变量在函数外访问不到),所以我们就可以模拟 class 的概念,在es5中,类其实就是保存了一个函数的变量,这个函数有自己的属性和方法。如果我们要把”属性”(property)和”方法”(method),封装成一个对象,甚至要从原型对象生成一个实例对象,我们就需要对对象进行封装。

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

创建对象的方式

以下封装的实例对象,均以Person为例,包含 name 这个基本属性和 say 这个方法

工厂模式

function Person() {
    var o = new Object();
    o.name = 'hanmeimei';
    o.say = function() {
        console.log(this.name);
    }
    return o;
}
var person = Person();     // 和 寄生构造函数模式 相比,没有new

缺点:

  • 对象识别问题:无法识别对象,以为都是来自Object,无法得知来自Person
  • 内存浪费问题:每次通过Person创建对象的时候,都会返回相同属性和方法的对象。

构造函数模式

  1. 构造函数名首字母大写
  2. 内部使用this:通过this定义的属性和方法,实例化对象的时候都会重新复制一份
  3. 使用 new 生成实例 (注意:构造函数模式隐试的在最后返回 return this 所以在缺少 new 的情况下,会将属性和方法添加给 全局对象,浏览器端就会添加给window对象。)
function Person() {
  this.name = 'hanmeimei';
  this.say = function() {
    console.log(this.name)
  }
  // return this /**隐式返回**/
}
var person = new Person();

优点:

  • 通过constructor或者instanceof可以识别对象实例的类别
          person instanceof Object // true
          person instanceof Person // true
          person.constructor === Person   // true
    
  • 可以通过new 关键字来创建对象实例,更像OO语言中创建对象实例

缺点:

  • 构造函数创建对象,每个方法都要在每个实例上重新创建一次

new的实质:

  1. 创建一个新对象(实例)
  2. 将构造函数的作用域赋给新对象(也就是重设了this的指向,this就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象

原型模式

原型模式把那些不变的属性和方法,直接定义在prototype对象上。

function Person() {}
Person.prototype.name = 'hanmeimei';
Person.prototype.say = function() {
  console.log(this.name);
}

var person = new Person();

优点:

  • 属性和方法共享:所有的实例对象共享它所包含的属性和方法,不必在构造函数中定义对象实例信息。
  • 动态添加属性和方法:可以动态的添加原型对象的方法和属性,并直接反映在对象实例上。
          Person.prototype.friends = ['lilei']
          console.log(person.friends) // ['lilei']
    

缺点:

  • 引用类型值会出现问题
          // 因为js对引用类型的赋值都是将地址存储在变量中,所以person1和person2的friends属性指向的是同一块存储区域。
          var person1 = new Person();
          var person2 = new Person();
          person1.friends.push('xiaoming');
          console.log(person2.friends)  //['lilei', 'xiaoming']
    
  • 向上查找:第一次调用say方法或者name属性的时候会搜索两次,第一次是在实例上寻找say方法,没有找到就去原型对象(Person.prototype)上找say方法,找到后就会在实力上添加这些方法or属性。
  • 所有的方法都是共享的,没有办法创建实例自己的属性和方法,也没有办法像构造函数那样传递参数。

构造函数和原型组合模式

这是使用最为广泛、认同度最高的一种创建自定义类型的方法。它可以解决上面那些模式的缺点

function Person(name) {
  this.name = name
}
Person.prototype.say = function() {
  console.log(this.name)
}
var person = new Person('hanmeimei')
person.say() //hanmeimei

优点:

  • 解决了原型模式对于引用对象的缺点:每个实例都会有自己的一份实例属性副本
  • 解决了原型模式没有办法传递参数的缺点
  • 解决了构造函数模式不能共享方法的缺点:共享着对方法的引用

动态原型模式

动态原型模式将所有信息都封装在了构造函数中,初始化的时候,通过检测某个应该存在的方法时候有效,来决定是否需要初始化原型。

function Person(name) {
  this.name = name
  // 只有在sayName方法不存在的时候,才会将它添加到原型中。这段代码只会初次调用构造函数的时候才会执行。
  if(typeof this.say !== 'function') {
    Person.prototype.say = function() {
        console.log(this.name)
    }
  }
}

优点:

  • 可以在初次调用构造函数的时候就完成原型对象的修改
  • 修改能体现在所有的实例中

寄生构造函数模式

和工厂模式基本一样,除了多了个new操作符

function Person(name) {
  var o = new Object()
  o.name = name
  o.say = function() {
    alert(this.name)
  }
  return o
}
var peron1 = new Person('hanmeimei')

稳妥构造模式

稳妥对象指的是没有公共属性,而且其方法也不引用this。

稳妥对象最适合在一些安全环境中(这些环境会禁止使用this和new),或防止数据被其他应用程序改动时使用。

function Person(name) {
  var o = new Object()
  o.say = function() {
    alert(name)
  }
}
var person1 = new Person('hanmeimei');
person1.name  // undefined
person1.say() //hanmeimei

优点:

  • 安全,name 好像成为了私有变量,只能通过say方法去访问

缺点:

  • 不能区分实例的类别

参考

继承(Inheritance)

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

继承方式

以下代码 以 SuperClass 为父类,SuperClass 为子类

继承分类

原型链继承

核心:将父类的实例作为子类的原型

核心实现代码:SubClass.prototype = new SuperClass();

// 声明父类
var SuperClass = function () {
    this.name = 'hanmeimei';
    this.say = function() {
        console.log(this.name);
    }
};

//声明子类
var SubClass = function () {};
SubClass.prototype = new SuperClass()

var sub = new SubClass()

优点:父类方法可以复用

缺点:

  • 父类的引用属性会被所有子类实例共享
  • 子类构建实例时不能向父类传递参数

构造函数继承

核心:将父对象的构造函数绑定在子对象上,直接改变this的指向。(这是所有继承中唯一一个不涉及到prototype的继承。)

核心代码实现:SuperType.bind(this); (也可以使用callapply)

// 声明父类
var SuperClass = function () {
    this.name = 'hanmeimei';
    this.say = function() {
        console.log(this.name);
    }
};

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

var sub = new SubClass()

优点:(和原型链继承完全反过来)

  • 父类的引用属性不会被共享
  • 子类构建实例时可以向父类传递参数

缺点:父类的方法不能复用,子类实例的方法每次都是单独创建的。

组合式继承

核心:原型式继承和构造函数继承的组合,兼具了二者的优点。

组合式继承就是汲取 类式继承 和 构造函数继承 的优点,即避免了内存浪费,又使得每个实例化的子类互不影响。

// 声明父类
var SuperClass = function () {
    this.name = 'hanmeimei';
};
SuperClass.prototype.say = function() {
    console.log(this.name);
}

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

SubClass.prototype = new SuperClass()

var sub = new SubClass()

原型式继承

核心:原型式继承的object方法本质上是对参数对象的一个浅复制。

function object(o){
  function F(){}
  F.prototype = o;
  return new F();
}
var SuperClass = {
    name: 'hanmeimei',
    say: function(){
        console.log(this.name)
    }
}
var subClass = object(SuperClass);

优点:父类方法可以复用

缺点:

  • 父类的 引用属性 会被所有子类实例共享 (写实例)
  • 子类构建实例时不能向父类 传递参数

寄生式继承

核心:使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。

这种继承方式是把原型式+工厂模式结合起来,目的是为了封装创建的过程。


寄生组合继承

先给父类的原型创建一个副本,然后修改子类constructor属性,最后在设置子类的原型就可以了

ES6 Class extends

核心: ES6继承的结果和 寄生组合继承 相似,本质上,ES6继承是一种语法糖。但是,寄生组合继承是先创建子类实例this对象,然后再对其增强;而ES6先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

class SuperClass {}

class SubClass extends SuperClass {
  constructor() {
    super();
  }
}

ES6实现继承的具体原理:

参考

多态(Polymorphism)

所谓多态,就是指一个引用类型在不同情况下的多种状态。
多态的最根本好处在于,你不必再向对象询问“你是什么类型”而后根据得到的答案调用对象的某个行为——你只管调用该行为就是了,其他的一切多态机制都会为你安排妥当。

普通写法:

var googleMap = {
    show: function(){
    console.log( '开始渲染谷歌地图' );
    }
};
var baiduMap = {
    show: function(){
    console.log( '开始渲染百度地图' );
    }
};

// 函数以字符串形式的传参形式
// 如果判断条件一旦增多,就会变全是if-else语句
var renderMap = function( type ){
    if ( type === 'google' ){
        googleMap.show();
    }else if ( type === 'baidu' ){
        baiduMap.show();
    }
};

renderMap( 'google' ); // 输出:开始渲染谷歌地图
renderMap( 'baidu' ); // 输出:开始渲染百度地图

改进:

var googleMap = {
    page: 10,
    show: function(){
        console.log( '开始渲染谷歌地图' );
    }
};
var baiduMap = {
    page: 1,
    show: function(){
        console.log( '开始渲染百度地图' );
    }
};

// 将render传入的参数换成相应的对象
var renderMap = function( map ){
    if ( map.show instanceof Function ){
        map.show();
        console.log(mape.page);
    }
};

renderMap( googleMap ); // 输出:开始渲染谷歌地图 10
renderMap( baiduMap ); // 输出:开始渲染百度地图 1

JavaScript面向对象

JS面向对象总结【封装,继承,多态,this,prototype】

文章作者: phoebe
文章链接: https://phoebecodespace.github.io/2018/09/10/javascript-OOP/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 phoebe's blog