创建JS对象的几种方式
第一种:Object构造函数
var Person = new Object();
Person.name = 'Nike';
Person.age = 29;
这行代码创建了Object引用类型的一个新实例,然后把实例保存在变量Person中。
第二种:对象字面量表示法
var Person = {};//相当于var Person = new Object();
var Person = {
name:'Nike'; age:29;
}
对象字面量是对象定义的一种简写形式,目的在于简化创建包含大量属性的对象的过程。也就是说,第一种和第二种方式创建对象的方法其实都是一样的,只是写法上的区别不同。
在介绍第三种的创建方法之前,我们应该要明白为什么还要用别的方法来创建对象,也就是第一种、第二种方法的缺点所在:它们都是用了同一个接口创建很多对象,会产生大量的重复代码,就是如果你有100个对象,那你要输入100次很多相同的代码。
那我们有什么方法来避免过多的重复代码呢,就是把创建对象的过程封装在函数体内,通过函数的调用直接生成对象。
第三种:工厂模式
工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽离了创建对象的具体过程。考虑到在 ECMAScript 中无法创建类,开发人员发明以一种函数,用函数来封装以特定接口创建对象的细节。
例子如下所示:
function createPerson(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson('Nike',29,'teacher');
var person2 = createPerson('Arvin',20,'student');
优点:解决了创建多个相似对象时,代码的复用问题
缺点:使用工厂模式创建的对象,没有解决对象识别的问题(就是怎样知道一个对象的类型是什么)
在使用工厂模式创建对象的时候,我们都可以注意到,在createPerson函数中,返回的是一个对象。那么我们就无法判断返回的对象究竟是一个什么样的类型。于是就出现了第四种创建对象的模式。
第四种:构造函数
ECMAScript 中的构造函数可用来创建特定类型的对象。像 Object 和 Array 这样的原生构造函数,在运行时会自动出现在执行环境中。此外我们也可以创建自定义的构造函数,从而定义对象类型的属性和方法。
例子如下所示:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person('Nike',29,'teacher');
var person2 = new Person('Arvin',20,'student');
当我们使用构造函数实例化一个对象的时候,对象中会包含一个 __proto__
属性指向构造函数原型对象,而原型对象中则包含一个 constructor
属性指向构造函数。因此在实例对象中我们可以通过原型链来访问到 constructor
属性,从而判断对象的类型。
对比工厂模式,我们可以发现以下区别:
- 没有显示地创建对象
- 直接将属性和方法赋给了this对象
- 没有return语句
- 终于可以识别的对象的类型。对于检测对象类型,我们应该使用instanceof操作符,我们来进行自主检测:
alert(person1 instanceof Object);//ture
alert(person1 instanceof Person);//ture
alert(person2 instanceof Object);//ture
alert(person2 instanceof Person);//ture
同时我们也应该明白,按照惯例,构造函数始终要应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。
优点:解决了工厂模式中对象类型无法识别的问题,并且创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型。
缺点:我们知道 ECMAScript 中的函数是对象,在使用构造函数创建对象时,每个方法都会在实例对象中重新创建一遍(方法指的就是我们在对象里面定义的函数)。拿上面的例子举例,这意味着每创建一个对象,我们就会创建一个 sayName 函数的实例,但它们其实做的都是同样的工作,如果方法的数量很多,就会占用很多不必要的内存。
于是就出现了第五种创建对象的方法。
第五种:原型模式
我们知道,我们创建的每一个函数都有一个 prototype
属性,这个属性指向函数的原型对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。我们可以通过使用原型对象让所有的对象实例共享它所包含的属性和方法,因此这样也解决了代码的复用问题。
下面代码:
function Person(){}
Person.prototype.name = 'Nike';
Person.prototype.age = 20;
Person.prototype.jbo = 'teacher';
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
console.log(person1.name); //'Nike' --来自原型
console.log(person2.name); //'Nike' --来自原型
person1.name ='Greg';
console.log(person1.name); //'Greg' --来自实例
与构造函数模式不同的是,原型对象上的属性和方法是有所有实例所共享的。也就是说,上面 person1 和 person2 访问的都是同一组属性和同一个 sayName() 函数。 而当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。
优点:解决了构造函数模式中多次创建相同函数对象的问题,所有的实例可以共享同一组属性和函数。
缺点:
- 首先第一个问题是原型模式省略了构造函数模式传递初始化参数的过程,所有的实例在默认情况下都会取得默认的属性值,会在一定程度上造成不方便。
- 因为所有的实例都是共享一组属性,对于包含基本值的属性来说没有问题,但是对于包含引用类型的值来说(例如数组对象),所有的实例都是对同一个引用类型进行操作,那么属性的操作就不是独立的,最后导致读写的混乱。我们创建的实例一般都是要有属于自己的全部属性的,因此单独使用原型模式的情况是很少存在的。
第六种:组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。使用这种模式的好处就是,每个实例都会拥有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。而且这种混成模式还支持向构造函数传递参数,可以说是取两种模式之长。
function Person(name,age,job){
this.name =name;
this.age = age;
this.job = job;
}
Person.prototype = {
constructor:Person,
sayName: function(){
alert(this.name);
}
}
var person1 = new Person('Nike',20,'teacher');
优点:采用了构造函数模式和原型模式的优点,这种混成模式是目前使用最广泛,认同度最高的一种创建自定类型的方法。
缺点:由于使用了两种模式,因此对于代码的封装性来说不是很好。
第七种:动态原型模式
由于上面混成模式存在封装性的问题,动态原型模式是解决这个问题的一个方案。这个方法把所有信息都封装到了构造函数中,而在构造函数中通过判断只初始化一次原型。下面我们来看一下例子
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
if(typeof this.sayName !== "function" ){
Person.prototype.sayName = function(){
alert(this.name);
}
}
}
var person1 = new createPerson("james",9,"student");
person1.sayName(); // "james"
注意在 if 语句中检查的可以是初始化后应该存在的任何属性或方法,不必要检查每一个方法和属性,只需要检查一个就行。
优点:解决了混成模式中封装性的问题
第八种:寄生构造函数模式
如果在前面几种模式不适用的情况下,可以使用寄生构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后返回新创建的对象。如下面的例子所示:
function Person(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = new Person("james",9,"student");
通过上面的例子我们可以发现,其实这个模式和工厂模式基本上是一摸一样的,只不过我们是采用 new 操作符最后来创建对象。
注意在构造函数不返回值的情况下,默认会返回新创建的对象,而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。
优点:我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。
缺点:和工厂模式一样的问题,不能依赖 instanceof 操作符来确定对象的类型。
第九种:稳妥构造函数模式
Douglas Crockford 发明了 JavaScript 中的稳妥对象这个概念。所谓稳妥对象,指的就是,没有公共属性,而且其方法也不使用 this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 this 和 new),或者在防止数据被其他应用程序改动时使用。
稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建的对象的实例方法不引用 this ;二是不使用 new 操作符调用构造函数。因此我们可以将前面的例子改写如下:
function Person(name, age, job){
//创建要返回的对象
var o = new Object();
//可以在这里定义私有变量和函数
//添加方法
o.sayName = function(){
console.log(this.name);
}
//返回对象
return o;
}
var person1 = Person("james",9,"student");
person1.sayName(); // "james"
优点:以上面为例,除了 sayName 方法外,没有别的方法可以访问数据成员,这就是稳妥构造函数提供的安全性。
缺点:和寄生构造函数一样,没有办法使用 instanceof 操作符来判断对象的类型
第十种:ES6 class创建对象
ES6 的class可以看作构造函数+原型创建对象的另一种写法,除了写法更符合面向对象编程的语法之外,并没有实质性的改变。
class Point {
constructor(x, y) { //相当于java中的构造函数,如果不写默认为空
this.x = x; //x、y定义在实例对象上
this.y = y;
}
toString() { //该方法定义在Point.prototype(原型对象)上
console.log(this.x + this.y) ;
}
}
var p = new Point(1,2);
p.toString(); //3
- 类Point中方法之间不用,号隔开,方法不用function进行定义,构造函数的
prototype
属性,在ES6的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。 - 类的内部所有定义的方法,都是不可枚举的(但是在es5中
prototype
的方法是可以进行枚举的) - 每一个类中都有一个
constructor
方法该方法返回实例对象 - 类的构造函数,不使用new是没法调用的,会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。
关于构造函数、原型对象、实例对象的指向关系
构造函数可以实例化对象;
构造函数中有一个属性叫prototype
,指向构造函数的原型对象;
构造函数的原型对象(prototype
)中有一个constructor
构造器,这个构造器指向的就是自己所在的原型对象所在的构造函数;
实例对象的隐含属性(__proto__
)指向的是该构造函数创造的原型对象;
用类进行实例和用普通的构造函数进行实例的区别
- 用类进行实例的必须使用new否则就会报错
- 与ES5一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。
- 与ES5一样,类的所有实例共享一个原型对象。
- Class不存在变量提升(hoist),这一点与ES5完全不同