------*本文默认读者已有面向对象语言(OOP)的基础*------
我们都知道在面向对象语言有三个基本特征 : 封装 ,继承 ,多态。而js初学者一般会觉得js同其他类C语言一样,有类似于Class这样的关键字可以让我们在js中更好的进行面向对象的操作。可事实并非如此。
严格地说,我们并不能称js是一种OOP语言,但是我们可以利用js里面的一些高级特性来进行OOP编程。
----封装
在js中,如何来创建一个对象呢?这非常简单,我们只需要new一个已封装好的函数(就是类C语言中的类),就可以实例化一个对象了。
那我们首先来构造这么一个"类",在构造之前必须知道一个类需要有"变量"和"方法",接着我们就来构造这个"类":
function Parent(){
this.name = "Parent";
this.sayName = function(){
console.log(this.name);
}
}
var p1 = new Parent();
p1.sayName();
怎么样,很简单吧?这样就封装好了一个"类"了。但是这个类看起来很笨重,因为名字是固定的,所以我们需要进行修改并扩展。
function Parent(name){
this.name = name;
this.sayName = function(){
console.log(this.name);
}
}
var p1 = new Parent("yxy");
p1.sayName(); //控制台打印yxy
这样我们封装的"类"就变得有变量,有方法,有外部参数,可复用。看上去已经非常完美了?
试着这样想想。每次创建一个对象,都会创建一个变量,同样也都会创建一个方法。而这个方法对所有对象都只是同一个方法效果,为什么你还要去对这个方法创建多次呢?学过java或c++的人可能会想,你怎么知道这个方法是被创建了多次,而不是引用的同一个呢?嗯?我们来做个测试。
function Parent(name){
this.name = name;
this.sayName = function(){
console.log(this.name);
}
}
var p1 = new Parent("yxy");
var p2 = new Parent("danshengou");
console.log(p1.sayName == p2.sayName); //false
利用"=="可以看到两个方法是不同的,那就是被创建了多次。所以当我们创建了多个对象后,每个对象的每个方法都是不同的!这显然会大大消耗内存,不利于web开发。那如何解决呢?
前面的文章中我提到的js中的"类"都是带双引号的,原因很简单,js不支持类(在ES6的规范中就可以支持了),但为了方便,我们可以称之为"伪类"。但在我们之前的例子中都还不能说得上是伪类!因为完全就没有方法的复用,不是吗?接下来我们会引入一个概念性很强的一个术语:"原型"。
原型,prototype,每一个函数都有一个原型属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。确实是非常不好理解。简单的说,原型属性就是通过调用构造函数而创建的那个对象实例的原型对象。如果你还不清楚,那我推荐你好好地看看<<javascript高级程序设计>>一书的第六章。
在这里先记住为什么我们要在封装中使用原型。如果你还对原型的概念模糊不清,不急,先理解使用它的好处。使用原型的好处是可以让所有对象实例共享它所包含的属性和方法。好像清晰多了?那我们来改装一下前面提到的那个例子。
function Parent(name){
this.name = name;
}
Parent.prototype.sayName = function(){
console.log(this.name);
}
var p1 = new Parent("yxy");
p1.sayName(); //调用原型中的sayName方法,打印出yxy
var p2 = new Parent("danshengou");
console.log(p1.sayName == p2.sayName); //true
console.log(p1.sayName === p2.sayName); //true
在这个例子中,我们用原型模式构造了一个函数。我们实例化了两个对象,用"=="和"==="比较了它的值和地址,发现都是一样的。看来原型真的是很好用啊,大大节省了我们的内存空间。你可能会问,为什么不把变量也放到原型中呢?因为原型是所有对象所共享的,每个对象的属性必须是该对象自己持有的,如果放到原型中,那这些对象便没有任何区别了。这就是我们所谓的"组合继承"。
到目前为止,我们就成功地创建了一个比较完美的js"伪类"了。而且你应该对原型有了很大的了解。
----继承
在你了解了整个封装过程后,你对js可能很失望了,既然有这样复杂的封装,那继承肯定也很难了。
可是,继承的语法其实很简单。
我们先来看看一个继承:
function Parent(name){
this.name = name;
}
Parent.prototype.sayName = function(){
console.log(this.name);
} function Child(name,position){
this.position = position;
Parent.call(this,name);
}
Child.prototype = new Parent();
Child.prototype.sayPos = function(){
console.log(this.position);
} var p1 = new Child("yxy","student");
p1.sayName();
p1.sayPos();
有两个地方很吸引我们眼球。
1.Parent.call(this,name)
2.Child.prototype = new Parent();
细讲可能容易绕晕,我从继承的概念入手,首先,子对象继承的是父对象的变量和方法。那我们可以很清晰地从代码作用范围看到,Parent.call(this,name)在构造函数中,显然是在继承变量。而Child.prototype = new Parent(),这个Parent对象不带参数地创建,显然是在继承原型。这样说是不是好理解多了?
Child.prototype = new Parent(),这条语句很简单,我不再去细究它。这里重点要讲的是Parent.call(this,name)这个东西。
Parent.call(this,name)还有另外几种写法:
1.Parent.call(this,arguments[0]);
2.Parent.apply(this,[name]);
3.Parent.apply(this,arguments);
虽然是四个写法都不同,但是效果都是一样的 : 将父类构造函数的上下文引用到子类的构造函数上下文中。
call和apply,都可以用来代替另一个对象调用一个方法。两者可将一个函数的对象上下文从初始的上下文改变为由this指定的新对象。又有点绕?简单地说,就是this对象在当前的上下文中执行了一次Parent函数,并且把参数(this后面那个参数)传递给Parent函数。
我们都知道函数怎么暴露自己内部的属性,就是把它执行一次,就可以在它的父级作用域中访问到。那理解这个apply,call的作用机理就很简单了。
继承语法很简单,但是其继承机制还是需要花时间去理解的。
终于我们把有关js面向对象的步骤给很详细的做了一次,可能大家觉得自己终于可以开始模拟一些有难度的面向对象的实例了。这一次我并不会反对大家,但是你难道没看出来少了什么吗?我来运行一下代码:
function Parent(name){
this.name = name;
}
Parent.prototype.sayName = function(){
console.log(this.name);
} function Child(name,position){
this.position = position;
//Parent.call(this,name);
Parent.apply(this,[name]);
}
Child.prototype = new Parent();
Child.prototype.sayPos = function(){
console.log(this.position);
} var p1 = new Child("yxy","student");
console.log(p1.name); //yxy
console.log(p1.position); //student
嗯?我明明想的是将变量私有化啊,可是为什么能访问到呢?细心地人可能早就在继承那个部分的时候就想到了,函数只要一执行,我的变量,方法都会暴露在外面了!其实这根本就不是封装啊!只是装而已。
没错,js并没有private,public这些关键字来定义属性和方法的私有和公有性质。可能你会很失望很失望,前面做了那么多,得来的是一个半瘸子的面向对象。其实不光你这么想,很多开发人员也觉得,因此他们用复杂的名字来命名一些变量,以保证其不会那么容易被访问到。这不失为一种办法,但是难道就没有别的办法?
仔细想想,还有什么办法能将模拟private,public这样的操作呢?我给出一个例子和一个概念,然后大家可以在这上开始自己对js真正面向对象的思考。
概念 : "模块模式"
代码 :
var Parent = function(name,publicAge){
var myName = name;
return {
age : publicAge,
sayName : function(){
console.log(myName);
}
};
}; var p1 = Parent("yxy","20");
p1.sayName(); //yxy
console.log(p1.age); //20
console.log(p1.myName); //undefined
当你能理解封装中访问等级的时候,javascript真正的思考才刚刚开始...
下一篇文章将会讲到"由封装引出的模块化思考以及闭包的运用"。