创建对象
基本方式:用Object构造函数或对象字面量来创建单个对象
缺点:使用同一个接口创建很多对象,会产生大量的重复代码,所以便有了下面的模式
工厂模式
工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程,用函数来封装以特定接口创建对象的细节
function createPerson(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function (){
console.log(this.name);
};
return o;
}
var person1 = createPerson('Tom',18,'Eat');
var person2 = createPerson('jack',6,'Drink');
console.log(person1.name); //Tom
console.log(person2.age); //6
函数createPerson()
能够根据接受的参数来构建一个包含所有必要信息的Person对象,可以无数次的调用这个函数,而每次它都会返回一个包含三个属性一个属性的对象。
不过工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(就是怎样知道一个对象的类型)
构造函数模式
除了原生的构造函数Object,Array,我们也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function (){
console.log(this.name);
};
}
var person1 = new Person('Tom',18,'Eat');
var person2 = new Person('jack',6,'Drink');
console.log(person1.name); //Tom
console.log(person2.age); //6
console.log(person1 instanceof Person);// 判断对象的类型,内部机制是通过判断对象的原型链中是不是能找到类型的prototype 这个返回结果为true
console.log(person1.sayName == person2.sayName) //false
跟工厂模式相比,这里没有显示地创建对象,直接将属性和方法赋给了this对象
有一点需要注意,按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头,主要是为了容易辨别,构造函数本身也是函数,只不过可以用来创建对象
如果要创建Person的新实例,必须要用new
操作符,关于new
我会找机会单独列出来总结一下
以这种方式调用构造函数实际上会经历一下4个步骤
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
在工厂模式中,person1
和person2
分别保存Person
的一个不同的实例,但这两个对象都有一个constructor
(构造函数)属性,该属性指向Person
console.log(person1.constructor == Person)
结果为true
对象的constructor
最初是用来标识对象类型的,但是检测对象类型,还是instanceof
操作符更可靠
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,而这正是构造函数模式胜过工厂模式的地方,这样构造函数是定义在window对象中的
将构造函数当作函数
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。
任何函数,只要通过new操作符来调用,那它就可以称为构造函数,而任何函数,如果不通过new操作符调用,那它就是普通函数
//作为构造函数
var person = new Person('Tom',14,'Eat');
person.sayName(); // Tom
//作为普通函数
Person('Tom',14,'Eat'); //添加到window
window.sayName() //Tom
//在另一个对象的作用域中调用
var o = new Object();
Person.call(o,'TTT',55,'qqq');
o.sayName() //TTT
上面三种调用,第一种是我们刚学的作为构造函数,用new操作符调用
第二种是在全局作用域调用,this对象会指向window对象
第三种使用call()
或者apply()
改变this对象指向,以此能在其他对象的作用域中调用
构造函数的问题
构造函数的缺点就是每个方法都要在每个实例上重新创建一边,就比如之前的例子中,person1
和person2
都有一个sayName()
方法,但这两个方法不是同一个Function的实例,因为在ECMAscript中的函数也是对象,因此每定义一个函数,就是实例化了一个对象
解决方法
将方法对应的函数定义转移到构造函数外面
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
console.log(this.name);
}
var person1 = new Person('Tom',18,'Eat');
var person2 = new Person('jack',6,'Drink');
console.log(person1.sayName()); //Tom
console.log(person2.sayName()); //javk
这样由于sayName
包含的是一个指向函数的指针,因此person1
和person2
对象就共享了在全局作用域中定义的同一个sayName()
函数
但是这样做产生的新问题就是每一个方法都要对应一个全局函数,那我们自定义的引用类型就没有封装性可言,并且只是对象调用却定义在全局,浪费内存
所以我们可以使用原型模式
原型模式
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象(Object),而这个对象的用途是包含可以有特定类型的所有实例共享的属性和方法
其实prototype就是通过调用构造函数而创建的那个对象实例的原型对象,使用原型对象的好处就是可以让所有的对象实例共享它所包含的属性和方法,也就是不在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中
function Person(){}
Person.prototype.name = 'KKK';
Person.prototype.age = 23;
Person.prototype.sayName = function (){
console.log(this.name);
};
var person1 = new Person();
person1.sayName(); //KKK
var person2 = new Person();
person2.sayName(); //KKK
console.log(person1.sayName == person2.sayName) //true
这样就解决了我们上面遇到的问题,所以接下来我们来认识一下原型对象
原型对象
无论什么时候,只要创建一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针
就比如前面的例子,Person.prototype.constructor
指向Person
而通过这个构造函数,我们还可以继续为原型对象添加其他属性和方法
创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性,至于其他方法,则都是从Object继承而来的。当调用构造函数创建的一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数原型对象
这个指针被称为[[Prototype]]
虽然在脚本中没有标准的方式访问它,但在Firefox,Safari和Chrome中每个对象上都支持一个属性_proto_
,这个属性对脚本来说是完全不可见的
这个连接存在于实例与构造函数的原型对象之间,而不是存在与实例与构造函数之间
哈哈,图画的有点丑
这是根据之前面原型模式的例子创建的图
从图中我们可以看出来,实例对象和构造函数没有直接的关系,他们之间靠构造函数的原型对象来联系
构造函数Person.prototype指向原型对象,然后原型对象中可以创建添加其他的新属性和方法,原型的constructor
属性又指回了构造函数
为什么实例对象什么也没有却可以调用方法,就是因为我们在调用实例对象时通过查找对象属性,找到指向原型对象的指针,从而找到原型对象创建的方法
可以通过isPrototypeOf()
方法来确定对象之间是否存在这种关系
Person.prototype.isPrototypeOf(person1)
结果为true
ES5中增加了一个方法,Object.getPrototypeOf()
,这个方法可以返回[[Prototype]]的值
Object.getPrototypeOf(person1) == Person.prototype
结果为true
代码读取某个对象的某个属性时,会执行以此搜索,目标是具有给定名字的属性,搜索首先从对象实例本身开始,如果找到了就返回该属性的值,如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,就返回该属性的值
这也是多个实例对象共享原型所保存的属性和方法的基本原理
当为对象添加一个属性时,这个属性会屏蔽原型对象中保存的同名属性,因为第一次搜索就能把属性返回,但这不能改变原型对象中的属性值
通过hasOwnPrototype()
方法检测一个属性是在实例还是原型
person1.hasOwnPrototype('name')
在实例就是true
原型与in操作符
两种方式使用in操作符:单独使用还有在for-in中使用
还是接上面原型模型的例子
单独使用in
function Person(){}
Person.prototype.name = 'KKK';
Person.prototype.age = 23;
Person.prototype.sayName = function (){
console.log(this.name);
};
var person1 = new Person();
person1.sayName(); //KKK
var person2 = new Person();
person2.sayName(); //KKK
console.log(person1.sayName == person2.sayName) //true
console.log('name' in person1); //true
无论属性存在于实例中还是原型中,只要通过对象可以访问到,那么结果都返回true
使用for-in循环
返回的是所有能够通过对象访问的,可枚举的属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性,屏蔽了原型中不可枚举属性的实例属性也会在for-in 循环中返回
不用for-in的话,可以用Object,keys()
方法取得对象上所有可枚举的实例属性
这个方法接受一个对象作为参数,返回一个包含所有可枚举属性的字符串
var keys = Object.keys(Person.prototype); //根据原型对象
console.log(keys); //['name','age','sayName']
这里如果我们根据实例对象,那返回空数组,因为我们并未定义实例对象的属性
如果想要所有实例属性,五论是否枚举,可以用Object.getOwnPrototypeNames()
更简单的原型语法
那就是用字面量的方式,不过是原型对象使用
function Person(){}
Person.prototype = {
name : 'Tom',
age : 5 ,
sayName : function (){
console.log(this.name);
}
};
这个方法有一个问题,我们每次创建一个函数,就会同时创建他的prototype对象,这个对象也会自动获得constructor属性,而我们这里相当于重写了默认的prototype对象,此时他的constructor已经不再指向Person,而是指向Object构造函数
所以如果我们真的需要constructor的值,那么就在重写时添加进去
constructor : Person,
原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改能可以动态的在实例上反映出来,即使先创建了实例后修改原型也是如此
因为实例和原型对象之间的连接只是一个指针,而不是一个副本,所以我们可以根据指针找到动态修改后的原型中的属性
但是如果重写了原型对象,那么就相当于切断了构造函数与最初原型之间的联系,这时拥有新属性的其实是新与构造函数建立联系的原型对象,但是实例对象还是指向原来的原型对象,所以这是调用实例对象访问不到原型中的新属性
原型对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(Object,Array,Srting,等)都在其构造函数的原型上定义了方法
其实就是内置对象的内置方法
原型对象的问题
原型对象的缺点就是省略了构造函数传递初始化参数这一环节,结果所有实例在默认情况下都取得了相同的属性值。
原型中的所有属性都是被很多实例共享的,这种共享对于函数很合适,但如果包含引用类型值的属性,就有很大问题
function Person(){}
Person.prototype.name = 'KKK';
Person.prototype.age = 23;
Person.prototype.arrs = ['www','uuu']
Person.prototype.sayName = function (){
console.log(this.name);
};
var person1 = new Person();
person1.arrs.push('vvv')
console.log(person1.arrs); //www,uuu,vvv
var person2 = new Person();
console.log(person2.arrs); //www,uuu,vvv
因为数组在原型中,所以实例person2也会发生改变
所以推荐组合使用构造函数模式和原型模式,也就是构造函数模式定义实例属性(独有的),原型模式用于定义方法和共享的属性
动态原型模式
把所有信息都封装在构造函数中,在构造函数中初始化原型
通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型
function Person(name,age){
this.name = name;
this.age = age;
//方法
if (typeof this.sayName != "function"){
Person.prototype.sayName = function (){
console.log(this.name);
};
}
}
var fff = new Person("Tom",25);
fff.sayName();
这里只会在不存在sayName()方法的时候,将这个方法添加到原型中,只是在初次调用构造函数时执行,之后原型已经得到初始化,无需修改