字面量就是用来描述变量的;一般来说,给变量赋值的时候,等号右边的都可以看作是字面量(因为等号右边的都是用来描述这个变量的,比如描述一个变量为字符串(字符串字面量)、一个数组(数组字面量)、一个对象(对象字面量),等等)。
var person = { name: "zhangsan", age: 18, gender: 'male', sayName: function(){ console.log(this.name); } }
缺点:用字面量的方式来创建对象,最大的缺点就是,这个对象是一次性的,如果有四十个同学,这个代码就要写四十次,有点小麻烦。
Object是JavaScript提供的构造函数;new Object()就是利用JavaScript提供的构造函数实例化了一个对象;
var person = new Object(); //为这个实例化的对象添加属性 person.name = "zhangsan"; person.age = 18; person.gender = 'male'; person.sayName = function(){ console.log(this.name) }
缺点:可以发现它是先实例化了一个对象,然后再为对象添加属性,这样就看不出来是个整体(像上面的用字面量来创建,属性都包在一个大括号里面,这样就很好看出这是个整体)。
我们为了使创建对象更加方便(不像字面量创建那样一次性),也为了写的代码更像个整体,就可以交给工厂模式来做。
什么是工厂模式?
工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。工厂模式是一种创建型模式,简单来说,工厂模式就是创建对象的一种方式。
工厂模式有什么用?
作用:创建对象;降低代码冗余度。
应用场景:当你想要批量生产同种类的对象的时候;比如,你想生成一个班级的40个学生,每个学生都有姓名、年龄等特征。这时候你创建一个“工厂”,把信息丢到工厂里,工厂就给你造一个人出来,非常方便。
为什么用工厂模式?
从工厂模式的作用出发来看,工厂模式的主要作用就是用来产生对象的。
使用工厂模式创建对象
//将创建对象的代码封装在一个函数中 function createPerson(name, age, gender) { var person = new Object(); person.name = name; person.age = age; person.gender = gender; person.sayName = function () { console.log(this.name); } return person; } //利用工厂函数来创建对象 var person1 = createPerson("zhangsan", 18, 'male'); var person2 = createPerson("lisi", 20, 'female');
优点:只要我们往工厂函数里面塞参数,工厂函数就会像生产产品一样造个人出来。
缺点:这种方式本质上是将创建对象的过程进行了封装,本质并没有改变,我们创建一个student时无法知道其具体的数据类型,只知道这是一个对象,往往实际开发中我们需要确定这个对象到底是个Person的实例还是Dog的实例。(只是返回对象,不知道是属于哪个实例对象)
所以,我们可以使用自定义构造函数模式。
ECMAScript 中的构造函数是用于创建特定类型对象的。像 Object 和 Array 这样的原生构造函数,运行时可以直接在执行环境中使用。当然也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。
JavaScript中可以自定义构造函数,从而自定义对象类型的属性和方法,构造函数本身也是函数,只不过可以用来创建对象。
// 自定义构造函数 function Person(name, age, gender) { this.name = name; this.age = age; this.gender = gender; this.sayName = function () { console.log(this.name); } } var person1 = new Person('zhangsan', 29, 'male'); var person2 = new Person('lisi', 19, 'female'); person1.sayName(); // zhangsan person2.sayName(); // lisi
与工厂模式区别:
没有显式地创建对象。(需要通过new的方式创建对象,工厂模式直接调用函数可以创建对象 )
属性和方法直接赋值给了 this。
没有 return。
首字母大写
可以识别对象类型,看到属于哪个实例对象。
创建Person的新实例,必须使用new操作符。调用构造函数会执行的步骤:
(1) 在内存中创建一个新对象。
(2) 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。([[Prototype]]就是 _proto__)
(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
构造函数缺点: 构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍。就是说每创建一个实例对象就会创建一个构造函数里面的同名的方法,创建的同名的这个方法还是不相等的。创建新 Function实例的机制是一样的。因此不同实例上的函数虽然同名却不相等,如下所示:
console.log(person1.sayName == person2.sayName); // false
解决方法:可以把函数定义转移到构造函数外部
function Person(name, age, gender) { this.name = name; this.age = age; this.gender = gender; this.sayName = sayName; } function sayName() { console.log(this.name); }
但是这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,因为那个函数实际上只能在一个对象上调用。如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好地聚集一起。因此这个新问题可以通过原型模式来解决。
什么是原型?
每一个函数都有一个属性叫做prototype,这个值是一个对象,默认只有一个叫做constructor的属性,指向这个函数对象
原型就是一个对象,其他的对象可以通过它实现一个属性的继承。
详细理解原型:
①所有引用类型都有一个__proto__(隐式原型)属性,属性值是一个普通的对象
②所有函数都有一个prototype(原型)属性,属性值是一个普通的对象
③所有引用类型的__proto__属性指向它构造函数的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(); person1.sayName(); // zhangsan var person2 = new Person(); person2.sayName(); // zhangsan console.log(person1.sayName == person2.sayName); // true
与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。因此 person1 和 person2 访问的都是相同的属性和相同的sayName()函数,也就解决了构造函数模式的缺点。
当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到就会再在构造函数的prototype的__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。
原型属性的覆盖:
//使用原型模式创建一个构造函数 使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。 function Person(){} //直接在Person的原型对象上进行属性和方法的定义 Person.prototype.name = "zhangsan"; Person.prototype.age = 29; Person.prototype.gender = "male"; Person.prototype.sayName = function () { console.log(this.name); }; var p1 = new Person(); p1.sayName();//zhangsan var p2 = new Person(); p1.sayName();//zhangsan console.log(p1.sayName === p2.sayName);//true,再定义执行不在创建新sayName,方法和属性已被共享 var p3 = new Person(); p3.name='lisi'//遮蔽了原型对象上的同名属性(覆盖),相当于在自己的实例原型中添加name属性,按原型链,找最近原则 p1.sayName();//zhangsan p3.sayName();//lisi // 通过hasOwnProperty()可以查看访问的是实例属性还是原型属性 console.log(p3.hasOwnProperty('name')); //true 自己的name console.log(p2.hasOwnProperty('name')); //false 原型上的name delete p3.name;//删除了自己的name,就会往上找,找到原型name console.log(p3.name);//zhangsan // 检测是否是原型属性 function hasPrototypeProperty(object, name) { //不在实例中但是可以访问到的属性属于原型属性 return !object.hasOwnProperty(name) && (name in object); } console.log(hasPrototypeProperty(p1,'name'));//true console.log(hasPrototypeProperty(p3,'name'));//true
在前面的案例中,每次定义一个属性或方法都会把 Person.prototype 重写一遍。为了减少代码冗余,也为了从视觉上更好地封装原型功能,直接通过一个包含所有属性和方法的对象字面量来重写原型成为了一种常见的做法,如下面的案例所示:
function Person() {} Person.prototype = { name: "zhangsan", age: 29, gender: "male", 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 被设置为等于一个通过对象字面量创建的新对象,相当与就是字面量再创建的对象,不再指向原来的构造函数(Person)。
解决:
1.设置constructor: Person。这种方式恢复 constructor 属性会创建一个[[Enumerable]]为 true 的属性。
2.利用自定义设置 恢复 constructor 属性
function Person(){} // 当一个Person.prototype使用对象字面量进行赋值的时候 它的constructor指向就会发生变化,通过对应的属性让他指回对应的构造函数 Person.prototype ={ //1.设置constructor: Person。这种方式恢复 constructor 属性会创建一个[[Enumerable]]为 true 的属性 //constructor: Person, name:"zhangsan", age:16, gender:'male', sayName:function(){ console.log(this.name); } } var p1=new Person(); console.log(p1.constructor == Object)//true console.log(p1.constructor == Person)//false // 2.利用自定义设置 恢复 constructor 属性 Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person }); console.log(p1.constructor )//[Function: Person] console.log(p1.constructor)//[Function: Person]
原型模式也不是没有问题。首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。虽然这会带来不便,但还不是原型的最大问题。原型的最主要问题源自它的共享特性。
我们知道,原型上的所有属性是在实例间共享的,这对函数来说比较合适。另外包含原始值的属性也还好,如前面案例中所示,可以通过在实例上添加同名属性来简单地遮蔽原型上的属性。真正的问题来自包含引用值的属性。来看下面的案例:
// 弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值 function Person(){} Person.prototype ={ constructor: Person, name:"zhangsan", friends:['zhangsan','lisi'] } var p1 = new Person() var p2 = new Person() var p3 = new Person() //原型属性的覆盖 p1.friends=[1,2,3]//覆盖了原型friend,有了自己的friends,不继承原型上的(原型链继承) console.log(p1.friends)//[ 1, 2, 3 ] //共享带来的问题 p2.friends.push('xilin') console.log(p2.friends)//[ 'zhangsan', 'lisi', 'xilin' ] console.log(p3.friends)//[ 'zhangsan', 'lisi', 'xilin' ] //简单原型问题解决打印 console.log(p2)//Person {}, 如果上面没添加constructor,则指向Object,结果为{},不知道是哪个构造函数 console.log(p1.friends == p2.friends)//false console.log(p2.friends == p3.friends)//true
如果这是有意在多个实例间共享数组,那没什么问题。但一般来说,不同的实例应该有属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因。
组合使用构造函数模式和原型模式。(最常用)
构造函数用于定义实例属性,原型模式用于定义方法和共享属性。这种模式是目前在ECMAScript中使用最广泛,认同度最高的一种创建自定义类型的方法。
//用构造函数保存实例属性 //解决了要修改某个属性产生修改共享的问题 function Person(name, age, gender) { this.name = name; this.age = age; this.gender = gender; this.firends = ['zhangsan', 'lisi']; } // 用原型模式的函数保存实例方法 Person.prototype = { constructor: Person, sayName: function () { console.log(this.name); } }; var p1 = new Person('tella') var p2 = new Person('larry') p1.firends.push('ww'); p1.sayName();//tella p2.sayName(); console.log(p1.sayName == p2.sayName)//true console.log(p1.firends)//[ 'zhangsan', 'lisi', 'ww' ] console.log(p2.firends)//[ 'zhangsan', 'lisi' ] console.log(p1.firends == p2.firends)//false