读书笔记:ES5-面向对象

所有方法

  • Object.defineProperty()
  • Object.definePropertise()
  • Object.getOwnPropertyDescriptor()
  • Object.getPrototypeOf()
  • instanceof
  • isPrototypeOf()
  • hasOwnProperty()
  • in
  • for-in
  • Object.keys()
  • Object.getOwnPropertyNames()
  • Object.create()

理解对象

属性类型

ES有两种属性:数据属性/访问器属性
默认对象字面量设置的属性都是数据属性,访问器属性需要defineProperty()/definePropertise()方法设置

数据属性:包含一个数据值的位置,此位置可以读取和写入
定义行为的特征值有以下:
[[Configurable]] 是否能通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否修改为访问器属性。开发人员直接在对象定义的属性该特性默认值是true
[[Enumerable]] 能否通过for-in返回。…默认值是true
[[Writable]] 能否修改属性的值。…默认值是true
[[Value]] 包含属性的数据值,读取和修改就在此位置。…默认值是undefined

修改属性特性方法: Object.defineProperty(),参数一对象,参数二属性名,参数三特性对象(可设置一或多个值)
⚠️IE9+
⚠️特性对象中不指定的特性默认是false/undefined

访问器属性:包含一对getter()/setter()函数,不包含数据值
定义行为的特征值有以下:
[[Configurable]] 是否能通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否修改为数据属性。开发人员直接在对象定义的属性该特性默认值是true
[[Enumerable]] 能否通过for-in返回。…默认值是true
[[get]] 读取属性时调用的函数,默认值undefined
[[set]] 写入属性时调用的函数,默认值undefined

利用Object.defineProperty()设置特性,不设置get不能读,不设置set不能写:

一次性定义多个属性:Object.definePropertise() 参数一对象,参数二(属性:特性对象)格式的对象
⚠️IE9+
⚠️可以针对任何对象包括DOM/BOM使用此方法

读取属性特性:Object.getOwnPropertyDescriptor() 参数一对象,参数二属性名,返回特性对象,仅是实例的非原型的属性描述,想返回原型上的属性描述则直接在原型上调用
⚠️IE9+

创建对象

Object构造函数或者对象字面量创建单个对象缺点:同一个接口创建很多对象,产生大量的重复代码

工厂模式

1
2
3
4
5
6
7
8
9
10
11
function createPerson(name,age){
var o = new Object();

o.name = name;
o.age = age;
o.sayName = function(){
console.log(this.name);
}

return o;
}

解决:封装函数,创建多个相似对象
问题:对象识别问题-怎样知道对象类型

构造函数模式

1
2
3
4
5
6
7
function CreatePerson(name,age){
this.name = name;
this.age = age;
this.sayName = function(){
console.log(this.name);
}
}

⚠️构造函数规定大写字母开头,为了区别于其他函数,构造函数本身也是函数,只不过是用途于创建对象
⚠️new调用构造函数过程:
1.创建一个新对象
2.构造函数作用域指向新对象
3.执行构造函数中代码
4.返回新对象
⚠️不存在构造函数特殊语法,通过new调用就是构造函数,不通过new调用就是普通函数

解决:实例标识为一种特定类型,能够instanceof判断具体对象类型
问题:每个实例上都实例化一遍函数对象,创建多个同样功能的函数没有必要,也不必要耦合函数方法和对象
(在构造函数外即全局作用域下写函数方法,然后在构造函数中定义新对象方法时指向全局函数,并不是好方法,虽然解决了共享同一个方法,但是全局函数仅被某个对象使用导致全局作用域名副其实、同时会污染全局环境、也失去了自定义的引用类型的封装性)

原型模式

理解原型

过程:
创建一个新函数,根据一组特定规则为该函数创建属性prototype,属性是指向原型对象的指针
原型对象自动获取constructor(构造函数)属性,是指向新函数的指针,其他方法均继承自Object
创建新实例,实例将包含[[prototype]]内部属性指针,指向原型,使其与原型关联(并非与构造函数关联)

1
2
3
4
5
6
7
8
9
10
11
function Person(){}

Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'software Engineer';
Person.prototype.sayName = function(){
console.log(this.name);
}

var person1 = new Person();
var person2 = new Person();

Object.getPrototypeOf() 获取实例的原型,IE9+

读取对象属性过程:
搜索实例本身 => 搜索原型对象

⚠️原型最初只包含constructor,这个属性也是被实例共享的,所以实例可以person1.constructor访问到构造函数,即进行了两次搜索在原型中找到该属性

⚠️可以通过实例访问到原型中的值,但不能通过实例更改原型中的值,即实例属性会屏蔽原型中的属性访问(这也是上面提到的访问属性过程中,第一次搜索实例本身就有则不再继续搜索)

各种获取属性方法

in操作符: for-in循环/单独使用
-单独使用: 属性在对象中返回true包括原型中存在

⚠️可以通过hasOwnProperty()方法和in操作符可以确认属性存在实例中还是原型中

1
2
3
function hasPrototypeProperty(object,name){
return !object.hasOwnProperty(name) && name in object
}

-for-in循环: 返回所有可枚举属性数组,包括原型中属性

其他相关方法:Object.keys()返回实例的所有可枚举属性;Object.getOwnPropertyNames返回实例中所有属性数组(无论是否可枚举)

更简单的原型语法/原型动态性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(){}

<!-- 视觉的封装性 -->
Person.prototype = {
name: 'Nicholas',
age: 29,
job:'software Engineer',
sayName: function(){
console.log(this.name);
}
}

var person1 = new Person();
var person2 = new Person();

⚠️上述写法其实是重写了Person原型,新原型是Object字面量对象实例,此实例自动获取的constructor(其实是实例的原型自动获取的属性)指向Object并非Person,但并不影响instanceof返回值,如果constructor很重要可以重写原型时重新定义(暂时不知道有何用处),但是字面量定义会导致其可枚举,可以通过defineProperty方法定义不可枚举属性

⚠️在原型中查找值是一次性的搜索,所以先创建实例,后改动原型也会在实例中反应,但是重写原型会导致原创建实例与新原型无关联,原创建实例的[[prototype]]依然指向的是原来自动生成的原型,注意重写原型要在创建实例之前

⚠️原生引用类型也是采用原型模式创建的,都在构造函数的原型上定义了方法,但是不推荐修改原生对象原型!!!

解决:所有实例通过原型共享属性和方法
问题:由于原型属性被共享的特性,对于引用类型比如数组,一个实例push修改了数组,另个实例上也能反应出来(重新在实例上创建属性不存在这种问题,因为可以屏蔽原型属性)

组合使用构造函数模式和原型模式

构造函数用于定义实例属性、原型模式用于定义方法和共享的属性,最大限度节省内存
还可以向构造函数传递参数
是使用最广泛、认同度最高的一种创建自定义类型的方法-默认模式

1
2
3
4
5
6
7
8
9
10
11
12
function Person(name, age){
this.name = name;
this.age = age;
this.friends = ['xiaohei'];
}

Person.prototype = {
constructor: Person,
sayName : function(){
console.log(this.name)
}
}

动态原型模式

所有信息都放在构造函数中,仅在必要情况下,才在构造函数中初始化原型,同时保留了构造函数和原型的优点

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;

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

寄生构造函数模式

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 除了要使用new操作符并叫其构造函数外,和工厂模式没有差别 -->
function Person(name,age){
var o = new Object();

o.name = name;
o.age = age;
o.sayName = function(){
console.log(this.name);
};

<!-- 不写返回值的情况下构造函数回自动返回新对象实例,但这里重写了返回值 -->
return o;
}

一般用来应对特殊情况,比如生成包含额外方法的数组

⚠️能使用其他模式的情况下不要使用这种模式,不能通过instanceof确定对象类型

稳妥构造函数模式

稳妥对象:没有公共属性、不引用this。适合在安全环境或者防止数据被其他应用程序改动时使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name,age){
var o = new Object();
<!-- 可以在此定义私有变量和函数 -->

<!-- 和寄生模式相比差距一添加实例方法不用this -->
o.sayName = function(){
console.log(name);
};

return o;
}

<!-- 差距二不使用new调用构造函数 -->
var person1 = Person('xiaohei',25)

⚠️这种方式,除了使用实例的sayName方法外没有别的途径能访问name属性、即便能为对象添加其他属性,也不能更改原始属性数据
以上其实是闭包原理

继承

oo语言支持两种继承方式:接口继承-继承方法的签名;实现继承-继承实际的方法
ES只支持实现继承,主要依靠原型链实现

原型链

基本模式

引用类型A继承引用类型B,重写A的原型是B的实例,作为B的实例,A的原型指向B的原型,如此可以层层递进,构成了实例与原型的链条

基本模式:

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


⚠️重写的原型没有constructor指向SubType构造函数,而作为SuperType的实例能访问到SuperType原型的constructor指向SuperType构造函数
⚠️重写的原型作为SuperType的实例,存在[[prototype]]内部属性指向SuperType原型
⚠️所有引用类型都继承Object,也是通过原型链实现的,所以SuperType原型是作为Object的实例存在的,存在[[prototype]]内部属性指向Object原型
⚠️继承的基础上添加自己的方法一定要在重写原型之后,添加的方式不能是字面量方式,那是又一次重写原型了!!!

确定原型与实例关系

方法一: instanceof
通过与原型链中出现的构造函数关系确定

方法二:isPrototypeOf
通过与原型链中出现的原型关系确定

原型链问题

问题一:引用类型副作用

问题二:无法在不影响所有对象实例的情况下,向超类型构造函数传递参数

借用构造函数(经典继承/伪造对象)

在子类型构造函数中调用超类型构造函数(利用callapply当作普通函数调用)

基本模式
1
2
3
4
5
6
7
8
9
10
11
function SuperType(name){
this.name = name;
}

function SubType(){
<!-- 继承了SuperType -->
<!-- 当作普通函数来调用,其实是在为SubType实例创建自己的属性而已 -->
<!-- 也能传递参数 -->
SuperType.call(this,'xiaohei');
this.age = 25;
}
问题

依然是构造函数模式的问题,函数复用无从谈起

组合继承(伪经典继承)

原型链实现对原型属性和方法的继承,借用构造函数实现对实例属性的继承,保证了既有函数复用又有实例自己的属性
最常用的继承模式

基本模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function SuperType(name){
this.name = name;
this.colors = ['grey','pink'];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
};

function SubType(name,age){
<!-- 2但是在这里为实例加入了name和colors属性,屏蔽了原型中同名属性的访问 -->
SuperType.call(this,name);
this.age = age;
}
<!-- 1其实这里执行new SuperType()为SubType原型加入了name和colors属性 -->
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
};

原型式继承

借助原型,基于已有对象创建新对象,同时不必创建自定义类型

基本模式
1
2
3
4
5
6
7
8
9
10
11
<!-- 称之为对o进行了一次浅复制 -->
function object(o){
<!-- 临时F构造函数 -->
function F(){}

<!-- 重写F构造函数原型 -->
F.prototype = o;

<!-- 返回F实例,实例的[[prototype]]指向o -->
return new F();
}

⚠️通过这种方式传入相同的对象而创建的对象实例原型指向同一个,共享原型方法和属性,所以就像原型模式一样存在引用类型副作用

Object.create

ES5新增Object.create()方法规范了原型式继承,参数一:原型对象,参数二(可选):新对象实例额外属性的对象(同Object.definePropertise()第二个参数形式一样)
IE9+

⚠️在不需要兴师动众创建构造函数,只想让一个对象与另一个对象保持类似的情况下,可以胜任

寄生式继承

与原型模式紧密相关的一种思路
和寄生构造函数和工厂模式类似,仅用于封装继承过程,增强对象

基本模式
1
2
3
4
5
6
7
8
function createAnother(original){
<!-- 任何返回新对象的函数都可以,这里用了原型式继承 -->
var clone = object(original);
clone.sayHi = function(){
console.log('hi');
};
return clone;
}

⚠️这种方式增强对象方法,与构造函数函数模式类似的点是函数不会得到复用
⚠️主要考虑对象,而不是自定义类型和构造函数的情况下,此方式是有用的模式

寄生组合式继承

最理想的继承范式
此方法为解决组合继承的问题(调用两次超类型构造函数,第一次重写子类型原型时使其作为超类型的实例具有超类型的属性,第二次创建子类型实例时又使实例重写了原型属性)

使用借用构造函数继承属性,原型链的混成模式继承方法,基本思路是重写子类型原型时不调用超类型构造函数,实际需要的是超类型原型的副本,可以使用寄生式继承来继承超类型原型,然后将结果指定给子类型的原型

基本模式
1
2
3
4
5
6
7
8
9
function inheritPrototype(subType,superType){
<!-- 创建原型是superType.prototype的prototype对象,即是前面原型式继承所说的浅复制 -->
var prototype = Object(superType.prototype);

prototype.constructor = subType;

<!-- 结果指定给子类型的原型,使其子类型的原型指向超类型的原型,维持了原型链 -->
subType.prototype = prototype;
}

利用上面方法替换调用超类型构造函数重写子类型原型即可

⚠️此方法的高效在于只调用一次超类型构造函数,避免在子类型原型上创建不必要的、多余的属性