Javascript 面向对象编程
最原始的类和对象声明方法
类声明如下所示:
var Cat = { name : '', color : '' }
如果我们要声明Cat类的实例的话,则代码如下所示:
var cat1 = {}; // 创建一个空对象 cat1.name = "大毛"; // 按照原型对象的属性赋值 cat1.color = "黄色"; var cat2 = {}; cat2.name = "二毛"; cat2.color = "黑色";
但是这样有两个显著的缺点:
- 如果多生成几个实例,写起来就非常麻烦
- 实例与原型之间,没有任何办法,可以看出有什么联系。
构造函数模式创建对象
事实上构造函数跟普通函数没有任何区别,唯一的技巧在于内部使用了this变量,这样对对构造函数使用new运算符,就能生成实例,然后this变量会绑定在实例对象上。
那么上述的代码我们可以使用构造函数这样来写:
function Cat(name,color){//构造函数 this.name=name; this.color=color; this.type = "猫科动物"; this.eat = function(){alert("吃老鼠");}; } //声明类实例 var cat1 = new Cat("大毛","黄色"); var cat2 = new Cat("二毛","黑色");//cat1和cat2会自动含有一个constructor属性,指向它们的构造函数。 console.log(cat1.constructor == Cat); //true console.log(cat2.constructor == Cat); //true //Javascript还提供了一个instanceof运算符,验证原型对象与实例对象之间的关系。 console.log(cat1 instanceof Cat); //true console.log(cat2 instanceof Cat); //true
大家有没有想过构造函数的缺点,那就是资源浪费。如上述代码所示,那么每个对象都会有一个属于自己的name和color属性,对于属性这样是无可厚非,可是如果是方法呢,每个对象都会有一个属于自己的但实现相同功能的方法,这样难道不是大大的资源浪费吗?
大家注意到上面的代码“cat1.constructor == Cat”
解释一下哦:任何一个prototype对象都有一个constructor属性,指向它的构造函数。每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性。
构造函数绑定,即用call(apply)把父对象的this指向改为子对象,
Prototype模式
Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。这样我们就可以把所有的属性放在构造函数当中,而把所有的方法放在Prototype上。
所以上述代码可以如下使用,
function Cat(name,color){//声明类 this.name = name;//所有属性都放在构造函数里面 this.color = color; } //所有方法都放在prototype链上 Cat.prototype.type = "猫科动物"; Cat.prototype.eat = function(){alert("吃老鼠")}; var cat1 = new Cat("大毛","黄色"); var cat2 = new Cat("二毛","黑色"); console.log(cat1.eat == cat2.eat); //true 这个时候不同实例共享原型链上的同一个方法
如何检测对象和实例的关系呢,有如下三种方法
console.log(Cat.prototype.isPrototypeOf(cat1)); //true
alert(cat1.hasOwnProperty("name")); // true
alert("name" in cat1); // true
hasOwnProperty和in唯一的区别就在于如果是从原型链上继承的属性in也会返回true,而hasOwnProperty则返回false。
上面不只是介绍说如何声明类以及实例化类的对象,那下面讲讲各个类如何继承。
介绍如下几种:
构造函数继承
prototype模式继承
拷贝继承
构造函数继承
function Animal(){//父类 this.species = "动物"; } function Cat(name,color){//子类 Animal.apply(this, arguments);//这句话就实现了继承 使用call或apply方法,将父对象的构造函数绑定在子对象上 this.name = name; this.color = color; }
prototype模式继承
function Animal(){//父类 this.species = "动物"; } function Cat(name,color){//子类 this.name = name; this.color = color; } Cat.prototype = new Animal(); Cat.prototype.constructor = Cat;
prototype模式继承的经典在于让子类的prototype指向父类的实例,因为每个prototype对象都会有一个constuctor来指向它的构造函数,而上述的“Cat.prototype = new Animal();”这句代码就使得prototype的constructor属性指向了Animal,大家想想这样不就乱套了吗,所以才用了“Cat.prototype.constructor = Cat;”来修正
大家想想上述代码有什么缺点,那就是每个子类都必须声明一个父类的实例,这样启不是资源的浪费。所以我们如果使用prototype模式来继承一般会使用如下方法。
function extend(Child, Parent) { var F = function(){};//声明一个空对象 F.prototype = Parent.prototype;//把空对象的原型链指向父对象的原型链 事实上F类就相当于Parent类 Child.prototype = new F();//依旧子对象的prototype为父对象的实例 只不过这时是一个空对象而已 节省了资源 Child.prototype.constructor = Child;//修正原型链 Child.uber = Parent.prototype;//在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质 } //使用的时候如下所示 extend(Cat,Animal);//Cat继承Animal var cat1 = new Cat("大毛","黄色");//实例化对象
拷贝继承
function extend(Child, Parent) { var p = Parent.prototype; var c = Child.prototype; for (var i in p) { c[i] = p[i]; } c.uber = p; }
拷贝继承的方式有些类似其它语言如C#的思想,循环遍历属性然后给另外一个对象赋值,似乎这样也能实现继承,但是大家有没有想过上述方法的弊端,如果属性是引用类型的呢,那是不是又乱了,修改了一个实例的引用类型的属性就导致其它对象的相应属性的值改变,这样的继承只能说是不彻底的。那么我们应该怎么样实现深度拷贝继承呢
function deepCopy(p, c) { var c = c || {}; for (var i in p) { if (typeof p[i] === 'object') { c[i] = (p[i].constructor === Array) ? [] : {}; deepCopy(p[i], c[i]); } else { c[i] = p[i]; } } return c; }
事实上JQuery库就是使用的深拷贝继承,大家可以想想为什么,因为拷贝继承很适合传递参数,大家想想编写jQuery插件时,写一些配置参数的时候是不是如下使用,结合想想会不会明白些许
options = $.extend({ name: 'Default Name', age: 22, man: true }, options);
object()方法继承
function object(o) { function F() {}//临时声明一个构造函数 使这个构造函数的PROTOTYPE属性指向父对象,从而使得子对象与父对象连在一起,然后生成一个实例并返回把子对象的prototype属性, F.prototype = o; return new F(); }
参考文献为阮大师的 JavaScript面向对象编程系列
最近发现阮大师的文章真的很值得一看,大家有空可以看看,他的文采很好,“散文”和“观点和感想”类的文章很多都写的很不错。真心有些小佩服他了,不过与其佩服别人不如做自己佩服的人。