今天的2018年12月31日,是2018年的最后一天,明天就是元旦的了,时间过得很快,2018年年初的自己定下的目标,看了几本书,还有各式各样的计划,都完成了多少了呢?趁着元旦放假的这几天,看了一下《JavaScript高级程序设计 第三版》,书中的内容比较的基础,同时也很实用的,边看边总结,感觉这样的看书的效果比较的使用。好了,让我们看看第6章 面向对象的程序设计有哪些内容的吧。
本章内容
- 理解对象的属性
- 理解并创建对象
- 理解继承
现在我们来看第一小结的内容,就是理解对象,到底什么是对象呢?
在ECMA-262把对象定义为:"无序属性的集合,其属性可以包含基本值、对象和函数。"严格来讲,这就相当于说对象是一组没有特定顺序的值,对象的每个属性和方法都有一个名字,而每个名字都映射到一个值。正因为这样(以及其他将要讨论的原因),我们可以把ECMAScript的对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。
6.1 理解对象
- 6.1.1 属性类型
- 6.1.2 定义多个属性
- 6.1.3 读取属性的特征
6.1.1 属性类型
ECMAScript中有两种属性:数据属性和访问器属性
1. 数据属性
数据属性包含一个数据值的位置,在这个位置可以读取和写入值。数据属性有四个描述其行为的特征。
-
Configurable
:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特征,或者能否把属性修改为访问器属性,默认值为true -
Enumerable
: 表示能否通过for-in循环返回属性,默认值为true -
Writable
:表示能否修改属性的值,默认值为true -
Value
:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。默认值为undefined
例子,直接在对象上定义的属性,如果不更改的话,那么他们的Configurable
、Enumerable
、Writable
都被设置为true,也就是默认值,而Value
特征被设置为指定的值。例如:
var person = {
name: 'Nicholas'
};
复制代码
这里创建一个名为
name
的属性,为它指定的值是Nicholas
也就是说,Value
特征将被设置为"Nicholas",而对这个值的任何修改都将反应在这个位置。
要修改属性默认的特征,必须使用ECMAScript5
的Object.defineProperty()
方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurable
、enumerable
、writable
和value
。设置其中的一或多个值,可以修改对应的特征值。例如:
var person = {};
Object.defineProperty(person,"name", {
writable: false, // 不可修改
value: "Nicholas" // name属性设置的值
});
alert(person.name); // "Nicholas"
person.name = "Greg";
alert(person.name);
复制代码
这个例子创建一个名为name
的属性,它的值"Nicholas"是只读的。这个属性的值是不可修改的,如果尝试为它指定新值,则在非严格模式下,赋值操作将被忽略;在严格模式下,赋值操作将会导致抛出错误。
类似的规则也适用于不可配置的属性。例如:
var person = {};
Object.defineProperty(person,"name", {
configurable: false,
value: "Nicholas"
});
alert(person.name); // "Nicholas"
delete person.name;
alert(person.name); // "Nicholas"
复制代码
把configurable
设置成false
,表示不能从对象中删除属性,如果对这个属性调用delete
,在非严格模式下,什么也不会发生,而在严格模式下会导致错误。而已,一旦把属性定义为不可配置的,就不能再把它变为可配置了。此时,再调用Object.defineProperty()
方法修改除writable
之外的特征,都会导致错误。
var person = {};
Object.defineProperty(person,'name',{
configurable: false,
value: "Nicholas"
});
// 抛出错误
Object.defineProperty(person,"name",{
configurable: true,
value: "Nicholas"
});
复制代码
也就是说,可以多次调用Object.defineProperty()
方法修改同一个属性,但在把configurable
设置为false
之后就会有限制了。
在调用Object.defineProperty()
方法创建一个新的属性时,如果不指定configurable
、Enumerable
、Writable
特征的默认值都是false
,如果调用Object.defineProperty()
是修改已定义的属性的特征,则无此限制。多数情况下,可能都没有必要利用Object.defineProperty()
方法提供的高级特征。不过,理解这些概念对理解JavaScript
对象却非常有用。
2.访问器属性
访问器属性不能直接修改,必须使用Object.defineProerty()
来定义。请看下面的例子。
var book = {
_year: 2018,
edition: 1
};
Object.defineProperty(book,"year", {
get: function() {
return this._year
},
set: function(newValue) {
if(newValue > 2018) {
this._year = newValue;
this.edition += newValue - 2018;
}
}
});
book.year = 2019;
alert(book.edition); // 2
复制代码
以上代码创建一个book
对象,并给它定义两个默认的属性: _year
和edition
。_year
前面的下划线是一种常用的记号,用于表示只能通过对象方法访问的属性。而访问器属性year
则包含一个getter
函数和一个setter
函数。getter
函数返回_year
的值,setter
函数通过计算来确定正确的版本。因此,把year
属性修改为2019会导致_year
变成2019,而edition
变为2。这是使用访问器属性常见方式,即设置一个属性的值会导致其他属性发生变化。
不一定非要同时指定getter
和setter
。只指定getter
意味着属性时不能写,尝试写入属性会被忽略。在严格模式下,尝试写入只指定了getter
函数的属性会抛出错误。类型地,只指定setter
函数的属性也不能读,否则在严格模式下会返回undefined
,而在严格模式下会抛出错误。
6.1.2 定义多个属性
现在我们来看一下例子:var book = {};
Object.defineProperties(book,{
_year: {
writable: true,
value: 2004
},
edition: {
writable: true,
value: 1
},
year: {
get: function() {
return this._year
},
set: function(newValue) {
if(newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
复制代码
以上代码在book
对象上定义了两个数据属性(_year和edition)和一个访问器属性(year)。最终的对象与上一节中定义的对象相同。唯一的区别是这里的属性都是在用一个时间创建的。
支持Object.defineProperties()
方法的浏览器有IE9+、Firefox4+、Safari5+、Opera12+和Chrome。
6.1.3读物属性的特征
我们看一下例子:var book = {}; // 定义一个空的对象
Object.defineProperties(book,{
_year: {
value: 2004
},
edition: {
value: 1
},
year: {
get: function() {
return this._year;
},
set:function(newValue) {
if(newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
var descriptor = Object.getOwnPropertyDescriptor(book,"_year");
alert(descriptor.value); // 2004
alert(descriptor.configurable); // false
alert(typeof descriptor.get); // "undefined"
var descriptor = Object.getOwnPropertyDescriptor(book,"year");
alert(descriptor.value); // undefined
alert(descriptor.enumerable); // false
alert(typeof descriptor.get); // "function"
复制代码
对于数据属性_year
,value
等于最初的值,configurable
是false
,而get
等于undefined
,对于访问器属性year
,value
等于undefined
,enumerable
是false
,而get
是一个指向getter
函数的一个指针。
在JavaScript中,可以针对任何对象-包括DOM和BOM对象,使用
Object。getOwnProperty-Descriptor()
方法。支持这个方法的浏览器有IE9+、Firefox4+、Safari5+、Opera12+和Chrome。
6.2 创建对象
6.2.1 工厂模式
这种模式抽象了创建具体对象的过程。考虑到在ECMAScript
中无法创建类,开发人员就发明了一种函数,用函数来封装以特点接口创建对象的细节,如下面的例子所示。
function createPerson(name,age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
alert(this.name);
};
return o;
}
var person1 = createPerson("Nicholas","29","sofaware Engineer");
var person2 = createPerson("Greg",27,"Doctor");
复制代码
工厂模式的特点就是把相同的属性放在一个函数的里面,然后通过接口的形式进行调用的,这样可以不用书写那么多的代码,可以去掉很多冗余的代码,而已也有利于后期的代码的维护,仅本人观点。
6.2.2 构造函数模式
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
alert(this.name);
};
}
var person1 = new Person("Nicholas",29,"software Engineer");
var person2 = new Person("Greg",27,"Doctor");
复制代码
这里需要注意:
- 构造函数始终都应该以一个大写字母开头,而非构造函数则以一个小写字母开头
要创建Person
的实例,必须使用new操作符。以这种方式调用构造函数实例上会经历以下4个步骤:
- 创建一个对象
- 将构造函数的作用域付给新对象(因此this就指向了这个新对象)
- 执行构造函数中的代码
- 返回新的对象
在前面例子的最后
person1
和person2
分别保存着Person
的一个不同的实例。这两个对象都有一个constructor
(构造函数)属性,该属性指向Person
,如下所示。
alert(person1.constructor == Person); // true
alert(person2.constructor == Person); // true
复制代码
对象的constructor
属性最初是用来标识对象类型的。但是,提到检测对象类型,还是instanceof
操作符要可靠一些。我们在这个例子中创建的所有对象即是Object
的实例同时也是Person
的实例,这一点通过instanceof
操作符可以得到验证。
alert(person1 instanceof Object); // true
alert(person1 instanceof Person); // true
alert(person2 instanceof Object); // true
alert(person2 instanceof Person); // true
复制代码
6.2.3 原型模式
我们创建的每一个函数都有一个prototype
(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途就是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么prototype
就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,如下面的例子所示。
function Person() {
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.sayName = function() {
alert(this.name);
};
var person1 = new Person();
person1.sayName(); // "Nicholas"
var person2 = new Person();
person2.sayName(); // "Nicholas"
alert(person1.sayName == person2.sayName); // true
复制代码
在此,我们将sayName()
方法和所有属性直接添加到了Person
的prototype
属性中,构造函数变成了空的函数。即使如此,也仍然可以通过调用构造函数来创建新对象,而已新对象还会具有相同的属性和方法,但与构造函数不同的是,新对象的这些属性和方法是由所有实例共享的,换句话说,person1
和person2
访问的都是同一个属性和同一个sayName()
函数。要理解原型模式的工作原理,必须先理解ECMAScript
中原型对象的性质。
6.2.4 组合使用构造函数模式和原型模式
创建自定义原型类型的最常见的方法,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时有共享这对方法的引用,最大限度地节省了内存,另外,这种混成模式还支持向构造函数传递参数;可谓是技两种模式之长。下面的代码重写了前面的例子。
<!---->这里的构造函数是定义实例属性
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);
}
}
var person1 = new Person("Nicholas",29,"software Engineer");
var person2 = new Person("Greg",27,"Doctor");
person1.friends.push("Van");
alert(person1.friends); // "shelby,count,Van"
alert(person2.friends); // "Shelby,count"
alert(person1.friends === person2.friends); // false
alert(person1.sayName === person2.sayName); // true
复制代码
在这个例子中,实例属性都是构造函数中定义的,而由所有实例共享的属性constructor
和方法sayName()
则是在原型中定义的。而修改了person1.friends
(向其中添加一个新的字符串),并不会影响到person2.friends
,因为他们分别引用不同的数组。
这种构造函数和原型混成的模式,是目前在ECMAScript
中使用最广泛,认可度最高的一种创建自定义类型的方法。可以说,这种用来定义引用类型的一种默认的模式。
6.2.5 动态原型模式
动态原型模式,它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点,换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型,来看一个例子。
function Person(name, age, job) {
<!---->属性
this.name = name;
this,age = age;
this.job = job;
<!---->方法
if(typeof this.sayName != "function") {
Person.prototype.sayName = function() {
alert(this.name);
};
}
}
var friend = new Person("Nicholas", 29, "software Engineer");
friend.sayName();
复制代码
这里我们需要注意的一点就是,只有在sayName()
方法不存在的情况下,才会把它添加到原型中。这段代码只会在初次调用构造函数时才会执行,此后,原型已经完成初始化,不需要再做什么修改了。不过要记住这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法可以说是非常完美的。其中,if
语句检查的可以是初始化之后应该存在的任何属性或方法,不必要一大堆if
语句检查每个属性和方法;只要检查其中一个即可。
6.2.6 寄生构造函数模式
寄生构造函数模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象,但从表面上看,这个函数又很像是典型的构造函数。下面是一个例子。
function Person (name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
alert(this.name);
};
return o;
}
var friend = new Person("Nicholas", 29, "software Engineer");
friend.sayName(); // "Nicholas"
复制代码
在这个例子中,Person
函数创建一个新对象,并以相应的属性和方法初始化该对象,然后又返回该对象。除了使用new
操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的,构造函数在不返回值的情况下,默认会返回新对象的实例。而通过在构造函数的末尾添加一个return
语句,可以重写调用构造函数时返回的值。
function SpecialArray() {
<!---->创建数组
var values = new Array();
<!---->添加值
values.push.apply(values,arguments);
<!---->添加方法
values.toPinedString = function() {
return this.join("|");
}
<!----> 返回数组
return values;
}
var colors = new SpecialArray("red","blue","green");
alert(colors.toPinedString()); // "red|blue|green"
复制代码
在这个例子中,我们创建一个名叫specialArray
的构造函数。在这个函数内部,首先创建了一个数组,然后push
方法(用构造函数接收到的所有的参数)初始化了数组的值,随后,又给数组实例添加了一个toPinedString
,该方法返回以竖线分割的数组值。最后,将数组以函数值的形式返回。接着,我们调用了specialArray
构造函数,向其中传入了用于初始化数组的值,此后又调了toPinedString()
方法。
6.2.7 稳妥构造函数模式
所谓稳妥对象,指的是没有公共属性,而已其方法也不引用this
的对象。稳妥对象最适合在一些安全的环境中,或者在防止数据被其他应用程序改动时使用。稳妥构造函数遵循与寄生构造函数类型的模式,但有两点不同,一是新创建对象的实例对象不引用this
;而是不使用new
操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的Person
构造函数重写如下。
function Person(name, age, job) {
<!----> 创建要返回的对象
var o = new Object();
<!----> 可以在这里定义私有属性和方法
<!----> 添加方法
o.sayName = function() {
alert(name);
};
<!----> 返回对象
return o;
}
复制代码
注意,在以这种模式创建的对象中,除了使用sayName()
方法之外,没有其他办法访问name
的值,可以像下面使用稳妥的Person
的构造函数。
var friend = Person("Nicholas", 29, "software Engineer");
friend.sayName(); // "Nicholas"
复制代码
这样,变量friend
中保存的是一个稳妥对象,而除了调用sayName()
方法外,没有别的方式可以用于访问起数据成员。