JavaScript中的面向对象设计
Building Objects
Object Literal
JS中构造Object的方式有很多种,比如使用Object Literal
var person = {
firstname: "John",
lastname: 'Doe',
greet: function(){
console.log('Hello' + this.firstname + ' ' + this.lastname)
}
};
person.greet();
Object Literal这种方式很直观,也很有表现力,并且它限制了this
的scope。
使用new
除了使用这种方式以外,也可以使用所谓的”构造函数”来创建一个对象,通常所说的造函数是指定义在某个类中用于完成对象初始化的函数,由于JavaScript没有类的概念,因此使用了一种特殊的函数来模拟构造函数
function Person(firstname, lastname){
this.firstname = firstname;
this.lastname = lastname;
}
var john = new Person("John","Doe");
可以看到,对于Person
和普通函数有几个不同的地方,分别是:
- 它的第一个字母是大写的,这个规则在JavaScript中表明它是一个”构造函数”
- 在它调用语句前面加上了一个关键字
new
- 函数内部没有
return
语句,但是它却有返回值。
显然这其中new
起到了关键作用,如果去掉new
,上述代码变为
function Person(firstname, lastname){
this.firstname = firstname;
this.lastname = lastname;
}
var john = Person("John","Doe");
这时,由于Person
没有返回值,因此john
为undefined
,而此时Person
为全局函数,里面的this
指向了window
。因此如果不使用new
,那么Person
就变成了一个普通函数。实际上,编译器在执行new
的时候,改变了Person
函数的执行逻辑,其过程如下:
- 创建一个空
object
- 创建
this
指向这个空object
- 在构造函数最后增加一行
return this
- 绑定
this.__proto__
为obejct.prototype
这里要注意的是,对于Person
,它是一个”构造函数”对象,由前一篇文章可知,它拥有一个prototype
成员,对于一个函数来说,只有它是”构造函数“时,这个prototype
对象才有意义。
使用Object.create
另一种创建Object的方法是使用Object.create(obj)
,这种方法是将obj
作为Prototype来构造新的对象
var person = {
firstname: "",
lastname: "",
greet: function(){
return this.firstname + ' ' + this.lastname;
}
}
var john = Object.create(person)
john.firstname="John"
john.lastname="Doe"
console.log(john.greet())
console.log(john.__proto__ == person) //true
上述例子中john
的Prototype对象为person
,因此它也具有person
的一系列属性。由于prototype
仅仅是个Object,因此john
同样也可以作为Prototype来继续构造其它对象
var jane = Object.create(john);
jane.firstname = "Jane"
jane.lastname = "Doe"
console.log(jane.greet()); //Jane Doe
使用Object.create
可以避免new
带来的一些问题,实际上使用new
并不是一个很好的方式,如果忘记写new
,那么程序在编译时不会报错,运行时也不会报错,这是极为危险的,不仅会造成隐蔽的bug,还污染了全局变量。
如果浏览器不支持Object.create
这时需要用到一个概念叫做Pollyfill,所谓”Pollyfill”是指如果用到的API浏览器不支持,我们需要自己实现这个API
if(!Object.create){
Object.create = function(o){
if(arguments.length>1){
throw new Error('Object.create implementation' + ' only accepts the first parameter. ')
}
function F(){}
F.prototype = o;
return new F();
}
}
Prototype
有了上面的铺垫,我们接着来理解Prototype,如果想要给上面的Person
函数对象增加一个成员greet
的方法,又不想修改Person
函数,则需要使用Prototype
Person.prototype.greet = function(){
console.log('Hello' + this.firstname + ' ' + this.lastname);
}
var john = new Person("John","Doe")
john.greet();
上面已经知道prototype
的类型是object
,而且每个函数对象数有一个成员叫做prototype
,让人困惑的是这个prototype
对象不仅可以被Person使用,还可以被其它类使用:
function Animal(name){
this.name = name;
}
//set prototype to Animal
Animal.prototype = Person.prototype;
var dog = new Animal("Pluto");
dog.greet(); //Hello undefined undefined
将Person
的prototype
赋给了Animal
,则dog
可以调用greet()
方法,由于dog
并没有firstname
和lastname
,因此输出undefined
。
Prototype设计的初衷是为了代码复用,通过让多个类共享Prototype来实现对公共API的抽象。对于不同的对象,如果他们share了同一个Prototype中的API,那么可以认为它们有共同的”父类”。但是这种机制看起来更像是Interface
或者Protocol
,而非继承。对于上面dog
和john
的例子,Person
和Animal
的关系是并列的,我们用Prototype
使dog
和john
都具有了greet
方法,因此这种结构更像是dog
和john
共享了某种Interface。
Inheritance
如果要模拟继承,我们可以换一个例子:
function Person(firstname, lastname){
this.firstname = firstname;
this.lastname = lastname;
}
Person.prototype.greet = function(){
return "Hello " + this.firstname + ' ' + this.lastname;
}
function Policemen(number){
this.number = number
}
Policemen.prototype = new Person("John","Doe")
var john = new Policemen("1234")
console.log(john.number) //1234
console.log(john.greet()) //Hello John Doe
这种写法和之前其实并无本质区别,都是改变Person.prototype
,但这种写法更像是继承关系,当join
调用greet
之后,会现在Person
中寻找,发现没有,然后在Person.prototype
中寻找。而Person.prototype
指向Parent
,感觉上像是在Person
的”父类“中寻找。因此这种写法相当于通过prototype
对象将两个对象链接起来了,如下图所示
JavaScript称这种方式的调用为Prototype Chain。当object调用一个方法,首先在自己的构造函数对象中寻找,找不到则在__proto__
中寻找,这个__proto__
对象也有自己的__proto__
会一直向上找,直到__proto__
对象为空,例如Array
的hasOwnProperty
方法:
Array arr = new Array()
arr. hasOwnProperty('length') //true
//hasOwnProperty这个方法定义在 Array.__proto__.__proto__中
回到上面的例子,虽然找到了类似”继承”的感觉,但是上代码不够优雅,甚至是有些丑陋,且模式无法复用,我们接下来可以想一些办法让上面代码稍微优雅一点,并且可以让”继承”的模式可以被复用起来,首先我们先写个Module:
//util.js
module.exports = {
inheritate : function(Child,Parent){
Child.prototype = Object.create(parent).prototype
Child.prototype.constructor = Child
}
}
显然,这个Module的作用是将”继承”这个动作抽象出来,但是仅有这个函数是不够的,它只能让Child
继承Parent
在prototype
上的API,而定义在Parent
内部的方法或者属性则无法被集成,因此我们还需要将this
指针进行关联:
//app.js
var util = require('./util')
var Person = require('./Person')
util.inheritate(Policemen,Person); //Policement是”基类“,Person是”父类“
function Policemen(firstname,lastname,number){ //需要传入”父类“需要的参数
//调用"父类"构造函数
Person.apply(this,[firstname,lastname])
this.number = number;
}
var john = new Policemen("John","Doe",1234)
console.log(john.number) //1234
console.log(john.greet()) //Hello John Doe
上述代码在可读性上有了很大的提高,并且Policemen
函数看起来有了点真正的构造函数的味道。但是上面的代码仍不够完美,比如当Policement
的构造参数多了,Policemen
自身也要修改,将这些参数透传给”父类“,这显然不利于程序扩展,因此,可以对上述代码再进行一次修改
function Policemen(options){ //需要传入”父类“需要的参数
//调用"父类"构造函数
Person.call(this,options)
this.number = option.number;
}
var john = new Policemen({firstname: "John", lastname:"Doe", number:1234})
console.log(john.number) //1234
console.log(john.greet()) //Hello John Doe
Overload Pitfalls
还是上面的例子,我们可以为Person
定义两个参数类型不同的greet
函数,
function Person(firstname, lastname){
this.firstname = firstname;
this.lastname = lastname;
}
Person.prototype.greet = function(){
console.log('Hello ' + this.firstname + ' ' + this.lastname);
}
Person.prototype.greet = function(msg){
console.log(msg);
}
var john = new Person("John","Doe");
john.greet(); //undefined
john.greet("Hello"); //Hello
按照其它语言的经验,两个greet
的函数签名不同,应该会各自调用不同版本的函数,而在JS中,第二个greet
函数则会覆盖第一个函数。这点要格外注意,解决办法是定义不同名称的函数。
Reflection
JavaScript的设计如此灵活,自然少不了强大的反射能力,这种动态语言所具备的优良特性对于静态语言来说简直是梦寐以求的神器,但是也会带来一定的安全问题,这里就不展开讨论了。这一节我们来试验一下JavaScript中关于反射相关的API
继续上面的例子,在JS中我们可以很容易反射出john
的所有成员
var person = {
firstname: 'Default',
lastname: 'Default',
getFullName: function(){
var fullname = this.firstname + ' ' + this.lastname;
return fullname;
}
}
var john = {
firstname: 'John',
lastname: 'Doe',
}
john.__proto__ = person
for(var key in john){
console.log(key+": "+john[key])
}
由于john
的__proto__
指向person
,上述代码会打印出john
所有属性,连同其在__proto__
上的方法:
firstname: John
lastname: Doe
getFullName: function (){
var fullname = this.firstname + ' ' + this.lastname;
return fullname;
}
如果只想输出自己的属性,需要使用hasOwnProperty
for(var key in john){
if(john.hasOwnProperty(prop)){
console.log(key+": "+john[key])
}
}
//firstname: John
//lastname: Doe
同样,可以单独反射其__proto__
的成员
for(var key in john.__proto__){
if(john.hasOwnProperty(prop)){
console.log(key+": "+john[key])
}
}
/*
firstname: Default
lastname: Default
getFullName: function (){
var fullname = this.firstname + ' ' + this.lastname;
return fullname;
}
*/
反射的另一个用处是做运行时的类型检查,在JavaScript中使用typeof
查看对象的类型
var a = "Hello"
console.log(typeof a) //string
var b = {}
console.log(typeof b) //object
var c = []
console.log(typeof c) //object
console.log(Object.prototype.toString.call(c)) //[Object Array]
最后我们可以用Object
上的一些API来实现“merge”两个Object
const obj1 = {
name: 'Jason',
greet: msg => {
console.log(msg);
}
};
const obj2 = {};
Object.getOwnPropertyNames(obj1).forEach(name => {
console.log(name); //name, greet
const descriptor = Object.getOwnPropertyDescriptor(obj1, name);
Object.defineProperty(obj2, name, descriptor);
});
console.log(obj2.name);
obj2.greet('msg');