文章目录
理解对象
ECMA-262将对象定义为无序属性的集合,其属性可以包含基本值、对象或者函数。
属性类型
ECMAScript通过特性描述了属性的各种特征,不能直接访问特性,规范中通过[[]]描述特性。
ECMAScript中有两种属性,数据属性和访问器属性。
- 数据属性
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性。
特性 | 说明 |
---|---|
[[Configurable]] | 是否可以通过delete删除属性;能否修改属性的特性;能否把属性修改为访问其属性;默认值为true |
[[Enumerable]] | 能否通过for-in循环返回属性,默认为true |
[[Writable]] | 能否修改属性,默认为true |
[[Value]] | 包含这个属性的数据值,默认为undefined |
- Object.defineProperty(属性所在对象,属性的名字,描述符对象),修改属性默认的特性
//创建一个只读的name属性
var person={}
Object.defineProperty(person,'name',{
wriabled:false,
value:'marry'
})
person.name //marry
person.name='mike' //在严格模式下会报错
person.name //marry
注意:一旦通过Object.defineProperty()定义了configurable为false,就不可以逆转了。
- 访问器属性
访问器属性不包含数据值,包含getter、setter函数,在读取访问器属性时,调用getter函数,返回有效的值,在写入访问器属性时,会调用setter函数并传入新值。
特性 | 说明 |
---|---|
[[Configurable]] | 是否可以通过delete删除属性;能否修改属性的特性;能否把属性修改为访问其属性;默认值为true |
[[Enumerable]] | 能否通过for-in循环返回属性,默认为true |
[[Get]] | 在读取属性时调用的函数,默认为undefined |
[[Set]] | 在写入属性时调用的函数,默认为undefined |
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
下划线用于表示只能通过对象方法访问的属性。只指定getter意味着属性不能写,只指定setter意味着属性不能读。
定义多个属性
Object.defineProperty()用于定义单个属性,Object.defineProperties()可以定义多个属性。
- Object.defineProperties(要添加和修改其属性的对象,对象的属性与第一个参数对象中要添加或修改的属性一一对应)
var person={}
Object.defineProperties(person,{
name:{
value:'marry'
},
_age:{
value:23
},
age:{
get:function(){
return this._age
}
}
})
person.name //"marry"
person.age //23
读取属性的特性
- Object.getOwnPropertyDescriptor(属性所在的对象,要读取其描述符的属性名称),返回值是一个对象(若为访问器属性,这个对象的属性有configurable,enumerable,get,set,若为数据属性,这个对象的属性有configurable,enumerable,writable,value)
var person={}
Object.defineProperties(person,{
name:{
value:'marry'
},
_age:{
value:23
},
age:{
get:function(){
return this._age
}
}
})
var desc=Object.getOwnPropertyDescriptor(person,"_age")
desc.value //23
desc.configurale //false
创建对象
使用构造函数或对象字面量创建对象,会产生许多重复的代码。可以采用以下几种模式避免重复代码。
工厂模式
工厂模式抽象了创建具体对象的过程,用函数封装了创建对象的细节。在函数内部创建对象,然后将该对象以返回值的形式传递出去。
function createPerson(name,age){
var o=new Object()
o.name=name
o.age=age
o.sayName=function(){
console.log(this.name)
}
return o
}
var p1=createPerson('ma',12)
var p2=createPerson('ss',23)
p1.sayName() //ma
p2.sayName()//ss
优点 | 缺点 |
---|---|
避免创建相同对象的重复代码 | 无法确定对象的类型 |
构造函数模式
通过构造函数来创建对象,构造函数的语法,一般构造函数的首字母需要大写,通过this将属性和方法赋给对象,不需要return语句。
function Person(name,age){
this.name=name
this.age=age
this.sayName=function(){
console.log(this.name)
}
}
var p1=new Person('ma',12)
var p2=new Person('ss',23)
通过new创建实例,一般经历以下几个步骤:
- 创建一个新对象;
- 将构造函数的作用域赋给新对象;
- 执行构造函数中的代码;
- 返回新对象。
通过构造函数创建的实例都有constructor属性,该属性指向创建实例的构造函数。可以通过该属性判断对象的类型。(判断类型还可以用instanceof操作符)
p1.constructor //Person(name,age){this.name=name;this.age=age;this.sayName=function(){console.log(this.name)}}
优点 | 缺点 |
---|---|
解决了工厂模式无法确定对象类型的缺点 | 若将方法定义在构造函数中,每创建一个实例都会重新创建一次方法;若将方法定义在构造函数外,该方法在全局作用域下失去了封装性 |
原型模式
每个函数都有prototype属性,这个属性是一个指针,指向一个包含所有实例共享的属性和方法的对象(原型对象)。
- 原型对象
每个函数都有prototype属性指向原型对象;原型对象有constructor属性指向prototype属性所在的函数;每个实例可以通过__proto__属性访问原型对象。
function Person(){}
Person.prototype.name='marry'
Person.prototype.age=23
Person.prototype.sayName=function(){
console.log(this.name)
}
var person=new Person()
person.sayName() //marry
Person.prototype===person.__proto__ //true
Person.prototype.constructor //Person(){}
- 对象与原型对象
- 判断对象和原型对象之间的关系
- isPrototypeOf(),判断对象的原型
- Object.getPrototypeOf(),返回实例的原型对象的值
Person.prototype.isPrototypeOf(person) //true
Object.getPrototypeOf(person)===Person.prototype //true
- 实例与原型中的属性
- 访问实例属性时,若实例中不存在该属性,会去原型对象中查找。
- 在实例中定义的属性和方法,若在原型中存在,该实例中的属性和方法会覆盖原型中的属性和方法。
- hasOwnProperty()和in操作符,检测属性是在原型中还是实例中
- hasOwnProperty(),当属性存在于对象实例中时,返回true
- in操作符,当属性存在于对象实例或者原型中时,返回true
person.hasOwnProperty('name') //false
"name" in person //true
- 原型对象语法
可以通过Person.prototype.的方式创建原型,还可以通过创建字面量的方式创建原型。
function Person(){}
Person.prototype={
name:'marry',
age:23,
sayName(){
console.log(this.name)
}
}
var p=new Person()
p.sayName() //marry
Person.prototype.constructor //Object(){}
p.constructor //Object(){}
通过对象字面量的方式创建原型对象相当于重新创建了对象,此时原型对象的constructor属性不再指向Person。
function Person(){}
Person.prototype={
constructor:Person,
name:'marry',
age:23,
sayName(){
console.log(this.name)
}
}
var p=new Person()
p.sayName() //marry
Person.prototype.constructor //Person(){}
p.constructor //Person(){}
通过上述方式添加constructor属性会将constructor属性的可枚举属性变为true。可以通过Object.defineProperty()设置constructor属性。
Object.defineProperty(Person.prototype,"constructor",{
enumerable:true,
value:Person
})
Person.prototype.constructor //Person(){}
p.constructor //Person(){}
优点 | 缺点 |
---|---|
所有实例可以共享属性和方法 | 实例共享引用类型属性会带来问题,一个实例改变了该属性,另一个实例中属性也会随之改变 |
组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性。
function Person(name,age){
this.name=name
this.age=age
}
Person.prototype={
constructor:Person,
sayName:function(){
console.log(this.name)
}
}
var p1=new Person('mike',23)
var p2.new Person('jack',34)
p1.sayName() //mike
p2.sayName() //jack
优点:每个实例都会有一份实例属性的副本,又共享的方法,节省了内存。
动态原型模式
将所有代码封装在构造函数中,通过在构造函数初始化原型。
function Person(name,age){
this.name=name
this.age=age
if(typeof this.sayName!="function"){
Person.prototype.sayName=function(){
console.log(this.name)
}
}
}
var p=new Person('mike',23)
p.sayName() //mike
优点:仅在有必要的情况下初始化原型,同时具有组合构造函数和原型的优点。
寄生构造函数模式
创建一个函数,该函数封装了创建对象的代码,然后返回新创建的对象。类似于工厂模式,不同的是通过构造函数的方式创建对象。以该方式创建对象无法确定对象类型。
function Person(name,age){
var o=new Object()
o.name=name
o.age=age
o.sayName=function(){
console.log(this.name)
}
return o
}
var p1=new Person('ma',12)
p1.sayName() //ma
p1.constructor //Object(){}
稳妥构造函数模式
稳妥对象指的是没有公共属性,而且其方法也不引用this的对象。(在一些安全的环境中使用)创建方式类似于寄生构造函数模式,但有两种不同,一是新创建的对象的实例方法不引用this,二是不使用new操作符调用构造函数。
function Person(name,age){
var o=new Object()
o.name=name
o.age=age
o.sayName=function(){
console.log(this.name)
}
return o
}
var p1= Person('ma',12)
p1.sayName() //ma
p1.constructor //Object(){}
继承
在面对对象语言中,一般继承分为接口继承和实现继承,接口继承只继承方法签名,实现继承则继承实际的方法。ECMAScript只支持实现继承。
原型链
- 创建原型链
基本思想:一个引用类型继承另一个引用类型的属性和方法。
方法:子类的原型对象等于父类对象的实例。
function Sub(){
this.property=true
}
function Super(){
this.property=false
}
Super.prototype.getProperty=function(){
return this.property
}
Sub.prototype=new Super() //继承了Super
var s1=new Sub()
s1.getProperty() //true
注意:
- 为原型添加方法时需要在继承语句之后,继承语句相当于重写了原型对象。
- 不能使用对象字面量的方式创建原型对象,会重写原型
- 原型链的问题
- 当子类原型对象继承父类实例时,父类实例中的属性变成了子类中共享的属性,对于引用类型的数据来说存在问题,当一个子类实例对属性进行了更改,其他实例的属性值也会改变。
- 在创建子类型的实例时,不能向超类型的构造函数中传递参数。
借用构造函数
- 基本思想
在子类型的构造函数的内部调用超类型的构造函数。通过apply或call在子类型中调用构造函数。
function Super(){
this.property=true
}
function Sub(){
Super.call(this)
}
var sub=new Sub()
sub.property //true
- 优缺点
优点:可以向超类型构造函数中传递参数;
缺点:方法都在构造函数中定义,复用性不好
组合继承
基本思想:组合使用原型链和构造函数,使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现实例属性的继承。
function Super(name){
this.name=name
}
Super.prototype.sayName=function(){
console.log(this.name)
}
function Sub(name,age){
Super.call(this,name)
this.age=age
}
Sub.prototype=new Super()
Sub.prototype.constructor=Sub
var s1=new Sub("marry",23)
var s2=new Sub("mike",14)
s1.sayName() //marry
s2.sayName() //mike
s1.age //23
最常用的继承方式。缺点是会调用两次父类的构造函数,一次是在创建子类型原型时,一次是在子类型构造函数内部。
原型式继承
基本思想:基于已有的对象创建新对象,在函数中创建一个空的构造函数,该构造函数的原型等于传入的对象,最后返回该构造函数的实例。
function object(o){
function F(){}
F.prototype=o
return new F()
}
ES5中使用Object.create()方法实现原型式继承,接收两个参数,一个是用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
var person={
name:'marry',
age:12
}
var p=Object.create(person)
p.name //marry
寄生式继承
创建一个用于封装继承过程的函数,在函数内部以某种方式来增强对象,最后返回该对象。
function createAnother(original){
var clone=Object.create(original)
clone.sayName=function(){
console.log(this.name)
}
return clone
}
var person={
name:'mike'
}
var p=createAnother(person)
p.sayName() //mike
寄生组合式继承
基本思想:通过构造函数来继承属性,通过原型链来继承方法。
function inheirt(subType,superType){
var clone=Object.create(superType.prototype) //创建父类型原型的一个副本
clone.constructor=subType //指定原型的构造函数
subType.prototype=clone //子类继承父类的原型属性和方法
}
function Super(name){
this.name=name
this.colors=['red','green']
}
Super.prototype.sayName=function(){
console.log(this.name)
}
function Sub(name,age){
Super.call(this,name) //继承构造函数的属性
this.age=age
}
inheirt(Sub,Super)
//定义子类型的特有原型方法
Sub.prototype.sayAge=function(){
console.log(this.age)
}
var s1=new Sub('zl',23)
s1.sayName() //zl
s1.sayAge() //23