javaScript高级程序设计读书笔记(六)

面向对象的程序设计,面向对象的语言有类的概念,通过类创造多个具有相同的属性和方法的对象。

对象

属性类型

  • 数据属性:包含一个数据值的位置,可以读取和写入
    • [[Configurable]]:是否可以delect删除属性
    • [[Enumerable]]:是否可以for-in循环对象
    • [[Writable]]:是否可以修改属性的值
    • [[Value]]:属性的数据值
  • 访问器属性:不包含数据值,包含下面两个函数
    • getter
    • setter

创建对象

以前使用的创建对象的方法(直接new Object和字面量),有明显的缺点:会出现大量冗余的代码。

工厂模式

用函数来封装以特定接口创建对象的细节,根据接收的参数来创建一个包含所有必要信息的对象。缺点是无法解决对象识别的问题,就是怎么知道一个对象的类型。

1
2
3
4
5
6
7
8
9
10
11
12
function creatPerson(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 preson=creatPerson('icessun',18,'sofeware engineer')
构造函数模式

Object和Array这样原生的构造函数,在运行时会自动出现在执行环境中,也可以自定义构造函数,可以直接创建对象。使用构造函数重写工厂模式代码,有以下不同:

  • 没有显示的创建对象
  • 直接将属性和方法赋给this对象
  • 没有return语句

问题:每个实例都重复的创建方法,包含了不同的Function实例,导致不同的作用域链和标识的解析,person1和person2中的方法是不相同的。虽然可以在全局定义一个函数,然后在构造函数里面将属性设置为全局的函数,但一个全局函数只能被一个对象调用,物不能尽其所用,也没有封装可言。

1
2
3
4
5
6
7
8
9
10
function Person(name,age,job){
this.age=age;
this.name=name;
this.job=job;
this.sayName=function(){
console.log(this.name);
};
}
var person1=new Person('icessun',18,"software Enginner");
var person2=new Person('icessun1',19,'student');

使用new方式(调用构造函数的方法)创建的对象实例, 会经历下面四步:

  • 创建一个新对象
  • 将构造函数的作用域赋给新对象,this就指向这个新对象
  • 执行构造函数中的代码,添加属性和方法
  • 返回新对象

这样的每一个实例对象身上都有一个constructor(构造函数)属性,指向Person,可以用来标识对象类型的(特定的类型);但是检测对象类型一般使用instanceof操作符可靠一些。

原型模式

创建的每一个函数都有一个prototype(原型)属性,指向一个对象;这个对象(原型对象 )的用途是包含可以由特定类型的所有实例共享的属性和方法。prototype就是通过调用构造函数而创建的实例的原型对象。好处:可以让所有的实例共享原型对象包含的属性和方法,不用在构造函数里面定义实例的信息,而是直接添加到原型对象中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Person.prototype.constructor------> Person
// 空的构造函数
function Person(){
}
// 属性和方法都添加到Person的prototype属性中,由实例共享
Person.prototype.name="icessun";
Person.prototype.age="18";
Person.prototype.job="engineer";
Person.prototype.sayName=function(){
console.log(this.name);
};
var person1=new Person(); // 调用构造函数创建对象实例
preson1.sayName(); // icessun
var person2=new Person();
person2.sayName();// icessun
console.log(person1.sayName == person2.sayName); // true
  • 理解原型对象

创建一个函数就会为该函数创建一个prototype属性,指向函数的原型对象函数.Prototype;所有的原型对象都自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。构造函数创建的实例,其内部有一个指针[[Prototype]](或者)__proto__,指向构造函数的原型对象,这个链接是在实例和构造函数的原型对象之间的。

原型对象和构造函数和实例对象之间的关系

Person.prototype指向原型对象,而Person.prototype..constructor有指向Person。原型对象中除了包含constructor属性之外,还包括后来添加的其他属性。Person的实例都包含一个内部属性指向Person.prototype,与构造函数没有直接的关系。在所有的实现中都无法访问到[[Prototype]],但是可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系,如果[[Prototype]]指向调用isPrototypeOf()方法(ECMAScript 5中新方法:Object.getPrototypeOf())的对象(Person.prototype),那就返回true

1
2
3
console.log(person.prototype.isPrototypeOf(person1)) //true
console.log(person.prototype == Object.getPrototypeOf(person1)) //true
console.log(Object.getPrototypeOf(person1).name);// icessun Object.getPrototypeOf(person1) 直接返回[[Prototype]]的值

当查找对象的属性时,现在实例找,没有找到才去原型对象上面找;可以通过实例对象访问保存在原型中的值,但是却不能通过对象实例重写原型中的值,当与原型中的属性同名的时,会自动屏蔽原型中的属性。对象实例可以访问到原型对象上constructor属性,使用delete 实例属性可以删除实例的属性,重新访问原型上面的属性;

确定属性是在原型对象上面还是实例对象上面
  • hasOwnProperty()可以检测一个属性是存在与实例中,还是存在原型中。属性存在实例对象里面就返回true
  • in操作符:对象能够访问给定属性的时候,返回true,无论属性是在原型还是实例上;属性 in 对象 // 只要存在就返回 true
1
2
3
4
function hasPrototypeProperty(object,name){
return !object.hasOwnProperty(name) && (name in object);
// 返回false 就是实例属性 否者是原型属性
}
  • 为了更好的减少输入,我们可以使用一个对象字面量来表示整个原型对象
1
2
3
4
5
6
7
8
9
10
11
12
function Person(){};
// 把Person.prototype设置为一个以字面量的形式创建的一个新对象,重写了默认的原型对象prototype对象
Person.prototype={
name:'icessun',
age:18,
job:'Engineer',
sayName:function(){
console.log(this.name);
},
constructor:Person; // 解决`constructor`属性不再指向`Person函数`的问题,但是会导致constructor属性可以枚举
}

但是这种方法的constructor属性不再指向Person函数,前面说过,每创建一个函数,就会同时创建它的原型对象prototype对象,而这个对象也会自动获取constructor属性。这方法的constructor属性指向新对象的constructor属性,指向Object构造函数,通过constructor属性无法确定对象的类型

原型的动态性

实例获取原型对象上的属性和方法的时候,是一次查找过程;所以在原型对象上面的修改,都能够立即在实例上面反映出来。即使是先创建实例后修改原型对象也是一样的。

  • 实例和原型之间的松散链接关系,连接是一个指针。
  • 调用构造函数的时候,会为实例添加一个指向最初原型的指针[[protorype]],要是把原型修改为另外一个对象就切断了实例与原型对象的最初的连接。
  • 实例中的指针仅仅指向原型,而不是构造函数
原生对象的原型

通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新的方法。可以向自定义对象的原型引用修改原生对象的原型,可以随时添加方法

1
2
3
4
5
6
7
8
9
10
11
Array.prototype找到sort()方法
String.prototype找到substring()方法
为基本的包装类型String添加自定义方法
String.prototype.start=function(text){
return this.indexOf(text) == 0; // 传入的字符串位于一个字符串的开始 返回true
};
var msg='icess un';
console.log(msg.start('icess')); // true
原型对象的问题
  • 原型模型省略了为构造函数传递初始化参数的环节,所有实例默认的获取相同的属性值。
  • 原型中的属性是被很多实例共享的,对于函数来说是方便的,对于包含基本值的属性,给实例添加一个同名的属性,就可以隐藏原型上面的属性。但是对引用类型的属性值来说不行,修改了会在其他的实例里面反映出来。
组合使用构造函数模式和原型模式 (重点)

创建自定义类型的最常见的方式;构造模式用于定义实例属性,原型模式用于定义方法和共享的属性。每一个实例都有一份实例属性的副本,并且同时共享着对方法的引用,最大限度节省内存,还支持向构造函数传递参数。

  • 重写上面的例子
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
// 构造函数 定义实例的属性
function Person(name,age,job){
this.name=name;
this.age=age;
this.job=job;
this.friends=['icessun1','icessun2']; // 引用类型
}
// 原型对象 定义共享的方法 constructor属性
Person.prototype={
constructor:Person,
sayName:function(){
console.log(this.name);
}
}
var person1=new Person('icessun',18,'engineer');
var person2=new Person('icessun2',19,'engineer');
person1.friends.push('ben'); // 引用的不同的数组
console.log(person1.friends); // icessun1 icessun2 ben
console.log(person2.friends); // icessun1 icessun2
console.log(person1.friends === person2.friends); // false
console.log(person1.sayName === person2.sayName); // true
动态原型模式

把所有的信息都封装在构造函数中,通过在构造函数中初始化原型,保持了同时使用构造函数和原型的优点。可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 构造函数
function Person(name,age,job){
this.name=name;
this.age=age;
this.job=job;
this.friends=['icessun1','icessun2']; // 引用类型
// 方法 IF 检查的可以是初始化之后应该存在的任何属性和方法
if(typeof this.sayName != 'function'){
Person.prototype.sayName=function(){
console.log(this.name);
};
}
}
var person1=new Person('icessun',18,'engineer');
person1.sayName();

只有在sayName()方法不存在的时候,才会将这个方法添加到原型上,只会在初次调用构造函数的时候才会执行。这里对原型的修改,会立即在所有实例中得到反映。,这就导致不能使用字面量重写原型,因为会切断现有实例和新原型之间的联系。可以使用instanceof操作符来确定它的类型。

寄生构造函数模式 (重点)

基本思想:创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后在返回新创建的对象。为对象创建构造函数。比如想创建一个具有额外方法的数组
返回的对象与构造函数或者构造函数的原型属性之间没有关系;构造函数返回的对象与在构造函数外部创建的对象实例没有什么不同,是一样的,不能使用instanceof来确定对象类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 这个函数创建一个新的对象 并且初始化了对象 返回了这个对象
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 person=new Person('icessun',18,'Engineer');
person.sayName(); // 'icessun'

除了使用new操作符并且把包装函数叫做构造函数之外,这个模式和工厂模式是一模一样的。构造函数在不返回值的情况下,默认会返回新对象的实例;但是返回值的话,可以重写调用构造函数时返回的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function MyArray(){
// 创建数组对象
var values=new Array();
// 添加值,初始化数组的值 接收构造函数收到的所有参数
values.push.apply(values,arguments);
// 添加方法
values.toJoin=function(){
return this.join('|');
};
// 返回值
return values;
}
var colors=new MyArray('red','blue','green');
console.log(colors.toJoin()); // 'red|bule|green'

继承

ECMAScript只支持实现继承,主要依靠原型链来实现继承

原型链

基本思想: 利用原型让一个引用类型继承另外一个引用类型的属性和方法。

  • 原型与实例的关系

    每一个构造函数都有一个原型对象prototype,原型对象都包含一个指向构造函数的指针constructor,而实例都包含一个指向原型对象的内部指针__proto__

  • 原型链基本概念

    让一个原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含一个指向另一个构造函数的指针,如此,层层递进,就实现了实例与原型的链条。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 一个属性和方法
function SuperType(){
this.property=true;
}
SuperType.prototype.getSuperValue=function(){
return this.property;
};
// 一个属性和方法
function SubType(){
this.subproperty=false;
}
// 原型链继承
// 继承 SuperType 创建SuperType的实例,将其赋给SubType.prototype实现继承
SubType.prototype=new SuperType();
SubType.prototype.getSubValue=function(){
return this.subproperty;
};
var instance =new SubType();
console.log(instance.getSuperValue()); // true

这段代码继承实现的本质是重写原型对象,使其为一个新类型的实例。原来存在SuperType的实例中的所有方法和属性,现在都存在SubType.prototype中了。所以这个重写的原型对象,不但有指向SuperType的实例中的所有方法和属性的指针,还有一个指向SuperType的原型指针__proto__.

实例,构造函数,原型之间的关系

  • 默认的原型

    所有函数的默认原型都是Object的实例,因此默认的原型都包含一个内部指针,指向Object.prototype,也就是自定义类型都会继承toString(),valueOf()等默认方法的根本原因。

上面例子的完整原型链图

  • 确定原型和实例的关系

    • instanceof操作符

    主要这个操作符测试的实例与原型链出现过的构造函数,就返回true

    1
    console.log(instance instanceof SubType); // true
    • isPrototype()方法

      只要原型链出现的原型,都可以说是该原型链所派生的实例的原型,返回true

      1
      console.log(SubType.prototype.isProtype(instance)); // true
  • 子类重写父类里面的方法,是在原型上面修改的,这段代码一定要放在替换原型的语句后面。

  • 通过原型链实现的继承,不能使用对象字面量创建原型方法,因为会重写原型链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 原型链继承
SubType.prototype=new SuperType();
// 对象字面量创建原型方法 会导致上一行代码无效
SubType.prototype={
getSubValue: function(){
return this.subproperty;
},
someOtherMathod:function(){
return false;
}
};
var instance=new SubType();
console.log(instance.getSuperValue()); // error!

因为后面的字面量方式的代码使得原型包含一个Object 实例,不是SuperType实例。

  • 原型链的问题

    引用类型的问题,引用类型的原型属性会被所有实例共享,也就为什么要在构造函数里面定义属性的原因。通过原型来实现继承的时候,原型实际上会变成另一个类型的实例,故原来的实例属性也就顺理成章的变成现在的原型属性了;还有就是在创建子类型的实例时候,不能向超类的构造函数中传递参数。

-------------本文结束,感谢您的阅读------------

本文标题:javaScript高级程序设计读书笔记(六)

文章作者:icessun

发布时间:2017年08月10日 - 10:08

最后更新:2017年09月28日 - 10:09

原始链接:http://icessun.github.io/javaScript高级程序设计读书笔记(六).html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!
显示 Gitment 评论
0%