第六章重点1:原型
第六章重点2:原型链
第六章重点3:最常用继承–组合继承
6.1 理解对象
6.1.1 属性类型
1.数据属性
包含一个数据值的位置,在这个位置可读取和写入值。数据属性有4个描述其行为的特性
- [ [Configurable] ] 能否删除属性从而重新定义属性,能否修改属性特性,能否把属性改为访问器属性,默认值为true
- [ [Enumerable] ] 能否通过for-in循环返回属性,默认值为true
- [ [Writable] ] 能否修改属性的值,默认值为true
- [ [Value] ] 包含这个属性的数据值;读取属性值时从这个位置读;写入属性值时把新值保存在这个位置,默认值为undefined
Object.defineProperty()方法可以修改以上四个特性
接收三个参数(属性所在对象,属性名,描述符对象(对象的属性就是以上四个特性))
var person = {};
Object.defineProperty(person,"name",{
writable : false, //设置writable特性为false,所以不能修改属性的值
value : "yuan" //给属性的值设置为"yuan"
});
console.log(person.name); //yuan
person.name = "zhang"; //意图修改person.name的值,但是修改不了的
console.log(person.name); //yuan
将第三行代码换为下面也是一样的结果
但是configurable设置为false后就调不回去了
configurable : false;
可以多次调用Object.defineProperty()修改同一个属性,但再把configurable特性设置为false之后就会有限制了
2. 访问器属性
不包含数据值,包含一对儿getter和setter函数(不过,这两个函数都不是必需的)
读取访问器属性时会调用getter函数,该函数负责返回有效的值
写入访问器属性时,会调用setter函数并传入新值
- [ [Configurable] ] 能否删除属性从而重新定义属性,能否修改属性特性,能否把属性改为数据属性,默认值为true
- [ [Enumerable] ] 能否通过for-in循环返回属性,默认值为true
- [ [Get] ] 读取属性时调用的函数,默认值为undefined
- [ [Set] ] 写入属性时调用的函数,默认值为undefined
var book = {
_year : 2018, //_下划线表示只能通过对象方法访问的属性,相当于对象私有变量,若要在对象外埠调用则是book.year;
edition : 1
};
Object.defineProperty(book,"year",{ //year是访问器属性,它包含一个getter函数和一个setter函数
get : function(){
return this._year; //读取_year属性,返回book._year的值
},
set : function(newValue){
if(newValue > 2018){
this._year = newValue;
this.edition += newValue - 2018;
}
}
});
book.year = 2019;
console.log(book.edition); //2
上面的_year和year是两个属性,_year是已经定义好的属性,year是访问器属性,它包含getter,setter这一对儿函数
6.1.2 定义多个属性
和上面代码定义的对象一样,只是下面的代码对象属性是在同一时间被定义的
var book = {};
Object.defineProperties(book.{
_year : { //数据属性
writable : true, //_year的值可修改
value : 2004
},
edition : { //数据属性
writable : true, //edition的值可修改
value : 1
},
year : { //访问器属性
get : function(){
return this._year;
},
set : function(newValue){
if(newValue > 2004){
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
6.1.3读取属性的特性
使用Object.getOwnPropertyDescriptor()方法可以取得给定属性的描述符
这个方法接收两个参数,属性所在的对象和要读取其描述符的属性名称
6.2 创建对象
Object构造函数或对象字面量都可以创建单个对象,但这些方式有个明显的缺点,使用同一个接口创建很多对象,会产生大量的重复代码
6.2.1 工厂模式
创建一个函数来包装对象,通过函数接收外来参数,无数次的调用该函数来创建对象,
优点:可解决创建多个相似对象的问题
缺点:但没有解决对象识别的问题(知道一个对象的类型)
function createObject(name,age,job){
var o = {};
o.name = name;
o.age = age;
o.job = job;
return o;
}
var o1 = createObject("yuan",20,"engineer");
var o2 = createObject("zhang",21,"teacher");
console.log(o1);
console.log(o2);
6.2.2 构造函数模式
function Person(name,age,job){ //构造函数用于创建新的类,函数名也是大写字母开头
this.name = name;
this.age = age;
this.job = job;
}
var person1 = new Person("yuan",20,"engineer");
var person2 = new Person("zhang",20,"teacher");
console.log(person1);
console.log(person2);
person1,person2这两个对象都有一个constructor(构造函数)属性,改属性指向Person
创建自定义的构造函数意味着将来可以将他的实例标识为一种特定的类型,这也是构造函数胜过工厂模式的地方
缺点:在构造函数中定义的方法,每次创建对象时,都会重新创建该方法,造成了资源浪费
(未完待续)
6.2.3 原型模式
1.理解原型对象(借鉴的博客地址)
在声明了一个函数之后,浏览器会自动按照一定的规则创建一个对象,这个对象就叫做原型对象。
构造函数会有一个prototype属性,它指向该函数的原型对象
原型对象会有一个constructor属性,它指向构造函数
以下是关系图
使用构造函数创建一个对象
function Students(){
//构造函数
}
var stu = new Students(); //用构造函数创建的对象
该对象有一个属性__proto__,它指向构造函数的原型对象
以下是构造函数,原型对象,构造函数创建的对象的关系图
重点
- 从上面的代码中可以看到,创建stu对象虽然使用的是students构造函数,但是对象创建出来之后,这个stu对象其实已经与students构造函数没有任何关系了,stu对象的__proto__属性(更偏理论,实际上不怎么使用它)指向的是students构造函数的原型对象。
- 如果使用new students()创建多个对象stu1、stu2、stu3,则多个对象都会同时指向students构造函数的原型对象。
- 我们可以手动给这个原型对象添加属性和方法,那么stu1,stu2,stu3…这些对象就会共享这些在原型中添加的属性和方法。
- 如果我们访问stu中的一个属性name,如果在stu对象中找到,则直接返回。如果stu对象中没有找到,则直接去stu对象的__proto__属性指向的原型对象中查找,如果查找到则返回。(如果原型中也没有找到,则继续向上找原型的原型—原型链)。
function Person(){
//构造函数
}
var person = new Person(); //通过构造函数创建的对象
Person.prototype.name = "yuan";
console.log(person.name); //yuan,在person没找到name,去原型对象找的name
- 如果通过stu对象添加了一个属性name,则stu对象来说就屏蔽了原型中的属性name。 换句话说:在stu中就没有办法访问到原型的属性name了。
function Person(){
//构造函数
}
var person = new Person(); //通过构造函数创建的对象
person.name = "zhang";
Person.prototype.name = "yuan";
console.log(person.name); //zhang,在person里面找到了name属性,就不去原型对象里面找了
- 通过stu对象只能读取原型中的属性name的值,而不能修改原型中的属性name的值。 stu.name = “李四”; 并不是修改了原型中的值,而是在stu对象中给添加了一个属性name。
2. .hasOwnProperty()方法
访问一个对象的属性时,这个属性有可能来自这个对象本身,也可能来自对象的_proto_属性指向的原型对象,所以就需要判断
对象.hasOwnProperty(“属性”)可以判断属性是否来自对象本身,是则返回true,否则返回false
function Person(){
}
var person = new Person();
Person.prototype.name = "yuan"; //给原型对象添加name属性,并赋值yuan
console.log(person.hasOwnProperty("name")); //false
若给对象person添加name属性呢?
function Person(){
}
var person = new Person();
person.name = "zhang"; //person有了自己的name属性,就不用取原型对象中找了
Person.prototype.name = "yuan"; //给原型对象添加name属性,并赋值yuan
console.log(person.hasOwnProperty("name")); //true
但是hasOwnProperty()方法只能判断一个属性对象本身有没有,并不能知道其原型对象有没有,意思说就是对象本身和原型对象都没有会返回false,对象本身没有但是原型对象有也会返回false
3. in操作符
in操作符判断一个对象是否在对象本身或原型对象中,在这两个中的至少一个就返回true,都没有则返回false
function Person(){
}
var person = new Person();
person.name = "yuan";
console.log("name" in person); //true
将第四行换一下可得
Person.protype.name = "yuan";
console.log("name" in person); //true
当用.hasOwnProperty()方法返回的是false时,再加上in操作符可具体确定属性的位置或有无该属性。例如下方代码就确定了属性name在原型对象上
function Person(){
}
var person = new Person();
Person.prototype.name = "yuan";
console.log("name" in person); //true
console.log(person.hasOwnProperty("name")); //false
4.重写原型对象会导致的后果
在创建对象之后用对象字面量表示法会重写原型对象
function Person(){
}
var person = new Person();
Person.prototype = { //重写原型对象
name : "yuan"
};
console.log(person.name); //undefined,本应该是yuan,但是原型重写了,所以person和重写后的原型联系被切断了
重写原型后,Person.protype会指向一个新创建的原型,这个新创建的原型和Person()创建的对象无关,这个对象还是指向以前的原型
若是非要用对象字面量表示法写原型对象也许,不过必须要把原型对象写在实例对象之前,原型对象写在前面那么实例对象得_proto_指向得还是原型对象
所以先写函数,再写原型,最后写实例对象是最好的
function Person(){
}
Person.prototype = { //重写原型对象
name : "yuan"
};
var person = new Person();
console.log(person.name); //yuan
5.原型对象的问题
小问题:所有实例在默认情况下都将取得相同的属性值
大问题:包含引用类型值得属性共享会引发大问题
function Person(){
}
Person.prototype = {
name : "yuan",
age : 20,
friends : ["wang","cheng"]
};
var per1 = new Person();
var per2 = new Person();
per1.friends.push("chen"); //因为friends属性不在per1上,所以加在了原型上
console.log(per2.friends); //["wang","cheng","chen"]
因为friends属性在原型上,是给per1,per2共享的,所以修改这个属性,其他实例对象属性也跟着修改了,不过可以单独修改per1属性,per1.name = [“wang”,“cheng”,“chen”];,但是这也违背了原型减少代码量得初衷,,,所以很少有人单独使用原型模式
6.2.4 组合使用构造函数模式和原型模式
构造函数模式用于定义实例对象的属性,原型模式定义方法和多个实例对象可共享得属性
优点:每个实例对象有自己独立的属性,可随时更改属性的值;并且还有可和其他实例对象共享的属性和方法,最大限度上节省了内存
缺点:将构造对象这个过程分成了两步,容易造成语义不明
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["wang","cheng"]; //不接受参数,所以每个实例对象的默认friends属性的值相同
}
Person.prototype = {
constructor : Person, //确保原型的constructor属性还是指向Person函数的
sayName : function(){
console.log(this.name);
}
};
var per1 = new Person();
var per2 = new Person();
per1.friends.push("chen");
console.log(per1.friends); //["wang","cheng","chen"]
console.log(per2.friends); //["wang","cheng"]
6.2.5 动态原型模式
优点:把所有信息都封装在了构造函数,不会出现组合使用构造函数模式和原型模式的两个部分,并且还保留了上述方法的优点
可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
}
//构建方法,若这个方法不存在这构建这个方法
if(typeof this.sayName != "function"){ //typeof 可换为 instanceof
Person.prototype.sayName = function(){
console.log(this.name);
};
}
var per1 = new Person("yuan",21,"engineer");
per1.sayName(); //yuan
在sayName()方法不存在时才将其添加到原型当中
这里对原型做的修改,能在实例对象中立即奏效,所以这个方法看似很完美
6.26 寄生构造函数模式
在前面几种模式都不适用的情况下,可使用寄生构造函数模式
基本思想是创建一个函数,该函数作用仅仅是封装创建对象的代码,然后再返回新创建的对象
function Person(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
console.log(this.name);
}
return o;
}
var per1 = new Person("yuan",21,"engineer");
per1.sayName(); //yuan
6.2.7 稳妥构造函数模式(用的较少)
稳妥对象:没有公共属性,其方法也不引用this的对象。
稳妥对象最适合再一些安全的环境中(这些环境会禁止使用this和new)
新创建对象的实例方法不引用this,不使用new操作符调用构造函数
6.3 继承
oo语言(面向对象)中的重要概念,许多oo语言支持两种继承方式:接口继承,实现继承
ECMAScript只接受实现继承,且主要是依靠原型链实现的
6.3.1 原型链
前提知识:所有的构建函数比如Function、Object、Number、Array、Boolean等等都是函数对象,比如你var obj=new Object();
console.log(typeof Object);
console.log(typeof Number);
console.log(typeof Array);
console.log(typeof Function);
console.log(typeof Boolean); //都是function
认真分析以下代码
易错点__proto__属性的两边下划线是两个小的下划线组在一起的
function Foo(name,age){
this.name = name;
this.age = age;
}
Object.prototype.toString = function(){
console.log("My name is " + this.name + ", and my age is " + this.age);
}
var fn = new Foo("yuan",21);
fn.toString(); //my name is yuan, and my age is 21,根据原型链在Object.protype找到了这个方法
console.log(fn.__proto__ === Foo.prototype); //true
console.log(Foo.prototype.__proto__===Object.prototype); //true
console.log(Object.prototype.__proto__ === null); //true
为什么Object呢?
因为每一个对象都可以用构建函数的方法来创建:var obj = new Object();所以Object可以是任何对象的构造函数
1.红线标注的,fn到Foo.prototype再到Object.protype最后到null就是原型链
2.若要调用一个属性,在fn中找不到,就顺着fn所在的原型链向上找上去直至null
3.Object是Foo.prototype的构造函数,可以说定义的构造函数protype属性指向的原型的构造函数,都可默认为Object,因为Object就是一个函数对象
4.Object.protype.__proto__一定等于null,类似Number,Boolean,Array,Function也都是一样的
5.在使用原型的时候,一般推荐将需要扩展的方法写在构造函数的prototype属性中,避免写在__proto__属性里面。(可能是因为protype是显性属性,__proto__是隐形属性吧)
1.确定实例对象和构造函数的关系
对象 instanceof 函数
返回true,我们可以说fn是原型链上出现的任何一个函数的实例对象
console.log(fn instanceof Foo); //true
console.log(fn instanceof Object); //true
2.确定原型和实例对象的关系
原型.isPrototypeOf(实例对象)
原型链上出现的原型都可视作实例对象的原型
console.log(Foo.prototype.isPrototypeOf(fn)); //true
console.log(Object.prototype.isPrototypeOf(fn)); //true
3.谨慎的定义方法
给原型添加方法的代码要放在替换原型的语句之后
在通过原型链实现继承时,不能使用对象字面法创建原型方法
4. 原型链的问题
继承时原型会变成另一个类型的实例对象,那么原先的实例对象属性就会变成现在的原型属性了
创建子类型实例时,不能向超类型构造函数中传递参数,换言之是不能在不影响所有对象实例的情况下给超类型的构造函数传递参数
所以实例中很少单独使用原型链
6.3.2借用构造函数
原理就是,构建个新函数,用call(),apply()方法调用原函数的属性方法,
function SuperType() {
this.colors = ["red","blue","green"];
}
function SubType() {
//继承了SuperType --重新创建SuperType构造函数属性的副本
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
var instances2 = new SubType();
console.log(instance2.colors); //"red,blue,green" --完美实现了继承构造函数属性
相对于原型链继承来说,构造函数有个优点,就是可以在子类型构造函数中向超类型构造函数中传递参数,而原型链中实例对象是不能改变原型的属性的
function SuperType(name){
this.name = name;
}
function SubType(){
SuperType.call(this,"yuan"); //用call()可以,apply()不行
}
var sup1 = new SubType();
console.log(sup1.name); //yuan
缺点: 方法都在构造函数中定义,函数复用则无从谈起,而且在超类型原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。所以构造函数技术也很少单独使用。
6.3.3 组合继承
将原型链和函数构造结合在一起 **使用原型链对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承**优点: 通过在原型上定义方法实现了函数复用,又能保证每个实例都有其自己的属性,这是目前用的最多的一种继承方法之一
//构造函数继承属性
function SuperType(name){
this.name = name;
this.colors = ["red","green"];
}
SuperType.prototype.sayName = function(){ //sayName方法
console.log(this.name);
};
function SubType(name,age){ //接收参数name,age
SuperType.call(this,name); //继承SuperType属性并传递参数name
this.age = age;
}
//原型链继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
};
var ins1 = new SubType("yuan",20);
ins1.colors.push("yellow");
console.log(ins1.colors); //["red","green","yellow"]
ins1.sayAge(); //20
ins1.sayName(); //yuan
var ins2 = new SubType("zhang",21);
console.log(ins2);
ins2.sayName(); //zhang
ins2.sayAge(); //21
6.3.4 原型式继承
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型
function object(o){
function F(){
}
F.prototype = o;
return new F();
}
function object(o){
function F(){
}
F.prototype = o;
return new F();
}
var person = {
name : "yuan",
friends : ["wang","cheng"]
}
var anotherPerson = object(person);
anotherPerson.name = "zhang";
anotherPerson.friends.push("chen");
console.log(person.name); //yuan
console.log(person.friends); //["wang","cheng","chen"]
上面的例子,person就是anotherPerson的原型,anotherPerson.friends.push(“chen”);其实是调用原型的friends属性,相当于
person.friends.push(“chen”);所以就改变了person的属性,所以console.log(person.friends); //[“wang”,“cheng”,“chen”]
ES5通过新增Object.create()方法规范了原型式继承
var anotherPerson = object(person);
可以代替上面的object()函数,所以以上代码变成下面即可
var person = {
name : "yuan",
friends : ["wang","cheng"]
}
var anotherPerson = Object.create(person);
anotherPerson.name = "zhang";
anotherPerson.friends.push("chen");
console.log(person.name);
console.log(person.friends);
Object.create()方法的第二个参数制定的任何属性都会覆盖原型对象上的同名属性
var person = {
name : "yuan",
friends : ["wang","cheng"]
}
var anotherPerson = Object.create(person,{
name : {
value : "zhang" //value代表name属性的值
}
});
console.log(person.name); //zhang
6.3.5 寄生式继承
创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象
function object(o){
return o;
}
function creatAnother(original){
var clone = object(original); //任何一个能返回新对象的函数都能替代object函数
clone.sayHi = function(){ //以某种方式来增强这个对象
console.log("Hi!");
}
return clone; //返回这个对象
}
var person = {
name : "yuan",
friends : ["wang","cheng"]
}
var anotherPerson = creatAnother(person);
console.log(anotherPerson.name);
anotherPerson.sayHi();
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式
6.3.6 寄生组合式继承
前面说过组合继承是JavaScript是最常用的继承模式,不过,它也有自己的不足
组合继承缺点:无论什么情况下都会调用两次超类型构造函数,一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性
如下
function SuperType(name){
this.name = name;
this.colors = ["red","green"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
};
function SubType(name,age){
//第一次调用SuperType函数,在新的实例对象创建了两个属性(使用call方法调用了SuperType的两个属性)
SuperType.call(this,name);
this.age = age;
}
//第二次调用SuperType函数,得到SuperType的两个实例属性name,colors
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
};
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混合形成形式来继承方法
基本模式
function inheritPrototype(SubType,SuperType){
var prototype = object(SuperType.prototype); //创建对象
prototype.constructor = SubType; //增强对象,protype是subType的原型
SubType.prototype = prototype; //指定对象
}
第一步是创建超类型原型的一个副本
第二步是为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性
第三步是将创建的对象(即副本)赋值给子类型的原型
这样就可调用inheritPrototype()函数的语句取替换前面例子中为子类型原型赋值的语句了
function inheritPrototype(SubType,SuperType){
var prototype = object(SuperType.prototype); //创建对象
prototype.constructor = SubType; //增强对象,protype是subType的原型
SubType.prototype = prototype; //指定对象
}
function SuperType(name){
this.name = name;
this.colors = ["red","green"];
}
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);
};
这个例子的高效率体现在它只调用了一次SuperType构造函数,并且因此避免了在SubType.prototype上面创建不必要的、多余的属性,与此同时,原型链还能保持不变。