类class是Object-Oriented面向对象的语言有一个标志,通过类我们可以创建任意多个具有相同属性和方法的对象。JavaScript中没有类的概念,但它也是面向对象的,只是实现方法会有所不同。
创建单个对象有两种基本方法:
1.使用Object的构造函数创建实例然后在实例上添加属性和方法;
2.使用对象字面量的方法。
在简单的场景这两种方法是很实用的,但如果遇到有很多对象有相同的属性,相同的方法的时候,这两种方法的弊端就暴露出来了,会产生大量的重复代码。
为了应对不同的场景和方法,创建对象有以下几种方法。
1、工厂模式。所谓的工厂模式指的是使用函数封装利用Object的构造函数创建实例再添加属性和方法的步骤,然后返回该实例。这样每次运行这个方法我们都会得到一个结构相同的对象。
function createPerson(name, age){
var person = new Object();
person.name = name;
person.age = age;
person.sayHi = function () {
alert("My name is " + person.name + ", I'm " + person.age);
}
return person;
}
var liLei = createPerson("LiLei", 12);
liLei.sayHi();
这个模式虽然解决了重复代码的问题,但是创建出的对象都是Object类型,没有解决对象识别问题。
2、构造函数模式。利用构造函数模式可以解决对象是别问题。改造上面的代码可得:
function Person(name, age){
this.name = name;
this.age = age;
this.sayHi = function () {
alert("My name is " + this.name + ", I'm " + this.age);
}
}
var liLei = new Person("LiLei", 12);
liLei.sayHi();
对比工程模式我们可以看到以下3处不同:
1、没有显示创建对象
2、直接将属性和方法赋值给了this对象
3、没有return
使用构造函数会经历一下4个步骤:
1、创建一个新的对象
2、将构造函数的作用域赋值给新的对象,这样this就指向了新的对象
3、执行构造函数中的代码(为这个新对象添加属性或方法)
4、返回新对象
所以上面的的形式就如同以下所示一般:
var hanMeimei = new Object();
Person.call(hanMeimei, "Han Meimei", 12);
hanMeimei.sayHi();
然而构造函数模式并非是完美的,就比如Person的sayHi方法,我们每次创建一个新的实例的时候,都会创建一个新的sayHi并为之开辟新空间,尽管这个sayHi方法做的事是一模一样的。
var liLei = new Person("LiLei", 12);
var hanMeimei = new Person("Han MeiMei", 12);
alert(liLei.sayHi == hanMeimei.sayHi); //false
创建多个相同的方法确实没有必要。虽然我们可以像下面的例子一样定义一个全局的函数来解决这个问题,但是由此引发的问题就是,如果我们有多个方法,就需要定义多个全局函数了,这不仅会破坏自定义引用类型的封装性,还会使全局作用域混乱,是一种很不安全的做法。
function Person(name, age){
this.name = name;
this.age = age;
this.sayHi = sayHi;
}
function sayHi () {
alert("My name is " + this.name + ", I'm " + this.age);
}
3、原型模式。为了解决构造函数的重复定义相同的方法的问题,原型模式便出现了。
首先我们得理解什么是原型,原型即prototype,它是一个指针,指向一个对象,这个对象的用途就是包含特定类型的所有示例共享的属性和方法,我们创建的每一个函数都会有原型(prototype)属性。使用原型的好处是,我们将共享的属性或方法赋值给原型对象,该类型的所有实例都可以访问该属性或方法。
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function () {
alert("My name is " + this.name + ", I'm " + this.age);
}
var liLei = new Person("Li Lei", 12);
liLei.sayHi();//My name is Li Lei, I'm 12
var hanMeimei = new Person("Han MeiMei", 12);
hanMeimei.sayHi();//My name is Han MeiMei, I'm 12
alert(liLei.sayHi == hanMeimei.sayHi); //true
关于原型对象有以下3点需要注意:
(1)、如果我们在实例中添加一个属性,该属性与原型对象中的属性同名的话,处理的结果是原型中的属性被屏蔽了,记住是被屏蔽了而不是重写了,因为如果这个时候再删除实例中的这个属性的话,再访问这个值就会访问原型中的同名属性,并且该值为原型初始化时的值,一直未改过。
function Dog(age){
this.age = age;
}
Dog.prototype.name = "Little White";
var littleWhite = new Dog();
alert(littleWhite.name); // Little White;
littleWhite.name = "Xiao Bai";
alert(littleWhite.name); //Xiao Bai
delete littleWhite.name;
alert(littleWhite.name); // Little White;
(2)、原型对象中的引用属性在其中一个实例里被改变,则所有的实例获取该属性都将得到新的值。
function MyToy(){
}
MyToy.prototype.group = ["Dingding", "Dingxi", "Lala"];
var toy1 = new MyToy();
var toy2 = new MyToy();
toy1.group.push("Po");
alert(toy2.group); // Dingding,Dingxi,Lala,Po
如上上述例子所示,我们通过实例toy1修改了原型对象的属性group,toy2去获取的时候值已经发生了变化。
(3)、初始化一个实例后,再给函数的原型对象重新赋值,已创建的实例与新的原型对象不会有联系,也就是说已创建的实例不能访问新原型对象的方法和属性。如下所示:
var toy = new MyToy();
MyToy.prototype = {
group: ["Dingding", "Dingxi", "Lala"]
};
alert(toy.group); // undefined
以上三点也可以很好的帮助我们理解原型prototype,它是一枚指针,默认指向一个Object对象,在创建函数时会相应为其初始化一枚这样的指针,当使用该函数new出一个新的实例时,该指针又会赋值给new出的实例。所以在new出一个新实例后,再给函数的原型对象赋值个新的对象是,函数的原型指针指向了新的对象,但是此时的实例的原型的指针指向的还是旧的对象。
如果单单只是用原型模式的话,事实上并不能满足所有的需求。原型模式也是没有缺点,主要体现在以下两点:
1、省去了构造函数传参
2、所有的实例共享原型的属性,这个是非常可怕的
4、组合使用构造函数和原型模式。单独使用原型模式会有弊端,但和其他模式组合起来使用的话,有些问题就迎刃而解了。典型的就是组合使用构造函数和原型模式,它是目前ECMAScript中,认同度最高,使用最广泛的一种创建自定义类型的方法。
function MyToy(owner){
this.owner = owner;
this.group = ["Dingding", "Dingxi", "Lala"];
}
MyToy.prototype.sayOwner = function(){
alert(this.owner);
};
var lileis = new MyToy("Li Lei");
var hanMeimeis = new MyToy("Han Meimei");
lileis.group.push("Po");
alert(lileis.group);//Dingding,Dingxi,Lala,Po
alert(hanMeimeis.group);//Dingding,Dingxi,Lala
alert(lileis.group == hanMeimeis.group); // false
alert(lileis.sayOwner == hanMeimeis.sayOwner); // true
5、动态原型模式。有其它OO语言开发经验的朋友看到独立的函数和独立的原型的时候会有点困惑,甚至难以理解。例如在C#类中,方法和属性是一个整体,是一起定义的
public class MyToy
{
public string owner { get; set;}
public string[] group { get; set; }
public void sayOwner()
{
//...
}
}
当然JS里也可以变成这样,只是方法有所不同。
function MyToy(owner){
this.owner = owner;
this.group = ["Dingding", "Dingxi", "Lala"];
if(typeof this.sayOwner != "function") {
MyToy.prototype.sayOwner = function(){
alert(this.owner);
};
}
}
var liLei = new MyToy("Li Lei");
liLei.sayOwner();//Li Lei
这便是动态原型模式,当你需要同时定义很多个原型函数的时候,不需要为每一个函数都做一次if判断,仅需要判断一个就可以了,然后把所有的定义都放在一个if语句中。
6、寄生构造函数模式。当我们想封装JS中的引用对象时,可能以上的方式都不适用,这个时候我们就可以使用这个所谓的寄生构造函数模式了。
function MyToy(){
// Create Array
var arr = new Array();
// Add values
arr.push.apply(arr, arguments);
// Add method
arr.toPipedString = function(){
return this.join("-");
}
return arr;
}
var toys = new MyToy("Dingding", "Dingxi", "Lala");
alert(toys.toPipedString()); // Dingding-Dingxi-Lala
alert(toys instanceof MyToy); // false
alert(toys instanceof Array); // true
我们可以注意到此时toys instanceof MyToy的值是false,这主要是因为构造函数返回的对象不是MyToy类型的实例,而由于在构造函数中出现了return arr,所以得到的arr,是一个Array类型的对象。
7、稳妥构造函数模式。所谓的稳妥模式,即使在函数体内不访问this,没有公共属性,变量私有化,这种模式比较适合在安全的环境中或者不想数据被第三方的应用程序改动时使用。
function MyToy (name) {
var obj = new Object();
obj.sayName = function(){
alert(name);
}
return obj;
}
var dingding = MyToy("Ding ding");
dingding.sayName();
在这个创建的对象中除了调用sayName方法,是没有其它办法可以访问变量name的。
看起来它和寄生构造函数很相似,都不能使用instanceof来判断创建的对象的类型,不过它们有以下两点是不同的。
1、不使用new操作符来调用构造函数
2、在方法体内不引用this
以上便是在JavaScript中创建对象7种常见方法,每个方法都有利有弊,最重要的还是在合适的场景中使用合适的方法。