JavaScript--面向对象与原型(15)

对象

ECMA-262把对象(object)定义为“属性的无序集合,每个属性存放一个原始值、对象或函数”。

面向对象语言的要求:

一种面向对象语言需要向开发者提供四种基本能力:

  1. 封装-把相关的信息(无论数据或方法)存储在对象中的能力
  2. 聚集-把一个对象存储在另一个对象内的能力
  3. 继承-由一个类(或多个类)得来类的属性和方法的能力
  4. 多态-编写能以多种方法运行的函数或方法的能力

对象的构成

在ECMAScript中,对象由特性(attribute)构成,特性可以是原始值,也可以是引用值。如果特性存放的是函数,它将被看作对象的方法(method),否则该特性被看作是对象的方法(property)。

一、创建对象

1.普通的创建对象

创建一个对象,然后给这个对象新的属性和方法

1
2
3
4
5
6
7
8
var box = new Object();              // 创建一个Object对象;
box.name = 'lee'; // 创建一个name属性并赋值;
box.age = 100;
box.run = function(){ // 创建一个run()方法并返回值;
return this.name+this.age+'运行中...';
}
console.log(box.run()); // 输入属性和方法的值;
// 缺点:想创建类似的对象,就会产生大量的代码;

2.工厂模式创建对象

这种方法就是为了解决实例化对象产生大量代码重复的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createObject(name,age){        // 集中创建函数体;
var obj = new Object;         // 函数体内创建Object;
obj.name = name;
obj.age = age;
obj.run = function(){
return this.name+this.age+"运行中...";
};
return obj;
}
var box1 = createObject("lee",100); // 实例化;调用函数并传参;
var box2 = createObject("jack",200); // 实例二;
console.log(box1.run()+box2.run()); // 实例保持相对独立;
// 缺点:对象与实例的识别问题;无法搞清楚它们到底是那个对象的实例;
console.log(typeof box1); // Object;

3.构造函数创建对象

ECMAScript采用构造函数可用来创建特定的对象

1
2
3
4
5
6
7
8
9
10
11
function Box(name,age){          // 构造函数模式;
this.name = name;           // this代表对象Box;
this.age = age;
this.run = function(){
return this.name+this.age+"运行中...";
};
}
var box1 = new Box("lee",100); // 要创建对象的实例必须用new操作符;
var box2 = new Box("jack",200); // box1和box2都是Box对象的实例;
console.log(box1 instanceof Box); // true;很清晰的识别box1从属于Box;
// 使用构造函数,即解决了重复实例化的问题,又解决了对象识别的问题;

构造函数与工厂模式不同之处:

  • 构造函数没有显式的创建对象(new Object)
  • 构造函数直接将属性和方法赋值给this对象
  • 构造函数没有return语句

构造函数规范:

  • 函数名和实例化构造名相同且首字母大写
  • 通过构造函数创建实例对象,必须使用new运算符

构造函数问题:

  • 使用构造函数创建每个实例的时候,构造函数里的方法都要在每个实例上重新创建一遍
  • 因为ECMAScript中的函数是对象,因此每定义一个函数,也就是实例化了一个对象
  • 以这种方式创建函数,会导致不同的作用域链和标识符解析

原型

我们创建的么个函数都有一个prototype(原型)属性,这个属性是一个对象
用途:它包含可以由特定类型的所有实例共享的属性和方法
理解:prototype是通过调用构造函数创建的那个对象的原型对象
好处:可以让所有对象实例共享它所包含的属性和方法
也就是说,不必在构造函数中定义对象信息(属性/方法),而是可以直接将这些信息添加到原型中

1.原型模式(prototype中添加属性和方法)

1
2
3
4
5
6
7
8
9
10
11
12
function Box(){}                                // 声明构造函数;
Box.prototype.name = 'Lee'; // 在原型里添加属性和方法;
Box.prototype.age = 100;
Box.prototype.run = function() {
return this.name+this.age+'运行中...';
};
var box1 = new Box();
var box2 = new Box();
console.log(box1.run==box2.run); // =>true;方法引用的地址保持一致;
// 在原型中多了两个属性,这两个原型属性都是创建对象时自动生成的;
// 1.__proto__:构造函数指向原型对象的一个指针;它的作用:指向构造函数的原型的属性constructor;
// IE浏览器在脚本访问__proto__会不能识别;


判断一个实例对象是否指向了该构造函数的原型对象,可以使用isPrototypeOf()方法来测试;

1
console.log(Box.prototype.isPrototypeOf(box));    // =>true; 只要实例化对象,即都会指向;

原型模式的执行流程

  1. 先查找构造函数对象的实例里的属性或方法,若有,立刻返回;
  2. 若构造函数对象的实例里没有,则去它的原型对象里找,若有,就返回;

2.原型与in操作符

如何判断属性是在构造函数的实例里,还是在原型里,可以使用hasOwnProperty()函数来验证

1
console.log(box.hasOwnProperty('name'));            // 实例里若有返回true,否则返回false;

in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在与实例中还是原型中

1
console.log('name' in box);                         // =>true,存在实例中或原型中;

3.更简单的原型语法(原型+字面量模式)

1
2
3
4
5
6
7
8
function Box(){};
Box.prototype = { // 以字面量形式创建包含属性和方法的新对象;
name:'Lee',
age:100,
run:function(){
return this.name+this.age+'运行中...';
}
};

创建使用构造函数原型对象和使用字面量创建原型对象在使用上基本相同;
但是,使用字面量创建的原型对象使用constructor属性不会指向实例,而是指向原型对象Object;构造函数的方式则相反;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var box = new Box();
console.log(box instanceof Box);
console.log(box instanceof Object);
console.log(box.constructor == Box); // 字面量方式,返回false;
console.log(box.constructor == Object); // 字面量方式,返回true;
// 如果想让字面量方式的constructor指向实例对象:
Box.prototype = {
constructor:Box, // 直接强制指向即可;
}

// PS:字面量方式为什么constructor会指向Object?
// 因为Box.prototype={}这种字面量写法就是创建一个新对象;

// 而每创建一个函数,就会同时创建它的prototype,这个对象也会自动获取constructor属性;
// 所以,新对象的constructor重写了Box原来的constructor,因此指向了新对象,
// 那个新对象没有指定构造函数,那么就默认为是Object;

4.原型的动态性(重写会覆盖之前的内容)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Box(){};
Box.prototype = {
constructor:Box,
name:'Lee',
age:100,
run:function(){

return this.age+'运行中...';
}
};
Box.prototype = { // 原型重写了,覆盖了之前的原型;
age:200,
run:function(){
return this.age+'运行中...';
}
}
var box = new Box();
console.log(box.run()); // =>200运行中...;
// 重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;对象实例引用的仍然是最初的原型;

5.原生对象的原型

原型对象不仅仅可以在自定义对象的情况下使用,而是ECMAScript内置的引用类型都可以使用这种方式,并且内置的引用类型本身也是用了原型;

1
2
console.log(Array.prototype.sort);                // =>function sort() { [native code] };
console.log(String.prototype.substring); // =>function substring() { [native code] };

6.原型对象的问题

原型模式创建对象缺点:省略了构造函数传参初始化这一过程,带来的缺点就是初始化的值都是一致的
优点:属性共享;但是,如果原型中属性包含引用类型(对象),共享就会存在一定问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Box(){};
Box.prototype = {
constructor:Box,
name:'Lee',
age:100,
family:['father','mother'],
run:function(){

return this.name+this.age+this.family;
}
};
var box1 = new Box();
box1.family.push('sister'); // 为box1的family属性添加了sister;而这个属性被共享到原型了;
console.log(box1.run()); // =>Lee100father,mother,sister;
var box2 = new Box();
console.log(box2.run()); // =>Lee100father,mother,sister;
// 数据共享导致实例化出的数据不能保存自己的特性;

7.组合使用构造函数模式(对象不共享的数据)和原型模式(对象共享的数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 为了解决构造传参和共享问题,组合构造函数+原型模式:
function Box(name,age){ // 不共享的使用构造函数;
this.name = name;
this.age = age;
this.family = ['father','moter'];
};
Box.prototype = { // 共享的使用原型模式;
constructor:Box,
run:function(){

return this.name+this.age+this.family;
}
};
// PS:这种混合模式很好的解决了传参和引用共享的大难题;是创建对象比较好的方法;

8.动态原型模式(将原型封装到构造函数里)

原型模式,不管是否调用了原型中的共享方法,它都会初始化原型中的方法;
并且在声明一个对象时,构造函数+原型让人感觉怪异;最好把构造函数和原型封装到一起;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Box(name,age){                            // 将所有信息封装到构造函数体内;
this.name = name;
this.age = age;
// 当第一次调用构造函数时,run()方法不存在,然后执行初始化原型;
// 当第二次调用,就不会初始化,并且第二次创建新对象,原型也不会载初始化;
// 这样既得到了封装,又实现了原型方法共享,并且属性都保持独立;
if(typeof this.run != 'function'){ // 仅在第一次调用时初始化;
Box.prototype.run = function (){
return this.name+this.age+'运行中...';
};
}
};
var box = new Box('lee',10);
console.log(box.run());
// PS:使用动态原型模式,要注意一点,不可以再使用字面量的方式重写原型,因为会切断实例和新原型之间的联系;

9.寄生构造函数

寄生构造函数,其实就是工厂模式+构造模式;这种模式比较通用,但不能确定对象关系

1
2
3
4
5
6
7
8
9
function Box(name,age){
var obj = new Object();
obj.name = name;
obj.age = age;
obj.run = function (){
return this.name+this.age+'运行中...';
};
return obj;
}

PS:闭包下的私有变量模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(function(){
var name = "";
People = function(val){
name = val;
};
People.prototype = {
constructor : People,
getName : function(){

return "People's name is " + name;
}
}
})();
var webdesinger = new People("guosk");
console.log(webdesinger.name); // undefined
console.log(webdesinger.getName()); // People's name is guosk