本博客基于(Javascript高级程序设计(第二版)和(第三版)),以及很多优秀的博客,以及我的个人见解,和个人总结。
前情提要
面向对象语言的三大特点:继承、封装、多态。
但Javascript是一种基于对象(object-based)的语言,可以说是万物皆对象。但是,它不是一种真正的面向对象编程(OO)语言,因为它的语法中没有class(类)这个概念,所以说它的对象(Object)模型会比较独特。(ES6中新增class,之后再了解一下,是一个创建对象的方法)
一般像java /c++语言它们的继承是通过class(类)实现。那 javascript 实现继承的机制是什么?答案是原型链!
先放两张原型链的经典图片(看不懂没关系,继续往下看,看完回过来就会发现很简单)![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/d275cdaacd5edda1686429497d50734d.png)
先普及一下基础知识,继承在后面。
一、 什么是对象
所谓的万物皆对象,顾名思义,每一个物体都可以当作对象。每一个对象都有着自己的 “属性” 和 “方法”。而我们将这些 “属性” 和 “方法“ 封装到一起就成为了一个对象 。
二、什么是原型对象和实例对象
原型对象
只要创建一个新函数,就会根据一系列的规则为这个函数创建一个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); //中国
而创建的子对象就已经带有父对象的属性。