javascript创建对象方法的总结。
对象是什么?
js中对象是“无序属性的集合,其属性可以包含基本值,对象或者函数”,简单来说,它就是一个名值对,有key和value,value可以是数据和函数。
怎样创建对象?
创建对象的方法有很多种,它们都有优缺点,下面从简单的开始介绍。
一、使用Object构造函数或者对象字面量方法创建实例对象
使用这种方法创建单个对象,如果创建多个对象会有很多重复代码。另外,和使用Object构造函数相比,字面量方法更直观,建议创建单个对象时都使用对象字面量方法
var object1 = new Object(); object1.name = "nameTest1"; object1.sayName = function(){ console.log(this.name); } var object2 = { name:"nameTest2", sayName: new Function("console.log(this.name);") }; object1.sayName();//nameTest1 object2.sayName();//nameTest2
二、工厂模式
抽象创建对象的过程,因为ES5中没有类的概念,所以使用函数来封装以特定接口创建对象的细节。
function createPerson(name){ return { name: name, sayName: function(){ console.log(this.name) } } } var person = createPerson("nameTest"); person.sayName();//nameTest
优点:可以创建多个对象
缺点:不能对象识别
三、构造函数模式
介绍构造函数前要知道new操作符。使用new操作符调用构造函数会经历4个步骤:
- 创建一个新的对象
- 将构造函数的作用域赋值给新的对象,则this对象指向新的对象。
- 执行构造函数中的代码,为新对象添加属性。
- 返回新对象
例如:
function Person(name){//2.将构造函数的作用域赋值给personCopy,this指向personCopy this.name = name;//3.执行构造函数中的代码,personCopy.name = name; this.sayName = new Function("console.log(this.name)")//3.执行构造函数中的代码 } var person = new Person("testName");//1.创建新的对象,personCopy
//4.返回personCopy给person person.sayName();//testName
了解了new操作符,下面介绍创建对象的构造函数模式。
有原生构造函数(Object Array)和自定义构造函数的概念。原生构造函数不做介绍。
下面是一个自定义的构造函数(构造函数名一般首字母大写)
function Person(name){ this.name = name; this.sayName = new Function("console.log(this.name)") }
和工厂模式比较,有一些不同
- 没有显式的创建对象,即没有var o = new Object()
- 直接把属性赋值给了this对象
- 没有return 语句
下面通过上面的构造函数创建2个对象
function Person(name){ this.name = name; this.sayName = new Function("console.log(this.name)") } var person1 = new Person("testName1"); var person2 = new Person("testName2"); console.log(person1 instanceof Object);//true 对象类型检测 console.log(person2 instanceof Object);//true 对象类型检测 console.log(person1 instanceof Person);//true 对象类型检测 console.log(person2 instanceof Person);//true 对象类型检测 console.log(person1.sayName === person2.sayName);//false 不同对象的同一方法不同 //通过实例对象可以访问构造函数的原型对象中的constructor属性,这个属性指向构造函数 console.log(person1.constructor === Person);//true console.log(person1.hasOwnProperty("constructor"));//false 可以说明实例对象本身没有constructor属性
通过上面的代码可以知道:
- instanceof可以用来判断元素是否是数组和对象
- 实例对象本身中没有constructor属性,它存在于原型对象中
- constructor属性指向构造函数
如上这个构造函数,其中的方法对于不同的实例对象是不同的,原因是每实例一个对象。都会新建一个function,可以通过移除function定义到构造函数外面解决
function Person(name){ this.name = name; this.sayName = fn; } function fn(){ console.log(this.name); } var person1 = new Person("testName1"); var person2 = new Person("testName2"); person1.sayName(); person2.sayName(); console.log(person1.sayName === person2.sayName);//true //通过这种方法使得构造函数中的方法只创建了一次,那么不同的实例对象的同一方法也就相同了 //但如上这种做法有问题,如果构造函数中有很多方法,都定义在全局作用域中没有封装,并且浪费(毕竟定义在全局作用域中的函数只给一个函数调用)
构造函数的问题在于其对方法的处理有不足。
如果方法定义在构造函数里面,不同的实例对象就会重复新建方法,使得同一方法对于不同的实例对象是不同的
如果定义在构造函数外面(也就是全局作用域中),多个全局方法只被一个函数(这个构造函数)使用,造成浪费,并且没有封装性
可以使用原型模式来解决这个问题
四、原型模型
要理解原型模式,首先要理解什么是原型对象
理解原型对象
新建一个函数时,就会根据一组特定的规则为该函数创建一个prototype属性(也叫作原型属性),该属性的值是一个指针,这个指针指向了函数的原型对象。
默认情况下,原型对象都会获取一个constructor属性(也叫作构造函数属性),该属性的值是一个指针,这个指针指向了prototype属性所在的函数。
function Person(name){ this.name = name; } Person.prototype.age = 15;
Person.prototype.sayName = function(){console.log(this.name)};
console.log(Person.prototype.constructor === Person);//true 原型对象的构造函数属性指向构造函数
理解原型模式
原型模式指的是把需要的属性都添加在原型对象上
这里需要指出的是,当使用new操作符实例一个对象时,实例对象内部会包含一个属性[[prototype]],这个属性是一个指针,它指向构造函数的原型对象。
要注意的是,这个连接存在与实例对象与原型对象之间,不是存在于实例对象与构造函数之间。
这样的话就可以将需要的属性全都定义在原型对象上。并且原型对象只有一个,不会因为new操作符新建原型对象(新建函数时就会默认新建原型对象),所以所有实例对象都可以共享原型对象上的属性和方法。
这就解决了构造函数模式中方法的不足的问题,原型模式即不会出现不同实例同一构造函数但方法不同的问题,也不会出现将方法放在全局作用域中没有封装性的问题,并且不浪费内存。
function Person(){} Person.prototype.name = "nameTest1"; Person.prototype.sayName = function(){console.log(this.name)}; console.log(Person.prototype.constructor === Person);//true 原型对象的构造函数属性指向构造函数 var person1 = new Person(); var person2 = new Person(); //Object.getPrototypeOf(value)取得value的原型对象,这个方法返回[[prototype]]的值(不能直接访问[[prototype]]属性) console.log(Object.getPrototypeOf(person1) === Person.prototype);//true console.log(person1.hasOwnProperty("name"));//false console.log(person1.hasOwnProperty("sayName"));//false console.log(person1.sayName === person2.sayName);//true 同一方法在不同实例对象上是相同的 person1.sayName();//nameTest1 person2.sayName();//nameTest1
上面的code通过把属性和方法添加在原型对象上实现了创建新的实例对象,可以看到:
- 实例对象本身并不存在属性和方法
- 实例对象可以访问属性和方法,这是因为原型对象中存在该属性和方法,这其中有一个查找对象属性的过程。
- 不同实例共享了原型对象,使得不同实例对象的同一方法是相同的
下面说一下查找对象属性的过程。
每次访问实例对象的属性和方法时,都会执行一次搜索,目标是具有给定名字的属性。
- 在实例对象本身进行寻找
- 实例对象内部属性指向的原型对象本身进行寻找
- 原型对象自动获取的constructor属性指向的构造函数中进行寻找
同名属性前面的属性会屏蔽后面的属性
function Person(){ this.job = "coder" } Person.prototype.name = "nameTest1"; Person.prototype.sayName = function(){console.log(this.name)}; var person1 = new Person(); person1.name = "nameTest2";//前面的属性会屏蔽后面的属性,可以用来设置不同的实例对象 console.log(person1.hasOwnProperty("name"));//true 搜索属性的过程到达:实例对象本身 console.log(person1.hasOwnProperty("sayName"));//false 搜索属性的过程到达:原型对象本身 console.log(person1.job);//coder 搜索属性的过程到达:构造函数 person1.sayName();//nameTest2 前面的属性会屏蔽后面的属性 person1.name = null; person1.sayName();//null 设置为null不会恢复与原型对象的连接 delete person1.name; console.log(person1.hasOwnProperty("name"));//false person1.sayName();//nameTest1 delete操作符可以恢复与原型对象的连接
重写原型对象的问题
使用原型模式常见的方式是使用对象字面量法来重写原型对象
正确的方式是在实例对象前面使用对象字面量法来重写原型对象,并且需要把constructor属性指向构造函数
function Person(){} var person1 = new Person(); Person.prototype = { constructor: Person,//如果没有这一步的话Person.prototype的constructor属性将指向Object构造函数 name: "nameTest1", sayName: function(){ console.log(this.name) } } var person2 = new Person(); console.log("name" in person1);//false 如果在实例对象后重写原型对象,实例对象与新的原型对象是不能建立连接的 console.log("name" in person2);//true 如果在实例对象前重写原型对象,是能够建立新的连接的
原型模式的问题
- 单独使用原型模式,将所有的属性和方法定义在原型对象上,省略了构造函数传递参数初始化的过程,使得所有实例对象默认情况下都取得了相同的值,但往往我们想要不同的实例对象用一些不同的属性,但这个问题可以通过在实例对象上添加同名属性屏蔽原型对象上的属性来解决,就是麻烦了点。
- 原型模式共享原型对象的本质导致如果通过一个实例对象修改原型对象的属性,那么另外一个实例对象的同名属性也会被修改。对引用类型值特别明显。
function Person(){} Person.prototype = { constructor: Person, name: "nameTest1", job: ["coder","ITer"], sayName: function(){ console.log(this.name) }, sayJob: function(){ console.log(this.job) }, } var person1 = new Person(); var person2 = new Person(); person1.name = "newName"; person1.sayName();//newName 对非引用类型的值在实例对象上修改会屏蔽原型对象上的同名属性 person2.sayName();//nameTest1 person1.job.push("LOLer"); //对引用类型的值在实例对象上修改会修改原型对象中的同名属性 person1.sayJob();//coder, ITer, LOLer person2.sayJob();//coder, ITer, LOLer 因为共享的本质,使得其他实例对象对原型对象的修改也会反映在该实例对象上
五、组合使用构造函数模式和原型模式
目前ES5环境下最好的创建对象的方法是组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性(也就是每个实例都不一样的属性),原型模式用来定义方法和共享的属性(每个实例都一样的属性)。
function Person(name){ this.name = name; this.job = ["coder","ITer"]; }
//对象字面量重写原型对象 Person.prototype = { constructor: Person, sayName: function(){ console.log(this.name) }, sayJob: function(){ console.log(this.job) }, } var person1 = new Person("name1"); var person2 = new Person("name2"); person1.name = "newName"; person1.sayName();//newName person2.sayName();//name2 person1.job.push("LOLer"); person1.sayJob();//coder, ITer, LOLer person2.sayJob();//coder, ITer
六、动态原型模式
上面提到的最好的方法将构造函数与原型对象分开了,动态原型模式把所有信息封装在了构造函数中,并且通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型对象的优点。
也就是说动态原型模式也使用到了原型,只不过是在构造函数中使用的。
function Person(name){ this.name = name; this.job = ["coder","ITer"]; //只有在sayName方法不存在的情况下,才给原型对象添加属性,也就是说下面这段代码只会在第一次调用构造函数时才会执行 //再次调用构造函数实例对象时,sayName属性已经存在于原型对象中,不会再重复创建,并且只要判断一个就可以了 if(typeof this.sayName != "function") { console.log("这个log只会在第一个调用构造函数时出现一次,再次调用时不会出现"); Person.prototype.sayName = function(){ console.log(this.name) }; Person.prototype.sayJob = function(){ console.log(this.job) }; //不能在已经创建实例的情况下使用对象字面量法重写原型对象,会切断实例与新原型之间的联系 // Person.prototype = { // constructor: Person, // sayName: function(){ // console.log(this.name) // }, // sayJob: function(){ // console.log(this.job) // } // } }; } var person1 = new Person("name1"); var person2 = new Person("name2"); person1.name = "newName"; person1.sayName();//newName person2.sayName();//name2 person1.job.push("LOLer"); person1.sayJob();//coder, ITer, LOLer person2.sayJob();//coder, ITer
余下两种创建对象的方式到现在也没找到使用场景,这里就不做记录了。
我的微信:t230124
欢迎讨论学习,打LOL