目录
1. 工厂模式
创建对象的方法
1. 用字面量的方式创建对象
var person = {
name: "zhangsan",
age: 18,
sayName: function(){
console.log(this.name);
}
}
缺点:对象一次性。例如有二十个人,代码就要写二十次。
2. new Object()创建对象
var person = new Object();
//为这个实例化的对象添加属性
person.name = "zhangsan";
person.age = 18;
person.sayName = function(){
console.log(this.name)
}
缺点:它先实例化一个对象,然后再为对象添加属性,这样就看不出来是一个整体。
3. 使用工厂模式创建对象
作用:创建对象,降低代码的冗余度。
// 将创建对象的代码封装在一个函数中
function createPerson(name, age) {
var person = new Object();
person.name = name;
person.age = age;
person.sayName = function () {
console.log(this.name);
}
return person;
}
// 利用工厂函数来创建对象
var p1 = createPerson('zhangsan', 19); //{ name: 'zhangsan', age: 19, sayName: [Function (anonymous)] }
var p2 = createPerson('lisi', 30);//{ name: 'lisi', age: 30, sayName: [Function (anonymous)] }
console.log(p1, p2);
console.log(p1 instanceof Object); //true
console.log(p1 instanceof createPerson); //false
优点:对创建对象的过程进行了封装,可以方便往里面添加方法和属性。
缺点:这种方式本质上是将创建对象的过程进行了封装,本质并没有改变,我们创建一个student时无法知道其具体的数据类型,只知道这是一个对象,往往实际开发中我们需要确定这个对象到底是个Person的实例还是Dog的实例。
因此:我们可以使用自定义构造函数。
2. 构造函数模式
2.1自定义构造函数
// 自定义构造函数
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
}
}
// 使用自定义构造函数
var p1 = new Person('zhangsan', 20);
var p2 = new Person;
p1.sayName(); // zhangsan
p2.sayName(); // undefined
console.log(p1, p2);
与工厂模式的区别:
- 没有显示地创建对象;
- 属性和方法直接赋值给了this;
- 没有return。
另外,要注意函数名 Person 的首字母大写了。按照惯例,构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。这是从面向对象编程语言那里借鉴的,有助于在 ECMAScript 中区分构造函数和普通函数。毕竟 ECMAScript 的构造函数就是能创建对象的函数。
2.2创建Person实例
要创建实例,应使用new操作符。
var person1 = new Person('zhangsan', 19);
var person2 = new Person('lisi', 16);
(1) 在内存中创建一个新的对象。
(2) 这个新对象内部的[[Prototype]] 特性被赋值为构造函数的 prototype 属性。
(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
person1 和 person2 分别保存着 Person 的不同实例。所有对象都会从它的原型上继承一个 constructor 属性,这两个对象的constructor 属性指向 Person。
console.log(person1.constructor === Person); // true
console.log(person2.constructor === Person); // true
2.3 instanceof
instanceof
运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。或者说判断一个对象是某个对象的实例。
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
自定义构造函数可以确保实例被标识为特定类型,在这个案例中,person1 和 person2 之所以也被认为是 Object 的实例,是因为所有自定义对象都继承自 Object。
2.4 使用函数表达式自定义构造函数
var Person = function(name,age){
this.name=name;
this.age=age;
this.sayName=function(){
console.log(this.name);
}
}
// 使用自定义构造函数
var p1=new Person('zhangsan',20);
var p2=new Person;
p1.sayName(); // zhangsan
p2.sayName(); // undefined
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
实例化时,如果不想传参数,那么构造函数后面的括号可加可不加。只要有 new 操作符,就可以调用相应的构造函数。
2.5 构造函数也是函数
任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数。
// 自定义构造函数
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
}
}
// 使用自定义构造函数
var p1 = new Person('zhangsan', 20);
var p2 = new Person;
p1.sayName(); // zhangsan
p2.sayName(); // undefined
console.log(p1, p2);
console.log(p1.constructor === Person);//true
console.log(p1.constructor === Object); //false
// instanceof 判断一个对象是否某个对象的实例
console.log(p1 instanceof Person); //true
console.log(p1 instanceof Object); //true
// 不使用new关键字进行调用,普通函数的执行
Person('lisi',20); //添加到全局对象 node global浏览器 window
sayName(); //lisi
global.sayName(); //lisi
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "wangwu", 25);
o.sayName(); // wangwu
console.log(p1.sayName === p2.sayName); //false
这时候没有使用 new 操作符调用 Person(),结果会将属性和方法添加到全局对象。这里要记住,在调用一个函数而没有明确设置 this 值的情况下(即没有作为对象的方法调用,或者没有使用call()/apply()调用),this 始终指向 Global 对象(在浏览器中就是 window 对象)。因此在上面的调用之后,Global 对象上就有了一个 sayName()方法,调用它会返回"zhangsan"。
最后的调用方式是通过 call()(或 apply())调用函数,同时将特定对象指定为作用域。这里的调用将对象 o 指定为 Person()内部的 this 值,因此执行完函数代码后,所有属性和 sayName()方法都会添加到对象 o 上面。
2.6 构造函数的问题
// 自定义构造函数
function Person(name,age){
this.name=name;
this.age=age;
this.sayName=sayName;
}
function sayName(){
console.log(this.name);
}
var p1=new Person('zhangsan',20);
var p2=new Person('lisi',18);
p1.sayName(); //zhangsan
p2.sayName(); //lisi
console.log(p1.sayName===p2.sayName); //true
在这里,sayName()被定义在了构造函数外部。在构造函数内部,sayName 属性等于全局 sayName()函数。因为这一次 sayName 属性中包含的只是一个指向外部函数的指针,所以 person1 和 person2共享了定义在全局作用域上的 sayName()函数。这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,因为那个函数实际上只能在一个对象上调用。如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好地聚集一起。
这个新问题可以通过原型模式来解决。
3. 原型模式
每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。 实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型。
function Person() { }
Person.prototype.name = "zhangsan";
Person.prototype.age = 29;
Person.prototype.gender = "male";
Person.prototype.sayName = function () {
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
// 通过hasOwnProperty()可以查看访问的是实例属性还是原型属性,实例返回true,原型返回false。
console.log(person1.hasOwnProperty('name')); //false
person1.name = "lisi";
console.log(person1.name); // lisi,来自实例
//只在重写 person1 上 name 属性的情况下才返回 true,表明此时 name 是一个实例属性,不是原型属性
console.log(person1.hasOwnProperty('name')); //true
console.log(person2.name); // zhangsan,来自原型
console.log(person2.hasOwnProperty('name'));//false
delete person1.name;
console.log(person1.name); // zhangsan,来自原型
console.log(person1.hasOwnProperty('name'));//false
使用 delete 删除了 person1.name,这个属性之前以"lisi"遮蔽了原型上的同名属性。然后原型上 name 属性的联系就恢复了,因此再访问 person1.name 时,就会返回原型对象上这个属性的值。
function Person() { };
Person.prototype.name = 'zhangsan';
Person.prototype.age = 19;
Person.prototype.sayName = function () {
console.log(this.name);
}
// var p1=new Person();
// p1.sayName(); //zhangsan
var p1 = new Person();
p1.name = 'wangwu';
p1.sayName(); //wangwu,来自实例
var p2 = new Person();
p2.sayName(); //zhangsan,来自原型
console.log(p1.sayName === p2.sayName); //true
// 无论属性是在实例上还是在原型上,都可以检测到
console.log("name" in p1); // true
console.log("name" in p2); // true
// 判断一个属性是否原型属性
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object);
}
console.log(hasPrototypeProperty(p1, 'name')); //false
console.log(hasPrototypeProperty(p2, 'name'));//true
3.1原生对象的原型
原型模式之所以重要,不仅体现在自定义类型上,而且还因为它也是实现所有原生引用类型的模式。所有原生引用类型的构造函数(包括 Object、Array、String 等)都在原型上定义了实例方法。比如,数组实例的 sort()方法就是 Array.prototype 上定义的,而字符串包装对象的 substring()方法也是在 String.prototype 上定义的,如下所示:
console.log(typeof Array.prototype.sort); // "function"
console.log(typeof String.prototype.substring); // "function"
通过原生对象的原型可以取得所有默认方法的引用,也可以给原生类型的实例定义新的方法。可以像修改自定义对象原型一样修改原生对象原型,因此随时可以添加方法。比如,下面的代码就给 String原始值包装类型的
实例添加了一个 last()方法:
//给字符串添加属性或方法 要写到对应的包装对象的原型下才行
var str = 'hello';
String.prototype.last = function () {
// 返回指定位置的字符
return this.charAt(this.length - 1);
};
console.log(str.last()); // o
果给定字符串调用 last()方法,那么该方法会返回 给定字符串的最后一个字符。因为这个方法是被定义在 String.prototype 上,所以当前环境下所有的字符串都可以使用这个方法。str是个字符串,在读取它的属性时,后台会自动创建 String 的包装实例,从而找到并调用 last()方法。
注意:尽管可以这么做,但并不推荐在产品/生产环境中修改原生对象原型。这样做很可能造成误会,而且可能引发命名冲突。另外还有可能意外重写原生的方法。
3.2更简单的原型模式
为了减少代码冗余,也为了从视觉上更好地封装原型功能,直接通过一个包含所有属性和方法的对象字面量来重写原型成为了一种常见的做法。
function Person() {}
Person.prototype = {
name: "zhangsan",
age: 29,
sayName() {
console.log(this.name);
}
};
//在这个案例中,Person.prototype 被设置为等于一个通过对象字面量创建的新对象。最终结果是一样的,
//只有一个问题:这样重写之后,Person.prototype 的 constructor 属性就不指向 Person了。在创建函数
//时,也会创建它的 prototype 对象,同时会自动给这个原型的 constructor 属性赋值。而上面的写法完全
//重写了默认的 prototype 对象,因此其 constructor 属性也指向了完全不同的新对象(Object 构造数),
//不再指向原来的构造函数。
var person1 = new Person()
console.log(person1.constructor === Person); //false
console.log(person1.constructor === Object); //true
Person.prototype相当于var a。
解决constructor问题
重写原型对象,专门设置constructor的值。
function Person() { }
Person.prototype = {
//这种方式恢复 constructor 属性会创建一个[[Enumerable]]为 true 的属性
//constructor: Person,
name: "zhangsan",
age: 29,
sayName() {
console.log(this.name);
}
};
// 恢复 constructor 属性
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
var person1 = new Person()
console.log(person1.constructor == Person); //true
console.log(person1.constructor == Object); //false
3.3原型的问题
function Person() { }
Person.prototype = {
constructor: Person,
name: "zhangsan",
friends: ["lisi", "wangwu"],
sayName() {
console.log(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("zhaoliu");
console.log(person1.friends); // [ 'lisi', 'wangwu', 'zhaoliu' ]
console.log(person2.friends); // [ 'lisi', 'wangwu', 'zhaoliu' ]
console.log(person1.friends === person2.friends); // true
这里,Person.prototype 有一个名为 friends 的属性,它包含一个字符串数组。然后这里创建了两个Person 的实例。person1.friends 通过 push 方法向数组中添加了一个字符串。由于这个friends 属性存在于 Person.prototype 而非 person1 上,新加的这个字符串也会在(指向同一个数组的)person2.friends 上反映出来。
4. 组合模式
组合使用构造函数模式和原型模式。
// 在构造函数中 定义一些不需要共享的属性
function Person(name, age) {
this.name = name;
this.age = age;
this.firends = ['zhangsan', 'lisi'];
}
// 将要共享的参数或方法定义在原型上
Person.prototype = {
sayName() {
console.log(this.name);
}
};
// 改变构造函数的指向
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
var p1 = new Person('larry', 23);
var p2 = new Person('terry', 17);
p1.firends.push('robin');
console.log(p1.firends); // [ 'zhangsan', 'lisi', 'robin' ]
console.log(p2.firends); // [ 'zhangsan', 'lisi' ]
console.log(p1.firends === p2.firends); // false
console.log(p1.sayName === p2.sayName); // true
1. 工厂模式
优点:批量创建对象,封装创建对象的函数,提高代码复用率。
缺点:无法区分创建出来的对象种类,方法冗余。(创建的实例都是Object的实例)
2. 构造函数模式
优点:可以批量创建对象和区分种类。
缺点:方法冗余。
3. 原型模式
将所有的属性和方法都放在原型对象中,构造函数中不存放任何属性和方法,不单独使用。
4. 组合模式(构造函数模式加原型模式)
构造函数中放实例私有的属性和私有方法,构造函数原型对象中放共有属性和方法。