前言
JavaScript中把对象定义成无序属性的集合,其属性包含基本值、对象值和函数,对象中的每个属性和方法都有一个名字,名字都会被映射到一个值,JS中的对象可以看成是散列表,里面包含了一系列的键值对,其中值可以是数据或函数。
属性类型
JS中的属性分为两大类:数据属性和访问器属性。数据属性就是常见的定义在对象内部的各种字段,访问器属性包含的是一对getter/setter两个方法。数据属性和访问器属性都有四个访问描述,不过不是完全一样。
描述特性 | 意义 |
---|---|
[[Configurable]] | 表示是否能通过delete删除属性而重定义属性,能否修改属性的特性,能否把属性修改成访问器属性 |
[[Enumerable]] | 表示能否通过for-in循环直接查找属性 |
[[Writable]] | 表示能否修改属性的值 |
[[Value]] | 包含这个属性的数值,当读取属性时从这个位置度;写入属性的值时,把新值保存在这个位置 |
普通的属性可以直接添加在对象上,直接添加的属性上面四种特性的值都是默认的,查看这些默认值可以使用Object.getOwnPropertyDescriptor()方法获取属性描述符对象,里面包含了这些特性的值。除了读取特性值用户还可以自定义上面四个特性值或者重写特性值,只需要调用defineProperty方法定义或者重定义属性即可。
var person = {
name: "zhangsan"
}
var descriptor = Object.getOwnPropertyDescriptor(person, "name");
alert(descriptor.value); // zhangsan
alert(descriptor.configurable); // true
alert(descriptor.writable); // true
alert(descriptor.enumerable); // true
var person = { name: "zhangsan" };
Object.defineProperty(person, "age", {
configurable: false,
value: 30,
writable: false,
enumerable: false
});
alert(person.age) // 30
person.age = 40
alert(person.age) // 30
for (var key in person) {
alert(key + " " + person[key]) // name zhangsan
}
delete person.age
alert(person.age) // 30
上面的例子使用defineProperty定义了age属性,不可写无法被遍历,不能被配置,虽然后面修改了内容为40,遍历它的属性值也只有name属性被遍历到了,最后delete定义的age属性由于无法被配置依然保持在person对象里。接着查看访问器属性的特性,它也包含四个特性,除了configurable和enumerable和数据属性一致外,另外两个是getter和setter特性,默认情况下它们都是undefined。
var person = {
name: "zhangsan",
age: 30
}
Object.defineProperty(person, "adult", {
get: function() {
return this.age > 18;
},
set: function(newValue) {
if (newValue) {
this.age = 30;
} else {
this.age = 17;
}
}
});
alert(person.adult) // true
person.adult = false
alert(person.age) // 17
创建对象
直接使用Object构造函数或者字面量语法都可以创建对象,这种方式对于简单的对象创建能够很好的满足。对于有些有些对象需要重复创建,而有些对象又过于复杂,如果还是用这种原始的创建方式就会导致代码中包含大量重复性逻辑,为了解决这种情况可以将对象的创建封装到函数中,方便开发者维护和复用。
工厂模式
工厂模式创建对象的原理很简单就是把创建对象的所有逻辑都放到一个方法中,调用这个方法就能够直接返回一个对象。工厂模式使用非常简单,但是它创造的对象都是Object类型的,无法解决创建多个不同的对象的类型识别问题。
function createUser(var name, var age) {
var obj = new Object();
obj.name = name;
obj.age = age;
return obj;
}
function createPerson(var name, var age, var sex) {
var obj = new Object()
obj.name = name;
obj.age = age;
obj.sex = sex;
return obj;
}
构造函数模式
JS中可以通过构造函数来创建特定类型的对象,相比于工厂模式,构造函数不需要显示创建对象,可以直接将属性赋值给this对象,不需要在最后返回创建的对象。从本质上来说构造函数其实就是一个函数对象,如果没有使用new操作符调用的话,它和普通函数的调用结果一样。
function Person(name, age) {
this.name = name
this.age = age
this.print = function() {
alert(name + " " + age);
}
}
var person1 = new Person("zhangsan", 20);
var person2 = new Person("lisi", 17);
person1.print() // zhangsan 20
person2.print() // lisi 17
alert(person1.print == person2.print) // false
Person('wangwu', 30) // 不用new操作符直接调用,会在全局对象上添加name和age
alert(name + " " + age) // wangwu 30
上面的构造函数使用简单,而且它还能够使用instanceof操作符判断不同的对象,不过判断对象的函数时发现两个不同的对象使用的函数也是不同的,这其实浪费了内存多生成了额外的方法对象。
原型模式
JS中每个函数都会有一个prototype属性,而构造函数也是一个函数类型,它自然也有这个属性,这个对象可以包含所有通过同一个构造函数生成的对象共享的属性和方法。
function Person() {
}
Person.prototype.name = "zhangsan"
Person.prototype.age = 27
Person.prototype.print = function() {
alert(this.name + " " + this.age);
}
var person = new Person()
person.print() // zhangsan 27
var person2 = new Person()
person2.print() // zhangsan 27
alert(person.print == person2.print); // true
从上面的例子可以看出Person构造函数创造的对象内部的方法使用的是同一个对象,它们在方法中访问的属性值也是prototype对象的值。需要注意如果直接给对象添加属性会覆盖prototype里的属性,想要修改prototype里的属性值还是需要直接修改prototype对象。
function Person() {
}
Person.prototype.name = "zhangsan"
Person.prototype.age = 27
Person.prototype.print = function() {
alert(this.name + " " + this.age);
}
var person = new Person()
person.print() // zhangsan 27
var person2 = new Person()
person2.name = 'lisi'
person2.age = 30
person2.print() // lisi 30
alert(person.print == person2.print);
person.print() // zhangsan 27
前面说过函数对象内部会包含prototype属性,构造函数返回的是对象类型,对象并没有prototype属性,它是如何访问到构造函数prototype对象里的属性的呢?其实通过构造函数生成的对象内部都会包含一个[[Prototype]]指针,它就是指向了构造函数的prototype对象,如果客户端代码访问属性会首先查看当前对象内部是否含有,如果有就直接访问没有就到prototype对象里查找。
组合构造函数和原型模式
前面直接使用构造函数会导致生成对象包含多余的对象方法,而只使用原型模式又会导致所有定义的属性都是共享状态,实际开发中需要每个对象的属性都有自己的副本,同时方法是每个对象共享的。可以把需要共享的属性都定义到prototype里,而需要每个对象分离开的属性都定义在构造函数中。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor : Person,
sayName : function() {
alert(this.name);
}
}
寄生构造函数
通常构造函数都不会返回值,使用new操作符调用构造函数默认返回的是新的对象;不过如果构造函数返回了值,也并不会出现错误,这个返回值会替代new操作符默认返回的对象。这种寄生构造函数可以用来创建某些内置对象的特殊类型,不过它无法使用instanceof操作符进行对象类型判定。
function Person(name, age){
var obj = new Object()
obj.name = name;
obj.age = age;
obj.print = function() {
alert(name + " " + age);
}
return obj;
}
var person1 = new Person("zhangsan", 20)
var person2 = new Person("lisi", 30);
person1.print() // zhangsan 20
person2.print() // lisi 30
alert(person1.print == person2.print) // false
alert(person1 instanceof Person) // false
继承
面向对象的继承通常包含接口继承和实现继承,接口继承只继承方法签名,实现继承则继承实际已经实现的方法。JS中由于没有通常的类接口等结构,想要实现继承就需要使用原型链,其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
原型链和借用构造函数继承
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.print = function() {
alert(this.name + " " + this.age);
}
function User(name, age, role, password) {
Person.call(this, name, age) // 一定要这样调用,不能直接用Person调用
this.role = role;
this.password = password;
}
User.prototype = new Person()
User.prototype.login = function() {
alert(this.name + " login with password " + this.password + " as " + this.role);
}
var u = new User("zhangsan", 30, "admin", "123456");
u.login()
// zhangsan login with password 123456 as admin
需要注意的是原型上的方法需要在原型对象被赋值之后在添加,如果在之前添加其实方法被添加在之前的原型对象上,新赋值的原型对象并不会包含之前添加的方法。前面已经提到Function对象里有一个prototype属性,而构造函数生成的对象里有一个[[Prototype]]的指针,上面的例子User.prototype = new Person();表明了User构造函数的prototype属性指向了一个Person构造出来的对象,那么在查询User里面的方法的时候可以再到这个Person对象的[[Prototype]]也就是Person的prototype属性里查找,这样就实现了User继承Person的属性和方法。
原型链虽然能够实现继承,不过由于prototype对象是共享对象,如果在原型中的属性有Array之类的引用类型,修改之后会导致所有的对象都发生变化,还有就是原型连无法向父构造函数传递参数,需要借用构造函数才能实现向父构造函数传参。调用父构造函数需要用apply或call方法是,如果不用这种方式Person内部的this其实指向的Window对象。
原型式继承
这种构造方式不适用严格的构造函数,而是使用已有的对象创建一个新的对象,主要的方法就是将原有的对象作为一个通用的构造函数的原型对象,这样每个从这个创建函数产生的对象都共享那个已有的对象。
var person = {
name: "zhangsan",
age: 30
}
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
var user1 = object(person)
user1.name = 'lisi'
alert(user1.name + " " + user1.age)