在这篇文章中,将介绍一下javascript的面向对象的程序设计。Javascript是脚本语言,其本质上和C++,C#这样面向对象的语言还是有区别的,其没有类的概念,在本质上都是以对象的形式存在。 我是一个新手,写这篇文章的目的主要是加深自己的理解以及学习分享。
对象是什么
在本质上,Javascript对象是一组无序属性的集合,属性可以是基本值类型,函数,对象, c可以理解为一个散列表, 关键字(key)是属性名,值(value)为属性名。
没有类,但是我们可以自己创建自定义对象,如下的例子:
var persion = new Object();
person.name = "Jesse";
person.age = 30;
person.job = "hr";
person.getName = function(){
return this.name
}
我们没有创建Person的类,但是我们可以以Object为基础,创建一个person的对象,让他具备name,age, job等属性和行为(getName), 从而具备类的功能特点 (数据 + 行为)。
对象既然是属性的集合,那么有那些属性类型呢?
Javascript具有两种属性,数据属性和访问器属性,分别介绍如下:
数据属性
数据属性就是包含属性值的位置,在该位置上可以读,写属性值,在行为上有【Configurable】, 【Enumerable】【Writable】【Value】这四种特性:
【Configurable】表示该属性是否可以删除(delete),能否修改属性的特性,默认值为true
【Enumerable】表示属性是否可以在for-in 中返回,默认都true
【Writable】 表示属性是否可写,默认为true
【Value】是表示具体的值,上面的三个属性都是描述控制权限相关,这个才是具体的值,默认为undefined。
具体的用法如下:
var person = {};
Object.defineProperty(person, "name", {
writable:false,
value:"Jesse",
});
alert (person.name) // Jesse
person.name = "test"; // 由于属性不可修改,所以值还是Jesse
alert (person.name) // Jesse
所有需要修改属性值就调用Object.defineProperty方法,第一个参数为对象,第二个参数为属性名,最后一个参数为属性名和值的集合(专业名字为descriptor)。具体用法可以参考文档https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
- 访问器属性
访问器属性不包含数据值,但是包含【get】与【set】属性函数,这两个函数不是必须的,其基本上是定义了可以如何读取和写入数据。具体的实例代码如下:
var book = {
year: 2000,
edition : 1
}
Object.defineProperty(book, "year"), {
get: function () { return this.year;}
set: function (newValue){
if(newValue > 2000)
{
this.year = newValue;
this.edition = newValue - 2000;
}
}
} );
book.year = 2002;
alert (book.edition) // 2
本质上除了value属性外,其他的属性都是提供”访问权限“相关的控制,从而实现“类”一样的功能和设计。
如何创建对象
创建对象有很多不同的方法,每个方法都有不同的优缺点,在实际中学会灵活运用
- 工厂模式
工厂模式其实说的设计模式中的一种思维,将创建对象的代码封装在一个函数内,例子代码如下:
function createPerson(name, age, job){
var ret = new Object;
ret.name = name;
ret.age = age;
ret.job = job;
ret.getName = function(){
return this.name;
};
return ret;
}
var person1 = createPerson ("Jesse", 30, "hr");
var person2 = createPerson ("JesseCopy", 30, "SE");
缺点,无法确认相似对象的“类型”。
- 构造器模式
构造函数的实例代码如下:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.getName = function(){
return this.name
};
}
var person1 = new Person ("Jesse", 30, "hr");
var person2 = new Person ("JesseCopy", 30, "SE");
很容易在实现上看出与工厂模式的区别:
1) 没有显示的创建object对象
2) 引入了this对象
3)没有return语句
基本的构造器用法如下:
// 当作构造函数用
var person = new Person ("Jesse", 30, "hr");
person.getName () // "Jesse"
// 当作函数用
Person("Nick", 20, 'tester');
window.getName()// "Nick"
// 在一个对象的作用域中使用
var o = new Object();
Person.call(o, 'Greg', 23, 'student');
o.getName () // "Greg"
缺点:每个方法在每个对象上都需要重新创建一次。也就是说: alert(person1.getName == person2.getName) // false
- 原型模式
我们创建的每个函数都有一个prototype属性,这个属性的指针指向一个对象,而这个对象的作用是包含的所有特定类型的所有对象共享的属性与方法。原型对象的好处是可以让实例对象共享所有的属性与方法。例子代码如下:
function Person(){
}
Person.prototype.name = "Jesse";
Person.prototype.age= 25;
Person.prototype.job = "hr";
Person.prototype.getName = function(){return this.name;}
var person1 = new Person();
var person2 = new Person;
person1.name // "Jesse"
person2.name // "Jesse"
person1.getName() == person2.getName // true
上述的实例关系如下图:
对于原型和具体的实例,我们需要注意一下几点:
1)hasOwnProperty方法可以检查,属性是属于本实例还是原型,如person1.hasOwnProperty(”name”) 会返回false。
2) in操作符: “name” in person1 会返回true,所以,确定一个属性是否是原型中的属性,可以用下面的函数:
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
3) 更简洁的原型语法
function Person(){
}
Person.prototype = {
name: "Jesse",
age: 25,
job: "hr",
getName: function (){return this.name;}
}
需要说明的是上面代码存在陷阱,上面会导致contructor属性重写,看下面代码的返回值:
var person = new Person();
person instanceof Object // true
person instanceof Person // true
person.constructor == Person // false
person.constructor == Object// true
可以重写上面代码,让其指向Person:
function Person(){
}
Person.prototype = {
constructor: Person, // add this one
name: "Jesse",
age: 25,
job: "hr",
getName: function (){return this.name;}
}
4) 原型对象问题: 原型对象创建无法传递参数,导致所有的实例在默认情况下的值都是一样的。
function Person(){
}
Person.prototype = {
constructor: Person, // add this one
name: "Jesse",
age: 25,
job: "hr",
getName: function (){return this.name;}
friends: ["Jesse1", "Jesse2"],
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Jesse3");
person1.frineds == person2.friends == "Jesse1, Jesse2, Jesse3";
- 组合使用构造和原型
创建对象常见的方式就是使用构造函数模式和原型模式,构造函数模式用于定义属性,而原型模式定义方法和共享的属性。基本的实例如下:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = {"Jesse1", "Jesse2"};
};
Person.prototype = {
constructor:Person,
getName: function(){
return this.name
},
};
这种创建对象的方式是目前使用最广泛,认同度最高的一种模式。
- 动态原型模式
动态原型模式是将原型封装在构造函数里面,在构造函数中初始化原型:
function Person(name, age, job){
// 属性
this.name = name;
this.age = age;
this.job = job;
// 方法
if(typeof this.getName != "function"){
Person.prototype.getName = function () {
return this.name;
};
};
};
缺点,不能使用字符量的形式重新定义原型
- 寄生构造函数模式
在前面几种方式都不适用的情况下,可以考虑寄生模式,这种模式的基本思想是封装函数返回创建的对象,但是从表面上看,和构造函数没有区别:
function Person(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.getName = function () {return this.name};
return o;
}
var friend = new Person ("Jesse", 29, "hr");
friend.getName() // Jesse
仔细一看,除了用new包装了一下构造函数,其实和工厂模式是一模一样的,缺点是,创建的对象其实和构造函数的原型没有一点关系了。 没有特别的情况,一般不用这样的模式来创建对象。
- 稳妥构造函数模式
稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;二是不使用new操作符调用构造函数。
function Person(name, age, job) {
var o = new Object();
// 创建私有的变量
var namePivate = name
var agePrivate = age;
.....
// public members
o.getName = function() {
return namePivate;
};
return o;
}
var person = Person ("Jesse", 25, "hr");
person.getName() // "Jesse"
person是一个稳妥对象,没有任何方法可以直接修改属性,所以能提供一定的安全性,但是其创建的对象和构造函数没有关系。
继承
继承是面向对象设计中最基本的概念,其分为接口继承和实现继承两种,接口继承继承的是签名,而实现继承是继承来实现的方法, 由于javascript没有函数签名,所以没有接口继承,只有实现继承,其通过原型链的方式来实现。
- 原型链
回忆一下前面的构造函数,原型与对象的关系可以得出,原型其实可以看作是对象的”基类“,举例如下代码:
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
}
function SubType(){
this.subProperty = false;
}
subType.prototype = new SuperType();
subType.prototype.getSubValue = function(){
return this.subProperty;
};
var instance = new Subtype();
instance.getSuperValue(); // true;
上述代码的原型链的关系如下:
所以SubType的原型是SuperType,其就可以使用其方法和变量了。实现了原型链,本质上就扩展了搜索的过程,javascript中在SubType原型链中按照下面的策略寻找方法与变量
1) 在本对象中查找
2)在SubType.prototype中查找
3)在SuperType.prototype中查找,其会按照原型链一直查找下去。
其实,上图中还缺少一个默认的原型链,任何的对象的原型其实都是Object,扩展后的原型链如下:
原型链存在的问题:
1) 原型的属性被子类型完全的共享,无法定制。
2)子类型也无法向原类型传递参数。
基于上面的问题,原型链在实际中很少直接用。
- 借用构造函数
这种思想比较简单,利用子类型的函数调用超类型的构造函数。基本的示例代码如下:
funtion SuperType(){
this.colors = {"R", "G", "B"};
}
function SubType(){
// 继承了SuperType属性
SuperType.call (this);
}
var instance1 = new SubType();
instance1.colors.push('T');
var instance2 = new SubType();
instance1.colors // 'R' ,'G' ,'B' ,'T'
instance2.colors // 'R' ,'G' ,'B' ,
借用构造函数可以解决参数传递和属性共享问题,但无法复用方法。在实际的继承中,很少单独使用。
- 组合继承
组合继承就是使用了原型链和借用构造的方式来实现继承,这是Javascript中常用的一种模式。基本的实例代码如下:
function SuperType(name) {
this.name = name;
this.color = ['red', 'blue', 'green'];
}
SuperType.prototype.getName () = function(){return this.name;}
function SubType(name, age)
{
// 继承属性
SuperType.call(this, name);
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.getAge = function() {return this.age;}
var instance1 = new SubType("Jesse", 25);
instance1.colors.push("black");
instance1.colors // 'red', 'blue', 'green', "black"
instance1.getName() // Jesse
instance1.getAge() // 25
var instance2 = new SubType("JesseCopy", 27);
instance2.colors // 'red', 'blue', 'green'
instance2.getName() // JesseCopy
instance2.getAge() // 27