说说JavaScript里面的面向对象编程

创建对象

Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)。

工厂模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"
alert(person1 instanceof createPerson) //false
alert(person1 instanceof Object) //true
alert(typeof person1) //object

缺点:无法解决对象识别问题

构造函数模式

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 Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
//必须使用new操作符
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
Person("Nicholas", 29, "Software Engineer"); //作为普通函数调用
windows.sayName(); //"Nicholas"
var o = new Object()
Person.call(o,"Nicholas", 29, "Software Engineer") //在另一个对象的作用域中调用
person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
alert(person1.sayName == person2.sayName); //false

问题:person1与person2的sayName()方法不是同一个Function实例。
可以这样解决。

1
2
3
4
5
6
7
8
9
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName; //这是指针,需是全局函数
}
function sayName(){
alert(this.name);
}

问题:分开写好麻烦,函数需是全局的。毫无封装性可言

原型模式

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象。而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true

新对象的属性和方法是由所有实例共享的。

理解原型对象

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。就拿前面的例子来说,Person.prototype.constructor指向Person而通过这个构造函数我们还可继续为原型对象添加其他属性和方法。



1
2
3
4
5
6
7
8
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
//only works if Object.getPrototypeOf() is available
//返回[[Prototype]]
if (Object.getPrototypeOf){
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas"
}

创建了自定义的构造函数之后,其原型对像默认只会取得constructor属性;至于其他方法,则都是从Object继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针[[Prototype]](内部属性),指向构造函数的原型对象。在脚本中没有标准的方式访问[[Prototype]], Flrefox、Safan和Chrome在每个对象上都支持一个属性proto;而在其他实现中,这个属性对脚本则是完全不可见的。这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。

首先寻找实例的属性,然后再寻找原型的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name); //"Greg" ?from instance
alert(person2.name); //"Nicholas" ?from prototype

使用hasOwnProperty()方法检测一个属性是存在实例中,还是存在原型中。
in操作符,无论属性存在示例中还是原型中

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
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
person1.name = "Greg";
alert(person1.name); //"Greg" ?from instance
alert(person1.hasOwnProperty("name")); //true
alert("name" in person1); //true
alert(person2.name); //"Nicholas" ?from prototype
alert(person2.hasOwnProperty("name")); //false
alert("name" in person2); //true
delete person1.name;
alert(person1.name); //"Nicholas" - from the prototype
alert(hasPrototypeProperty(person, "name")); //false
alert(person1.hasOwnProperty("name")); //false
alert(hasPrototypeProperty(person1, "name")); //true
alert("name" in person1); //true

在使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性屏蔽了原型中不可枚举属性(即将[[Enumerable]]标记为false属性)的实例属性也会在for-in循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的一一只有在IE8及更早版本中外。
Object.keys()
接受一个对象作为参数,返回一个包含所有可枚举属性的字符串数组

1
2
var keys = Object.keys(Person.prototype);
alert(keys); //"name,age,job,sayName"

Obejct.getOwnProperyNames()
取得所有实例属性,无论是否枚举

1
2
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); //"constructor,name,age,job,sayName"

更简单的原型语法

1
2
3
4
5
6
7
8
9
10
11
function Person(){
}
Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
var friend = new Person();

在上面的代码中我们将person.prototype设置为等于一个以对象字面量形式创建的新对象最终结果相同,但有一个例外:constructor属性不再指向person了。前面曾经介绍过,每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。而我们在这里使用的语法,本质上完全重写了默认的prototype对象因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向person函数。此时,尽管instanceof操作符还能返回正确的结果,但通过constructor已经无法确定对象的类型了

1
2
3
4
alert(friend instanceof Object); //true
alert(friend instanceof Person); //true
alert(friend.constructor == Person); //false
alert(friend.constructor == Object); //true

重设constructor属性会导致他的[[Enumerable]]特性被设为true。默认是false

1
2
3
4
5
6
7
8
9
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};

问题:原型中所有属性都是被实例们共享的。对于那些包含基本值的属性还说得过去(通过在实例上添加一个同名属性,可以隐藏原型中对应的属性),然而对于包含引用类型值的属性来说就有问题了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person(){
}
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
friends : ["Shelby", "Court"],
sayName : function () {
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van"
alert(person1.friends === person2.friends); //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 = ["Shelby", "Court"];
}
Person.prototype = {
constructor: Person,
sayName : function () {
alert(this.name);
}
};
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true

动态原型模式

把所有信息息都封装在了构造函数中,而通过在构造数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name, age, job){
//properties
this.name = name;
this.age = age;
this.job = job;
//methods
if (typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

寄生构造函数模式

创建一个函数,该函数的作用仅仅是封装对象的代码,然后返回新创建的对象

1
2
3
4
5
6
7
8
9
10
11
12
function Person(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"

这个模式可以在特殊情况下用来为对象创建构造函数。例如,由于不能直接修改Array构造函数,因此可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SpecialArray(){
//create the array
var values = new Array();
//add the values
values.push.apply(values, arguments);
//assign the method
values.toPipedString = function(){
return this.join("|");
};
//return it
return values;
}
var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green"
alert(colors instanceof SpecialArray);//false

返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。因此,不能用instanceof来确定对象类型。

稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性。

1
2
3
4
5
6
7
function Person(name, age, job){
var o = new Object();
o.sayName = function(){
alert(this.name);
};
return o;
}

在以这种模式创建的对象中,除了使用方法之外,没有其他办法访问属性值。
在某些情况下使用。

继承

通常OO语言都支持两种继承方式:
接口继承:只继承方法签名
实现继承:继承实际的方法

原型链

基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
假如我们让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。如此层层递进。




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;
}
//inherit from SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
alert(instance.getSubValue()); //false
alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true
//只要原型链中出现过的原型,都为true
alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true

实例的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于SuperType的实例中的所有属性和方法,现在也存在于SubType.prototype中。只不过子类中同名属性会覆盖父类中的同名属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.property = false;
}
//inherit from SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
return this.property;
};
var instance = new SubType();
alert(instance.getSuperValue()); //false
alert(instance.getSubValue()); //false

所有的函数的默认原型都是Object的实例,因为默认原型都会有一个内部指针,指向Object.prototype。



重写父类的方法

1
2
3
4
//override existing method
SubType.prototype.getSuperValue = function (){
return false;
};

使用对象字面量创建原型方法,会重写原型链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//inherit from SuperType
SubType.prototype = new SuperType();
//try to add new methods ?this nullifies the previous line
SubType.prototype = {
getSubValue : function (){
return this.subproperty;
},
someOtherMethod : function (){
return false;
}
};
var instance = new SubType();
alert(instance.getSuperValue()); //error!

原型链的问题

  1. 包含引用类型值的原型属性会被所有实例共享
  2. 在创建子类型的实例时,不能向父类的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给父类的构造函数传递参数

借用构造函数

在子类构造函数的内部调用父类的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function SuperType(name){
this.name = name;
this.sayName = function () {
return this.name
}
}
function SubType(){
//在新创建的SubType实际的环境下调用了SuperType的构造函数,SubType的每个实例都会有自己的属性副本
SuperType.call(this, "Nicholas");
//instance property
this.age = 29;
}
var instance = new SubType();
alert(instance.name); //"Nicholas";
alert(instance.age); //29
var instance = new SubType();
var instance2 = new SubType()
alert(instance.sayName === instance2.sayName) //false

问题:

  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
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){
alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
alert(instance.sayName === instance2.sayName) //true

原型式继承

先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。
本质上,object()对传入其中的对象执行了一次浅复制

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 object(o) {
function F() {}
F.prototype = o
return new F()
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
Object.create()方法
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
alert(anotherPerson.name); //"Greg"

在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下使用。
包含引用类型值的属性始终都会共享相应的值

寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后像真的是它做了所有工作一样返回对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 相当于继承
function object(o) {
function F() {}
F.prototype = o
return new F()
}
function createAnother(original) {
var clone = object(original)
clone.sayHi= function () {
alert("hi")
}
return clone
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person)
anotherPerson.sayHi()

问题:不能函数复用

寄生组合式继承

组合式继承最大的问题就是无论什么情况下,都会调用两次构造函数:一次是在创建子类原型的时候,另一次是在子类构造函数内部。

1
2
3
4
5
6
7
8
9
10
11
12
13
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
SubType.prototype = new SuperType();
//详见组合继承的代码

第一次调用SuperType构造函数时。SubType.prototype会得到两个属性,name和colors;它们是SuperType的实例属性
第二次调用SuperType构造函数时。在新对象上创建了实例属性name和colors,这两个属性屏蔽了原型中的两个同名属性




寄生组合式继承:通过借用构造函数的来继承属性,通过原型链的混成形式来继承办法。
不必为了指定子类的原型而调用父类的构造函数,我们所需要的无非就是父类原型的一个副本。本质上,使用寄生式继承来继承父类的原型,然后再讲结果指定给子类的原型。
1
2
3
4
5
6
7
8
// 用来代替SubType.prototype = new SuperType();
// subType.prototype=superType.prototype
function inheritPrototype(subType,superType){
var prototype=object(superType.prototype)
// 弥补因重写原型而失去的默认constructor
prototype.constructor=subType
subType.prototype=prototype
}

只调用一次父类构造函数,原型链保持不变,正常使用instanceof 和isPrototypeOf

文章目录
  1. 1. 创建对象
    1. 1.1. 工厂模式
    2. 1.2. 构造函数模式
    3. 1.3. 原型模式
      1. 1.3.1. 理解原型对象
      2. 1.3.2. 更简单的原型语法
    4. 1.4. 组合使用构造函数模式和原型模式
    5. 1.5. 动态原型模式
    6. 1.6. 寄生构造函数模式
    7. 1.7. 稳妥构造函数模式
  2. 2. 继承
    1. 2.1. 原型链
      1. 2.1.1. 重写父类的方法
      2. 2.1.2. 使用对象字面量创建原型方法,会重写原型链。
    2. 2.2. 原型链的问题
    3. 2.3. 借用构造函数
    4. 2.4. 组合继承
    5. 2.5. 原型式继承
    6. 2.6. 寄生式继承
    7. 2.7. 寄生组合式继承
|