- A+
所属分类:Web前端
一、使用Object构造函数或对象字面量创建对象
缺点:使用同一个接口创建对象,会产生大量的重复性代码。
解决方法:使用工厂模式创建对象。
1 //构造函数 2 let obj1 = new Object(); 3 obj1.name = '我和鸽子有个约会'; 4 obj1.age = 22; 5 //字面量 6 let obj2 = { 7 name: '我和鸽子有个约会', 8 age: 22, 9 };
二、使用工厂模式创建对象
核心思想:创建一种函数,用函数来封装以特定接口创建对象的细节。
优点:
createPerson()可以根据接收来的三个参数来构建一个包含所有必要信息的person对象。
可以无数次地调用这个函数,每次都能返回一个包含三个属性和一个方法的对象。
这样就解决了创建多个相似对象,产生大量重复代码的问题。
缺点:
无法识别对象的类型,比如:我们想创建一个person类型(人类)的对象和cat类型(猫类)的对象,
但是我们无法将它们区分开来,因为它们本质上都是通过Object构造函数创建的。
解决方法:构造函数模式
1 function createPerson(name, age, job) { 2 let obj = new Object(); 3 obj.name = name; 4 obj.age = 22; 5 obj.job = job; 6 obj.sayName = function () { 7 console.log(this.name); 8 }; 9 return obj; 10 } 11 12 let person1 = createPerson('鸽子1', 22, 'programmer'); 13 let person2 = createPerson('鸽子2', 20, 'student'); 14 let cat = createPerson('猫', 3, 'Catch mice');
三、构造函数模式
ECMAScript中的构造函数可以用来创建特定类型的对象,像Object、Array这样的原生构造函数,在运行时会自动出现在
执行环境中,所以我们可以直接使用它们创建Object或者Array类型的对象。
当然,我们也可以自定义构造函数,从而定义自定义对象类型的属性和方法。
自定义构造函数与工厂模式中的函数的不同:
1.没有显示地创建对象;
2.直接将属性和方法赋给了this对象;
3.没有return语句
4.函数名首字母大写,不是硬性要求只是一种规范,目的是为了将构造函数和普通函数区分开来,
因为构造函数也是函数,只不过它可以用来创建对象而已。
使用构造函数创建对象这一行为,被称为该构造函数的实例化(类的实例),其对象被称为构造函数的实例或实例对象。
构造函数与其它函数的唯一区别就在于调用它们的方式不同。任何函数,只要通过new操作符调用,那它就是构造函数,
如果不通过new操作符来调用,那和普通函数没什么区别。
注意:
使用构造函数创建对象需要使用 new 操作符。
这种方式调用构造函数实际上会经历以下4个步骤:
1.创建一个空对象;
2.将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
3.执行构造函数中的代码(为这个新对象添加属性);
4.返回新对象。
缺点:
所有方法都要在每个实例上重新创建一遍。
例如:person3和person4中的sayName()方法,虽然它们同属于Person的实例,但其中的方法是独立的,
也就是说,每实例化一个对象,都会在该对象中创建一个新的方法,这样十分损耗性能。
解决方法:
1.将函数定义到构造函数外面,这样每个实例对象中的方法都是同一个方法。
1 function Person(name, age, job) { 2 this.name = name; 3 this.age = age; 4 this.job = job; 5 this.sayName = sayName; 6 } 7 function sayName() { 8 console.log(this.name); 9 } 10 console.log(person3.sayName === person4.sayName); //true 说明两个实例对象中的方法是同一个
但这样也有一个缺点:污染命名空间,比如一个构造函数有好多方法,就要定义好多变量,代码多了就
有可能出现重名的情况。
2.使用原型模式
四、原型模式
每个函数都有一个prototype(原型)属性,该属性是一个指针,指向一个对象,而这个对象的用途是包含由构造
函数创建的所有实例对象共享的属性和方法。
优点:
可以让所有实例对象共享它所包含的属性和方法。换句话说,不必在构造函数中定义实例对象的信息,可以将
这些信息直接添加到原型对象中,这样每个实例对象就能共用一个函数了,并且也不会污染命名空间。
1 function Person() { 2 } 3 4 Person.prototype.name = '鸽子1'; 5 Person.prototype.age = 22; 6 Person.prototype.job = 'programmer'; 7 Person.prototype.sayName = function () { 8 console.log(this.name); //鸽子1 9 }; 10 let person5 = new Person(); 11 let person6 = new Person(); 12 person5.sayName(); 13 person6.sayName(); 14 console.log(person5.sayName === person6.sayName); //true
4.1 原型对象
在创建函数的时候,系统会为该函数创建一个prototype指针,这个指针指向的对象被称为原型对象。
每个原型对象中都有一个constructor(构造函数)属性,该属性也是一个指针,指向其对应的构造函数。
每个实例对象中有两个指针: constructor(指向其构造函数) __proto__(指向其构造函数的原型对象)
1 console.log(person5.__proto__ === Person.prototype); //true 2 console.log(person5.constructor === Person); //true
两个方法:
A.prototype.isProtypeof(B)
判断实例对象B中是否存在一个指针,指向构造函数A的原型对象,是返回true,否返回false,
因为实例对象中有一个指针指向其构造函数的原型对象,所以我们可以间接的判断B是不是A的实例对象。
Object.getPrototypeOf(obj) 返回实例对象的原型对象
1 console.log(Person.prototype.isPrototypeOf(person5));//true 2 console.log(Object.getPrototypeOf(person5) === Person.prototype); //true 3 console.log(Object.getPrototypeOf(person5).name); //鸽子1
代码读取实例对象属性的过程:
每当代码读取某个对象的某个属性时,都会先在实例对象中查找该属性,如果查找到,则返回该属性的值;如果没找到,
则去往该对象的原型对象中查找,如果找到了,则返回该属性的值。也就是说,在我们通过person5调用sayName方法
时,会先后执行两次搜索,第一次是在person5对象中,第二次是在person5的原型对象中。当我们通过person6调用
sayName方法时,会重现相同的搜索过程,这正是多个实例对象共享原型对象中保存的属性和方法的基本原理。
注意:
虽然可以通过实例对象访问其原型对象中的值,但是却不能通过实例对象重写原型中的值。如果我们在实例对象中添
加一个属性,并且该属性与实例对象的原型中的一个属性同名,那么该属性就会将原型中的同名属性屏蔽,换句话说,
添加这个属性只会阻止我们访问原型中的那个属性,而不会修改那个属性,因为代码读取对象属性的时候,首先会在
该对象中查找,查找到就不再搜索其原型中的属性了。
1 console.log(person5.name);// 鸽子1 name来自原型对象 2 person5.name = '兔子'; //将原型中的name属性屏蔽 3 console.log(person5.name);// 兔子 name来自实例对象
in 操作符
语法: 'name' in obj
判断是否可以通过对象obj访问到name属性,可以则返回true,否则返回false,无论是该属性存在于实例中还是
原型中。
hasOwnProperty()方法
语法:obj.hasOwnProperty('name')
判断对象obj【自身中】是否含有name属性,有则返回true,无则返回false
该方法用于检测某属性是存在于实例对象中还是原型对象中,只有给定属性存在于实例对象中,才会返回true。
1 console.log('name' in person6);//true 2 console.log(person5.hasOwnProperty('name'));//true 3 console.log(person6.hasOwnProperty('name'));//false 4 //创建一个函数判断一个属性到底是存在于对象中,还是存在于其原型中 5 function hasPrototypeProperty(object, name) { 6 return !object.hasOwnProperty(name) && name in object;//返回false则表示存在于对象中,返回true表示存在于原型中 7 } 8 9 hasPrototypeProperty(person5, 'name');//false
4.2.更简单的原型语法
上面例子中,每给原型对象添加一个属性或方法就要敲一遍Person.prototype。
为减少代码量,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的字面量对
象来重写整个原型对象。
1 function Person() { 2 3 } 4 5 Person.prototype = { 6 name: '鸽子1', 7 age: 22, 8 job: 'programmer', 9 sayName: function () { 10 console.log(this.name); 11 }, 12 }; 13 console.log(Person.prototype.constructor === Person);//false
注意:
上面的代码中,我们将Person.prototype指向一个新的对象,虽然结果相同,但有一个例外:
该原型对象的constructor属性不再指向Person了,而是指向Object。
这是因为每创建一个函数,都会生成一个prototype属性指向它的原型对象,并且该原型对象有一个constructor
属性指向这个函数,然而,Person.prototype = {},本质上是将prototype属性指向一个新的对象,而这个新对
象的构造函数是Object。
如果constructor的值真的很重要,我们可以手动改回来
1 Person.prototype = { 2 constructor: Person,//手动更改原型对象中的constructor指向 3 name: '鸽子1', 4 age: 22, 5 job: 'programmer', 6 sayName: function () { 7 console.log(this.name); 8 }, 9 }; 10 console.log(Person.prototype.constructor === Person);//true
4.3 原型的动态性
因为在原型中查找值的过程是一次搜索,因此我们对原型对象的任何修改都能够立即从实例上反映出来,即使是先创
建了实例后修改原型也是如此。
就像下边的例子:
虽然friend实例是在新方法之前创建的,但是它仍能访问到这个新方法,其原因可以归结为实例与原型之间的
松散连接关系。当我们调用friend.sayHello()方法时,首先会在friend对象中搜索sayHello属性,在没
找到的情况下,会继续搜索原型。
1 let friend = new Person(); 2 Person.prototype.sayHello = function () { 3 console.log('hello'); 4 }; 5 friend.sayHello();//hello
注意:
尽管可以随时为原型添加属性和方法,并且修改能够立即在对象实例中反映出来,但是如果重写整个原型对象,那么
情况就不一样了。
因为调用构造函数时会为实例添加一个指向最初原型的指针__proto__,而把原型修改为另一个对象就等于切断了
构造函数与原型之间的联系。
1 function Person() { 2 } 3 4 let person7 = new Person(); 5 Person.prototype = { 6 constructor: Person, 7 gugugu: '鸽子', 8 }; 9 console.log(person7.gugugu);//undefined 10 console.log(person7.__proto__ === Person.prototype);//false
原型模式的缺点:
1.我们通过上面的例子可以发现,它省略了为构造函数传递初始化参数这一环节,结果所有实例对象在默认情况下都
将取得相同的属性值。
2.原型模式最大的问题是由原型对象共享的特性所导致的。
原型中所有属性是被多个实例所共享的,这种共享对于函数非常适合,对于那些包含基本数据类型值的属性倒也说的
过去,毕竟,通过在实例上添加一个同名属性,就可以隐藏原型中对应的属性。然而,对于包含引用类型值的属性来说,
这个问题就比较突出了。
1 function Animal() { 2 3 } 4 5 Animal.prototype = { 6 constructor: Animal, 7 name: '鸽子', 8 friends: ['猫咪', '大黄', '二哈'], 9 }; 10 let animal1 = new Animal(); 11 let animal2 = new Animal(); 12 animal1.friends.push('小白'); 13 console.log(animal1.friends);//['猫咪', '大黄', '二哈','小白'] 14 console.log(animal2.friends);//['猫咪', '大黄', '二哈','小白'] 15 console.log(animal1.friends === animal2.friends);//true
从上面的代码中,我们就能看出问题:
当我们修改了animal1.friends引用的数组,向该数组中添加了一个字符串'小白'。由于friends数组存在于
Animal.prototype而非animal1中,所以刚才的修改也会通过animal2.friends(因为animal1.friends
和animal2.friends指向同一个数组)反映出来。
假如我们的初衷就是像这样所有实例共享一个数组,那没什么问题,但是实例一般都是要有属于自己的全部属性的,
所以很少有人会单独使用原型模式。
解决方法:组合使用构造函数模式和原型模式
五、组合使用构造函数模式和原型模式
创建自定义类型的最常见的方式,就是组合使用构造函数模式和原型模式,构造函数模式用于定义每个实例独立的属
性,而原型模式用于定义方法和共享的属性。
优点:
1.每个实例都会有自己的一份实例属性副本,但同时又能共享者对方法的引用,最大限度地节省了内存;
2.支持向构造函数传递参数
1 function Animal(name, age, job) { 2 this.name = name; 3 this.age = age; 4 this.job = job; 5 this.friends = ['猫咪', '大黄', '二哈']; 6 } 7 8 Animal.prototype.sayName = function () { 9 console.log(this.name); 10 }; 11 let animal3 = new Animal('鸽子', 3, 'gugugu'); 12 let animal4 = new Animal('大白鹅', 2, 'gugugu'); 13 animal3.sayName(); 14 animal4.sayName(); 15 animal3.friends.push('小白'); 16 console.log(animal3.friends);//['猫咪', '大黄', '二哈','小白'] 17 console.log(animal4.friends);//['猫咪', '大黄', '二哈'] 18 console.log(animal3.friends === animal4.friends);//false
作者:我和鸽子有个约会
出处:http://www.cnblogs.com/gugugu/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。