面向对象语言都有类的概念,但是ECMAScript没有类的概念,所以它的对象与基于类的语言中的对象有所不同。
一、创建对象的几种方式及对比
1.对象字面量与创建Object实例方式
var person = {
name:'Zhangsan',
age:20,
gender: 'male',
sayName: function(){
console.log(this.name);
}
}
var person = new Object();
person.name = 'Zhangsan';
person.age = 20;
person.gender = 'male';
person.sayName = function(){
console.log(this.name);
}
缺点:使用同一个接口创建很多对象,会产生大量的重复代码
2.工厂模式
因为ECMAScript没有类的概念,所以用函数来封装创建对象的细节
function createPerson(name,age,gender){
var o = new Object();
o.name = name;
o.age = age;
o.gender = gender;
o.sayName = function(){
console.log(this.name);
}
return o;
}
var person1 = createPerson('Zhangsan',20,'male');
var person2 = createPerson('Lisi',24,'male');
console.log(person1 instanceof Object); //true
缺点:工厂模式解决了创建多个相似对象的重复代码问题,但没有解决对象类型识别的问题
3.构造函数模式
function Person(name, age, gender){
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = function(){
console.log(this.name);
}
}
var person1 = new Person('Zhangsan',20,'male');
var person2 = new Person('Lisi',24,'male');
console.log(person1 instanceof Person); //true
console.log(person1 instanceof Object); //true
console.log(person1.constructor == Person); //true
console.log(person1.sayName == person2.sayName); //false
以这种方式调用构造函数会经历以下四步:
- 创建一个新对象
- 将构造函数的作用域赋给新对象
- 执行构造函数中的代码
- 返回新对象
任何函数,只要通过new操作符来调用,那它就可以作为构造函数;如果不通过new操作符来调用,那它就是普通函数
var person = new Person('Zhangsan',20,'male');
person.sayName(); //'Zhangsan'
Person('Lisi',24,'male');
window.sayName(); //'Lisi'
var o = new Object();
Person.call(o,'Wangwu',22,'female');
o.sayName(); //'Wangwu'
缺点:每个方法都要在每个实例上重新创建一遍,上述例子中的sayName方法就会在每个实例中重新创建一遍
function Person(name, age, gender){
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = sayName;
}
function sayName(){
console.log(this.name);
}
var person1 = new Person('Zhangsan',20,'male');
var person2 = new Person('Lisi',24,'male');
上述代码将sayName()函数的定义转移到构造函数外部,这样构造函数中的的sayName是一个指向函数的指针,因此person1、person2就共享了在全局作用域定义的同一个sayName()函数。这么做虽然解决了问题但却带来了新问题:如果对象需要定义很多方法那么就需要定义很多全局函数,那么自定义的引用类型就丝毫没有封装性可言了
4.原型模式
4.1原型对象
每个函数都有一个prototype属性,这个属性是一个指针指向一个对象,而这个对象包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处就是让所有对象实例共享它所包含的属性和方法
function Person(){
}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.gender = 'male';
Person.prototype.sayName = function(){
console.log(this.name);
};
var person1 = new Person();
person1.sayName(); //Nicholas
var person2 = new Person();
person2.sayName(); //Nicholas
console.log(Person.prototype.isPrototypeOf(person1));//true
console.log(Person.prototype.isPrototypeOf(person2));//true
console.log(Object.getPrototypeOf(person1) == Person.prototype);//true
console.log(Object.getPrototypeOf(person1).name);//Nicholas
当代码读取某个对象的某个属性时,先从对象实例本身开始搜索,如果找到给定名称的属性则返回该属性的值;如果没找到则搜索其指针指向的原型。当为对象实例添加一个属性时,则这个属性就会屏蔽原型对象中保存的同名属性
person1.name = 'Zhangsan';
console.log(person1.name); //Zhangsan
console.log(person1.hasOwnProperty('name')); //true
console.log(person2.hasOwnProperty('name')); //false
delete person1.name;
console.log(person1.name); //Nicholas
4.2原型对象赋值
function Person(){
}
Person.prototype = {
name:'Nicholas',
age:29,
gender:'male',
sayName:function(){
console.log(this.name);
}
}
var person = new Person();
console.log(person instanceof Object); //true
console.log(person instanceof Person); //true
console.log(person.constructor == Person); //false
console.log(person.constructor == Object); //true
Person.prototype.constructor = Person;
console.log(person.constructor == Person); //true
使用原型对象赋值操作是会覆盖原型对象中的constructor属性,就会切断原型与构造函数之间的关联
function Person(){
}
var person = new Person();
Person.prototype = {
name:'Nicholas',
age:29,
gender:'male',
sayName:function(){
console.log(this.name);
}
}
person.sayName(); //error
缺点:由于原型中的所有属性和方法都是共享的,所以对于引用类型属性问题就比较突出
function Person(){
}
Person.prototype = {
constructor:Person,
name:'Nicholas',
age:29,
gender:'male',
love:['swimming','running'],
sayName:function(){
console.log(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.love.push('playing games');
console.log(person1.love); //["swimming", "running", "playing games"]
console.log(person2.love); //["swimming", "running", "playing games"]
console.log(person1.love == person2.love); //true
5.组合使用构造函数和原型模式
function Person(name,age,gender){
this.name = name;
this.age = age;
this.gender = gender;
this.love = ['swimming','running'];
}
Person.prototype = {
constructor:Person,
sayName:function(){
console.log(this.name);
}
}
var person1 = new Person('Zhangsan',20,'male');
var person2 = new Person('Lisi',24,'male');
person1.love.push('playing games');
console.log(person1.love); //["swimming", "running", "playing games"]
console.log(person2.love); //["swimming", "running"]
console.log(person1.love == person2.love); //false
这种模式是使用最广泛、认同度最高的一种创建自定义类型的方法
6.寄生构造函数模式
function SpecialArray(name,age,gender){
var array = new Array();
array.push.apply(array,arguments);
array.toPipedString = function(){
return this.join('|');
}
return array;
}
这种模式可以用来为原生引用类型做扩展,寄生构造函数模式返回的对象与构造函数或者与构造函数原型之间没有关系,因此不能依赖instanceof操作符来确定对象类型
二、继承
1.原型链
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();
console.log(instance.getSuperValue()); //true
原型链虽然很强大,可以用它来实现继承,但它也存在一些问题,其中,最主要的问题来自包含引用类型值的原型;第二个问题是创建子类型的实例时不能向超类的构造函数中传递参数。
function SuperType(){
this.colors = ['red','yellow','blue'];
}
function SubType(){
}
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push('green');
console.log(instance1.colors); //["red", "yellow", "blue", "green"]
var instance2 = new SubType();
console.log(instance2.colors); //["red", "yellow", "blue", "green"]
2.借用构造函数
借用构造函数用于解决原型链中包含引用类型值所带来的问题
function SuperType(){
this.colors = ['red','yellow','blue'];
}
function SubType(){
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push('green');
console.log(instance1.colors); //["red", "yellow", "blue", "green"]
var instance2 = new SubType();
console.log(instance2.colors); //["red", "yellow", "blue"]
问题:方法都在构造函数中定义,因此函数复用无从谈起,而且在超类原型中定义的方法对子类而言也是不可见的,结果所有类型都只能使用构造函数模式
3.组合继承
function SuperType(name){
this.name = name;
this.colors = ["red", "yellow", "blue"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name, age){
SuperType.call(this,name);
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
}
var instance1 = new SubType('Zhangsan',20);
instance1.colors.push('green');
console.log(instance1.colors); //["red", "yellow", "blue", "green"]
instance1.sayName(); //Zhangsan
instance1.sayAge(); //20
var instance2 = new SubType('Lisi', 24);
console.log(instance2.colors); //["red", "yellow", "blue"]
instance2.sayName(); //Lisi
instance2.sayAge(); //24
组合继承避免了原型链和借用构造函数的缺陷,融合了他们的优点,成为JavaScript中最常用的继承模式
4.原型式继承
var person = {
name : 'Zhangsan',
colors : ["red", "yellow", "blue"]
}
var anotherPerson = Object.create(person);
anotherPerson.name = "Lisi";
anotherPerson.colors.push("green");
var otherPerson = Object.create(person);
otherPerson.name = "Wangwu";
otherPerson.colors.push("black");
console.log(person.colors); //["red", "yellow", "blue", "green", "black"]
console.log(person.name); //Zhangsan
这种继承方式在想让一个对象与另一个对象保持类似的情况下是完全可以胜任的
5.寄生式继承
var person = {
name : 'Zhangsan',
colors : ["red", "yellow", "blue"]
}
var anotherPerson = Object.create(person);
anotherPerson.sayHi = function(){
console.log("hi");
}
anotherPerson.sayHi();
使用寄生式继承不能做到函数复用而降低效率
6.寄生组合式继承
组合继承最大的问题就在于无论什么情况下都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。子类型最终回报寒潮类型对象的全部实例属性,但是我们不得不在调用子类型构造函数时重写这些属性(处理引用类型共用问题)
组合式继承代码如下:
function SuperType(name){
this.name = name;
this.colors = ["red", "yellow", "blue"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name, age){
SuperType.call(this,name); //第二次调用
this.age = age;
}
SubType.prototype = new SuperType(); //第一次调用
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
}
寄生组合式继承代码如下:
function SuperType(name){
this.name = name;
this.colors = ["red", "yellow", "blue"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name, age){
SuperType.call(this,name);
this.age = age;
}
SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
}