『Week NO.3』
对象的每个属性和方法都有一个名字,而每个名字都映射到一个值。将js的对象想象成散列表,则其就是一组名值对,其中值可以是数据和函数。
每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型也可以是自定义的类型。
6.1理解对象
6.1.1 属性类型
(表示特性是内部值用两个方括号如:[[name]])
数据属性和访问器属性
a. 数据属性包含一个数据值的位置。4个特性:
[[Configurable]]:表示能够通过delete删除属性从而重新定义新属性,能否修改属性的特性,能否修改属性为访问器属性。直接在对 象上定义的属性,默认值为true。
[[Enumerable]]:表示能够通过for...in循环返回属性,默认值为true
[[Writable]]:表示能够修改属性的值,默认值为true
[[Value]]:包含这个属性的数据值 默认值为undefined
要修改属性默认的特性,必须使用Object.defineProperty()方法(由于实现不彻底,建议不再ie8中使用)。此方法接收三个参数:属性所在的对象 属性的名字 和一个描述符对象。描述符对象即为上面所说4个特性中的一个或多个。
一旦把属性定义为不可配置的,不能再把它变回可配置的了
在使用Object.defineProperty()方法中,如果不指定Configurable,Enumerable,Writable(注意是在该方法中)默认值都为false。
b. 访问器属性
访问器属性不包含数据值。4个特性:
[[Configurable]]:表示能够通过delete删除属性从而重新定义新属性,能否修改属性的特性,能否修改属性为访问器属性。直接在对象上定义的属性,默认值为true。
[[Enumerable]]:表示能够通过for...in循环返回属性,默认值为true
[[get]]:读取属性时调用函数,默认值为undefined
[[set]]:写入属性时调用函数,默认值为undefined
访问器属性不能直接定义,必须使用Object.defineProperty()方法来定义。
下划线是一种常用标记。用于表示只能通过对象方法访问的属性 _name
6.1.2 定义多个属性
了解Object.defineProperties()方法
6.1.2 定义多个属性
了解Object.getOwnPropertyDescriptor()方法
6.2创建对象
object构造函数或对象字面量都可以用来创建单个对象,但使用同一个接口创建多个对象变会产生大量重复代码,所以引进工程模式的一种变体。
6.2.1工厂模式
函数封装以特定接口创建对象的细节,工厂模式没有解决对象识别的问题于是又有了新模式。
6.2.2 构造函数模式
* 构造函数始终都应该以一个大写字母开头
* 要创建新实例,必须使用new操作符。
调用构造函数经历4个步骤
1.创建一个新对象
2.将构造函数的作用域赋给新对象(因为this就指向了这个新对象)
3.执行构造函数中的代码(为这个新对象添加属性)
4.返回新对象
将构造函数当做函数,与其他函数不同在于调用它们的方式不同。
任何函数,只要通过new操作符来调用,那它就可以作为构造函数。不用new操作符调用,那跟普通函数没啥两样。
构造函数的问题
构造函数方法很好用,但是存在一个浪费内存的问题。每个方法都要在每个实例上重新创建一遍,在js中函数就是对象,因此定义一个函数,也就是实例化了一个对象。
6.2.3原型模式
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。 如下:
function Person(){
}
//将属性方法直接添加到Person的prototype属性中,构造函数为空函数。
Person.prototype.name = "meiyzeng";
Person.prototype.sayName = function(){
//
}
var person1 = new Person();
person1.sayName(); // "meiyzeng"
var person2 = new Person();
person2.sayName(); // "meiyzeng"
新对象有相同的属性和方法,也访问同一组属性和函数。
1.理解原型对象
只要创建一个新函数 就会根据一组特定的规则创建一个prototype属性,这个属性指向函数的原型对象。而在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。见图解:
代码读取某个属性时,执行搜索,如果对象实例本身没有找到,则继续搜索指针所指向原型对象 上面person1和person2正是多个对象实例共享原型所保存的属性和方法的基本原理。
虽然/*可以*/通过对象实例/*访问*/保存在原型中的值,但却/*不能*/通过桂香实例/*重写*/原型中的值。如果在实例中添加了一个属性与实例原型中的一个属性同名,该属性则会屏蔽原型中的那个属性。即添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。使用delete操作符则可以完全删除实例属性,从而能够重新访问原型中的属性。
通过hasOwnProperty()方法可以检测属性是存在实例中还是原型中,在实例中返回true。 //person1.hasOwnProperty("name")
2.原型与in操作符
有两种方式使用in操作符:单独使用和在for...in循环中使用.
单独使用时,in操作符会在通过对象能够访问给定属性时返回true。/*无论属性存在实例还是原型中*/。
在使用for...in(ie9+)循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性。要取得对象上所有可枚举的实例属性,可以使用Object.keys()方法。可以使用Object.getOwnProperty()方法返回所有实例属性。(constructor属性就是不可枚举.)
3.更简单的原型语法
在前面原型方法中添加一个属性和方法就要写一遍 Person.prototype,为了减少代码。可使用一个包含所有属性和方法的对象字面量来重写原型对象 如
function Person(){
}
Person.prototype = { //constructor属性不再指向Person函数 指向Object构造函数?
constructor:Person //如果constructor值很重要可以特意设置 但会是属性不可枚举变成可枚举
name : "123"
age : "11"
sayName : function(0{
//
}
}
4.原型的动态性
由于在原型中查找值是一次搜索所以我们在原型对象所做的的修改都能该从实例反映出来。即使是先创建了实例后修改原型也是如此。其原因可归结为实例与原型之间的松散联系关系。
重写原型对象会切断构造函数与最初原型之间的联系 实例的指针指向原型,而不指向构造函数
5.原生对象的原型 (不推荐修改)
6.原型对象的问题
如省略了构造函数传递初始化参数环节,默认情况都取得相同属性值。
6.2.4组合使用构造函数模式和原型模式
6.2.5 动态原型模式
6.2.6 寄生构造函数模式
这种模式的思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;返回的对象与构造函数或构造函数的原型属性之间没有关系,与在构造函数的对象没有区别。建议在可以使用其他模式的情况下,不使用这模式。
6.2.7 稳妥构造函数模式
所谓稳妥对象 指的是没有公共属性,而且其方法也不引用this对象。适用于安全的环境中(这些环境中会禁止使用this和new),或者防止数据被其他应用程序改动时使用。
6.3继承
接口继承和实现继承
6.3.1 原型链
基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
回顾下:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例中都包含一个指向原型对象的内部指针。
假如:让原型对象等于另一个类型的实例,此时原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中包含着一个指向另一个构造函数的指针.假如另一个原型又是一个实例,这样便层层递进构成了链条。**
1.别忘记默认的原型,我们知道所有类型默认都继承了object而这个继承也是通过原型链实现的。所有函数默认原型都是object实例,因此默认原型都会包含一个内部指针指向object.prototype,这也是所有类型会继承tostring()、valueof()等默认方法的根本原因。上图的实质如下:
2.确定原型和实例的关系
使用instanceof操作符(或者使用isprototypeof()方法),测试实例与原型链中出现过的构造函数,结果会返回true。
3.谨慎地定义方法
给原型添加方法的代码一定放在替换原型的语句之后 顺序和不能使用对象字面量创建原型方法。
4.原型链问题 包含引用类型所带来的问题实践中很少单独使用
6.3.2 借用构造函数
即在子类型构造函数的内部调用超类型构造函数。 函数只不过是在特定环境中执行代码的对象,因此通过使用applay()和call()方法也可以在(将来)新创建的对象上执行构造函数,
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}
function SubType() {
SuperType.call(this); //调用SuperType()构造函数,并将当前this传入
}
var st1 = new SubType();
st1.colors.push('black');
alert(st1.colors); //'red,blue,green,black'
var st2 = new SubType();
alert(st2.colors); //'red,blue,green'
代码中SubType()构造函数中“借调”了超类型的构造函数。 通过使用call()方法,我们实际上是在(未来将要)新创建的SubType实例的环境下调用了SuperType构造函数。 这样一来,就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。 结果,SubType的而每个实例都会具有自己的colors属性副本了。
1、传递参数
相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。
function SuperType(name) {
this.name = name;
}
function SubType() {
//继承了SuperType,同时传递了参数
SuperType.call(this,'nico');
this.age = 22;
}
var st = new SubType();
alert(st.name); //'nico'
alert(st.age); //22为了确保SuperType构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。
2、借用构造函数的问题
无法避免的构造函数模式存在的问题—方法都在函数中定义,因此函数复用就无从谈起了。 而且,在超类型的原型中定义的方法,对子类型而言也是不可见的。 结果所有类型都只能使用构造函数模式。 考虑到这些问题,借用构造函数技术也是很少单独使用的。
a.组合继承 组合继承,有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。 其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。 这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性。
b.原型式继承 可以在不先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制。而复制得到的副本可以得到进一步改造
c.寄生式继承
d.寄生组合式继承 集寄生式继承和组合继承的优点与一身,是实现基于类型继承最有效的方式。 开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。