本篇内容
  • 对象的属性
  • 创建对象的几个模式
  • 继承的几个模式

ECMA-262把对象定义为:无序属性的集合,其属性可以包含基本值、对象或函数。

理解对象

1
2
3
4
5
6
7
8
var persion = {
name: 'Micheal',
age: 18
};

persion.sayName = function () {
alert(this.name);
};

1.属性类型

ES中有两种属性:数据属性和访问器属性。

数据属性:数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性:

  • [[Configurable]]:表示是否能通过delete删除属性而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。默认true;
  • [[Enumberable]]:表示能否通过for-in循环访问属性。默认true;
  • [[Writable]]:表示能否修改属性的值。默认true;
  • [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。默认undefined。

要修改属性默认的特性,必须使用ES5中的Object.defineProperty()(mdn-Object.defineProperty())方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。描述符对象的属性必须是上面四个钟的一个或多个(configurable, enumerable, writable, value)。如

1
2
3
4
5
6
7
8
9
var person = {};
Object.defineProperty(person, 'name', {
value: 'Micheal',
writable: false
});

console.log(person.name); // 'Micheal'
person.name = 'Jack';
console.log(person.name); // 'Micheal'

(上述例子在严格模式会报错)

可以多次调用Object.defineProperty()方法修改同一个属性,但在把configurable特性设置为false之后就会有限制了。
在调用Obect.defineProperty()方法时,如果不指定,configurable, enumerable和writable特性的默认值都是false。

IE8是第一个实现Object.defineProperty()方法的浏览器版本,但是只能用在DOM对象上。

访问器属性:访问器属性不包含数据值,它们包含一对getter和setter函数。在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下4个属性:

  • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。默认值为true;
  • [[Enumerable]]:表示是否通过for-in循环返回属性。默认true;
  • [[Get]]:在读取属性时调用的函数。默认值undefined;
  • [[Set]]:在写入属性时调用的函数。默认值undefined。

访问器属性不能直接定义,必须使用Object.defineProperty()来定义。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var book = {
_year: 2014,
edition: 1
};

Object.defineProperty(book, 'year', {
get: function () {
return this._year;
},
set: function (newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
});

book.year = 2005;
console.log(book.edition); // 2

(_year前面的下划线是一种常用的记号,用于表示只能通过对象方法访问的属性。)
支持这个方法的浏览器:IE9+,Firefox4+、Safari5+、Opera12+和Chrome。在这个方法之前,要创建访问器属性,一般使用两个非标准的方法:defineGetter()和defineSetter()。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var book = {
_year: 2004,
edition: 1
};

book.__defineGetter__('year', function () {
return this._year;
});
book.__defineSetter__('year', function (newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004
}
});

在不支持Object.defineProperty()方法的浏览器中不能修改[[Configurable]]和[[Enumerable]]。

定义多个属性:Object.defineProperties()方法。利用这个方法可以通过描述符一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中药添加或修改的属性一一对应。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var book = {};
Object.defineProperties(book, {
_year: {
value: 2004
},
{
edition: 1
},
year: {
get: function () {
return this._year;
},
set: function (newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newVaule - 2004;
}
}
}
});

读取属性的特性:Object.getOwnPropertyDescriptor()方法(mdn-Object.getOwnPropertyDescriptor),可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象。如

1
2
3
4
5
6
7
var book = {
year: 2004
};

var desc = Object.getOwnPropertyDescriptor(book, 'year');
console.log(desc.value); // 2004
console.log(desc.configurable); // true

创建对象

工厂模式

解决了创建多个相似对象的问题,但没有解决对象识别的问题,即怎样知道一个对象的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
function createPerson (name, age, job) {
var o = {};

o.name = name;
o.age = age;
o.job = job;
o.sayName = function () {
alert(this.name);
};
return o
}

var person1 = createPerson('Micheal', 18, 'FE');

构造函数模式

构造函数始终都应该以一个大写字母开头,而构造器函数则应该以一个小写字母开头。
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,constructor属性指向构造函数。
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。

1
2
3
4
5
6
7
8
9
10
function Person (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
alert(this.name);
}
}

var person1 = new Person('Micheal', 18, 'FE');

原型模式

我们创建的每个函数都有一个prototype(原型)属性。这个属性是一个指针,指向一个对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
isPrototypeOf()、getPrototypeOf()和hasOwnProperty()方法(mdn-isPrototypeOfmdn-getPrototypeOfmdn-hasOwnProperty)。
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person () {}
Person.prototype.sayName = function () {
alert(this.name);
};
Person.prototype.name = 'Micheal';
Person.prototype.age = 18;
Person.prototype.job = 'FE';

var person1 = new Person();
person1.sayName(); // 'Micheal'
person1.sayName = function () {
alert('name:' + this.name);
};
person1.sayName(); // 'name:Micheal'
console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true

in操作符:in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。

1
2
// ...
console.log('name' in person1); // true

要取得对象上所有可枚举的实例属性,可以使用ES5的Object.keys()方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。数组的顺序也是它们在for-in循环中出现的顺序。

1
2
// ...
console.log(Object.keys(person1)); //["name", "age", "job", "sayName"]

要取得对象的所有实例属性,无论它是否可枚举,可以使用Object.getOwnPropertyNames()方法

1
2
// ...
console.log(Object.getOwnPropertyNames(Person.prototype)); //["constructor", "sayName"]

原型的动态性:由于原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来。如

1
2
3
4
5
6
var firend = new Person();
Person.prototype.sayHi = function () {
alert('hi');
};

firend.sayHi(); // 'hi'

如果是重写整个原型对象,那么情况就不一样了,因为把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。如

1
2
3
4
5
6
7
8
var firend = new Person();
Person.prototype = {
sayHi: function () {
alert('hi');
}
};

firend.sayHi(); // error

原型模式的重要性不仅体现在创建自定义类型方法,就连所有原生的引用类型,都是采用这种模式创建的。通过原生对象的类型,不仅可以取得所有默认方法的引用,也可以重新定义新方法。如

1
2
3
4
5
6
String.prototype.startsWith = function () {
return this.indexOf(text) == 0
};

var msg = 'Hello world';
console.log(msg.startsWith('Hello')); // true

组合使用构造函数模式和原型模式(使用最广泛)

1
2
3
4
5
6
7
8
9
10
11
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
}
Person.prototype = {
constructor: Person,
sayName: function () {
alert(this.name);
}
}

动态原型模式

它把所有信息都封装在了构造函数中

1
2
3
4
5
6
7
8
9
10
11
function Person(name, age, job) {
this.name = name;
this.job = job;
this.age = age;

if (typeof this.sayName !== 'function') {
Person.prototype.sayName = function () {
alert(this.name);
}
}
}

寄生构造函数模式

基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后返回新创建的对象。

1
2
3
4
5
6
7
8
9
10
11
12
function Person(name, age, job) {
var o = {};
o.name = name;
o.age = age;
o.job = job;
o.sayName = function () {
alert(this.name)
};
return o;
}

var firend = new Person('Micheal', 18, 'FE')

除了使用new操作符并把使用的包装函数叫做构造函数外,这个模式跟工厂模式其实是一模一样的。

稳妥构造函数模式

稳妥对象指的是没用公共属性,而且其方法也不引用this对象。

1
2
3
4
5
6
7
8
9
10
function Person (name) {
var o = {};
o.sayName = function () {
alert(name)
};

return o
}

var firend = Person('Micheal');

继承

原型链

JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。

利用原型让一个引用类型继承另一个引用类型的属性和方法。
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。假如让原型对象等于另一个类型的实例,此时原型对象将包含一个指向另一个原型的指针;相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}

存在问题:

  • 引用类型值的原型会被所有实例共享

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function SuperType() {
    this.colors = ['red', 'bule', 'green'];
    }

    function SubType() {}
    SubType.prototype = new SuperType();

    var e1 = new SubType();
    e1.colors.push('black');
    console.log(e1.colors); // ["red", "bule", "green", "black"]

    var e2 = new SubType();
    console.log(e2.colors); // ["red", "bule", "green", "black"]
  • 创建子类型的实例时,不能向超类型的构造函数中传递参数。

借用构造函数

在子类型构造函数内部调用超类型构造函数。通过apply()和call()方法可以在新创建的对象上执行构造函数。如

1
2
3
4
5
6
7
8
9
10
11
12
13
function superType() {
this.colors = ['red', 'blue', 'green']
}
function subType () {
SuperType.call(this);
}

var e1 = new SubType();
e1.colors.push('black');
console.log(e1.colors); // ["red", "bule", "green", "black"]

var e2 = new SubType();
console.log(e2.colors); // ["red", "bule", "green"]

(call、apply以及bind方法不过多介绍,可参考callapplybind

组合继承(最广泛)

结合原型链和借用构造函数,使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
alert(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 () {
alert(this.age)
}

原型链式继承

借助原型可以基于已有的对象创建新对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function object(o) {
function F() {}
F.prototype = o;
return new F();
}

var person = {
name: 'Micheal',
firends: ['Shelby', 'Van']
};

var anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.firends.push('Rob');

ES5中Object.create()方法(mdn-Object.create)规范了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和一个为新对象定义额外属性的对象(可选)。

寄生式继承

寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。

1
2
3
4
5
6
7
function createAnother(original) {
var clone = object(original);
clone.sayHi = function () {
alert('hi');
}
return clone
}

寄生组合继承(最理想)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function inheritPropertype(subType, superType) {
var prototype = object(superType, subType);
prototype.constructor = subType;
subType.prototype = prototype;
}

function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SueprType.prototype.sayName = function () {
alert(this.name);
}

function SubType(name, age) {
SuperType.call(this, name);

this.age = age;
}

inheritPropertype(SubType, SuperType);

SubType.prototype.sayAge = function () {
alert(this.age);
}

解决了组合继承无论什么情况下都会调用两次超类型构造函数的问题。是最理想的继承范式。

温习:

  • 对象的数据属性和访问器属性;
  • Object.defineProperty()和Object.defineProperties()方法;
  • 工厂模式、构造函数模式、原型模式、组合使用构造函数模式和原型模式、动态原型模式、寄生构造函数模式、稳妥构造函数模式;
  • 原型链继承、借用构造函数继承、组合继承、原型链式继承、寄生式继承、寄生组合继承

(完)