## 6.2 创建对象
Object构造函数或对象字面量创建对象有明显的缺点:使用同一个接口创建很多对象,会产很更大量的重复代码。
### 6.2.1 工厂模式
**工厂模式**在JavaScript中是指**用函数来封装以特定接口创建对象的细节**。
这种模式抽象了创建具体对象的过程。例:
~~~
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;
}
~~~
工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即**怎么知道一个对象的类型**)。
### 6.2.2 构造函数模式
**构造函数模式**通过创建自定义的构造函数,从而定义自定义对象类型的属性和方法。
~~~
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name)
};
}
~~~
Person()函数与createPerson()函数之间的不同之处在于:
* 没有显式地创建对象;
* 直接将属性和方法赋给了this对象;
* 没有return语句;
*按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。这个做法借鉴自其他OO语言,主要为了区别于ECMAScript中的其他函数;因为构造函数本身也是函数,只不过可以用来创建对象而已。*
~~~
var person1 = new Person("Tom", 18, "Student");
~~~
要创建Person对象的新实例,必须使用**new 操作符**。以这种方式调用构造函数实际上会经历以下四个步骤:
1. 创建一个新对象;
2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
3. 执行构造函数中的代码(为这个新对象添加属性);
4. 返回新对象。
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型。
**1.将构造函数当做函数**
任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数没有什么区别。当一个原意用作构造函数的函数不通过new操作符调用,那函数体内的this对象就指向全局作用域window。
~~~
//当作构造函数使用
var person1 = new Person("Tom", 18, "Student");
person1.sayName(); //"Tom"
//作为普通函数调用
Person("Tom", 18, "Student"); //添加到window
window.sayName(); //"Tom"
//在另一个对象的作用域中调用
var o = new Object();
Person.call(0, "Ken" ,28, "Teacher");
o.sayName(); //"Ken"
~~~
**2. 构造函数的问题**
构造函数的主要问题在于,每个方法都要在每个实例上重新创建一遍。可以通过把函数定义转移到构造函数外面来解决这个问题:
~~~
……
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
~~~
但这样的做法又带来了新问题,全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言。
### 6.2.3 原型模式
每个创建的函数都有一个**prototype属性**,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。换言之,**prototype就是通过调用构造函数而创建的那个对象实例的原型对象**。使用原型对象的好处是可以就让所有对象实例共享它所包含的属性和方法。例:
~~~
function Person(){
}
Person.prototype.name = "ken";
Person.prototype.age = 28;
Person.prototype.job = "Teacher";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.sayName == person2.sayName); //true (不加括号,否则对比的是运行值)
~~~
**1. 理解原型对象**
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个**prototype属性,这个属性指向函数的原型对象。**
在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针。例如Person.prototype.constructor指向Person。
![](https://box.kancloud.cn/2f1db2bbb7df552bc0acdee9de3393c5_675x325.png)
当调用构造函数创建一个新实例后,该实例的内部将包含一个[[prototype]]指针(主流浏览器中的proto属性)。要明确的重要一点是,这个连接存在于实例与原型对象之间,而不是存在于实例与构造函数之间。
虽然在所有实现中都无法访问[[prototype]],但可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。
~~~
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
~~~
ECMAScript5新增了`Ojbect.getPrototypeOf()`方法返回[[prototype]]的值。
~~~
alert(Object.getPrototypeOf(person1)) //Person.prototype
alert(Object.getPrototypeOf(person1).name) //ken
~~~
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性,但不会修改那个属性。可以使用delete操作符完全删除实例属性,重新访问原型中的属性。
使用`hasOwnProperty()`方法可以检测一个属性存在于实例还是原型中,存在于实例中返回true。
~~~
var person1 = new Person();
person1.name = 'jason';
delete person1.name;
alert(person1.name); //ken
~~~
**2. 原型与in操作符**
* 单独使用`in`操作符,会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。
~~~
alert('name' in person1); //true;
~~~
* 使用for-in循环时,返回的是所有能够通过对象访问的,可枚举的属性,其中既包含存在于实例中的属性,也包括存在原型中的属性。屏蔽了原型中不可枚举属性的实例属性也会在for-in循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的(除了IE8及更早版本)。
要取得对象上所有可枚举的实例属性,可以使用ECMAScript5中的`Object.keys()`方法。这个方法接收一个对象作为参数,返回一个**包含所有可枚举属性的字符串数组**。
如果想要得到所有实例属性,无论它是否可枚举,都可以使用`Object.getOwnPropertyNames()`。
总结:
~~~
for-in 实例+原型、可枚举;
Object.keys() 实例、可枚举;
Object.getOwnPropertyNames() 实例、可枚举+不可枚举
~~~
**3. 更简单的原型语法**
~~~
function Person(){
}
Person.prototype = {
name:'Ken',
age:28,
job:'FrontEnd Engineer',
sayName:function(){
alert(this.name);
}
};
~~~
使用这样的语法创建原型对象,**constructor属性不再指向Person**了。前面曾经介绍过,每创建一个函数,就会同时创建他的prototype对象。在这里使用的语法,本质上完全重写了默认的prototype对象,因此,constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。此时,尽管instanceof操作符还能返回正确的结果,但通过constructor已经无法确立对象的类型了。
如果constructor的值真的很重要,那么在使用上面的语法重写原型对象时,可以手动将construtor的属性值设为Person。注意,以这种方式重设constructor属性会导致他的[[Enumerable]]特性被置为true。可以通过Object.defineProperty()方法将属性置为默认的不可枚举:
~~~
Object.defineProperty(Person.prototype,'constructor',{
enumerable: false,
value: Person
});
~~~
**4. 原型的动态性**
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。
尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另一个对象就等于切断了构造函数与最初原型之间的联系。
**实例中的指针仅指向原型,而不指向构造函数。**
~~~
function Person(){
}
var friend = new Person();
Person.prototype = {
constructor:person,
name:"Ken",
age:28,
job: "Teacher",
sayName:function(){
alert(this.name);
}
};
friend.sayName(); //error
~~~
**5. 原生对象的原型**
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。
**注意:不推荐在产品化的程序中修改原生对象的原型。**
**6. 原形对象的问题**
* 省略了为构造函数传递初始化参数这一环节;
* 原型中所有属性是被很多实例共享的,对于包含引用类型值的属性来说,存在问题。
### 6.2.4 组合使用构造函数模式和原型模式
~~~
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friend = ["Shelby", "Court"];
}
Person.prototype = {
constructor: Person,
sayName: function () {
alert(this.name);
}
}
var person1 = new Person("Ken", 29, "Teacher");
var person2 = new Person("Tom", 18, "Student");
person1.friends.push("Van");
alert(person1.friends); //"Shelby","Court","van"
alert(person2.friends); //"Shelby","Court"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
~~~
上例中,实例属性都在构造函数中定义,由所有实例共享的属性constructor和方法sayName()则在原型中定义。
### 6.2.5 动态原型模式
动态原型模式解决了构造函数与原型分别独立、没有封装在一起的问题。
~~~
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ['wesley','fox'];
if (typeof this.sayName !== 'function') {
Person.prototype.sayName = function(){
alert(this.name)
}
}
}
~~~
### 6.2.6 寄生构造函数模式
基本思想:创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
寄生构造函数模式解决了这样一个场景下的问题:假设想创建一个具有额外方法的特殊数组,又不能直接修改Array构造函数,就可以使用这个模式。除了使用new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。
~~~
function SpecialArray(){
var values = new Array();
values.push.apply(values,arguments); //apply可以方便地把arguments添加到values数组。
values.toPipedString = function(){
return this.join('|');
}
return values;
}
var colors = new SpecialArray('red','blue','green');
alert(colors.toPipeString()); //'red|blue|green'
~~~
关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与构造函数外部创建的对象没什么不同。为此,不能依赖instanceof操作符来确定对象类型。由于存在上述问题,我们建议在在可以使用其他模式的情况下,不要使用这种模式。
~~~
alert(SpecialArray.prototype.isPrototypeOf(colors)); //false
alert(colors instanceof SpecialArray); //false
~~~
### 6.2.7 稳妥构造函数模式
所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。稳妥对象最适合在一些安全的环境中(这些环境会禁止使用this和new),或者防止数据被其他应用程序改动时使用。稳妥构造函数模式遵循与寄生构造函数模式类似的模式,但有两点不同:一是新创建对象实例方法不引用this;二是不使用new操作符调用构造函数。
~~~
function Person(name,age,job){
//var name = name,age = age,job = job;
var o = new Object();
o.sayName = function(){
alert(name);
};
return o;
}
var friend = Person('ken','28','FrontEnd Engineer');
friend.sayName(); //ken
~~~
这样,变量friend中保存的是一个稳妥对象,而除了调用sayName()方法外,没有别的方式可以访问其数据成员。即使有其他代码会给这个对象添加方法和数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此instanceof操作符对这种对象也没有意义。
- 前言
- 第一章 JavaScript简介
- 第三章 基本概念
- 3.1-3.3 语法、关键字和变量
- 3.4 数据类型
- 3.5-3.6 操作符、流控制语句(暂略)
- 3.7函数
- 第四章 变量的值、作用域与内存问题
- 第五章 引用类型
- 5.1 Object类型
- 5.2 Array类型
- 5.3 Date类型
- 5.4 基本包装类型
- 5.5 单体内置对象
- 第六章 面向对象的程序设计
- 6.1 理解对象
- 6.2 创建对象
- 6.3 继承
- 第七章 函数
- 7.1 函数概述
- 7.2 闭包
- 7.3 私有变量
- 第八章 BOM
- 8.1 window对象
- 8.2 location对象
- 8.3 navigator、screen与history对象
- 第九章 DOM
- 9.1 节点层次
- 9.2 DOM操作技术
- 9.3 DOM扩展
- 9.4 DOM2和DOM3
- 第十章 事件
- 10.1 事件流
- 10.2 事件处理程序
- 10.3 事件对象
- 10.4 事件类型
- 第十一章 JSON
- 11.1-11.2 语法与序列化选项
- 第十二章 正则表达式
- 12.1 创建正则表达式
- 12.2-12.3 模式匹配与RegExp对象
- 第十三章 Ajax
- 13.1 XMLHttpRequest对象
- 你不知道的JavaScript
- 一、作用域与闭包
- 1.1 作用域
- 1.2 词法作用域
- 1.3 函数作用域与块作用域
- 1.4 提升
- 1.5 作用域闭包
- 二、this与对象原型
- 2.1 关于this
- 2.2 全面解析this
- 2.3 对象
- 2.4 混合对象“类”
- 2.5 原型
- 2.6 行为委托
- 三、类型与语法
- 3.1 类型
- 3.2 值
- 3.3 原生函数