JavaScript高程第六章:继承-理解与实践

JavaScript高程第六章:继承-理解与实践
昨日细细的读了一遍JavaScript高程,现在写篇文章来巩固下认知吧.

首先是从中读到了什么,我自己也在读书的时候用笔记下了各个部分的点,现在等于阅读笔记回忆下书本.

理解基础

ECMA-262(第五版)
ECMA中规定了两种属性:数据属性 and 访问器属性

数据属性

包含一个数据值的位置(读取和写入)
4个描述行为的特性

  • [[Configurable]] 默认值为true,描述了可否delete,可否修改其特性(变更为访问器属性)
  • [[Enumerable]] 默认值为true,描述了能否通过for-in循环返回属性.
  • [[Writable]] 默认为true,能否修改属性的值
  • [[Value]] 默认为undefined,就是属性的值

相关函数 Object.defineProperty(属性所在对象,属性名,描述符对象(可多个,{}))
注!修改configurable为false,则对后续调用该方法有限制,变得只能修改Writable和Value特性.

访问器属性

不包含属性值,包含一对getter和setter函数(非必需),同样有4个特性,相同功能不多加解释.

  • [[Configurable]] 默认值为true
  • [[Enumerable]] 默认值为true
  • [[Get]] default:undefined getter函数
  • [[Set]] default:undefined setter函数
    注!访问器属性不能直接定义,必须使用Object.defineProperty()定义,在严格模式中,尝试写入只指定了getter函数的属性会抛出错误,尝试读取只指定了setter函数的属性同理.
    非严格模式中,则会忽略/返回undefined

相关函数和兼容

Object.defineProperty(属性所在对象,属性名,描述符对象(可多个,{}))
支持:IE9+(IE8部分实现),Firefox4+,Safari5+,Opera 12+和Chrome
不兼容解决方案:__defineGetter__(属性名,函数),__defineSetter__(属性名,函数)
但是无法解决对[[Configurable]]和[[Enumerable]]的修改

Object.defineProperties(对象,{属性1:{描述符},属性2:{}...})
支持:IE9+(IE8部分实现),Firefox4+,Safari5+,Opera 12+和Chrome

Object.getOwnPropertyDescriptor(对象,属性名)
返回:对象(访问器/格式)
可以对JS中任何对象,包括BOM,DOM使用.

创建对象

  1. 工厂模式
  2. 构造函数模式
  3. 原型模式 - 引申出原型对象的理解
  4. 组合模式 解决原型模式问题
  5. 动态原型模式
  6. 寄生构造函数模式
  7. 稳妥构造函数模式

工厂模式

缺点:未解决识别问题(怎么知道一个对象的类型)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function makePerson(name,age,job){
var o =new Object();
o.name = name;
o.age = age;
o.job = job;
o.arr = ["a","b"];
o.sayName = function(){
alert(this.name);
}
return o;
}
var a = makePerson("jack",18,"programmer");
var b = makePerson("james",20,"designer");
a.arr.push("c");
console.log("a:"+a.arr); //a:a,b,c
console.log("b:"+b.arr); //b:a,b
console.log(a instanceof makePerson);//false
console.log(b instanceof makePerson);//false
console.log(a.prototype); //undefined
console.log(b.prototype); //undefined
console.log(a.prototype); //undefined
console.log(b.prototype); //undefined

构造函数模式

应该值得注意的是构造函数我们是大写字母开头,这是约定俗成的.创建一个Person示例我们会有如下步骤.

  1. 创建一个新对象
  2. 将构造函数作用域赋给新对象(this指向)
  3. 执行构造函数中的代码(为新对象添加属性)
  4. 返回新对象
    instanceof操作符和constructor属性都能让我们分辨出这是一种特定的类型,这也是构造函数模式胜过工厂模式的地方.
    如果直接作为普通函数调用,则会将属性赋值给window对象(Global)

问题:函数不复用问题,实例中的方法不是同一个Function的实例,鉴定方法.
console.log(a.sayName == b.sayName)
解决:放到全局定义,构造函数中设置即可
导致新问题:毫无封装性,而为了解决这些问题,我们可以使用后续的原型模式来解决.

注!所有对象都继承自Object,所以a,b使用instanceof操作符判断是否为Object的实例是true.

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.arr = ["a","b"];
this.sayName = function(){
alert(this.name);
};
}
var a = new Person("jack",18,"programmer");
var b = new Person("james",20,"designer");
a.arr.push('c');
console.log("a:"+a.arr); //a:a,b,c
console.log("b:"+b.arr); //b:a,b
console.log(a instanceof Person);//true
console.log(b instanceof Person);//true
console.log(a.prototype); //undefined
console.log(b.prototype); //undefined
console.log(a.constructor); //[Function: Person]
console.log(b.constructor); //[Function: Person]

原型模式

每一个function都有一个prototype(原型)属性,为一个指针,指向一个对象(用途:包含可以由特定类型的所有实例共享的属性和方法).

通过prototype设置的属性和方法都是共享的,接下来让我们理解一下原型对象.

理解原型对象

在任何时候,我们创建一个新函数都意味着我们会根据一个特定规则创建prototype属性,该属性指向函数的原型对象.
在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针.
调用构造函数创建一个实例后,实例内部包含一个指针[[Prototype]] (Firefox,Safari,Chrome访问使用__proto__),
对于判断可以使用Person.prototype.isPrototypeOf(a)函数.Person的prototype为a的prototype
Object.getPrototypeOf可以访问[[Prototype]]的值.

值得注意的是,我们可以通过对象实例来访问保存在原型的值,但是我们不能通过对象实例重写原型的值(对象.属性 = 值,这样是添加属性到实例,覆盖屏蔽了原型的值而已,并没有重写,但是对于引用类型不同,即使设置对象.属性=null也是不会恢复其指向,只是在实例中写入属性.对象为null而已,要想恢复,可以使用delete操作符)

原型与in操作符

方式一:for-in循环中使用
方式二:单独使用,会在能访问(不管通过对象还是原型)给定属性时返回true(所有能通过对象访问,可枚举的属性)
所有开发人员定义的属性都是可枚举的(IE8以及更早例外,其中屏蔽的不可枚举属性的实例属性不会出现在for-in循环中)

相关函数:
a.hasOwnProperty(属性名),可以确定属性是否存在于实例中,是则返回true
var keys = Object.keys(Person.prototype)
变量中保存一个数组,Object.keys返回的是一个包含所有可枚举属性的字符串数组.
Object.getWenPropertyNames()可以获取所有实例属性(无论是否可枚举)

更简单的原型语法

1
2
3
4
5
6
7
8
Person.prototype = {
name : "Nicholas",
age: 29,
job: "software engineer",
sayName:fuinction(){
alert("this.name");
}
}

在上面代码中,我们相当于完全重写了prototype对象,同时其constructor不再指向Person(指向Object构造函数),尽管instanceof操作符能返回正确结果,但是constructor已经无法确定对象类型了.当然我们可以自己在新建对象时候设置constructor: Person,但是这样做会导致它变为可枚举属性(原生不可枚举,解决方法:Object.defineProperty()).

原型的动态性

使用上述原型语法,会切断构造函数与最初原型的联系.
var friend = new Person()出现在完全重写之前,则我们无法通过friend访问重写的原型.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(){
}
var friend = new Person();
Person.prototype = {
constructor: Person,
name: "Jack",
age: 29,
job: "programmer",
sayName:function(){
console.log(this.name);
}
}
console.log(friend.age); // undefined
friend.sayName(); //报错

friend中的[[Prototype]]指向的仍然是原来的空无一物的Prototype,而不是我们后来重写的原型对象.

原生对象的原型

原生引用类型(Object,Array,String等)都采用原型模式创建
注!不推荐修改原生对象的原型,可能导致命名冲突/重写原生方法.

原型对象的问题

共享引用类型值的属性,如Array,修改则会共享

示例:

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
26
27
28
29
30
function Person(){
}
Person.prototype.name = "Jack";
Person.prototype.age = 18;
Person.prototype.job = "Software Engineer";
Person.prototype.arr = ["a","b"];//引用类型
Person.prototype.sayName = function(){
console.log(this.name);
}
var a = new Person();
a.sayName(); //Jack
var b = new Person();
b.name = "James";//创建值,屏蔽了原型的值
console.log(b.age);//18
console.log(b);//Person { name: 'James' }
b.sayName();//James
console.log(a.sayName == b.sayName);//true
a.arr.push("c");//修改引用类型
console.log("a:"+a.arr); //a:a,b,c
console.log("b:"+b.arr); //b:a,b,c
console.log(a instanceof Person);//true
console.log(b instanceof Person);//true
console.log(a.prototype); //undefined
console.log(b.prototype); //undefined
console.log(a.constructor); //[Function: Person]
console.log(b.constructor); //[Function: Person]

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

解决原型模式的问题-共享引用类型值的属性
其中特点在于,实例属性在构造函数中定义,共享的constructor与方法在原型中定义,如下.

目前来说最广泛,认同度最高的一种方式来创建自定义类型.

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["a", "b"];
}
Person.prototype = {
constructor:Person,
sayName:function(){
console.log(this.name);
}
}
var a = new Person("jack",18,"programmer");
var b = new Person("james",20,"designer");
a.friends.push('c');
console.log(a.friends);//a,b,c
console.log(b.friends);//a,b
console.log(a.friends === b.friends); //false
console.log(a.sayName === b.sayName); //true

动态原型模式

在构造函数中,if检查初始化后应存在的任何属性或方法.从而对构造函数和原型方法进行封装.

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["a","b"];
//注意不要使用对象字面量重写原型
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
var a = new Person("jack",18,"programmer");
var b = new Person("james",20,"designer");
a.friends.push("c");
console.log(a.friends);//a,b,c
console.log(b.friends);//a,b
console.log(a.friends === b.friends);//false
console.log(a.sayName === b.sayName);//true
console.log(a instanceof Person);//true
console.log(b instanceof Person);//true

寄生构造函数模式(不推荐

相当于工厂模式,通常用于在特殊情况下为对象创建构造函数,如我们要创建一个具有额外方法的特殊数组,又不能直接修改Array构造函数,就可以使用该模式.
注!返回对象和构造函数外部创建对象没有不同,所以无法确定对象类型.不推荐使用

示例:

1
2
3
4
5
6
7
8
9
10
function SpecialArray(){
var values = new Array();
//添加值
values.push.apply(values,arguments);
//添加方法
values.toPipedString = function(){
return this.join("|");
};
return values;
}

稳妥构造函数模式(不推荐

稳妥对象:没有公共属性,方法都不引用this的对象
和寄生构造函数模式的相似点:

  1. 创建对象实例不引用this
  2. 不使用new操作符调用构造函数
  3. instanceof无效

注意,稳妥对象中,除了定义的方法之外没有其他方法访问某值.
注!和寄生构造函数模式一样,不推荐使用

示例:

1
2
3
4
5
6
7
8
9
10
function Person(name,age,job){
var o = new Object();
o.sayName = function(){
alert(name);
};
return 0;
}
var friend =Person("Jack",18,"Software Enginner");
friend.sayName();

继承

在ECMAScript中支持的是实现继承,并且其实现继承主要依靠原型链实现,所以明白原型链就很重要了.

  1. 原型链
  2. 借用构造函数
  3. 组合继承
  4. 原型式继承
  5. 寄生式继承
  6. 寄生组合式继承

原型链

基本思想:利用原型链让一个引用类型继承另一个引用类型的属性和方法.
注!和我们之前提到的一样,所有函数的默认原型都是Object的实例.内部指针->Object.prototype

原型与实例的关系

instanceof操作符,可以测试实例与原型链中的构造函数.
isPrototypeOf()方法 ,与instanceof操作符返回效果相同.

谨慎定义方法

子类重写超类/父类中某个方法,或者添加父类/超类不存在的某个方法时,要放在替换原型语句后.
注!不要使用对象字面量创建原型方法,这会重写原型链

原型链问题

  1. 引用类型问题
  2. 创建子类型实例时不能(或者说没办法在不影响所有对象实例的情况下)向超类型的构造函数传递参数.

根据上述问题,实践中很少单独使用原型链.

示例:

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
26
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;
}
// 谨慎定义方法
//SubType.prototype.getSuperValue=function(){
// return false;
//}该方法会屏蔽原来的方法,即通过SuperType的实例调用getSuperValue时依然调用原来的方法,而通过SubType的实例调用时,会执行这个重新定义的方法.必须在SubType.prototype = new SuperType();之后,再定义getSubValue和该方法.
var a = new SubType();
console.log(a.getSubValue());//false
console.log(a.getSuperValue());//true
//原型与实例的关系
console.log(a instanceof Object);//true
console.log(a instanceof SuperType);//true
console.log(a instanceof SubType);//true

借用构造函数

伪造对象/经典继承.
目的:解决引用类型问题->借用构造函数(constructor stealing)
基本思想:子类型构造函数内部调用超类/父类构造函数
缺点:无法避免构造函数模式存在的问题(函数无法复用)
所以该方式很少单独使用.

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
function SuperType(name){
this.name = name;
this.arr = ["a","b","c"];
}
function SubType(){
SuperType.call(this,"jack");//传递参数
this.age = 18;//实例属性
}
var a = new SubType();
a.arr.push("d");
var b = new SubType();
console.log(a.arr);//a,b,c,d
console.log(b.arr);//a,b,c

组合继承

combination inheritance
也称伪经典继承,将原型链和借用构造函数技术结合一起的继承模式.
基本思想:使用原型链实现对原型属性和方法的继承,借用构造函数实现对实例属性的继承.constructor重指向
相当于:属性继承(借用构造函数),函数外定义方法,constructor重新指向

组合继承避免了原型链和借用构造函数的缺陷,融合了优点,成为了JS中最常用的继承模式,而且instanceofisPrototypeOf()都能够识别

示例:

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
26
27
28
29
30
function SuperType(name){
this.name = name;
this.arr = ["a","b"];
}
SuperType.prototype.sayName =function(){
console.log(this.name);
};
function SubType(name,age){
SuperType.call(this,name);
this.age = age;
}
//inherit
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
};
var a = new SubType("Jack",18);
a.arr.push("c");
console.log(a.arr);//a,b,c
a.sayName();//Jack
a.sayAge();//18
var b = new SubType("James",20);
console.log(b.arr);//a,b
b.sayName();//James
b.sayAge();//20

原型式继承

Prototypal inheritance
将传入的对象作为函数内定义的构造函数的原型(要求必须有一个对象可以作为另一个对象的基础),在ECMAScript5中新增Object.create()方法规范了原型式继承,它接收两个参数,一个用作新对象原型的对象和(可选)一个为新对象定义额外属性的对象.
单个参数情况下Object.create()Object()行为相同
兼容性:IE9+,Firefox4+,Safari5+,Opera12+,Chrome
缺点:和原型模式一样,引用类型共享.

示例:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function object(o){
function F(){};
F.prototype = o;
return new F();
}
var person = {
name: "Jack",
arr: ["a","b"]
};
var a = object(person);
var b = object(person);
a.name = "James";
a.arr.push("c");
b.name = "Ansem";
b.arr.push("d");
console.log(person.arr);//a,b,c,d
console.log(a.arr);//a,b,c,d
console.log(b.arr);//a,b,c,d
//Object.create
var person2 = {
name: "Jack",
arr: ["a","b"]
};
var c = Object.create(person2,{
name:{
value: "James"
}
});
var d = Object.create(person2,{
name:{
value: "Ansem"
}
});
c.arr.push("c");
d.arr.push("d");
console.log(c.name);//James
console.log(d.name);//Ansem
console.log(person.arr);//a,b,c,d
console.log(c.arr);//a,b,c,d
console.log(d.arr);//a,b,c,d

寄生式继承

parasitic inherit
思路与寄生构造函数和工厂模式类似,创建新对象,增强对象,返回对象.
缺点:函数复用不了,对于引用类型为共享.

示例:

1
2
3
4
5
6
7
function createAnother(original){
var clone = object(original);
clone.sayHi = function(){
console.log("HI");
};
return clone;
}

寄生组合式继承(重点)

组合继承的问题:无论什么情况都会两次调用超类型构造函数
第一次:SubType.prototype = new SuperType()
第二次:new SuperType()内->SuperType.call(this,name);
这造成的结果是,第一次时:SuperType的实例(SubType的原型)初始化属性.第二次时:新对象上又新创建了相同的属性,于是这两个属性就屏蔽了原型中两个同名属性.

解决方法就是寄生组合式继承.通过借用构造函数来继承属性,通过原型链的混成形式来继承方式.
基本思路:不必为了指定子类型的原型而调用超类/父类的构造函数,我们需要的知识超类/父类原型的一个副本.在这点上使用寄生式继承来继承超类/父类的原型,再将结果指定给子类的原型.

高效率体现在避免了创建多余不必要的属性,原型链还能保持不变.instanceofisPrototypeOf()都能正常使用.

可以说寄生组合式继承是引用类型最理想的继承范式,这也被YUI库所采用.

示例:

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
26
27
28
29
30
31
32
33
//基本模式
function inheritPrototype(subType,superType){
var prototype = Object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
function SuperType(name){
this.name = name;
this.arr = ["a","b"];
}
SuperType.prototype.sayName =function(){
console.log(this.name);
};
function SubType(name,age){
SuperType.call(this,name);
this.age = age;
};
inheritPrototype(SubType,SuperType);//避免了多次执行,提高了效率
SubType.prototype.sayAge = function(){
console.log(this.age);
};
var c = new SubType("Jack",18);
var d = new SubType("Ansem",25);
c.arr.push("c");
d.arr.push("d");
console.log(c.name);//Jack
console.log(d.name);//Ansem
console.log(c.arr);//a,b,c
console.log(d.arr);//a,b,d