js对象的定义是:无序属性的集合,其属性可以包含基本值、对象或者函数。也就是说对象是一组没有特定顺序的值。
先来聊聊对象的中的两类特殊的内部属性:数据属性和访问器属性
1.数据属性:除了最基本的属性本身的值之外,还有描述该属性是否可以被读取、写入以及是否能够通过for-in循环遍历返回属性,这些描述我们也称之为特性。(为了表示特性是内部值,该规范把它们放在了两对方括号中)
[[Configurable]](意为可配置,可修改的):该属性是否可被删除。表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
[[Enumerable]](意为可枚举的):该属性是否可以被遍历。表示能否通过for-in循环遍历。
[[Value]]:包含属性值。默认值为undefined
[[Writbale]]:表示能否直接修改属性的值。
上面的四个特性除了Value之外,在不定义属性类型的时候默认是true,也就是说当我们直接在对象中定义属性时,这些特性都会默认为true。但是一旦调用defineProperty方法来设置的时候,它就会转换为false。
*如何修改这四个特性?要修改属性默认的特性,就必须使用Object.defineProperty()。这个方法接受三个参数:属性所在对象、属性的名字和一个描述符对象(四个特性)。例如:
var person={};Object.defineProperty(person,"name",{writable:false,//设置为只读value:"Nicholas";});alert(person.name);//Nicholasperson.name="Greg";alert(person.name);//Nicholas
这里创建了一个名为name的属性,它的值"Nicholas"是只读的.
*同样的,我们也可以将configurable设置为false,即不可配置,意为不能从对象中删除属性。但是一旦属性被设置为不可配置的,就不能再把它变回可配置的了要是再调用Object.defineProperty只会导致错误。
2.访问器属性
访问器属性中也有[[Configurable]]、[[Enumerable]]两种属性,而且这两种属性和数据属性的一样。另外访问其属性还有两个函数:getter()和setter()
[[Get]]:在读取属性时调用的函数。默认值为undefined。
[[Set]]:在写入属性时调用的函数。默认值为undefined。
访问器属性和数据属性之间的区别在于,访问器属性只能通过Object.defineProperty()来定义。而数据属性可以用过这个方法定义,也可以通过对象直接定义。因此一般来讲,在使用Object.defineProperty()来定义属性的时候,会默认是数据属性,但是如果定义中出现了Value和writable属性,则就会定义为数据属性。
使用访问器属性的常见方式,就是设置一个属性的值会导致其他属性会发生变化。
var book={ _year:2004, edition:1};Object.defineProperty(book,"year",{ get:function(){ return this._year; }, set:function(newValue){ if(newValue>2004){ this._year=newValue; this.edition+=newValue-2004; } }});book.year=2005;console.log(book.edition);//2
3.定义多个属性
js提供了Object.defineProperties方法来同时定义数据属性以及访问器属性:
var book={};Object.defineProperties(book,{ _year:{ writable:true, value:2004 }, edition:{ writable:true, value:1 }, year:{ get:function(){ return this._year; }, set:function(){ if(newValue>2004){ this._year=newValue; this.edition+=newValue-2004; } } }});
*支持该方法的浏览器有IE 9+、Firefox 4+、Safari 5+、Opera 12+、Chrome
4.读取属性的特性:
利用Object.getOwnPropertyDescriptor()方法,可以取得属性的描述符。这个方法接受两个参数,要访问的对象,以及要读取其描述符的属性名称。接着上面的代码,我们加入下面的读取属性代码:
var descriptor=Object.getOwnPropertyDescriptor(book,"_year");console.log(descriptor.value);//2004console.log(descriptor.configurable);//false
在了解了对象中的数据属性和访问器属性之后,就是要知道如何创建一个对象,以及创建一个对象有多少种方法,这些方法又有些什么样的问题。
1.工厂模式创建对象:在内部创建一个对象,然后把参数赋予给该对象的对应属性,再返回该对象。
function createPerson(name,age,job){ var o=new Object(); o.name=name; o.age=age; o.job=job; o.sayname=function(){ console.log(o.name); }; return o;}var person1=createPerson("Nicholas",55,"engnier");var person2=createPerson("Nichols",5,"en");
该模式存在的缺点:首先,这个模式是在函数内部创建一个实例对象,里面的方法每创建一次就会同样被创建一次造成了空间的浪费。其次,因为这个对象是用Object构造函数创建的,所以我们不能知道这个对象的对象类型(无论利用instanceof操作符如何检测,都只会在Object对象检测时才会返回true)
2.构造函数模式:用来创建特定类型的对象。
function Person(){ this.name=name; this.age=age; this.job=job; this.sayname=function(){ console.log(this.name); };}var person3=new Person("Nicholas",2,"dfd");
在构造函数模式中创建对象的函数,和工厂模式中的不同在于。不会直接创建一个对象,而是使用了this指针来添加对象属性,并且没有return语句。之所以能做到这一点,是因为在后面创建对象实例的时候,我们用到了new操作符。下面是new操作符在执行工作时候的步骤:
(1)新建一个对象:instance=new Object();
(2)设置原型链:instance._proto_=person3.prototype;
(3)让person3中的this指向instance。执行Person中的函数体。(这时候,由new创建出来的instance对象中的原型、方法属性这些都已经被定义完毕。原型从Object中继承,属性方法由构造函数中的语句赋予)
(4)判断Person有无return语句,并判断返回值的类型。如果是值类型,就舍弃,返回new创建的instance对象(没有return就意味着是返回undefine,同样也是返回instance);如果是引用类型,就返回这个引用类型的对象,替换new自创建的instance对象。(在后面的寄生构造函数模式中就是实现这种替换)
*用构造函数模式创建对象的时候,在创建的同时,每一个对象都会伴随着一个新的属性的创建:constructor属性(构造函数属性)。该属性指向person。
console.log(person3.constructor==Person);//true console.log(person3 instanceof Person);//true console.log(person3 instanceof Object);//true,因为所有的对象均继承自Object
构造函数和普通函数之间的不同其实主要在于彼此之间的作用域不同。任何函数,只要用new操作符来调用,它就可以作为构造函数。
//当做构造函数使用var person=new Person("Nicholas",29,"Software Engineer");person.sayName();//"Nicholas"//作为普通函数使用Person("Greg",27,"Doctor");//添加到windowwindow.sayName();//"Greg"//在另一个对象的作用域中调用var o=new Object();Person.call(0,"Kristen",25,"Nurse");o.sayName();//"Kristen"
构造函数存在的缺点:构造函数虽然弥补了工厂模式中不可以区分对象类型的缺点,但是同样的,它也无法解决每个方法都要在每一个实例上重新创建一遍的问题。尽管我们可以通过下面这样的方法来解决:
function person(){ this.name=name; this.age=age; this.job=job; this.sayname=function(){ console.log(this.name); };}function sayName(){ console.log(this.name);}var person3=new Person("Nicholas",2,"dfd");
通过将sayName方法设置为全局方法就可以做到每一个实例都引用同一个函数,但是如果在有很多个方法的情况下,这样的做法会使得自定义的引用类型毫无封装性可言。
3.原型模式:利用prototype属性(指针),来指向一个对象。这个对象的用途就是用于包含可以由特定类型的所有实例共享的属性和方法。
function Person(){}Person.prototype.name="邓润桦";Person.prototype.age=29;Person.prototype.job="码农";Person.prototype.sayname=function(){ console.log(this.name);};var person1=new Person();person4.sayname();var person5=new Person();person5.sayName();console.log(person5.sayName==person5.sayName);//true
如何来理解原型对象:只要创建了一个函数,就会根据一组特定的规则来为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor属性,并且这个属性指向prototype属性所在对象的指针。当我们创建一个实例对象之后,该实例对象内部将包含一个指针,指向构造函数的原型对象。也就是说,实例只与构造函数的原型对象之间有联系,而与构造函数没有联系。
*我们可以访问原型对象中的属性和方法,但是不能够重写这些属性和方法:
function Person(){}Person.prototype.name="Nicholas";Person.prototype.age=29;Person.prototype.job="Software Engineer";Person.prototype.sayName=function(){ console.log(this.name);}var person=new Person();var person2=new Person();person.name="Greg";console.log(person.name);//"Greg"console.log(person2.name);//"Nicholas"
当为对象实例创建一个属性时,这个属性就会屏蔽原型对象中的同名属性,注意是“屏蔽”。因此,原型对象中的属性实际上并没有被修改。要重新访问原型中的属性,我们可以使用delete操作符完全删除实例属性。但是,如果实例属性是引用类型的话,在我们为这个引用类型进行修改的时候,永远都是对同一个引用类型进行操作。
要检测一个属性到底是在实例当中还是在原型当中,我们可以使用hasOwnProperty()方法和in操作符来完成检测,其中hasOwnProperty()方法只在给定的属性存在于实例当中时,才会返回true;in则不管属性位于实例中还是原型中,只要该属性能被我们访问,就会返回true。看下面的结合使用:
function hasPrototypeProperty(Object,name){ return !object.hasOwnProperty(name)&&(name in object);}
这个函数只要对象存在于实例中时,就会返回true。
简化原型模式:
function Person(){}Person.prototype={ constructor:Person, name:"Nichilas", age:29, job:"Software Engineer", sayName:function(){ console.log(this.name); }};var friend=new Person();
上面的代码利用对象字面量的方法,相当于创建另一个新的对象并赋值给Person.prototype。其中,注意上方代码中加粗的部分,因为重写代码的时候,constructor属性也将重写,它将会指向Object,而不是Person,因此我们需要重写它。
*理解原型对象的动态性:
var friend=new Person();Person.prototype.sayHi=function(){ console.log("hi");};firend.sayHi();//"hi"
尽管在上面的代码中,我们是先定义了对象friend,然后再给它的原型进行修改,但是因为实例与原型之间的松散连接关系,所以我们依旧可以执行最后一条语句。但是要注意的是,如果修改方式使用的是字面量方法,就完全不一样了。
function Person(){}var friend=new Person();Person.prototype={ constructor:Person, name:"Nicholas", age:29, job:"Software ENgineer", sayName:function(){ console.log(this.name); }};friend.sayName();
上面这段代码由于重写了原型对象,friend的原型中不再有sayName()方法,所以将会报错。
原型对象存在的问题在于,由于实例的中的部分属性和方法被共享,在被共享的属性中,如果其中的属性是引用类型值的话,就会导致多个对象同时修改该引用类型值,而一般情况下我们是不愿意这样的
4.组合使用构造函数模式和原型模式
基于上面提出的种种问题,目前开发人员比较青睐的创建对象方法就是这种组合模式
function Person(name,age,job){ this.name=name; this.age=age; this.job=job; this.friendd=["Shelby","Court"];}Person.prototype={ constructor:Person, sayName:function(){ console.log(this.name); }}var person1=new Person("Nicholas",29,"Software Engineer");var Person2=new Person("Greg",27,"teacher");person1.friends.push("Van");console.log(person1.friends);//Shelby,Count,Vanconsole.log(person2.friends);//Shelby,COunt
不过,构造函数和原型独立分开来进行定义声明,很有可能在阅读代码的时候带来困惑,因此,也可以采用下面这种动态原型模式来创建对象,通过检查某个方法是有有效,来动态初始化原型。
function Person(name,age,job){ this.name=name; this.age=age; this.job=job; this.friendd=["Shelby","Court"]; if(typeof this.sayName !="function"){ Person.prototype.sayName=function(){ console.log(this.name); }; }} var friend=new Person("Nicholas",29,"Software Engineer"); friend.sayName();
5.寄生构造函数模式
寄生,意为在原有的原生构造函数的基础上,添加属性或者方法。
function Person(name,age,job){ this.name=name; this.age=age; this.job=job; o.sayName=function(){ console.log(this.name); }; return 0;}var friend=new Person("Nicholas",29,"Software Engineer");friend.sayName();
构造函数在不返回值的情况下,默认会返回新对象的实例。而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。
这个模式在特殊情况下可以用来为对象创建构造函数,假设我们想创建一个具有额外方法的特殊数组。但是不能直接修改Array构造函数,因此可以使用该模式:
function SpecialArray(){ //创建数组 var values=new Array(); //添加值 values.push.apply(values,arguments); //添加方法 values.toPipedString=function(){ return this.join("|"); }; //返回数组 return values;}var colors=new SpecialArray("red","blue","green");console.log(colors.toPipedString());//"red|blue|green"
*在寄生构造函数中返回的对象,和我们在构造函数外部创建的对象没有什么不同,因此无法用instanceof操作符来确定对象类型。这点需要注意
6.稳妥构造函数:可以在这个构造函数模式当中设置私有变量和函数,除了我们定义的访问接口之外,不能通过其他方法访问这些属性和函数:
function Person(name,age,job){ //创建要返回的对象 var o=new Object(); //定义私有变量 var name=name; var age=age; o.sayName=function(){ console.log(name); }; //返回对象 return o;}var friend=Person("runhua",29,"webqianduan");friend.sayName();//“runhua”