面向对象(Object - Oriented)OO 是现在高级程序语言必备的技能,面向对象一般封装,继承,多态等特性,JavaScript不是高级语言,但也用自己的方式实现了面向对象的部分功能。
ECMA-262把对象定义为 无序属性的集合,其属性可以包含基本值、对象、或者函数-----JavaScript的对象在我们看来就是一个键值对,值可以是数据或函数。下面我们讨论在ES6之前的对象和继承的实现。
理解对象
我们来创建一个对象万能的Person
var person = new Object();
person.name = "lee";
person.age = 16;
person.sayName = function(){
console.log(this.name);
}
变形后:
var person={
name:'lee',
age:16,
sayName:function(){
console.log(this.name)
}
}
这个对象有2个属性和1个方法,说道属性我们先提出一个现在非常火的VUE,VUE有一个响应式原理核心Obejct.defineProperty()原话是这样的(当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter),这里对象中的属性和我们今天讲的是一样的。下面我们来看看属性的特性
属性分为数据属性和访问器属性
数据属性
数据属性有4个特性:configuerable enumerable writable vaule,分别的意思就是属性是否delete默认为true,能否用过for-in循环,能否修改属性的值,属性的属性值。
我们可以对特性进行修改:
点击查看详情 Object.defineProperty(obj, prop, descriptor)
obj
要在其上定义属性的对象。
prop
要定义或修改的属性的名称。
descriptor
将被定义或修改的属性描述符。
var person = {}; // 定义一个空对象
Object.defineProperty(person,'name',{
wirtable:false,//只读属性不可修改
value:'lee'
});
console.log(person.name);// lee
person.name="egg";//严格模式下抛出异常
console.log(person.name);// lee
如果是修改configurable属性,设置一次后就不能再次配置,并且 除了修writable属性其他的都不能进行修改。
访问器属性
访问器属性也有4个特性 configurable enumerable get set
var person ={
_name:'lee',
age:16
};
Object.defineProperty(book,'name',{
get:function(){
return this._name;
},
set:function(newName){
if(newName == "wang"){
this._name = newName;
this.age = 18;
}
}
})
person.name = 'wang';
console.log(person.age) //18
我们给name赋值,通过set方法对_name 和 age 都进行了更新,在vue中实现数据响应式相应就是通过这个方法。具体详细的可以通过点击Object.defineProperty查看。
上面的内容可以让我们对对象进行一个深入的认识,虽然平时我们用到的很少,学习上面的内容可以让我们对底层数据的处理更深刻。
创建对象
new Object();
不推荐,Object 是JavaScript的一个超类(实际中JavaScript没这么定义)他是JavaScript自己定义好的一个类,其实我们所有的创建对象Object都是最终基于Object。代码书写方式也不符合我们的习惯,不便于我们进行封装。
工厂模式
function creatPerson(name,age){
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function(){
console.log(this.name)
};
return o
}
工厂模式其实上面的一种变体,解决了创建多个相似对象的问题,没解决对象识别的问题。
构造函数模式
function Person(name,age){
this.name = name;
this.age = age;
this.sayName = function(){
console.log(this.name)
}
}
var person = new Person('lee',16);
构造函数模式没有显示的创建对象,直接把属性赋值给了this对象,通过new方法创建了一个实例,this就指向了这个实例,这样我们就把对象分离开了。
在我们使用过程中会出现这样的问题,每次我们new一个对象出来的时候都会有一个sayName的方法,更多的时候我们想是共享一个方法,不用每次都出现。如果我们定义很多方法比如什么sayAge,当方法多了以后就完全失去了我们的封装性,后面我们会解决这个问题就是原型和构造组合模式,我们先来看看原型模式。
原型模式
我们创建的每一个函数都有一个prototype(原型)属性。
点击查看什么是原型
在高级程序设计一书中对原型模式讲解得比较仔细,在这里我们把最后实现写出来,详情在书中【147-158】页。
function Person(){};
Person.prototype = {
constructor:Person,
name:'lee',
age:16,
frinds:['wang','zhang']
sayName:function(){
console.log(this.name);
}
}
这就是原型的基本模式
当我们new Person的时候,创建了一个实例,并且将新的实例的构造函数指派给了Person对象,如果不指派会在Object上。
他的缺点就是公用属性,或者说当修改一个属性后不同实例都会修改
var p1 = new Person();
var p2 = new Person();
p1.frinds.push('zhao');
console.log(p1.prinds)//wang,zhang,zhao
console.log(p2.prinds)//wang,zhang,zhao
我们通常2个人总有不同的朋友,在这里就有了完全一样的朋友,解决这一问题我就就要使用接下来说道的模式
组合构造函数和原型模式
function Person(name,age){
this.name = name;
this.age = age;
this.friends = ['wang','zhang'];
}
Person.prototype = {
constructor:Person,
sayName:function(){
console.log(this.name);
}
}
var p1 = new Person('lee',18);
var p2 = new Person('xiao',16);
p1.friends.push('zhao');
console.log(p1.friends)// wang zhang xiao
console.log(p2.friends)// wang zhang
上面我们就保证了修改p1 不动p2 并且sayName也进行了共享new的时候只 初始化了Person中的属性,组合构造函数和原型模式是我们平时使用过程中用得最多也是推荐的一种方式,Arry Object String中有能看到。
其他模式在书中还有动态原型模式,寄生构造函数模式,稳妥构造函数模式。
这里我们就创建了对象,既然是面向对象,继承肯定是一大要素,下面我们来看看关于JavaScript中的继承
继承
JavaScript只能做实现继承,没有其他高级语言中的接口(Interface),JavaScript的继承是用过原型链来实现的。
原型链
点击查看基于原型链的继承
如果不好理解原型链我们简单的理解为:函数具有原型(prototype),对象具有原型指针proto,原型和原型指针之间的关系形成一个链路,其实对象的proto指向了函数原型(prototype)。
var o = new Object();
o.__proto__ == Object.prototype
通过对原型链的初步理解,我们知道我们之前创建对象有多种方法,在实现继承也有多种方式 原型链继承 构造函数继承 组合继承 原型式继承 寄生式继承 在我们常用使用的方式是 组合继承
组合继承
组合继承:是将原型链和构造函数寄生组合到一块的继承方式
原型链实现对原型属性和方法的继承,构造函数来实现对实例属性的继承,实现了函数复用和实现了每个实例都有它自己的属性。
例子:
//构造函数定义了2个属性
function SuperType(name){
this.name = name;
this.colors = ['red','blue','green'];
};
//通过原型定义了一个方法
SuerType.prototype.sayName = function(){
console.log(this.name);
};
function SubType(name,age){
//通过call方式继承了属性并且传入了name值
SuperType.call(this,name);
this.age = age;//定义了自己的age
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;//将Subtype的实例赋值给Subtype
//定义自己的方法
SubType.prototype.sayAge = function(){
console.log(this.age);
}
var p1 = new SubType('lee',18);
p1.colors.push('black');
console.log(p1.colors);//'red','blue','green','black'
p1.sayName();//lee
p1.sayAge();//18
var p1 = new SubType('wang',16);
console.log(p1.colors);//'red','blue','green'
p1.sayName();//wang
p1.sayAge();//16
这样就能让2个不同的SubType实例拥有自己的属性,并且拥有相同的方法。
组合继承是JavaScript最常用的继承模式
组合继承有一个小的问题就是会调用2次SuperType,第一次是在new的时候,第二次是在SubType继承的属性的时候。为了解决这个问题出现了寄生组合继承,我们来看看具体实现
添加了一个中间函数代替继承 SubType.prototype = new SuperType();
function inheriPrototype(subType,superType){
var prototype = Object(superType.prototype);//创建对象
prototype.constructor = subType;//增强对象
subType.prototype = prototype;//指定对象
}
//构造函数定义了2个属性
function SuperType(name){
this.name = name;
this.colors = ['red','blue','green'];
};
//通过原型定义了一个方法
SuerType.prototype.sayName = function(){
console.log(this.name);
};
function SubType(name,age){
//通过call方式继承了属性并且传入了name值
SuperType.call(this,name);
this.age = age;//定义了自己的age
}
//继承方法
inheriPrototype(SubType,SuperType);
//定义自己的方法
SubType.prototype.sayAge = function(){
console.log(this.age);
}
总结:在没有类(ES6以后有类了)的情况下可以采用工厂,构造,原型方式定义类,并且可以通过原型式,寄生式,寄生组合式等进行继承,通过情况下达到比较理想的情况我们采用 寄生组合继承进行我们面向对的实现。
参考文档:
《JavaScript高级程序设计》
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/prototype
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain