再说面向对象(3)

279 阅读7分钟

这是我参与8月更文挑战的第21天,活动详情查看:8月更文挑战

一、常用测数据类型方式

在js中常用的检测数据类型的方法有四种,在项目开发中,数据类型穿插在每个重要的环节中,如果巧用数据类型检测,常常会起到意想不到的效果。在bug调试过程中,当山穷水尽的时候,如果尝试检测数据类型,往往会有柳暗花明的效果。尤其在nodejs的bug调试中十分有效,因为node在io操作时常常进行数据类型的转换,可以说屡试不爽。

之所以在这篇文章中才讲述,数据类型的检测,是因为数据类型的检测的方法和面向对象有着千丝万缕的联系,如果你有空可以看看前面三篇文章,相信能对数据类型检测有更深的体会,正所谓磨刀不费砍柴功。闲言少叙,奔正题。

1、 typeof

typeof的应用场景大多是检测基本数据类型或者不需要具体细分的对象数据类型检测亦或者是区分基本数据类型和引用数据类型。 typeof的返回值是字符串包裹的被检值对应的数据类型,一共有6个,”number”、”string”、”boolean”、”undefined”、”function”、”null”、”object”。示例:

console.log(typeof     5);//”number”
console.log(typeof     “binary”);//”string”
console.log(typeof     true);//”boolean”
console.log(typeof     undefined);//” undefined”
console.log(typeof  function (){});//”function”
console.log(typeof     null);//” object”
console.log(typeof     {});//” object”
console.log(typeof     []);//” object”
console.log(typeof  /^\d+$/);//” object”
………..

从上面的例子中不难看出typeof有局限性,具体体现在:

  1. 检测null的时候返回object
  2. 不能具体细分对象数据类型
  3. 函数本身不是一种数据类型,但是因为函数有许多特殊之处,这里typeof一个函数时返回一个function也是有道理的

2、instanceof:

instanceof操作符是检测某个实例是否属于一个类,检测结果是布尔值,是,则返回true,否则返回false; 示例:

var ary=[1m,2m,23];
console.log(ary instanceof Array);//true
console.log(ary instanceof Object);//true
var num=12var num2=new Number(12);
console.log(num instanceof Number);//false
console.log(num2  instanceof  Number);//true
  • 但是 instanceof 也有其局限性,
  1. 不能检测用字面临创建的基本数据类型的值,这是由于js是弱类型的语言,从这个角度讲,实例创建和字面量创建还是有区别的,只有通过实例创建的才算是对象;
  2. 只要当前被检值在当前类的原型链上,检测结果就是true;
  3. 在使用类的继承后,检测结果将不够准确;

3、 constructor:

constructor是来弥补instanceof的不能检测字面量创建的数据的;
var num=12console.log(num.constructor===Number);//true
var ary=[1,2,3];
console.log(ary.constructor===Array);//true;
console.log(ary.constructor===Object);//false;

constructor 虽然弥补了一部分 instanceof 的不足,但是其缺点仍然不可忽视:当类的原型被重写时,其原型的constructor很可能被修改,导致检测结果不准确;

4、 Object.prototype.toString.call();方法

每一种数据类型都自带一个 toString() 方法,但是功用却不尽相同,如数组的 toString 是转化成字符串的,而对象的toString是检测数据类型的。该方法返回值是一个字符串,如"[object 数据所属类]"。示例

var num=24;
console.log(Object.prototype.toString.call(num));//”[object Number]”;
var ary=[1,2,3];
console.log(Object.prototype.toString.call(ary));//”[object Array]”;
var reg=/^\w+$/g;
console.log(Object.prototype.toString.call(reg));//”[object RegExp]”
………..

如果要求检测结果精准时推荐使用最后一种方法。

二、类的继承

前面两篇文章,我们介绍了对象的一些特性以及面向对象的常见的封装方式,今天我们来继续讨论另外两个的关于面向对象中的重点:继承和多态; 继承是所有面向对象语言津津乐道的概念,后面讲述的继承方式大多基于原型模式、构造函数模式的,如果想更好的了解继承的概念,还希望大家好好看看上一篇文章中有关原型模式的论述。        本篇文章重点讨论几种常用的继承方式,并尝试从每种继承方式的优缺点,及继承之后父类的方法归属的角度探讨;

2.1 原型继承

       原型继承的实现原理,子类的原型指向父类的一个实例;这样父类公有和私有的方法和属性都成为了子类公有的方法和属性; 示例:

function Super(){//父类
    this.delete=functinon(item){}
}
Super.prototype.edit=function(){};
function Sub(){//子类}

子类想调用父类的delete和edit方法,只需要将子类的原型指向父类的实例;
Sub.prototype=new Super();//继承实现的核心代码

原型继承虽然操作很简单,但是应该注意以下问题;

  1. 原型继承并非复制一份父类的公有和私有的属性及方法,而是让子类和父类有了原型链上的联系,实现继承之后,子类的实例的__proto__属性指向了父类的实例,而这个实例的__proto__又指向父类的原型,是这样实现的继承;
  2. 通过原型链来实现继承时,子类的原型实际上是变成了另一个类的实例,原来父类实例的属性顺理成章的成为了子类原型的上的属性,这是我们不可改变的;
  3. 通过原型继承后,子类的原型是被更换了(有人习惯称之为“修改子类原型的指向”,但是我觉得说“更换”更为形象),这样一来子类原型上的Constructor(构造函数)属性的值便成为父类的构造函数,这也是我们不愿意看到的,因此在实现继承之后,我们要手动的修改子类的构造函数属性;
Sub.prototype.constructor=Sub;//手动修改子类的构造函数指向;

2.2 call继承(借用构造函数继承)

原型继承中,是将父类的公有和私有的方法都继承到了子类的公有的,但有时我们并不希望都继承了,很多场景下我们只需要继承某个类私有的方法即可,这时候就要用到call继承了。

call继承的实现原理,call方法是用来this关键字的方法,而在构造函数中的this都是当前类的实例,所以在子类的构造函数中call执行父类的构造函数(不通过new关键字),把父类构造函数中的this关键字修改为子类构造函数中的this,这样,父类实例的私有属性和方法都随着父类构造函数的执行继承到了子类的的实例身上。示例:

function Super(){//父类
  this.delete=functinon(item){}
};
  Super.prototype.edit=function(){};
function Sub(){//子类
  this.generation=son;
  Super.call(this);//call继承的实现的核心代码
};

2.3 混合继承

原型继承是将父类的私有和公有的都变成子类的公有的,而call继承是将父类私有的变成子类私有的,那么有没有一种继承,让父类公有的变成子类公有的,父类私有的变成子类私有的?混合继承就解决了这个问题;

混合继承是原型继承和call继承混合,如此一来类公有的变成子类公有的,父类私有的变成子类私有。但是在子类的公有中多了一份父类私有的,但是原型查找机制是先查找子类私有,瑕不掩瑜,混合继承的优势还是很突出的。示例:

function Super(){//父类
  this.delete=functinon(item){}
};
Super.prototype.edit=function(){};
Sub.prototype=new Super();//原型继承核心代码
Sub.prototype.contructor=Sub;//修改constructor指向
function Sub(){//子类
   this.generation=son;
   Super.call(this);//call继承的实现的核心代码
}

2.4 中间件继承

中间件继承的实现依赖于__proto__属性,子类原型上的__proto__默认是指向Object的原型的,我们在在__proto__指向Object.prototype之前插入父类的原型。中间件继承其实是延长了子类的原型链,中间件继承是将父类公有的继承到了子类公有的。示例:

function Super(){//父类
  this.delete=functinon(item){
  }
};
Super.prototype.edit=function(){};
function Sub(){//子类
    this.generation=son;
}
Sub.prototype.__proto__=Super.prototype;//中间件继承核心代码

值得注意的是,.__proto__在ie下不兼容,所以慎重使用;

扩展:巧用Object.create(proObj)方法实现继承 该继承方式很巧妙,利用的是这个方法的返回值是一个以proObj为原型的对象,其实现原理和中间件继承是一样的。但是在IE8- 不兼容。示例:

function Super(){//父类
  this.delete=functinon(item){};
};
Super.prototype.edit=function(){};
function Sub(){//子类
   this.generation=son;
};
Sub.prototype. = Object.create(Super.prototype);//核心代码
虽然该方法不兼容,我尝试了简单模拟Object.create方法,代码如下
function(obj){
  function Fn(){};
  Fn.prototype=obj;
  return new Fn();
};

以上是有关于面向对象的继承,最后在说说js中面向对象的多态,多态包括重载和重写;在这里重点强调,js中不能像传统的面向对象那样实现重载,在其他语言中(如java)中,可以为一个函数编写两个定义,只要这两个定义的签名不同即可。(这句话是说,在java等后台语言中,可以定义两个同名函数,只要这两个函数的参数的类型或个数不同即可),当调用时,根据传递参数的不同,机制会自动调用方法。但是,在js中,如果定义多个同名函数,在预解释阶段就会被覆盖,以最后一个为准;在js中有一个现象类似于重载,但是绝不是重载;示例:

function sum(num){
   if(typeof num===”undefined”){
      return 0
}
return num++;
}

在上面的例子中,虽然参数传与不传时结果不同,但是请大家注意中重点,重载是定义多个同名方法,当参数不同时自动调用对应方法执行,而这里是虽然参数不同,但是调用的始终都是同一个sum方法;