在ECMAScript中,两个核心主题就是对象与函数,而这两个主题也有些互相缠绕的,在前面几个博文中大略的过了一遍函数相关的基础知识,这篇文章再回到对象主题上来。函数
1、对象再认识测试
(1)对象属性和特性this
什么是属性(Property),什么是特性(Attribute),这有什么区别?我不想也不会从语义学上去区分,对于这系列文章来讲,属性就是组成对象的一个部分,广义上也包括对象的方法,而特性则是指被描述主体所具备的特征,换句话说,属性是咱们能够经过编码来访问的具体存在,而特性则主要是为了便于理解概念的抽象存在,固然,特性也能够经过相应的属性来具体外化。这一小节所讲的对象属性的特性就是对对象属性特征的一个描述,主要来自于ECMA-262规范的第5版,该规范使用两个中括号的形式来描述不能直接访问的内部特性。编码
A、属性类型(先给属性分下类):spa
B、对象内部属性prototype
内部属性不能经过代码直接访问,它主要是为了描述规范,也是给ECMAScript实现者参考的,而对于开发者来讲,http://chang.fuhao2.com 了解这些能够便于理解一些内部机制。好比在给一个属性赋值时,在实现中会调用[[Put]]内部方法,而读取一个属性值时,则调用[[Get]]方法。code
全部对象公共的内部属性orm |
个别对象特有的内部属性对象 |
|||
名称继承 |
规范 |
名称 |
规范 |
对象 |
[[Prototype]] |
Object/Null |
[[PrimitiveValue]] |
primitive |
Boolean|Date|Number|String |
[[Class]] |
String |
[[Construct]] |
SpecOp(a List of any) → Object |
new |
[[Extensible]] |
Boolean |
[[Call]] |
SpecOp(any, a List of any) → any|Reference |
call |
[[Get]] |
SpecOp (propName) →any |
[[HasInstance]] |
SpecOp(any) → Boolean |
Function |
[[GetOwnProperty]] |
SpecOp (propName) →Undefined|Property Descriptor |
[[Scope]] |
Lexical Environment |
Function |
[[GetProperty]] |
SpecOp (propName) →Undefined|Property Descriptor |
[[FormalParameters]] |
List of Strings |
Function |
[[Put]] |
SpecOp (propName, any, Boolean) |
[[Code]] |
ECMAScript code |
Function |
[[CanPut]] |
SpecOp (propName) → Boolean |
[[TargetFunction]] |
Object |
Function.prototype.bind |
[[HasProperty]] |
SpecOp (propName) → Boolean |
[[BoundThis]] |
any |
Function.prototype.bind |
[[Delete]] |
SpecOp (propName, Boolean) → Boolean |
[[BoundArguments]] |
List of any |
Function.prototype.bind |
[[DefaultValue]] |
SpecOp (Hint) → primitive |
[[Match]] |
SpecOp(String, index) → MatchResult |
RegExp |
[[DefineOwnProperty]] |
SpecOp (propName, PropDesc, Boolean) → Boolean |
[[ParameterMap]] |
Object |
|
说明:
C、属性特性(用来描述属性的特性)。
注意:
上述的默认是指经过构造函数或对象字面量建立的对象所自身拥有的属性特性的默认值都为true,而仅仅用Object.defineProperty()方法建立的对象属性的特性前三个的默认值为false。
1.当使用构造函数或者对象字面量建立的属性和使用Object.defineProperty()方法建立的对象属性的名字相同时,Object.defineProperty()方法建立的对象属性的特性前三个默认值为变为true。
2.在 configurable , writable 特性都为false 时,不能够删除属性,不能修改属性的任意特性。也就是说,不能再把configurable修改成true了。
在 configurable 特性为 false 时且 writable 为true时,能够更改 value 和 writable 特性。
3.在 configurable 特性为 true 时,无论writable是什么值,均可以把数据属性修改成访问器属性。
在未使用 defineProperty 的状况下添加的数据属性
若是您在未使用 Object.defineProperty、Object.defineProperties 或 Object.create 函数的状况下添加数据属性,则writable、enumerable 和 configurable 特性都将设置为 true。在添加属性后,可使用 Object.defineProperty 函数修改属性。
咱们能够经过Object.defineProperty()方法来修改属性默认的特性。
可使用如下方式来添加数据属性:
1赋值运算符 (=),以下所示:obj.color = "white";
2对象文本,以下所示:obj = { color: "white", height: 5 };
3构造函数,如使用构造函数定义类型中所述
内部特性 |
配置属性 |
属性类型 |
数据类型 |
默认值 |
含义 |
备注 |
[[Configurable]] |
configurable |
数据属性 访问器属性 |
Boolean |
true |
可否经过delete删除属性从而从新定义属性 可否修改属性的特性 数据属性和访问器属性是否能够相互修改 |
一旦把属性定义为不可配置的,就不能再变为可配置的 非严格模式下会忽略相应操做,严格模式下则抛出异常 |
[[Enumerable]] |
enumerable |
数据属性 访问器属性 |
Boolean |
true |
可否经过for-in循环返回属性 |
为true时能够经过for-in来枚举,不然不能经过for-in枚举 |
[[Writable]] |
writable |
数据属性 |
Boolean |
true |
可否修改属性的值 |
为false时不能修改属性值,非严格模式下会忽略相应操做,严格模式下则抛出异常 |
[[Value]] |
value |
数据属性 |
任意类型 |
undefined |
属性值 |
|
[[Get]] |
get |
访问器属性 |
Undefined/Function |
undefined |
读取属性时调用的函数 |
为一个函数时,会无参数调用这个函数,并将返回值做为属性值返回 |
[[Set]] |
set |
访问器属性 |
Undefined/Function |
undefined |
写入属性时调用的函数 |
为一个函数时,会将传入的值做为参数调用这个函数,赋给属性 |
说明:
D、属性定义方法(用来定义属性的方法)
最多见的定义属性的方法就是直接在对象上添加属性,好比obj.name = 'linjisong',这种状况下定义的属性所具备的内部特性都是默认的,若是想定义一个值不能被修改的属性要怎么作呢?在ES中给咱们提供了几个方法用于实现相似的功能。
方法名 |
功能说明 |
参数和返回值 |
说明 |
调用示例 |
defineProperty() |
定义一个属性 |
(1)目标对象 (2)属性的名字 (3)属性描述符对象 |
使用属性定义方法时 |
// 建立一个包含一个默认属性job的对象(job属性能够修改、删除、在for-in中枚举) var person = {job:'it'}; // 添加一个不能被修改、删除的name属性 Object.defineProperty(person, 'name', { value:'linjisong',//这里的配置属性和上面特性列表中的配置属性一致 enumerable:true }); // 定义多个属性(数据属性year和访问器属性age) Object.defineProperties(person, { year:{ value : 2012, configurable:true, writable:true }, age:{ get : function(){ return this.year-1983; } } });
var job = Object.getOwnPropertyDescriptor(person, 'job'); console.info(job.configurable);//true,直接添加属性时默认为true
var name = Object.getOwnPropertyDescriptor(person, 'name'); console.info(name.configurable);//false,使用属性定义方法添加属性时默认为false console.info(person.name);//linjisong person.name = 'oulinhai';//因为不能修改,因此值不会改变,在严格模式下会抛出异常 console.info(person.name);//linjisong
person.year = 2015; console.info(person.year);//2015 console.info(person.age);//32,在修改year的同时,也修改了age属性
|
defineProperties() |
定义一组属性 |
(1)目标对象 (2)多个属性描述符组成的一个对象 |
||
getOwnPropertyDescriptor() |
获取属性的特性 |
(1)目标对象 (2)属性的名字 (3)返回一个包括了属性特性的对象 |
|
注:这些方法设置或获取的属性特殊和属性的类型有关,好比数据属性只能设置[[Confirurable]]、[[Enumerable]]、[[Writable]]、[[Value]]。
(2)防篡改对象
所谓防篡改对象,就是给对象必定级别的保护以防止在这个级别上对对象的变动,在ES5规范中,定义了依次升高的三种保护级别:
保护级别 |
描述 |
操做方法 |
判断方法 |
说明 |
不可扩展 |
不能给对象添加新属性和方法,但能够修改已有属性和方法 |
preventExtensions() |
isExtensible():不能扩展时返回false |
|
密封 |
不可扩展,而且已有成员的[[Configurable]]设置为false,不能删除属性,但能够修改属性值 |
seal() |
isSeal():被密封时返回true |
isSeal()为true时必定有isExtensible()为false |
冻结 |
密封,其[[Writable]]设置为false,但若是定义了[[Set]],访问器属性仍然可写 |
freeze() |
isFrozen():被冻结时返回true |
isFrozen()为true时必定有isSeal()为true,isExtensible()为false |
注:一旦定义成了防篡改对象,就不能撤销。
(3)对象的其它方法
名称 |
描述 |
create(prototype[,descriptors]) |
建立一个具备指定原型且可选择性地包含指定属性的对象 |
getOwnPropertyNames(object) |
返回对象的属性(方法)的名称 |
getPrototypeOf(object) |
返回对象的原型 |
keys(object) |
返回对象的可枚举属性(方法)的名称 |
这里的create(prototype[,descriptors])是一个很是有意思的方法,规范中这样描述它的行为:
①若是prototype不是Null或Object,抛出TypeError异常
②var obj = new Object()
③设置obj的内部属性[[Prototype]]为prototype
④若是descriptors存在且不为undefined,使用Object.defineProperties(obj,descriptors)来添加属性
⑤返回obj
因为通常对象的[[Prototype]]不能直接访问,可使用函数来进行下面模拟实现:
(function(){
function Base(){};
Object.create = function(prototype, descriptors){
var typeVal = typeof prototype;
if(typeVal !== null && typeVal !== 'object' && typeVal !== 'function'){
throw new TypeError('类型错误,请检查第一个参数的类型');
}
Base.prototype = prototype;
var result = new Base();
if(descriptors){
Object.defineProperties(result, descriptors);
}
return result;
};
})();
测试一下:
try{
var one = Object.create(1);//异常
}catch(e){
console.info(e);//TypeError
}
var base = {
name:'linjisong',
getName:function(){
return this.name;
}
};
var two = Object.create(base);
console.info(two.name);//linjisong
console.info(two.getName());//linjisong
var three = Object.create(base, {
name:{value:'oulinhai'},
age:{value:23}
});
console.info(three.getName());//oulinhai
console.info(three.age);//23
这里实现了一个简单的继承,这也引出下一个主题。
2、原型对象
(1)原型与原型链
每一个对象都有一个原型对象,而原型对象自己也是一个对象,也有本身的原型对象,这样就造成了一个原型链直至null对象。对象的内部属性[[Prototype]]指向的就是对象的原型对象,而Object.prototype的原型对象为null???原型对象的原型对象?Object.prototype.prototype===undefined
(2)属性查找
在访问一个对象的属性(方法)时,引擎会先查找对象自己有没有这个属性,若是有,返回这个属性值,若是没有,则查找对象的原型是否有这个属性,若是有,返回,若是没有就继续查找原型对象的原型直至最后一个原型对象。
注意区分属性查找和和前面说过的标识符查找的异同。属性查找是沿着原型链,标识符查找是沿着做用域链,但都有一个逐级查找的过程。
(3)对象的原型对象[[Prototype]]与函数的原型属性prototype
var obj = {};
console.info(obj.__proto__===Object.getPrototypeOf(obj));//true
console.info(obj.__proto__===Object.prototype);//true
var fn = function(){};
console.info(typeof fn.prototype);//object,一旦定义了函数,就会添加prototype属性,指向原型对象
var obj1 = new fn();
console.info(fn.prototype === Object.getPrototypeOf(obj1));//true,全部使用fn建立的实例的原型对象都指向fn.prototype
var obj2 = new fn();
console.info(fn.prototype === Object.getPrototypeOf(obj2));//true
console.info(Object.getPrototypeOf(fn) === Function.prototype);//true
固然,fn自己也是一个对象,也有本身的原型对象,它的原型对象就是Function的属性prototype了(fn.__proto__===Function.prototype)。
咱们知道,每个对象均可以访问一个属性constructor,指向建立这个对象的函数(构造函数),实际上,constructor属性只不过是构造函数的原型对象的一个属性而已,所以经过构造函数建立的实例都可以访问constructor。
var fn = function fn(){};
var obj1 = new fn();
console.info(fn.constructor);//Function()
console.info(fn.prototype.constructor);//fn(),函数原型对象的constructor指向函数自己
console.info(obj1.hasOwnProperty('constructor'));//false,实例自己没有constructor属性
console.info(fn.prototype.constructor === obj1.constructor);//true,实例能够访问到原型对象中的constructor属性
var fn = function fn(){};
var obj = new fn();
console.info(obj.name);//undefined
fn.prototype.name = 'linjisong';
console.info(obj.name);//linjisong
3、建立对象
建立方式 |
示例 |
说明 |
传统方式 |
var person = new Object(); person.name = 'linjisong'; person.job = 'it'; |
传统方式建立对象容易产生大量重复的代码 |
对象字面量 |
var person = { name : 'linjisong', job : 'it' }; |
使用对象字面量建立简洁明了,很是适合建立做为函数实参的对象 |
工厂模式 |
function createPerson(name, job){ var o = new Object(); o.name = name; o.job = job; return o; } var person = createPerson('linjisong','it');
|
1、工厂模式能有效解决重复代码问题。 2、可是不能断定对象的类型 |
构造函数模式 |
function Person(name, job){ this.name = name; this.job = job; this.getName = function(){ return this.name; } } var person = new Person('linjisong','it');
|
构造函数模式能解决重复代码问题,也可以断定对象的类型 可是这种模式下建立的每一个实例都有一份属性和方法的Copy 对于方法来讲,每一个实例都保存一份是没有必要的 使用new调用构造函数的内部步骤: (1)建立一个新对象 (2)将构造函数的做用域赋给新对象(构造函数内this指向新建立对象) (3)执行构造函数中的代码 (4)返回新对象 |
原型模式 |
function Person(){} Person.prototype.name = 'linjisong'; Person.prototype.job = 'it; Person.prototype.getName = fucntion(){ return this.name; }; var person = new Person();
|
原型模式可以解决构造函数模式的方法实例有多个副本的问题 可是同时每一个实例的属性也共享了,对于引用类型的属性来讲 这会致使很是严重的问题,修改一个实例的属性会致使另外一个实例也修改 并且也不能接受参数
function Angle(){}; Angle.prototype.coordinate = [0,0];
var a1 = new Angle(); var a2 = new Angle();
a1.coordinate[0] = 1; console.info(a2.coordinate);//[1,0]修改a1会致使a2变动
|
组合构造原型模式 |
function Person(name, job){ this.name = name; this.job = job; } Person.prototype.getName = fucntion(){ return this.name; }; var person = new Person('linjisong','it');
|
结合构造函数模式和原型模式 使用构造函数模式建立属性,每一个实例保存一份 使用原型模式共享方法,全部实例共享保存一份 这是目前使用最普遍的对象建立方式 |
动态原型模式 |
function Person(name, job){ this.name = name; this.job = job; if(!Person.prototype.getName){ Person.prototype.getName = fucntion(){ return this.name; }; } } var person = new Person('linjisong','it');
|
这种模式其实是对于不习惯将构造函数和原型分离而引入的 在判断的时候,能够只判断其中一个属性 |
寄生构造函数模式 |
function Person(name, job){ var o = new Object(); o.name = name; o.job = job; o.getName = fucntion(){ return this.name; }; return o; } var person = new Person('linjisong','it');
|
工厂模式不使用new,寄生构造函数模式使用new操做符 构造函数模式不返回,寄生构造函数模式返回对象 不能使用instanceof判断类型 |
稳妥构造函数模式 |
function Person(name, job){ var o = new Object(); o.getName = fucntion(){ return name; }; return o; } var person = Person('linjisong','it');
|
稳妥对象:不使用this和new 稳妥构造模式相似寄生构造模式,但只能经过提供的方法访问成员 不能使用instanceof判断类型 |
各类建立对象的模式须要根据具体状况来看,最经常使用的仍是对象字面量和组合构造原型方式。
4、继承
在ECMAScript中,没有接口继承,只有实现继承,这些继承主要是经过原型链来实现的。像对象建立同样,下面也经过一张表格来浏览一下一些实现继承的方法。
继承方式 |
示例 |
说明 |
原型链 |
function Square(){//正方形 this.width = 10;//边长 this.coordinate = [0,0];//左上顶点的坐标 } Square.prototype.getArea = function(){//计算面积 return this.width * this.width; };
function ColorSquare(){//有颜色的正方形 this.color = 'red'; } ColorSquare.prototype = new Square();//实现了继承 ColorSquare.prototype.getColor = function(){//获取颜色 return this.color; }
var cs = new ColorSquare(); console.info(cs.width);//10 console.info(cs.getArea());//100 console.info(cs.color);//red console.info(cs.getColor());//red
|
1、经过修改子类型建立函数的原型实现继承。 2、经过原型给子类型添加新方法时,必定要在替换子类型原型以后添加,然后也不能经过对象字面量修改子类型的原型。 3、能够经过两种方法肯定原型和实例之间的关系:只要实例原型链中出现过构造函数fn,都返回true (1)instance instanceof fn (2)fn.prototype.isPrototype(instance) 4、使用原型链继承时,建立子对象时没法传递参数。 5、引用类型的父类属性会被全部子类型实例共享从而产生问题: 修改一个子类型实例的引用类型属性会致使其它全部子类型实例相应的修改 var cs2 = new ColorSquare(); console.info(cs2.coordinate);//[0,0] cs.coordinate[1] = 1; console.info(cs2.coordinate);//[0,1],修改cs会致使cs2也修改 |
借用构造函数 |
function Square(){//正方形 this.width = 10;//边长 this.coordinate = [0,0];//左上顶点的坐标 }
Square.prototype.getArea = function(){//计算面积 return this.width * this.width; };
function ColorSquare(){//有颜色的正方形 Square.call(this);//实现继承 this.color = 'red'; }
var cs = new ColorSquare(); var cs2 = new ColorSquare(); console.info(cs.coordinate);//[0,0] console.info(cs2.coordinate);//[0,0] cs.coordinate[1] = 1; console.info(cs.coordinate);//[0,1] console.info(cs2.coordinate);//[0,0],互相独立,修改cs不影响cs2 try{ console.info(cs.getArea());//异常,不能访问父类原型中方法 }catch(e){ console.info(e);//TypeError }
|
1、使用借用构造函数时,能够在call调用时传递参数。 2、同时也不存在引用类型共享的问题。 3、借用构造函数的缺点是,子类不能访问父类原型中定义的方法 |
组合继承 |
function Square(){//正方形 this.width = 10;//边长 this.coordinate = [0,0];//左上顶点的坐标 }
Square.prototype.getArea = function(){//计算面积 return this.width * this.width; };
function ColorSquare(){//有颜色的正方形 Square.call(this);//建立子类实例时,第二次调用父类构造函数 this.color = 'red'; }
ColorSquare.prototype = new Square();//第一次调用 ColorSquare.prototype.getColor = function(){//获取颜色 return this.color; }
var cs = new ColorSquare(); var cs2 = new ColorSquare(); console.info(cs.coordinate);//[0,0] console.info(cs2.coordinate);//[0,0] cs.coordinate[1] = 1; console.info(cs.coordinate);//[0,1] console.info(cs2.coordinate);//[0,0],互相独立,修改cs不影响cs2 console.info(cs.getArea());//100,能够访问
|
1、组合继承也称为伪经典继承,是将原型链和借用构造函数两种方式结合起来的继承方式。 2、基本思想是: (1)使用原型链实现对原型属性和方法的继承 (2)使用借用构造函数实现对实例属性的继承 3、组合继承避免了原型链和借用构造函数的缺点,融合了它们的优势,是最经常使用的继承方式。 4、组合继承的缺点是须要调用两次父类的构造函数 |
原型式继承 |
function create(o){ var fn = function(){}; fn.prototype = o; return new fn(); }
var square = { width:10, coordinate:[0,0] };
var cs = create(square); var cs2 = create(square); console.info(cs.coordinate);//[0,0] console.info(cs2.coordinate);//[0,0] cs.coordinate[1] = 1; console.info(cs.coordinate);//[0,1] console.info(cs2.coordinate);//[0,1],和原型链同样,会有共享问题
|
1、这种方式实际上就是前面说的模拟ES5中create函数来实现继承。 2、ES5及前面模拟的create还能够接受另外的属性描述参数。 3、和原型链与借用构造函数不一样的是,这种方式须要先有一个对象,而后直接建立子对象。 前者是构造函数的继承,然后者是对象实例的继承。 4、和使用原型链继承同样,也会有引用类型实例属性的共享问题。 |
寄生式继承 |
function create(o){ var fn = function(){}; fn.prototype = o; return new fn(); }
var square = { width:10, coordinate:[0,0] };
function colorSquare(original){ var s = create(original); s.color = 'red'; return s; }
var cs = colorSquare(square); console.info(cs.width);//10 console.info(cs.coordinate);//[0,0]
|
1、首先,这里的create函数不是必需的,任何返回新对象的函数均可以。 2、其次,这种模式也有引用类型实例属性共享的问题。 3、这种方式,能够当作将上面的对象继承包装成构造函数。 |
寄生组合式继承 |
function create(o){ var fn = function(){}; fn.prototype = o; return new fn(); }
function inherit(sub, sup){ var prototype = create(sup.prototype); prototype.constructor = sub; sub.prototype = prototype; }
function Square(){//正方形 this.width = 10;//边长 this.coordinate = [0,0];//左上顶点的坐标 } Square.prototype.getArea = function(){//计算面积 return this.width * this.width; };
function ColorSquare(){//有颜色的正方形 Square.call(this); this.color = 'red'; } inherit(ColorSquare, Square); ColorSquare.prototype.getColor = function(){//获取颜色 return this.color; }
var cs = new ColorSquare(); console.info(cs.width);//10 console.info(cs.getArea());//100 console.info(cs.color);//red console.info(cs.getColor());//red
var cs2 = new ColorSquare(); console.info(cs2.coordinate);//[0,0] cs.coordinate[1] = 1; console.info(cs2.coordinate);//[0,0]
|
1、这种方式只调用了一次父类构造函数,从而避免了在子类型的原型对象上建立没必要要的属性。 2、可以保证原型链不变,从而能够正常使用instanceof和isPrototypeOf()。 |