标签:
创建自定义对象最简单的方法就是创建一个Object 的实例,然后再为它添加属性和方法:
var person = new Object(); person.name = "Lilei"; person.age = 15; persion.sayName = function(){ console.log(this.name); }
或者使用对象字面量的形式:
var person = { name: "Lilei", age: 15, sayName: function(){ console.log(this.name); } }
创建自定义对象最简单的方法就是创建一个Object 的实例,然后再为它添加属性和方法:
var person = new Object();
person.name = "Lilei";
person.age = 15;
persion.sayName = function(){
console.log(this.name);
}
或者使用对象字面量的形式:
var person = {
name: "Lilei",
age: 15,
sayName: function(){
console.log(this.name);
}
}
1.属性类型
ECMAScript 5 在定义只有内部采用的特性时,描述了属性的各种特性,这些特性都是为了实现JavaScript引擎用的,因此在JavaScript中不能直接访问。为了表示特性是内部值,该规范把他们放在了两对方括号中。
要修改属性默认的特性,必须使用ECMAScript 5 的Object.defineProperty()方法。这个方法接受三个参数:属性所在的对象、属性名字和一个描述符对象。其中描述符对象的属性必须是:configurable、enumerable、writable 和 value。设置其中的一个或多个值,可以修改对应的特性值。
var person = {}; Object.defineProperty(person, "name", { writable: false, value: "Lilei" }); console.log(person.name); //Lilei person.name = "HanMeimei"; console.log(person.name); //Lilei
需要注意的是,如果把 configurable 特性改为false 后,就不可以再对 除 writable之外的特性进行修改了,也就是说configurable 改成false 后就改不回来了。
2.访问器属性
访问器属性包括一对setter getter 函数,在读取访问器属性时,会调用getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下4个特性。
访问器属性不能直接定义,必须用Object.defineProperty() 来定义。
var person = { name: "Lilei", age: 18, _year: 2015 }; Object.defineProperty(person, "year", { get: function(){ console.log("getFunction"); return this._year; }, set: function(newValue){ console.log("setFunction"); this._year = newValue; this.age += this.year - 2015; } }) person.year = 2016; console.log(person.age + " " + person._year); /** setFunction getFunction 19 2016 */
_year前面的下划线是一种常用几号,用于表示只能通过对象方法访问的属性(但是从例子中可以看出,外部还是可以访问的,只是一种规范约束)。
ECMAScript 5 提供了一个可以一次性定义多个属性的方法 Object.defineProperties()。接受两个参数,第一个参数是要添加和修改其属性的对象,第二个参数的属性与第一个对象中要添加或修改的属性一一对应。
var person = {}; Object.defineProperties(person, { _year:{ value: 2015 }, age:{ value: 18 }, year:{ get:function(){ console.log("getFunction"); return this._year; }, set: function(newValue){ console.log("setFunction"); this._year = newValue; this.age += newValue - 2015; } } }); person.year=2016; console.log(person.age + " " + person._year); //18 2015 var descriptor = Object.getOwnPropertyDescriptor(person, "_year"); console.log(descriptor); /** { value: 2015, writable: false, enumerable: false, configurable: false } */
需要注意的是,使用这种方法定义属性,需要显示定义属性的特性,如果不指定,则默认为false。有ECMA-262规则不一致。
var person = {}; Object.defineProperties(person, { _year:{ value: 2015, writable: true, enumerable: true, configurable: true }, age:{ value: 18, writable: true, enumerable: true, configurable: true }, year:{ get:function(){ console.log("getFunction"); return this._year; }, set: function(newValue){ console.log("setFunction"); this._year = newValue; this.age += newValue - 2015; } } }); person.year=2016; console.log(person.age + " " + person._year); //19 2016 var descriptor = Object.getOwnPropertyDescriptor(person, "_year"); console.log(descriptor); /** { value: 2016, writable: true, enumerable: true, configurable: true } */
上例中同时也用到了读取属性特性的函数:Object.getOwnPropertyDescriptor()方法。该函数接受两个参数,属性所在的对象和要读取其描述符的属性名称。
前文涉及的两种创建对象的方式有个明显的缺点:使用同一个接口创建很多的对象,会产生大量的重复代码。为了解决这个问题,人们开始使用工厂模式的一种变体。
1.工厂模式
实际上就是使用一种函数来封装以特定接口创建对象的细节。
function createPerson(name, age){ var o = new Object(); o.name = name; o.age = age; o.sayName = function(){ console.log(this.name); }; return o; } var person1 = createPerson("Leilei", 18); var person2 = createPerson("Hanmeimei", 17);
该方法有一个问题就是,没有解决对象识别的问题,也就是person1 或 person2 看起来都是Object类型,不是Person类型。
于是人们继续探索,随着JavaScript的发展,又一个新的模式出现了。
2.构造函数模式
像Object Array这种的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数。
function Person(name, age){ this.name = name; this.age = age; this.sayName = function(){ console.log(this.name); } } var person1 = new Person("Lilei", 18); var person2 = new Person("Hanmeimei", 17); console.log(person1 instanceof Person); //true
这样Person 就和Array这种的类型及其相似了。使用new操作符调用函数后,会发生以下事情:
通过该方法构造的对象既是Object对象又是Person对象(因为Object是所有类的基础),如上,使用instanceof 操作符可以得到验证。
构造函数和其他函数的唯一区别就在于调用他们的方式不同。其实任何函数只要通过new操作符来调用,那它就可以作为构造函数;任何函数不通过new 操作符来调用,那它跟普通函数也不会有什么两样。
function Person(name, age){ this.name = name; this.age = age; this.sayName = function(){ console.log(this.name); } } //作为构造函数 var person1 = new Person("Lilei", 18); person1.sayName(); //Lilei //作为普通函数 Person("HanMeimei", 17); sayName(); //HanMeimei //在另一个对象的作用域中调用 var o = new Object(); Person.call(o, "Lily", 16); o.sayName(); //Lily
构造函数的问题是:每个方法都要在每个实例上重新创建一遍。例如前例中的sayName()函数,在person1和 person2 中都包含各自的Function 实例(Function 在Js中也是对象)。
因此person1 和 person2 的sayName()实例不不想等的。
console.log(person1.sayName == person2.sayName); //false
可一个做一个改进如下:
function Person(name, age){ this.name = name; this.age = age; this.sayName = sayName; } function sayName(){ console.log(this.name); } var person1 = new Person("Lilei", 18); var person2 = new Person("HanMeimei", 17); console.log(person1.sayName == person2.sayName); //true
但这样破坏了对象的封装性,sayName可以在全局中调用,加上作用域以后可以在任何对象上调用。
于是人们又创造出了原型模式。
3.原型模式
JavaScript中,每一个函数都有一个prototype (原型)属性,这个属性是个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
或者说,prototype 就是通过调用构造函数而创建的哪个对象实例的原型对象。而那个原型对象中的属性和方法是所有对象实例共享的。
只要我们把属性和方法放到原型对象中,就可以让所有的对象实例共享这些属性和方法了。
function Person(){ } Person.prototype.name = "Lilei"; Person.prototype.age = 18; Person.prototype.sayName = function(){ console.log(this.name); } var person1 = new Person(); person1.sayName(); //Lilei var person2 = new Person(); person2.sayName(); //Lilei console.log(person1.sayName == person2.sayName); //true
每当读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。
搜索首先从对象实例本身开始,如果在实例中找到了具有给定名字的属性,则返回该属性的值,如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性,如果在原型对象中找到了该属性,则返回。
上例中我们执行person1.sayName()时,先查找实例person1 有 sayName 属性吗,没有,则继续查找prototype 中有sayName属性吗?有,则返回。
所以,如果person1 中存在name属性,则不会访问到原型的name 属性。
使用delete 操作符,可以完全删除实例属性,但是不会删除原型属性,删除后,就可以访问到原型中的属性了。
function Person(){ } Person.prototype.name = "Lilei"; Person.prototype.age = 18; Person.prototype.sayName = function(){ console.log(this.name); } var person1 = new Person(); person1.name = "HanMeimei"; person1.sayName(); //HanMeimei var person2 = new Person(); person2.sayName(); //Lilei delete person1.name; person1.sayName(); //Lilei
使用hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在与原型中。当给定属性存在与对象实例中时返回true,否则返回false。
function Person(){ } Person.prototype.name = "Lilei"; Person.prototype.age = 18; var person1 = new Person(); console.log(person1.hasOwnProperty("name")); //false person1.name = "HanMeimei"; console.log(person1.hasOwnProperty("name")); //true delete person1.name; console.log(person1.hasOwnProperty("name")); //false
in 操作符有两种用法:单独使用 和 在for-in 循环中使用。单独使用时,in 会在对象能够访问属性时,返回true,否则返回false。
也就是说,不管属性是在对象实例中还是原型中,只有属性存在,in 就能返回true。
function hasPrototypeProperty(object, name){ return !object.hasOwnProperty(name) && (name in object); }
上面函数可以判断属性是否只在原型中。
要取得对象上所有可以枚举的实例属性,可以使用ECMAScript 5 的 Object.keys() 方法,
使用Object.getOwnPropertyName() 方法可以返回所有的实例属性(包括不可枚举的属性)。
function Person(){ } Person.prototype.name = "Lilei"; Person.prototype.age = 18; Person.prototype.sayName = function(){ console.log(this.name); } var person1 = new Person(); person1.weight = 60; var keys = Object.keys(person1); //[ ‘weight‘ ] console.log(keys); var keys2 = Object.getOwnPropertyNames(Person.prototype); console.log(keys2); //[ ‘constructor‘, ‘name‘, ‘age‘, ‘sayName‘ ]
也可以直接把prototype 指针指向一个对象,这样就不需要每添加一个属性或方法都 要敲一遍 Person.prototype了。
但是只是把prototype指向一个对象,那么原型中的constructor就指向Object 了,而不会指向Person 函数了,所以可以显示制定constructor 属性。
function Person(){ console.log("hello"); } Person.prototype = { constructor: Person, name: "Lilei", age:18, sayName: function(){ console.log(this.name); } } var person1 = new Person(); //hello console.log(person1 instanceof Person); //true console.log(person1.constructor()); //hello
但这样设置constructor 属性会导致它的[[Enumerable]] 特性被设置为true。默认情况下 constructor 属性是不可枚举的,因此可以用Object.defineProperty()来设置constructor 属性。
function Person(){ } Person.prototype = { name: "Lilei", age:18, sayName: function(){ console.log(this.name); } } Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person });
原型模式的问题是:所有的实例在默认情况下都将取得相同的属性值,对于那些包含基本值的属性倒还好,毕竟通过在实例上添加一个同名属性,可以隐藏原型中的对象属性。然而对于包含引用类型的属性来说,一个实例上的修改会影响其他实例的属性。
function Person(){ } Person.prototype = { constructor: Person, name: "Lilei", age: 18, friends: ["Polly", "Tom"], sayName: function(){ console.log(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("Jim"); console.log(person1.friends); //[ ‘Polly‘, ‘Tom‘, ‘Jim‘ ] console.log(person2.friends); //[ ‘Polly‘, ‘Tom‘, ‘Jim‘ ]
4.组合使对于包含引用类型的属性来说,一个实例上的修改会影响其他实例的属性用构造函数和原型模式
创建自定义类型最常见的方式,就是组合使用构造函数模式与原型模式,构造函数模式用于构造实例属性,而原型模式用于定义方法和共享的属性。
结果是,每个实例都有自己的一份实例属性的副本,但同事有共享着对方法的引用,最大限度地节省了内存。
function Person(name, age){ this.name = name; this.age = age; this.friends = ["Polly", "Tom"]; } Person.prototype = { constructor: Person, sayName : function(){ console.log(this.name); } } var person1 = new Person("Lilei", 18); var person2 = new Person("HanMeimei", 17); person1.friends.push("Jim"); console.log(person1.friends); //[ ‘Polly‘, ‘Tom‘, ‘Jim‘ ] console.log(person2.friends); //[ ‘Polly‘, ‘Tom‘ ]
这种构造函数与原型混成的模式,是目前ECMAScript 中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。
5.动态原型模式
该模式把所有信息都封装到构造函数内部,而在构造函数中初始化原型(在必要的情况下),又保持了同事使用构造函数和原型的优点。
function Person(name, age){ this.name = name; this.age = age; this.friends = ["Polly", "Tom"]; if(typeof this.sayName != "function"){ Person.prototype.sayName = function(){ console.log(this.name); } } } var person1 = new Person("Lilei", 18); var person2 = new Person("HanMeimei", 17); person1.friends.push("Jim"); console.log(person1.friends); //[ ‘Polly‘, ‘Tom‘, ‘Jim‘ ] console.log(person2.friends); //[ ‘Polly‘, ‘Tom‘ ]
上例中 if语句只需要检测一个函数的类型,而不必挨个检查。if 语句内可以定义多个原型函数。
6.寄生构造函数模式
有点类似与装饰者模式,该模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。从表面上看,这个函数又很像是典型的构造函数。
function Person(name, age){ var o = new Object(); o.name = name; o.age = age; o.sayName = function(){ console.log(this.name); } return o; } var person1 = new Person("Lilei", 18); person1.sayName();
是不是和工厂模式很像,但是这里一般用的时候是新建一个复杂的类似的对象,并不是Object对象,比如我们要想创建一个拥有额外方法的特殊数组。由于不能直接修改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
console.log(colors instanceof SpecialArray); //false
实际上,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说构造函数返回的对象与在构造函数外部创建的对象没有什么不同,所以同工厂模式一样,返回的对象不能用instanceof来确定对象类型,所以这种方法不推荐使用。
一般OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。
由于JavaScript中没有函数签名,所以无法实现接口继承。JavaScript 只支持实现继承,而且其实现继承主要依赖原型链来实现。
1.原型链
ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
每个对象都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
加入我们让原型对象等于另一个类型的实例,结果该实例的原型对象就将包含yi额指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; }; function SubType(){ this.subproperty = false; } // SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function(){ return this.subproperty; }; var instance = new SubType(); console.log(instance.getSuperValue()); //true
在代码中,没有使用SubType默认的原型,而是给他换了一个新原型,就是SuperType 实例。于是新原型不仅具有作为一个SuperType 的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了SuperType 的原型。
最终结果是这样:instance 指向SubType的原型, SubType的原型有指向 SuperType 的原型。SubType.prototype.getSubValue 相当于在SuperType实例上增加方法。而getSuperValue 方法在SuperType 实例的原型对象中。此外,instance.constuctor 现在指向的是SuperType,这是因为原来 SubType.prototype 中的 constructor 被重写了的缘故。
通过原型链,本质上扩展了前面介绍的原型搜索机制。
可以用两种方法来判断实例的类型。
console.log(instance instanceof Object); //true console.log(instance instanceof SuperType); //true console.log(instance instanceof SubType); //true console.log(Object.prototype.isPrototypeOf(instance)); //true console.log(SuperType.prototype.isPrototypeOf(instance)); //true console.log(SubType.prototype.isPrototypeOf(instance)); //true
使用原型链方法需要注意,给子类型添加原型方法或者重写原型方法必须在替换原型语句之后进行,而且不能使用家对象字面量创建原型的方法实现。否则原型链将会被切断。
如果父类包含引用类型的属性,那么它成为子类对象的原型后,引用类型的属性将被所有子类对象共享。
function SuperType(){ this.colors = ["red", "blue", "green"]; } function SubType(){ } // SubType.prototype = new SuperType(); var sub1 = new SubType(); var sub2 = new SubType(); sub1.colors.push("yellow"); console.log(sub1.colors); //[ ‘red‘, ‘blue‘, ‘green‘, ‘yellow‘ ] console.log(sub2.colors); //[ ‘red‘, ‘blue‘, ‘green‘, ‘yellow‘ ]
2借用构造函数
这种技术的基本思想是在子类构造函数的内部调用超类构造函数。(别忘了,函数只是在特定环境中执行代码的对象,因此通过使用 apply() 和 call() 方法也可以在将来新创建的对象上执行构造函数)
function SuperType(){ this.colors = ["red", "blue", "green"]; } function SubType(){ SuperType.call(this); }var sub1 = new SubType(); sub1.colors.push("yellow"); console.log(sub1.colors); //[ ‘red‘, ‘blue‘, ‘green‘, ‘yellow‘ ] var sub2 = new SubType(); console.log(sub2.colors); //[ ‘red‘, ‘blue‘, ‘green‘ ]
通过借调父类的构造函数,我们实际上是在新创建的SubType 实例中调用了SuperType 构造函数,结果SubType 的每个实例都会具有自己的 colors 属性的副本了。
借用构造函数的方法存在一个问题:就是方法都在构造函数中定义,因此函数服用就无从谈起了。而且在超类型的原型中定义的方法,对子类型那个而言也是不可见的。
3.组合继承
也叫做伪经典继承,值得是将原型链和借用构造函数的技术组合到一起,从而发挥二者之长的一种继承模式。
其思路是 使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,即通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ console.log(this.name); }; function SubType(name, age){ SuperType.call(this, name); this.age = age; } SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ console.log(this.age); }; var sub1 = new SubType("Lilei", 18); sub1.colors.push("yellow"); console.log(sub1.colors); //[ ‘red‘, ‘blue‘, ‘green‘, ‘yellow‘ ] sub1.sayName(); //Lilei sub1.sayAge(); //18 var sub2 = new SubType("HanMeimei", 17); console.log(sub2.colors); //[ ‘red‘, ‘blue‘, ‘green‘ ] sub2.sayName(); //HanMeimei sub2.sayAge(); //17
组合继承避免了原型链和借用构造函数的缺陷,融合了他们的优点,成为JavaScript中最常用的继承模式。而且,instanceof 和 isProtorypeOf() 也能够用于识别基于组合继承创建的对象。
4.原型式继承
原型式继承要求你必须有一个对象可以作为另一个对象的基础。然后在根据具体需求对得到的对象加以修改即可。
function object(o){ function F(){}; F.prototype = o; return new F(); } var person = { name: "Lilei", friends: ["Polly", "Tom"] }; var anotherPerson = object(person); anotherPerson.name = "Lei Li"; anotherPerson.friends.push("Jim"); var yetAnotherPerson = object(person); console.log(yetAnotherPerson.friends); //[ ‘Polly‘, ‘Tom‘, ‘Jim‘ ] yetAnotherPerson.name = "HanMeimei"; yetAnotherPerson.friends.push("Lily"); console.log(person.friends); //[ ‘Polly‘, ‘Tom‘, ‘Jim‘, ‘Lily‘ ]
ECMAScript 5 通过新增Object.create()方法 规范化了原型式继承。这个方法接收两个参数,一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。当传入一个参数是,和上例中object()函数功能是一样的。下例演示传入两个参数的方法。
var person = { name: "Lilei", friends: ["Polly", "Tom"] } var anotherPerson = Object.create(person, { name: { value: "HanMeimei" } }); console.log(anotherPerson.name); //HanMeimei console.log(person.name); //Lilei
在没有必要兴师动众地创建构造函数,而只是想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。包含引用类型的属性始终都会共享相应的值,就像使用原型模式一样。
5.寄生式继承
寄生式继承是与原型模式机密相关的一种思路,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象。
function createAnother(original){ var clone = Object.create(original); clone.sayHi = function(){ console.log("Hi"); }; return clone; } var person = { name: "Lilei", friends: ["Polly", "Tom"] }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); //Hi
上例中Object.create()函数可以用任何返回对象的函数代替。
使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。
6.寄生组合式继承
组合式继承是Javascript中最常用的继承模式,但是他也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型的构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数的内部。
寄生组合式继承,通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其思路是:不必为了制定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型的一个副本而已。
function inheritPrototype(subType, superType){ var prototype = Object(superType.prototype); prototype.constructor = subType; subType.prototype = prototype; } function SuperType(name){ this.name = name; this.friends = ["Polly", "Tom"]; } SuperType.prototype.sayName = function(){ console.log(this.name); }; function SubType(name, age){ SuperType.call(this, name); this.age = age; } inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function(){ console.log(this.age); };
这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType.prototype 上面创建不必要的、多余的属性。于此同时,原型链还能保持不变。因此还能正常shiyo个instanceof 和 isPrototypeOf()。寄生组合式继承是引用类型最理想的继承范式。
ECMAScript 支持面向对象编程,但不使用类或者接口。对象可以在代码执行过程中,创建和增强,因此具有动态性而非严格定义的实体。可以采用下面的犯法创建对象。
JavaScript 主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样子类型就能够访问超类型的所有属性和方法,这一点与基于类的继承很相似。原型链的问题是对象实例共享所有继承的属性和方法,因此不适合单独使用。解决方法是借用构造函数,即在子类型构造函数的内部调用父类型的构造函数。这样就可以做到每个实例都具有自己的属性,同事还能保证是使用构造函数模式来定义类型。使用最多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,而通过借用构造函数继承是实例属性。
另外,还存在下列可供选择的继承模式:
标签:
原文地址:http://www.cnblogs.com/liangflying521/p/5121528.html