建立自定义对象最简单的方法就是建立一个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 的实例,而后再为它添加属性和方法:app
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.属性类型this
ECMAScript 5 在定义只有内部采用的特性时,描述了属性的各类特性,这些特性都是为了实现JavaScript引擎用的,所以在JavaScript中不能直接访问。为了表示特性是内部值,该规范把他们放在了两对方括号中。spa
要修改属性默认的特性,必须使用ECMAScript 5 的Object.defineProperty()方法。这个方法接受三个参数:属性所在的对象、属性名字和一个描述符对象。其中描述符对象的属性必须是:configurable、enumerable、writable 和 value。设置其中的一个或多个值,能够修改对应的特性值。prototype
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.访问器属性code
访问器属性包括一对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 主要经过原型链实现继承。原型链的构建是经过将一个类型的实例赋值给另外一个构造函数的原型实现的。这样子类型就可以访问超类型的全部属性和方法,这一点与基于类的继承很类似。原型链的问题是对象实例共享全部继承的属性和方法,所以不适合单独使用。解决方法是借用构造函数,即在子类型构造函数的内部调用父类型的构造函数。这样就能够作到每一个实例都具备本身的属性,同事还能保证是使用构造函数模式来定义类型。使用最多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,而经过借用构造函数继承是实例属性。
另外,还存在下列可供选择的继承模式: