时间:2021-07-01 10:21:17 帮助过:6人阅读
(1)对象属性和特性
什么是属性(Property),什么是特性(Attribute),这有什么区别?我不想也不会从语义学上去区分,对于这系列文章来说,属性就是组成对象的一个部分,广义上也包括对象的方法,而特性则是指被描述主体所具有的特征,换句话说,属性是我们可以通过编码来访问的具体存在,而特性则主要是为了便于理解概念的抽象存在,当然,特性也可以通过相应的属性来具体外化。这一小节所讲的对象属性的特性就是对对象属性特征的一个描述,主要来自于ECMA-262规范的第5版,该规范使用两个中括号的形式来描述不能直接访问的内部特性。
A、属性类型(先给属性分下类):
B、对象内部属性
内部属性不能通过代码直接访问,它主要是为了描述规范,也是给ECMAScript实现者参考的,而对于开发者来说,了解这些可以便于理解一些内部机制。比如在给一个属性赋值时,在实现中会调用[[Put]]内部方法,而读取一个属性值时,则调用[[Get]]方法。
所有对象公共的内部属性 | 个别对象特有的内部属性 | |||
名称 | 规范 | 名称 | 规范 | 对象 |
[[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) | [ 代码如下: ] |
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、属性特性(用来描述属性的特性)
内部特性 | 配置属性 | 属性类型 | 数据类型 | 默认值 | 含义 | 备注 |
[[Configurable]] | configurable |
数据属性 访问器属性 |
Boolean |
true |
能否通过delete删除属性从而重新定义属性 能否修改属性的特性 能否把属性修改为访问器特性 |
一旦把属性定义为不可配置的,就不能再变为可配置的 如果为false,不能做删除、也不能修改属性特性,但是允许修改属性值 非严格模式下会忽略相应操作,严格模式下则抛出异常 |
[[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)属性描述符对象 |
使用属性定义方法时 |
|
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])是一个非常有意思的方法,规范中这样描述它的行为:
[code]
①如果prototype不是Null或Object,抛出TypeError异常
②var obj = new Object()
③设置obj的内部属性[[Prototype]]为prototype
④如果descriptors存在且不为undefined,使用Object.defineProperties(obj,descriptors)来添加属性
⑤返回obj
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()。 |