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使用.
创建对象
工厂模式
构造函数模式
原型模式 - 引申出原型对象的理解
组合模式 解决原型模式问题
动态原型模式
寄生构造函数模式
稳妥构造函数模式
工厂模式
缺点:未解决识别问题(怎么知道一个对象的类型)
示例:
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示例我们会有如下步骤.
创建一个新对象
将构造函数作用域赋给新对象(this指向)
执行构造函数中的代码(为新对象添加属性)
返回新对象
而instanceof
操作符和constructor
属性都能让我们分辨出这是一种特定的类型,这也是构造函数模式胜过工厂模式的地方.
如果直接作为普通函数调用,则会将属性赋值给window
对象(Global
)
问题:函数不复用问题,实例中的方法不是同一个Function的实例,鉴定方法.console.log(a.sayName == b.sayName)
解决:放到全局定义,构造函数中设置即可
导致新问题:毫无封装性,而为了解决这些问题,我们可以使用后续的原型模式来解决.
注!所有对象都继承自Object
,所以a,b使用instanceof
操作符判断是否为Object
的实例是true
.
示例:
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()
可以获取所有实例属性(无论是否可枚举)
更简单的原型语法
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
访问重写的原型.
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
,修改则会共享
示例:
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
与方法在原型中定义,如下.
目前来说最广泛,认同度最高的一种方式来创建自定义类型.
示例:
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检查初始化后应存在的任何属性或方法.从而对构造函数和原型方法进行封装.
示例:
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
构造函数,就可以使用该模式.
注!返回对象和构造函数外部创建对象没有不同,所以无法确定对象类型.不推荐使用
示例:
function SpecialArray(){
var values = new Array();
//添加值
values.push.apply(values,arguments);
//添加方法
values.toPipedString = function(){
return this.join("|");
};
return values;
}
稳妥构造函数模式(不推荐
稳妥对象:没有公共属性,方法都不引用this的对象
和寄生构造函数模式的相似点:
创建对象实例不引用this
不使用new操作符调用构造函数
instanceof无效
注意,稳妥对象中,除了定义的方法之外没有其他方法访问某值.
注!和寄生构造函数模式一样,不推荐使用
示例:
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中支持的是实现继承,并且其实现继承主要依靠原型链实现,所以明白原型链就很重要了.
原型链
借用构造函数
组合继承
原型式继承
寄生式继承
寄生组合式继承
原型链
基本思想:利用原型链让一个引用类型继承另一个引用类型的属性和方法.
注!和我们之前提到的一样,所有函数的默认原型都是Object
的实例.内部指针->Object.prototype
原型与实例的关系
instanceof
操作符,可以测试实例与原型链中的构造函数.isPrototypeOf()
方法 ,与instanceof
操作符返回效果相同.
谨慎定义方法
子类重写超类/父类中某个方法,或者添加父类/超类不存在的某个方法时,要放在替换原型语句后.
注!不要使用对象字面量创建原型方法,这会重写原型链
原型链问题
引用类型问题
创建子类型实例时不能(或者说没办法在不影响所有对象实例的情况下)向超类型的构造函数传递参数.
根据上述问题,实践中很少单独使用原型链.
示例:
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)
基本思想:子类型构造函数内部调用超类/父类构造函数
缺点:无法避免构造函数模式存在的问题(函数无法复用)
所以该方式很少单独使用.
示例:
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中最常用的继承模式,而且instanceof
和isPrototypeOf()
都能够识别
示例:
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
缺点:和原型模式一样,引用类型共享.
示例:
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
思路与寄生构造函数和工厂模式类似,创建新对象,增强对象,返回对象.
缺点:函数复用不了,对于引用类型为共享.
示例:
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的原型)初始化属性.第二次时:新对象上又新创建了相同的属性,于是这两个属性就屏蔽了原型中两个同名属性.
解决方法就是寄生组合式继承.通过借用构造函数来继承属性,通过原型链的混成形式来继承方式.
基本思路:不必为了指定子类型的原型而调用超类/父类的构造函数,我们需要的知识超类/父类原型的一个副本.在这点上使用寄生式继承来继承超类/父类的原型,再将结果指定给子类的原型.
高效率体现在避免了创建多余不必要的属性,原型链还能保持不变.instanceof
和isPrototypeOf()
都能正常使用.
可以说寄生组合式继承是引用类型最理想的继承范式,这也被YUI库所采用.
示例:
//基本模式
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