创建对象及原型
一、使用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); //true2 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));//true2 console.log(Object.getPrototypeOf(person5) === Person.prototype); //true3 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);//true2 console.log(person5.hasOwnProperty('name'));//true3 console.log(person6.hasOwnProperty('name'));//false4 //创建一个函数判断一个属性到底是存在于对象中,还是存在于其原型中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);//undefined10 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
作者:我和鸽子有个约会
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
赞 (0)