javascript面向对象编程(创建对象和继承的常用方法)(比较长,仔细看完收获会很大哦)(干货)

本博客基于(Javascript高级程序设计(第二版)和(第三版)),以及很多优秀的博客,以及我的个人见解,和个人总结。

前情提要

面向对象语言的三大特点:继承、封装、多态。
但Javascript是一种基于对象(object-based)的语言,可以说是万物皆对象。但是,它不是一种真正的面向对象编程(OO)语言,因为它的语法中没有class(类)这个概念,所以说它的对象(Object)模型会比较独特。(ES6中新增class,之后再了解一下,是一个创建对象的方法)
一般像java /c++语言它们的继承是通过class(类)实现。那 javascript 实现继承的机制是什么?答案是原型链!

先放两张原型链的经典图片(看不懂没关系,继续往下看,看完回过来就会发现很简单)在这里插入图片描述

在这里插入图片描述

先普及一下基础知识,继承在后面。

一、 什么是对象

所谓的万物皆对象,顾名思义,每一个物体都可以当作对象。每一个对象都有着自己的 “属性” 和 “方法”。而我们将这些 “属性” 和 “方法“ 封装到一起就成为了一个对象 。

二、什么是原型对象和实例对象

原型对象

只要创建一个新函数,就会根据一系列的规则为这个函数创建一个prototype属性(就是一个指针),而这个属性指向的就是原型对象,而原型对象中也会获得一个相应的constructor属性(也是一个指针)而这个属性指向它prototype属性所在的这个函数。

实例对象

通过调用构造函数产生的对象,叫做实例对象,它拥有一个内部属性([[prototype]])(但没有标准的方法去访问这个属性),指向了原型对象。实例对象能够访问原型对象上的所有属性和方法。

三、创建对象

1. 工厂模式(原始模式)

假定把一个猫看作一个对象,假设有 ”名字“和”颜色“两个属性.假设猫会说自己的名字,所以再定义一个 ”输出它自己名字“的方法 及下面的这个样子。

//创建完后对象的样子
var Cat = {
	name : "",
	color : "",
	sayname : function(){
			alert(this.name);
		}
}

然后根据这个样子,生成两个实例。

//实际创建方法
var cat1 = {}; // 创建一个空对象
    cat1.name = "大毛"; // 按照原型对象的属性赋值
    cat1.color = "黄色";
    cat1.sayname = function(){
				alert("this.name":);
		}
var cat2 = {};
    cat2.name = "二毛";
    cat2.color = "黑色";
    cat2.sayname = function(){
				alert("this.name":);
		}

这是最简单的封装,把两个属性,和一个方法封装在一个对象里。但是这样写法有两个缺点 一、如果多生成几个实例,写起来恒麻烦;二、是创建的实例和原型之间,没有什么联系。

所以改进一下写一个函数,解决代码重复的问题(及工厂模式)
 function cat(name,color){
 			//var o={};
           var o=new Object();
           //两种创建空对象的方法一样
           o.name = name;
           o.color = color;
           o.sayname= function(){
               alert("this.name");
           };
           return o;
       }

然后调用函数创建实例对象;

	var cat1 = cat("大毛","黄色");

  var cat2 = cat("二毛","黑色");

这种方法的问题依然是,cat1和cat2之间没有内在的联系,也不能反映出它们是同一个原型对象的实例。

2.构造函数模式

为了解决从原型对象生成实例的问题,Javascript提供了一个构造函数(Constructor)模式。

所谓"构造函数",其实就是一个普通函数,但是内部使用了this变量。对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上。

  function Cat(name,color){
    this.name = name;
    this.color = color;
    this.sayname = function(){
			alert("this.name");
		};
  }

现在就可以生成实例对象了。

	var cat1 = new Cat("大毛","黄色");
  var cat2 = new Cat("二毛","黑色");

这时cat1和cat2会自动含有一个constructor属性,指向它们的构造函数,并且这两个实例中将包含一个指针(一个内部属性),指向构造函数的原型对象,ES5叫这个指针为 [[Prototype]]但是没有一个标准的方法去访问 [[Prototype]]。但再Firefox,Safari,Chrome中每个对象都支持一个属性_proto_;可以访问去访问它的原型对象。

	alert(cat1.constructor == Cat); //true
  alert(cat2.constructor == Cat); //true

构造函数方法很好用,但是存在一个浪费内存的问题。
请看,我们现在为Cat对象添加一个不变的属性type(种类),再添加一个方法eat(吃)。那么,Cat就变成了下面这样。

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

  }

如果生成好多个实例,type 和 eat()的方法都是一样的东西。每生成一个实例,就会多占用内存。不环保也缺乏效率。

3.Prototype模式(原型模式)

回忆 每一个构造函数都有一个 prototype属性,指向一个对象(原型对象),并且这个对象(原型对象)的所有属性和方法,都会被构造函数的实例继承。
这意味着,我们可以把那些不变的属性和方法,直接定义在prototype(原型)对象上。

function Cat(){

  }
  Cat.prototype.type = "猫科动物";
  Cat.prototype.eat = function(){alert("吃老鼠")};
	var cat1 = new Cat();
  var cat2 = new Cat();
  alert(cat1.type); // 猫科动物
  cat1.eat(); // 吃老鼠

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

.
.
.
但是原来需要输入的属性和方法却没了,所以我们将构造函数模式和原型模式组合起来使用,就是下面的。(这种方法是最常用的)

function Cat(name,color){
    this.name = name;
    this.color = color;
    this.sayname = function(){
			alert("this.name");
		};
  }
  Cat.prototype.type = "猫科动物";
  Cat.prototype.eat = function(){alert("吃老鼠")};
    var cat1 = new Cat("大毛","黄色");
  var cat2 = new Cat("二毛","黑色");
  alert(cat1.type); // 猫科动物
  cat1.eat(); // 吃老鼠

当然《javascript高级程序设计》第三版的书上还有寄生构造函数模式和稳妥构造函数模式。大家自己看吧。(最重要的方法已经讲完了)

四、继承

呼,终于到继承。
那么什么是继承?

比如,现在有一个"动物"对象的构造函数。

function Animal(){
    this.species = "动物";
  }

还有一个"猫"对象的构造函数。

 function Cat(name,color){
    this.name = name;
    this.color = color;
  }

继承就是可以让 “猫”这个对象 也有 "动物"对象的属性。

1、原型链的基本模式

是最常用的一种方法,回忆一下构造函数,原型,实例的关系:每一个构造函数都有一个原型对象,原型对象也包含一个指向构造函数的指针,而实例对象也都包含一个指向原型对象的内部指针。

想一个问题,假如我们让原型对象(Prototype)等于另一个类型的实例,结果会是怎样?
Cat.prototype = new Animal();

看代码的话;
Cat的原型对象调用了Animal函数,所以Animal函数里面的this函数就指向了Cat的原型对象,也就是说此时Cat的原型对象中存在Animal对象的属性,也就是实现了继承。

但是有一个问题

   //测试代码
   alert(Cat.prototype.constructor == Cat); //结果为flase
   alert(Cat.prototype.constructor == Animal); // 结果为true

正常情况下第一行代码的结果为 true 第二行为 flase 。
原来Cat的原型对象调用了Animal函数后,相当于Animal的实例对象替换了原来的Cat原型对象,那么此时Cat的原型对象中的应有的constructor属性的指向就变成了Animal的。就造成了继承紊乱。
所以我们需要手动纠正

Cat.prototype.constructor = Cat;

然后就正常了。
然后创建一个实例。

//这是继承部分和创建实例部分,
//需要在这里面先写创建对象部分就是上面Cat和Animal的构造函数
   Cat.prototype = new Animal();
  Cat.prototype.constructor = Cat;
  var cat1 = new Cat("大毛","黄色");
  alert(cat1.species); // 动物

然后优化一下在Animal对象中,不变的属性都可以直接写入Animal.prototype那么如果需要继承的属性是不变的,我们就可以让Cat()跳过调用Animal(),直接继承Animal.prototype,就节省了内存。
所以要先将Animal对象改写成原型模式中创建对象的样子

function Animal(){ }
  Animal.prototype.species = "动物";

然后直接将Cat的prototype(原型)对象,指向Animal的prototype(原型)对象,这样就完成了继承。

	Cat.prototype = Animal.prototype;
  Cat.prototype.constructor = Cat;
  var cat1 = new Cat("大毛","黄色");
  alert(cat1.species); // 动物

然后这种方式有限制但是省内存,并且会出现bug(哈哈哈)
分析一下此时Cat的原来的原型对象被 Animal的原型对象替代,那现在Cat的原型对象的constructor属性就会指向Animal的构造函数,所以如果我改写Cat的原型对象的任何属性都会反映到Animal的原型对象上(相当于Animal这个基本就废了,因为它的值会受到其他对象的干扰)

那么再优化一下,我们可以在创建一个空对象作为中介

    var F = function(){};
  F.prototype = Animal.prototype;
  Cat.prototype = new F();
  Cat.prototype.constructor = Cat;

相当于前两种方法的综合,存在上一种方法的限制,但是没有bug ,而且空对象几乎不占内存,也保留了优点。

然后我们把他用函数封装一下:

function extend(Child, Parent) {
    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    Child.uber = Parent.prototype;
    //uber就是现在的super方法,
    //可以直接指向父对象的prototype属性
    //等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。
  }

而封装的这个函数就是YUI库如何实现继承的方法
.
.
.
那么转化一下思路,我们可以直接将父对象的所有属性和方法,拷贝到子对象里
同样把 Animal 的所有不变的属性,放到prototype(原型)对象上

 function Animal(){}
  Animal.prototype.species = "动物";

然后,再写一个函数,实现属性拷贝的目的.

function extend2(Child, Parent) {

    var p = Parent.prototype;
    var c = Child.prototype;
    for (var i in p) {
      c[i] = p[i];
      }
    c.uber = p;
  }

使用的时候这样写

	extend2(Cat, Animal);
  var cat1 = new Cat("大毛","黄色");
  alert(cat1.species); // 动物
2、借用构造函数

最简单的一种方法,使用 call 或 apply 方法。

  function Cat(name,color){
    Animal.apply(this, arguments);
    this.name = name;
    this.color = color;
  }
  
  var cat1 = new Cat("大毛","黄色");
  alert(cat1.species); // 动物
3.非构造函数的继承

比如有一个对象,叫做 ”中国人"

var Chinese = {
    nation:'中国'
  };

还有一个对象,叫”医生“

var Doctor ={
    career:'医生'
  }

两个对象都是普通对象,不是构造函数,所以不能使用造函数方法实现"继承"。

1.原型式继承

json的发明者Douglas Crockford,提出了一个object函数。

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
  }

这个object()函数,其实只做一件事,就是把子对象的prototype属性,指向父对象,从而使得子对象与父对象连在一起。
函数返回的是一个对象,所以它是一个基于父对象的基础上,生成子对象的函数。

var Doctor = object(Chinese);
Doctor.career = '医生';
alert(Doctor.nation); //中国

而创建的子对象就已经带有父对象的属性。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值