2.2 包装明星——封装
2.2.1 创建一个类
“在JavaScript中创建一个类很容易,首先声明一个函数保存在一个变量里。按编程习惯一般将这个代表类的变量名首字母大写。然后在这个函数(类)的内部通过对this(函数内部自带的一个变量,用于指向当前这个对象)变量添加属性或者方法来实现对类添加属性或者方法,例如:”
var Book = function(id, bookname, price){
this.id = id;
this.bookname= bookname;
this.price = price;
}
“也可以通过在类的原型(类也是一个对象,所以也有原型prototype)上添加属性和方法,有两种方式,一种是一一为原型对象属性赋值,另一种则是将一个对象赋值给类的原型对象。但这两种不要混用。例如:”
Book.prototype.display = function(){
// 展示这本书
};
或者
Book.prototype = {
display : function(){}
};
“这样我们将所需要的方法和属性都封装在我们抽象的Book类里面了,当使用功能方法时,我们不能直接使用这个Book类,需要用new关键字来实例化(创建)新的对象。使用实例化对象的属性或者方法时,可以通过点语法访问,例如:”
var book = new Book(10, 'JavaScript设计模式', 50);
console.log(book.bookname) // JavaScript设计模式
小白看了看对类添加的属性和方法部分,感觉不是很理解,于是问:“通过this添加的属性和方法同在prototype中添加的属性和方法有什么区别呀?”
“通过this添加的属性、方法是在当前对象上添加的,然而JavaScript是一种基于原型prototype的语言,所以每创建一个对象时(当然在JavaScript中函数也是一种对象),它都有一个原型prototype用于指向其继承的属性、方法。这样通过prototype继承的方法并不是对象自身的,所以在使用这些方法时,需要通过prototype一级一级查找来得到。这样你会发现通过this定义的属性或者方法是该对象自身拥有的,所以我们每次通过类创建一个新对象时,this指向的属性和方法都会得到相应的创建,而通过prototype继承的属性或者方法是每个对象通过prototype访问到,所以我们每次通过类创建一个新对象时这些属性和方法不会再次创建。(如图2-1所示)。”
“哦,对了,解析图中的constructor又是指的什么呀。”
“constructor是一个属性,当创建一个函数或者对象时都会为其创建一个原型对象prototype,在prototype对象中又会像函数中创建this一样创建一个constructor属性,那么constructor属性指向的就是拥有整个原型对象的函数或对象,例如在本例中Book prototype中的constructor属性指向的就是Book类对象。”
2.2.2 这些都是我的——属性与方法封装
“原来是这样,”小白似乎明白些,“面向对象思想在学校里也学过,说的就是对一些属性方法的隐藏与暴露,比如私有属性、私有方法、共有属性、共有方法、保护方法等等,那么JavaScript中也有这些么?”
“你能想到这些很好。说明你有一定面向对象的基础了。不过你说的这些在JavaScript中没有显性的存在,但是我们可以通过一些灵活的技巧来实现它。”小铭继续解释说,“面向对象思想你可以想象成一个人,比如一位明星为了在社会中保持一个良好形象,她就会将一些隐私隐藏在心里,然而对于这位明星,她的家人认识她,所以会了解一些关于她的事情。外界的人不认识她,即使外界人通过某种途径认识她也仅仅了解一些她暴露出来的事情,不会了解她的隐私。如果想了解更多关于她的事情怎么办?对,还可以通过她的家人来了解,但是这位明星自己内心深处的隐私是永远不会被别人知道的。”
“那么在JavaScript中又是如何实现的呢?”小白问。
“由于JavaScript的函数级作用域,声明在函数内部的变量以及方法在外界是访问不到的,通过此特性即可创建类的私有变量以及私有方法。然而在函数内部通过this创建的属性和方法,在类创建对象时,每个对象自身都拥有一份并且可以在外部访问到。因此通过this创建的属性可看作是对象共有属性和对象共有方法,而通过this创建的方法,不但可以访问这些对象的共有属性与共有方法,而且还能访问到类(创建时)或对象自身的私有属性和私有方法,由于这些方法权利比较大,所以我们又将它看作特权方法。在对象创建时通过使用这些特权方法我们可以初始化实例对象的一些属性,因此这些在创建对象时调用的特权方法还可以看作是类的构造器。如下面的例子。”
// 私有属性与私有方法,特权方法,对象公有属性和对象共有方法,构造器
var Book = function(id, name, price){
//私有属性
var num = 1;
//私有方法
function checkId(){
};
//特权方法
this.getName = function(){};
this.getPrice = function(){};
this.setName = function(){};
this.setPrice = function(){};
//对象公有属性
this.id = id;
//对象公有方法
this.copy = function(){};
//构造器
this.setName(name);
this.setPrice(price);
};
小白心中暗喜:“原来是这样呀,通过JavaScript函数级作用域的特征来实现在函数内部创建外界就访问不到的私有化变量和私有化方法。通过new关键字实例化对象时,由于对类执行一次,所以类的内部this上定义的属性和方法自然就可以复制到新创建的对象上,成为对象公有化的属性与方法,而其中的一些方法能访问到类的私有属性和方法,就像例子中家人对明星了解得比外界多,因此比外界权利大,因而得名特权方法。而我们在通过new关键字实例化对象时,执行了一遍类的函数,所以里面通过调用特权方法自然就可以初始化对象的一些属性了。可是在类的外部通过点语法定义的属性和方法以及在外部通过prototype定义的属性和方法又有什么作用呢?”
“通过new关键字创建新对象时,由于类外面通过点语法添加的属性和方法没有执行到,所以新创建的对象中无法获取他们,但是可以通过类来使用。因此在类外面通过点语法定义的属性以及方法被称为类的静态共有属性和类的静态共有方法。而类通过prototype创建的属性或者方法在类实例的对象中是可以通过this访问到的(如图2.1新创建的对象的proto指向了类的原型所指向的对象),所以我们将prototype对象中的属性和方法称为共有属性和共有方法,如:”
//类静态公有属性(对象不能访问)
Book.isChinese = true;
//类静态公有方法(对象不能访问)
Book.resetTime = function(){
console.log('new Tiem')
};
Book.prototype = {
//公有属性
isJSBook : false,
//公有方法
display : function(){}
}
“通过new关键字创建的对象实质是对新对象this的不断赋值,并将prototype指向类的prototype所指向的对象,而类的构造函数外面通过点语法定义的属性方法是不会添加到新创建的对象上去的。因此要想在新创建的对象中使用isChinese就得通过Book类使用而不能通过this,如Book.isChinese,而类的原型prototype上定义的属性在新对象里就可以直接使用,这是因为新对象的prototype和类的prototype指向的是同一个对象。”
于是小白半信半疑地写下了测试代码:
var b = new Book(11,'JavaScript设计模式',50);
console.log(b.num); // undefined
console.log(b.isJSBook); // false
console.log(b.id); // 11
console.log(b.isChinese); // undefined
“真的是这样,类的私有属性num以及静态共有属性isChinese在新创建的b对象里是访问不到的。而类的共有属性isJSBook在b对象中却可以通过点语法访问到。”
“但是类的静态公有属性isChinese可以通过类的自身访问。”
console.log(Book.isChinese); // true
Book.resetTime(); // new Tiem
2.2.3 你们看不到我——闭包实现
“有时我们经常将类的静态变量通过闭包来实现。”
// 利用闭包实现
var Book = (function() {
//静态私有变量
var bookNum = 0;
//静态私有方法
function checkBook(name) {
}
//返回构造函数
return function(newId, newName, newPrice) {
//私有变量
var name, price;
//私有方法
function checkID(id){}
//特权方法
this.getName = function(){};
this.getPrice = function(){};
this.setName = function(){};
this.setPrice = function(){};
//公有属性
this.id = newId;
//公有方法
this.copy = function(){};
bookNum++
if(bookNum > 100)
throw new Error('我们仅出版100本书.');
//构造器
this.setName(name);
this.setPrice(price);
}
})();
Book.prototype = {
//静态公有属性
isJSBook : false,
//静态公有方法
display : function(){}
};
“小白,你知道闭包么?”
“不太了解。你能说说么?”
“闭包是有权访问另外一个函数作用域中变量的函数,即在一个函数内部创建另外一个函数。我们将这个闭包作为创建对象的构造函数,这样它既是闭包又是可实例对象的函数,即可访问到类函数作用域中的变量,如bookNum这个变量,此时这个变量叫静态私有变量,并且checkBook()可称之为静态私有方法。当然闭包内部也有其自身的私有变量以及私有方法如price,checkID()。但是,在闭包外部添加原型属性和方法看上去像似脱离了闭包这个类,所以有时候在闭包内部实现一个完整的类然后将其返回,看下面的例子。”
// 利用闭包实现
var Book = (function() {
//静态私有变量
var bookNum = 0;
//静态私有方法
function checkBook(name) {}
//创建类
function _book(newId, newName, newPrice) {
//私有变量
var name, price;
//私有方法
function checkID(id){}
//特权方法
this.getName = function(){};
this.getPrice = function(){};
this.setName = function(){};
this.setPrice = function(){};
//公有属性
this.id = newId;
//公有方法
this.copy = function(){};
bookNum++
if(bookNum > 100)
throw new Error('我们仅出版100本书.');
//构造器
this.setName(name);
this.setPrice(price);
}
//构建原型
_book.prototype = {
//静态公有属性
isJSBook : false,
//静态公有方法
display : function(){}
};
//返回类
return _book;
})();
“哦,这样看上去更像一个整体。”
2.2.4 找位检察长——创建对象的安全模式
“对于你们初学者来说,在创建对象上由于不适应这种写法,所以经常容易忘记使用new而犯错误。”
“可是对于我们来说,这种错误发生也是不可避免的,毕竟不像你们工作了这么多年。但是你有什么好办法么?”
“哈哈,那是当然,如果你们犯错误有人实时监测不就解决了么,所以赶快找一位检察长吧。比如JavaScript在创建对象时有一种安全模式就完全可以解决你们这类问题。”
// 图书类
var Book = function(title, time, type){
this.title = title;
this.time = time;
this.type = type;
}
// 实例化一本书
var book = Book('JavaScript', '2014', 'js');
“小白,你猜book这个变量是个什么?”
“Book类的一个实例吧。”为了验证自己的想法,小白写下测试代码。
console.log(book); // undefined
“怎么会是这样?为什么是一个undefined(未定义)?”小白不解。
“别着急,你来看看我的测试代码。”
console.log(window.title); // JavaScript
console.log(window.time); // 2014
console.log(window.type); // js
“怎么样发现问题了么”,小铭问道。
“明明创建了一个Book对象,并且添加了title、time、type3个属性,怎么会添加到window上面去了,而且book这个变量还是undefined。”小白又看了看实例中的代码恍然大悟,“哦,原来是忘记了用new关键字来实例化了,可是为什么会出现这个结果呢?”
“别着急,首先你要明白一点,new关键字的作用可以看作是对当前对象的this不停地赋值,然而例子中没有用new,所以就会直接执行这个函数,而这个函数在全局作用域中执行了,所以在全局作用域中this指向的当前对象自然就是全局变量,在你的页面里全局变量就是window了,所以添加的属性自然就会被添加到window上面了,而我们这个book变量最终的作用是要得到Book这个类(函数)的执行结果,由于函数中没有return语句,这个Book类自然不会告诉book变量的执行结果了,所以就是undefined(未定义)。”
“原来是这样,看来创建时真是不小心呀,可是该如何避免呢?”小白感叹道。
“‘去找位检察长’呀,哈哈,使用安全模式吧。”
// 图书安全类
var Book = function(title, time, type){
// 判断执行过程中this是否是当前这个对象(如果是说明是用new创建的)
if(this instanceof Book){
this.title = title;
this.time = time;
this.type = type;
// 否则重新创建这个对象
}else{
return new Book(title, time, type);
}
}
var book = Book('JavaScript', '2014', 'js');
“好了小白,测试一下吧。”
console.log(book); // Book
console.log(book.title); // JavaScript
console.log(book.time); // 2014
console.log(book.type); // js
console.log(window.title); // undefined
console.log(window.time); // undefined
console.log(window.type); // undefined
“真的是这样呀,太好了,再也不用担心创建对象忘记使用new关键字的问题了。”
“好了说了很多,你也休息一下,好好回顾一下,后面还有个更重要的面向对象等着你——继承,这可是许多设计模式设计的灵魂。”