6.2 创建对象
虽然 Object 构造函数或 对象字面量都可以用来 创建单个对象,但这些方式有个 明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码(不能复用,不同对象的相同属性重复多次定义,代码不简洁)。本节讲述的就是各种创建对象模式,已找到趋紧完美的创建模式()。
6.2.1 工厂模式
1、工厂模式示例
考虑到在 ECMAScript 中无法创建类,开发人员 就发明了一种函数,用函数来封装以特定接口创建对象的细节。
2、工厂模式优缺点
- 优点:可以无数次地调用这个函数
- 缺点:虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题,即怎样知道一个对象的类型。比如:
alert(person1 instanceof createPerson);//false
person1和person2都只是Object的实例。
6.2.2 构造函数模式
ECMAScript 中的构造函数可用来创建特定类型的对象。有两种构造函数:
- 原生构造函数:像
Object 和 Array
这样的,在运行时会自动出现在执行环境中(js中已经写好的构造函数,直接调用即可)。 - 创建自定义的构造函数:从而定义自定义对象类型的属性和方法。
1、构造函数示例
注意⚠️:
- 函数名 Person 使用的是大写字母 P。
构造函数始终都应该以一个大写字母开头
,而非构造函数则应该以一个小写字母开头
。 - 构造函数本身也是函数,只不过可以用来创建对象而已。
2、调用函数经历的步骤
要创建实例,必须使用 new
操作符。以这种方式调用构造函数实际上会经历以下 4 个步骤:
- 创建一个新对象;
- 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象,上例中this指向person1和person2);
- 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象。
3、constructor(构造函数)属性
- 实例中都又一个
constructor(构造函数)属性
,该属性指向他们的构造函数
,比如在前面例子的最后,person1 和 person2 分别保存着 Person 的一个不同的实例。这两个对象的constructor(构造函数)属性
都指向Person,如下所示:
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
- 对象的
constructor
属性最初是用来标识对象类型的,比如上例中表明person1实例对象和person2实例对象都是Person对象类型。 - 提到
检测对象类型
,还是instanceof
操作符要更可靠一些。我们在这个例子中创建的所有对象既是 Object 的实例(因为所有对象均继承Object),同时也是 Person 的实例,这一点通过instanceof
操作符可以得到验证。
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true
4、将构造函数当作函数
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。任何函数,只要通过 new
操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过 new
操作符来调用,那它跟普通函数也不会有什么两样。
- 例如,前面例子中定义 的 Person()函数可以通过下列任何一种方式来调用。
当在全局作用域中调用一个函数时,this 对象总是指向 Global 对象(在 浏览器中就是 window 对象)。
5、构造函数的问题
- 构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。
- 比如:在前面的例子中,person1 和 person2 都有一个名为
sayName()
的方法,但那两个方法不是同一个 Function 的实例
。 - ECMAScript 中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。(这里有疑问:既然构造函数也是函数,那么定义构造函数时,实例化的对象是什么?是
Object
吗?)
- 因此,不同实例上的同名函数是不相等的,以下代码可以 证明这一点。
alert(person1.sayName == person2.sayName); //false
- 创建**两个完成同样任务(功能一样)**的
Function 实例
的确没有必要;况且有 this 对象在根本不用在 执行代码前就把函数绑定到特定对象上面(在Person中调用sayName时,this指向Person)。 - 因此,大可像下面这样,通过把函数定义转移到构造函数外部来解决这个问题。
这样做确实解决了两个函数做同一件事的问题,也只实例化一个Founction对象。
6、构造函数优缺点总结
- 优点:创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方;
- 缺点:构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。虽然通过把函数定义转移到构造函数外部解决了这个问题,但是新问题又来了:
- 在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。(对这句话有疑问)
- 而更让人无法接受的是:如果对象需要定义很多方 法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。
“在全局作用域中定义的函数只能被某个对象调用”:对于这句话参考理解是,你在全局作用域中定义了一个函数sayName,可是这个函数是针对Person对象定义的,只有Person对象调用了这个函数(如果你定义了其他对象,比如Car,Book或Home,这些对象都不会有sayName方法,都不会去调用它)。既然只有Person会用到sayName,为什么要在全局作用域中定义sayName呢,这不是很浪费么,而且也不安全。所以尽量别定义全局变量,否则很容易与其他的代码冲突(你不能保证很多人一起工作时,别人不会定义一个相同名字的全局变量),这也是为什么一般每个人对自己的代码外层都要用闭包创建一个块级作用域。
6.2.3 原型模式
- 我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象(原型对象);
- 原型对象的用途:包含可以由特定类型的所有实例共享的属性和方法。
- 使用原型对象的好处:可以让所有对象实例
共享
原型对象所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
如下图所示:
function Person(){
}
//直接在原型上定义属性
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName()