本博客基于(Javascript高级程序设计(第二版)和(第三版)),以及很多优秀的博客,以及我的个人见解,和个人总结。
所谓的万物皆对象,顾名思义,每一个物体都可以当作对象。每一个对象都有着自己的 “属性” 和 “方法”。而我们将这些 “属性” 和 “方法“ 封装到一起就成为了一个对象 。
只要创建一个新函数,就会根据一系列的规则为这个函数创建一个prototype属性(就是一个指针),而这个属性指向的就是原型对象,而原型对象中也会获得一个相应的constructor属性(也是一个指针)而这个属性指向它prototype属性所在的这个函数。
通过调用构造函数产生的对象,叫做实例对象,它拥有一个内部属性([[prototype]])(但没有标准的方法去访问这个属性),指向了原型对象。实例对象能够访问原型对象上的所有属性和方法。
假定把一个猫看作一个对象,假设有 ”名字“和”颜色“两个属性.假设猫会说自己的名字,所以再定义一个 ”输出它自己名字“的方法 及下面的这个样子。
//创建完后对象的样子
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之间没有内在的联系,也不能反映出它们是同一个原型对象的实例。
为了解决从原型对象生成实例的问题,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()的方法都是一样的东西。每生成一个实例,就会多占用内存。不环保也缺乏效率。
回忆 每一个构造函数都有一个 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;
}
继承就是可以让 “猫”这个对象 也有 "动物"对象的属性。
是最常用的一种方法,回忆一下构造函数,原型,实例的关系:每一个构造函数都有一个原型对象,原型对象也包含一个指向构造函数的指针,而实例对象也都包含一个指向原型对象的内部指针。
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); // 动物
最简单的一种方法,使用 call 或 apply 方法。
function Cat(name,color){
Animal.apply(this, arguments);
this.name = name;
this.color = color;
}
var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物
比如有一个对象,叫做 ”中国人"
var Chinese = {
nation:'中国'
};
还有一个对象,叫”医生“
var Doctor ={
career:'医生'
}
两个对象都是普通对象,不是构造函数,所以不能使用造函数方法实现"继承"。
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); //中国
而创建的子对象就已经带有父对象的属性。