参考文章:
《JavaScript高级程序设计》
JavaScript深入之创建对象的多种方式以及优缺点
创建对象
虽然 Object 构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同 一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,人们开始使用工厂模式的一种变体。
工厂模式
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("Nicholas", 29, "hello word");
var person2 = createPerson("greg", 24, "Doctor");
函数 createPerson()能够根据接受的参数来构建一个包含所有必要信息的 Person 对象。可以无 数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建 多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
缺点:对象无法识别,因为所有的实例都指向一个原型
构造函数模式
重写上面的例子:
function Person(name, age, obj) {
this.name = name;
this.age = age;
this.obj = obj;
this.sayName = function() {
alert(this.name);
}
}
var person1 = new Person("nicholas", 29, "hello word");
var person2 = new Person("grag", 27, "doctor");
在这个例子中,Person()函数取代了 createPerson()函数。我们注意到,Person()中的代码 除了与 createPerson()中相同的部分外,还存在以下不同之处:
- 没有显式地创建对象;
- 直接将属性和方法赋给了 this 对象;
- 没有 return 语句.
要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4 个步骤:
(1) 创建一个新对象;
(2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
(3) 执行构造函数中的代码(为这个新对象添加属性);
(4) 返回新对象。
在这个例子中创建的所有对象既是 Object 的实例,同时也是 Person 的实例.
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式 胜过工厂模式的地方。在这个例子中,person1 和 person2 之所以同时是 Object 的实例,是因为所 有对象均继承自 Object(详细内容稍后讨论) .
缺点:构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个 实例上重新创建一遍。。在前面的例子中,person1 和 person2 都有一个名为 sayName()的方法,但那 两个方法不是同一个 Function 的实例。
this.sayName = new Function("alert(this.name)"); // 与声明函数在逻辑上是等价的
更容易明白每个 Person 实例都包含一个不同的 Function 实例
不同实例上的同名函数是不相等的
alert(person1.sayName == person2.sayName); //false
优点:实例可以识别为一个特定的类型
缺点:每次创建实例时,每个方法都要被创建一次
构造函数模式优化
创建两个完成同样任务的 Function 实例的确没有必要;况且有 this 对象在,根本不用在 执行代码前就把函数绑定到特定对象上面。因此,大可像下面这样,通过把函数定义转移到构造函数外 部来解决这个问题。
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = sayName; }
function sayName(){ alert(this.name); }
var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor");
优点:解决了每个方法都要被重新创建的问题
缺点:如果对象需要定义很多方 法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。
原型模式
function Person(name) {
}
Person.prototype = {
name: 'kevin',
getName: function () {
console.log(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
alert(person1.sayName == person2.sayName); //true
hasOwnProperty()方法,访问的是实例属性,有才会返回true,原型上的会返回false。
in 操作符会在通 过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中
由于 in 操作符只要通过对象能够访问到属性就返回 true,hasOwnProperty()只在属性存在于 实例中时才返回 true,因此只要 in 操作符返回 true 而 hasOwnProperty()返回 false,就可以确 定属性是原型中的属性。
我们将 Person.prototype 设置为等于一个以对象字面量形式创建的新对象。 终结果相同,但有一个例外:constructor 属性不再指向 Person 了。前面曾经介绍过,每创建一 个函数,就会同时创建它的 prototype 对象,这个对象也会自动获得 constructor 属性。而我们在 这里使用的语法,本质上完全重写了默认的 prototype 对象,因此 constructor 属性也就变成了新 对象的 constructor 属性(指向 Object 构造函数),不再指向 Person 函数
alert(friend.constructor == Person); //false
alert(friend.constructor == Object); //true
原型模式优化
function Person(name) {
}
Person.prototype = {
constructor : Person,
name : "Nicholas", age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
以上代码特意包含了一个 constructor 属性,并将它的值设置为 Person,从而确保了通过该属 性能够访问到适当的值。
注意,以这种方式重设 constructor 属性会导致它的[[Enumerable]]特性被设置为 true。默认 情况下,原生的 constructor 属性是不可枚举的,因此如果你使用兼容 ECMAScript 5的 JavaScript引 擎,可以试一试 Object.defineProperty()。
原型模式再优化
function Person(name) {
}
Person.prototype = {
name : "Nicholas", age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
Object.defineProperty(Person.prototype,"constructor" {
enumerable: false,
value: Person
});
优点:实例可以通过constructor属性找到所属构造函数
缺点:原型模式该有的缺点还是有.
组合模式
构造函数模式与原型模式双剑合璧。构造函数模式用于定义实 例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本, 但同时又共享着对方法的引用,大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参 数;可谓是集两种模式之长。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ["shelby","court"];
}
Person.prototype = {
constructor = Person;
sayName: function() {
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor 和方 法 sayName()则是在原型中定义的。而修改了 person1.friends(向其中添加一个新字符串),并不 会影响到 person2.friends,因为它们分别引用了不同的数组。
优点:该共享的共享,该私有的私有,使用最广泛的方式
缺点:有的人就是希望全部都写在一起,即更好的封装性
动态原型模式
function Person(name, age, obj) {
//属性
this.name = name;
this.age = age;
this.job = job;
if(typeof this.sayName != "function") {
Person.prototype.sayName = function() {
alert(this.name);
}
}
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();
注意:使用动态原型模式时,不能用对象字面量重写原型
解释下为什么:
function Person(name) {
this.name = name;
if (typeof this.getName != "function") {
Person.prototype = {
constructor: Person,
getName: function () {
console.log(this.name);
}
}
}
}
var person1 = new Person('kevin');
var person2 = new Person('daisy');
// 报错 并没有该方法
person1.getName();
// 注释掉上面的代码,这句是可以执行的。
person2.getName();
使用字面量方式直接覆盖 Person.prototype,并不会更改实例的原型的值,person1 依然是指向了以前的原型,而不是 Person.prototype。而之前的原型是没有 getName 方法的,所以就报错了!
寄生构造函数模式
这种模式 的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但 从表面上看,这个函数又很像是典型的构造函数。
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 friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
返回的对象与构造函数或者与构造函数的原型属 性之间没有关系
寄生构造函数模式,我个人认为应该这样读:
寄生-构造函数-模式,也就是说寄生在构造函数的一种方法。
也就是说打着构造函数的幌子挂羊头卖狗肉,你看创建的实例使用 instanceof 都无法指向构造函数.
彩蛋
1.使用字面量方式直接覆盖 Person.prototype,并不会更改实例的原型的值?
function Person(name) {
this.name = name;
if (typeof this.getName != "function") {
Person.prototype = {
constructor: Person,
getName: function () {
console.log(this.name);
}
}
}
}
var person1 = new Person('kevin');
以这个例子为例,当执行 var person1 = new Person('kevin') 的时候,
person1.的原型并不是指向 Person.prototype,
而是指向 Person.prototype 指向的原型对象,
我们假设这个原型对象名字为 O,
然后再修改 Person.prototype 的值为一个字面量,
只是将一个新的值赋值给 Person.prototype,
并没有修改 O 对象,也不会切断已经建立的 person1 和 O 的原型关系,
访问 person.getName 方法,依然会从 O 上查找.
再看个例子:
var a = {
b: O
}
你看 a.b 指向了 O 对象,就相当于 Person.prototype 指向了原型对象这句话。
再看这个例子:
function Person(name) {
this.name = name;
if (typeof this.getName != "function") {
Person.prototype = {
constructor: Person,
getName: function () {
console.log(this.name);
}
}
}
}
var person1 = new Person('kevin');
当 new Person() 的时候,是先建立的原型关系,
即 person .proto = Person.prototype,
而后修改了 Person.prototype 的值,这就相当于:
// O 表示原型对象
var O = {};
var a = {
b: O
}
先建立原型关系,指的是 c.proto = a.b = O
而后修改 Person.prototype 的值,相当于
var anotherO = {};
a.b = anotherO;
即便修改了 Person.prototype 的值,但是 c.proto 还是指向以前的 O
不知道这样解释的清不清楚,欢迎交流~
https://github.com/mqyqingfeng/Blog/issues/15
2.请问组合模式哪些方法和属性写在prototype,哪些写在构造函数里?
共享的写在 prototype 中,独立的写在构造函数中。
我们以弹窗组件举个例子:
function Dialog(options) {
this.options = options;
}
Dialog.prototype.show = function(){...}
如果我们一个页面用到多个 dialog:
var dialog1 = new Dialog({value: 1});
var dialog2 = new Dialog({value: 2});
dialog1 和 dialog2 传入的参数不一样,写在构造函数中,我们可以通过
console.log(dialog1.options) 访问 dialog1 的配置选项
而对于 show 方法而言,所以的 dialog 都是公用的,所以写在 prototype 属性中.
加油~