Javascript面向对象编程 之 (封装)
Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)。
那么,如果我们要把"属性"(property)和"方法"(method),封装成一个对象,甚至要从原型对象生成一个实例对象,我们应该怎么做呢?
一、 生成实例对象的原始模式
<script>
//假定我们把猫看成一个对象,它有"名字"和"颜色"两个属性。
var cat = {
name: '',
color:''
}
</script>
现在,我们需要根据这个原型对象的规格(schema),生成两个实例对象。
<script>
var cat1 = {}; // 创建一个空对象,var cat1={}是一种简写,它是一种语法糖,它实际上等于 var cat1=new Object()
cat1.name = "旺财"; // 按照原型对象的属性赋值
cat1.color = "黄色";
var cat2 = {};
cat2.name = "来福";
cat2.color = "黑色";
</script>
好了,这就是最简单的封装了,把两个属性封装在一个对象里面。但是,这样的写法有两个缺点,一是如果多生成几个实例,写起来就非常麻烦;二是实例与原型之间,没有任何办法,可以看出有什么联系。
二、 原始模式的改进
<script>
function Cat(name, color) {
return{
Name : name,
Color : color}
}
//现在我们要生成两个猫的实例对象
var cat1 = new Cat("旺财", '黄色');
var cat2 = new Cat("来福", '黑色');
alert(cat1.Name);//打印出:旺财
alert(cat2.Name);//打印出:来福
</script>
这种方法的问题依然是,cat1和cat2之间没有内在的联系,不能反映出它们是同一个原型对象的实例
三、 构造函数模式
为了解决从原型对象生成实例的问题,Javascript提供了一个构造函数(Constructor)模式。
所谓"构造函数",其实就是一个普通函数,但是内部使用了this变量。对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上。
比如,猫的原型对象现在可以这样写,
<script>
function Cat(name, color) {
this.Name = name;
this.Color = color;
}
//现在我们要生成两个猫的实例对象
var cat1 = new Cat("旺财", '黄色');
var cat2 = new Cat("来福", '黑色');
alert(cat1.Name);//打印出:旺财
alert(cat2.Name);//打印出:来福
//这时cat1和cat2会自动含有一个constructor属性,指向它们的构造函数。(下面就来验证一下)
alert(cat1.constructor === Cat) //打印出:true
alert(cat2.constructor === Cat) //打印出:true
//Javascript还提供了一个instanceof运算符,验证原型对象与实例对象之间的关系。
alert(cat1 instanceof Cat); //true
alert(cat2 instanceof Cat); //true
</script>
四、构造函数模式的问题
构造函数方法很好用,但是存在一个浪费内存的问题。
请看,我们现在为Cat对象添加一个不变的属性type(种类),再添加一个方法eat(吃)。那么,原型对象Cat就变成了下面这样:
<script>
function Cat(name, color) {
this.Name = name; //不同的猫有不同的名字, 那么其中的this就代表它即将new出来的对象
this.Color = color; //不同的猫有不同的颜色
this.Type = '猫科动物'; //所有猫都有一致的类型属性
this.Eat = '吃老鼠'; //所有猫都有一致的行为属性
}
//现在我们要生成两个猫的实例对象
var cat1 = new Cat("旺财", '黄色');
var cat2 = new Cat("来福", '黑色');
alert(cat1.Eat); //打印出:吃老鼠
alert(cat2.Eat); //打印出:吃老鼠
alert(cat1.Eat == cat2.Eat); //打印出:false 那是因为cat1和cat2是不同的对象
</script>
表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。那就是对于每一个实例对象,type属性和eat()方法都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存。这样既不环保,也缺乏效率。
能不能让type属性和eat()方法在内存中只生成一次,然后所有实例都指向那个内存地址呢?回答是可以的。
五、 Prototype模式
<script>
function Cat(name, color) {
this.Name = name; //不同的猫有不同的名字
this.Color = color; //不同的猫有不同的颜色
}
Cat.prototype.Type = '猫科动物'; //所有猫都有一致的类型属性
Cat.prototype.Eat = '吃老鼠'; //所有猫都有一致的行为属性
//现在我们要生成两个猫的实例对象
var cat1 = new Cat("旺财", '黄色');
var cat2 = new Cat("来福", '黑色');
alert(cat1.Eat); //打印出:吃老鼠
alert(cat2.Eat); //打印出:吃老鼠
alert(cat1.Eat == cat2.Eat); //打印出:true 这时所有实例的type属性和eat()方法,其实都是同一个内存地址,指向prototype对象
</script>
这时所有实例的type属性和eat()方法,其实都是同一个内存地址,指向prototype对象,因此就提高了运行效率。
JS核心系列:浅谈 原型对象和原型链
一、函数的定义
在Javascript中,万物皆对象,但对象也有区别,大致可以分为两类,即:普通对象(Object)和函数对象(Function)
一般而言,通过new Function产生的对象是函数对象,其他对象都是普通对象【注:对象是通过函数来创建的,Object,Array都是函数】
<script>
//Function是JS自带的对象,f1,f2在创建的时候,JS会自动通过new Function()的方式来构建这些对象,
//因此,这f1,f2,f3这三个对象都是通过new Function()创建的。
function f1() { }; //最常见的函数定义方式
var f2 = function () { }; //定义了一个匿名函数
var f3 = new Function('x', 'y', 'return x+y'); //不常见,但也是一种函数的创建方式,它相当于var f3=function(x,y){ return x+y};
var a1 = {}; //创建一个空对象,var cat1={}是一种简写,它是一种语法糖,它实际上等于 var cat1=new Object()
var a4 = { name: '旺财', age: 2 };//它实际等于 var a4=new Object(); a4.name='旺财';a4.age=2;
var a2 = new Object();
var a3 = new f1();
alert(typeof (f1)); //打印出:function 函数对象
alert(typeof (f2)); //打印出:function 函数对象
alert(typeof (f3)); //打印出:function 函数对象
alert(typeof (a1));//打印出:object 普通对象
alert(typeof (a2));//打印出:object 普通对象
alert(typeof (a3));//打印出:object 普通对象
</script>
二、函数与对象的关系
function Dog(name,age) {
this.Name = name;
this.Age = age;
}
//创建自定义对象
var dog = new Dog("旺财", 2);
//当代码 var dog = new Dog("旺财", 2)执行的时候,其实内部做了如下几件事情:
//1>创建了一个空白对象 new Object()
//2>拷贝Dog.prototype中的属性(键值对)到这个空对象中(我们前面提到,内部实现时不是拷贝而是一个隐藏的链接)[这就是我们说的继承]
//3>将这个对象通过this关键字传递到构造函数中并执行构造函数。
//4>将这个对象赋值给变量dog
通过以上注释可以得出:当执行var dog=new Dog('旺财',2)的时候,其实我们是创建了一个空对象,然后这个空对象继承了Dog函数对象。所以我们可以通过dog.Name得到Dog对象中的属性
三、Javascript中的原型和原型链
通过以上的之后,我们再来了解一下Javascript中的原型和原型链:
在JS中,每声明一个函数的时候就会默认内置了一个prototype属性,这个prototype的属性值是一个对象(属性的集合,再次强调!),默认的只有一个叫做constructor的属性,指向这个函数本身。一般construtor就是我们平时对函数设置的实例化对象(我理解为:这个prototype属性指向了函数本身,它就是这个函数的对象)
那么我们在创建这个函数对象的时候(即:new 一个函数对象)它会自动在对象里内置一个__proto__的属性,这个属性是隐藏的,这个属性又指向了该函数的原型prototype
function f1() { }//通过function定义的对象是有prototype属性的,我们可以通过 f1.prototype 来点出来
var v = new f1(); //通过使用new操作符生成的对象是没有prototype属性的,原因是他已经实例化,但是它有一个隐藏的__proto__属性,可以硬性的调出来
alert(f1.prototype === v.__proto__)//打印出:true 表示v的__proto__属性的确指向了f1函数的原型 即:f1.prototye
看下面的例子
function Dog() {
this.Name= '旺财';//创建一个函数,并给这个函数添加了一个属性Name
}
Dog.prototype.Age = '2' //创建这个Dog函数的时候,函数内部就自动添加了一个prototype属性,这个属性也是一个对象,它指向了函数本身,所以我们可以通过.的形式给函数添加属性,或者方法
Dog.prototype.Eat = function () { //给Dog函数添加一个Eat属性,这个属性的值是一个匿名函数,即:Eat指向了一个函数
alert("吃骨头");
}
var dog = new Dog();//当我们创建这个Dog对象的时候,JS会自动给内置一个__proto__属性,这个属性是隐藏的,这个属性就指向了Dog的原型,即:指向了Dog.prototype
alert(dog.Age) //当我们通过dog.Age去Dog对象中找这个Age属性的时候,发现找不到,于是就去隐藏的__proto__属性(对象)中找,于是就找到了这个Age 而__proto__又有自己的__proto__,假如没有找到,就会去__proto__的__proto__中找,一直于是就这样 一直找下去,也就是我们平时所说的原型链的概念。【其实prototype只是一个假象,他在实现原型链中只是起到了一个辅助作用,换句话说,他只是在new的时候有着一定的价值,而原型链的本质,其实在于__proto__】
alert(Dog.prototype === dog.__proto__)//打印出:true 表示dog的__proto__属性的确指向了Dog函数的原型 即:Dog.prototye
console.log(dog)
看一个例子:
function C1(name) {
if (name) this.name = name;
}
function C2(name) {
this.name = name;
}
function C3(name) {
this.name = name || 'john';
}
//
C1.prototype.name = "Tom";
C2.prototype.name = "Tom";
C3.prototype.name = "Tom";
alert((new C1().name) + (new C2().name) + (new C3().name)); //这里打印出Tom undefined john
//想想为什么?
//1> newC1().name 这个括号中没有带值,所以根据if语句的条件,C1中没有声明name属性,而C1的原型中又声明了name属性,所以C1函数的实例对象在读取name属性的时候会从实例对象本身开始找这个name,结果没有找到,接着又去C1的prototype原型中找,找到了这个name,所有这个name是Tom
//2> new C2().name 这个括号里没有带值,而C2声明了一个name属性,根据赋值得到 name的值为undefined,所以C1函数的实例对象在读取name属性的时候会从对象实例本身开始找这个name 结果找到了这个name,既然找到了就不会去原型中找了,所有这个name 是undefined
//3>同上,name的值为john
Javascript实现继承的三种方式
第一种:原型链继承
<script>
//原型链继承
function Animal() {
this.species = "动物";
}
function Cat(name, color) {
this.name = name;
this.color = color;
}
Cat.prototype = new Animal(); //如果"猫"的prototype对象指向一个Animal的实例,那么所有"猫"的实例,就能继承Animal了。
Cat.prototype.constructor = Cat;
var cat1 = new Cat('小黄', '黄色');
alert(cat1.species)//打印出:动物
/*
注解:Cat.prototype = new Animal(); 这个new Animal()做了什么事情?
第一:创建了一个空对象,即:new Object()
第二:将Animal.prototype中的属性(键值对)拷贝到这个空对象中(我们前面提到,内部实现时不是拷贝而是一个隐藏的链接)
第三:将这个对象通过this关键字传递到构造函数中并执行构造函数
第四:将这个对象赋值给Cat.prototype
第五:通过以上步骤,这个Cat对象中就有了Animal的所有属性和方法了。
注解:Cat.prototype.constructor = Cat; 为什么要将Cat原型的构造器设置为Cat呢?
第一:原来,任何一个prototype对象都有一个constructor属性,指向它的构造函数。如果没有"Cat.prototype = new Animal();"这一行,Cat.prototype.constructor是原本是指向Cat的;但是加了这一行以后,Cat.prototype.constructor指向Animal。
可以通过 alert(Cat.prototype.constructor == Animal); //true 来验证。
第二:更重要的是,每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性。
可以通过: alert(cat1.constructor == Cat.prototype.constructor); // true 来验证
因此,在运行"Cat.prototype = new Animal();"这一行之后,cat1.constructor也指向Animal!
这显然会导致继承链的紊乱(cat1明明是用构造函数Cat生成的),因此我们必须手动纠正,将Cat.prototype对象的constructor值改为Cat。这就是Cat.prototype.constructor = Cat 的意思。
*/
</script>
特点:
1.非常纯粹的继承关系,实例是子类的实例,也是父类的实例
2.父类新增原型方法,原型属性,子类都能访问到
3.简单,易于实现
缺点:
1.要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
2.无法实现多继承
3.来自原型对象的引用属性是所有实例共享的
4.创建子类实例时,无法向父类构造函数传参
第二种:构造继承
首先我们让来了解下call方法与apply方法的作用:
//call方法与apply方法都的作用都是:调用一个对象的一个方法,以另一个对象替换当前对象
//call(需要替换的函数名称,参数一,参数二,参数N)
//apply(需要替换的函数名字,[参数数组])
//call 和 apply二者的作用完全一样,只是接受参数的方式不太一样。
function add(a, b) {
return a + b
}
function sub(a, b) {
return a - b;
}
alert(add.call(sub, 3, 1)); //打印出:4
alert(add.apply(sub, [3, 1]));//打印出:4
实例:
//构造继承
function Animal() {
this.species = "动物";
}
function Cat(name, color) {
Animal.apply(this,arguments);//Animal在 Cat中执行,实现了Cat继承Animal得功能
this.name = name;
this.color = color;
}
var cat1 = new Cat('小黄', '黄色');
alert(cat1.species)//打印出:动物
特点:
1.解决了原型链继承中,子类实例共享父类引用属性的问题
2.创建子类实例时,可以向父类传递参数
3.可以实现多继承(call多个父类对象)
缺点:
1.实例并不是父类的实例,只是子类的实例
2.只能继承父类的实例属性和方法,不能继承原型属性/方法
3.无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
第三种:组合继承(推荐)
核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
//组合继承
function Animal() {
this.species = "动物";
}
function Cat(name, color) {
Animal.apply(this);//Animal在 Cat中执行,实现了Cat继承Animal得功能 (这里调用了一次Animal的构造函数)
this.name = name;
this.color = color;
}
Cat.prototype = new Animal(); //将父类实例作为子类原型,实现函数复用(这里又调用了一次Animal的构造函数)
var cat1 = new Cat('小黄', '黄色');
alert(cat1.name)//打印出:动物
特点:
1.弥补了方式构造继承的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
2.既是子类的实例,也是父类的实例
3.不存在引用属性共享问题
4.可传参
5.函数可复用
6.仅仅多消耗了一点内存,性能好
缺点:
调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
继承方式:
1、拷贝继承:通用型 有new无new都可以用
2、类式继承:new构造函数---利用构造函数(类)继承的方式
3、原型继承:无new的对象---借助原型来实现对象继承对象
属性继承:调用父类的构造函数call
方法继承:用for in的形式 拷贝继承(jq也用拷贝继承)