JavaScript中创建对象的7种模式和实现继承的6种方式

目录

前言

  JavaScript是一种面向对象(OO)的语言,但由于ECMAScript没有类的概念(ES6引入关键字class),所以在js中创建对象的方法也与其他基于类的语言有所不同。本篇总结在JavaScript中创建对象的7种方法;并在此基础上,总结实现继承的6种方式。


JavaScript中对象的基本概念

创建对象的2种基本方法


创建一个Object实例

1
2
3
4
5
6
var person = new Object();
person.name = "Bob";
person.age = 24;
person.greet = function(){
alert(this.name);
}

通过上述方法,创建了一个对象,并为其添加了3个属性,属性值有字符串,数字,函数。

对象字面量

1
2
3
4
5
6
7
var person = {
name: "Bob",
age: 24,
greet: function(){
alert(this.name);
}
}

创建的对象与上述相同,只不过使用{}的字面量方式创建。

属性与特性

  关于属性和特性的细节,可以参考JavaScript中attribute(特性)和property(属性)的对比分析。对象就是由众多的属性(property)构成的,而每个属性(property)又具有多个特性(attribute)。这些特性(attribute)是为了实现JavaScript引擎使用的,在JavaScript语言使用中不能直接访问它们。为了表示是内部值,将各个特性(attribute)放在两对方括号[[]]中表示。

数据属性的特性

1.[[Configurable]]

  • 名称:可配置性。
  • 默认值:true
  • 含义:能否通过delete删除属性并重新定义;能否修改属性的这几个特性;能否把数据属性修改为访问器属性。

2.[[Enumerable]]

  • 名称:可枚举性。
  • 默认值:true
  • 含义:能否通过通过for-in循环返回当前属性。

3.[[Writable]]

  • 名称:可写性。
  • 默认值:true
  • 含义:能否修改当前属性的值。

4.[[Value]]

  • 名称:值。
  • 默认值:undefined
  • 含义:读属性值,从这个位置读;写入属性时,保存在这个位置。

访问器属性的特性

1.[[Configurable]]

  • 名称:可配置性。
  • 默认值:true
  • 含义:能否通过delete删除属性并重新定义;能否修改属性的这几个特性;能否把访问器属性修改为数据属性。

2.[[Enumerable]]

  • 名称:可枚举性。
  • 默认值:true
  • 含义:能否通过通过for-in循环返回当前属性。

3.[[Get]]

  • 名称:值。
  • 默认值:undefined
  • 含义:读属性值时调用该函数。

4.[[Set]]

  • 名称:值。
  • 默认值:undefined
  • 含义:写属性值时调用该函数。

ES5新增的关于Object的几个实用方法

1.Object.defineProperty()
使用示例:

1
2
3
4
5
var person = {};
Object.defineProperty(person, "name",{
configurable: false,
value: "Bob"
})

2.Object.defineProperties()
使用示例:

1
2
3
4
5
6
7
8
9
10
11
var book = {};
Object.defineProperties(book, {
_year: {
writable: true,
value: 2016
},
edition: {
writable: true,
value: 1
}
})

3.Object.getOwnPropertyDescriptor()
使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var book = {};
Object.defineProperties(book, {
_year: {
writable: true,
value: 2016
},
edition: {
writable: true,
value: 1
}
})

var desc = Object.getOwnPropertyDescriptor(book, "_year");
console.log(desc.value); //2016

4.Object.getOwnPropertyNames()
5.Object.isPrototypeOf()
6.Object.create()
7.Object.keys()
8.XXX.getPrototypeOf()
9.XXX.hasOwnProperty()


创建对象的7种模式

工厂模式

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
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("A", 25, "teacher");
var person2 = createPerson("B", 26, "engineer");

说明:
1.函数createPerson()就是一个工厂,该工厂内部封装了生成对象的细节(新建对象实例并初始化等)

2.正如名字表示的,通过该函数可以批量创建多个同类型对象,而不必书写大量重复代码

3.工厂模式缺点:未解决对象识别问题—无法知道对象的类型

构造函数模式

示例:

1
2
3
4
5
6
7
8
9
10
11
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}

var person1 = new Person("A", 25, "teacher");
var person2 = new Person("B", 26, "engineer");

说明:
1.函数Person()未显式创建对象,且无返回值

2.使用new关键字调用构造函数

3.约定构造函数首字母大写

4.关于对象类型:

1
2
3
4
5
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true

alert(person1 instanceof Person); //true
alert(person2 instanceof Person); //true

5.如果构造函数不像

1
var person1 = new Person("A", 25, "teacher");

这样通过new关键字来调用,而是直接

1
Person("A", 25, "teacher");

也是可以使用的,只不过生成的对象会直接添加到宿主对象上,在浏览器中就是window对象上。

6.构造函数模式缺点:
由于将函数绑定到了特定的对象实例上面,每个方法都要在每个实例上重新创建一遍,浪费空间且并没有什么用处。
以下可以证明这一点:

1
alert(person1.sayName == person2.sayName); //false

为此,我们可以让构造函数创建的对象中的方法共享函数,也就是在方法属性处不显式声明函数,而只是写明一个函数引用,将这个函数移至构造函数外部。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}

function sayName(){
alert(this.name);
};

var person1 = new Person("A", 25, "teacher");
var person2 = new Person("B", 26, "engineer");

可是这种方法又会有新的缺点:

  • 身在全局作用于中的函数sayName()却只能用于让构造函数调用,让全局作用域名不副实
  • 如果构造函数有很多方法属性,那么就要在全局作用于中写很多函数,代码结构松散,毫无封装性可言

在这种情况下,考虑使用原型模式来解决。

原型模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true

原型模式的缺点:
1.所有实例在默认情况下都将取得相同的值。
2.由于共享属性,一旦一个对象修改,可能引发其他变量意外地受到影响。
如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
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"

以上方式是目前应用最广泛、认可度最高的方式。

动态原型模式

该种方式将所有信息封装在构造函数中,通过在构造函数中“有选择”地初始化原型,来保持构造函数模式+原型模式的有点。所谓的“有选择的”,是指针对工厂模式中多次创建实例上功能相同的函数的问题,在构造函数内部通过判断实例方法的类型(即是否存在)来保证实例方法只创建一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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();

寄生构造函数模式

这是一种和工厂模式非常类似的方法,只不过使用时要加上关键字new,要说明的是,在构造函数内部创建的对象与构造函数的原型没有什么关系,因此不能通过instanceof操作来判断由此创建的对象的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);

稳妥构造函数模式

与寄生模式相比,这种模式的实例方法不引用this,不用new操作符。在这种模式下,除了调用相应属性的get方法,无法访问内部变量,保证了安全性。

1
2
3
4
5
6
7
8
9
10
11
function Person(){       

var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(name);
};
return o;
}


实现继承的6种方式

原型链

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
28
29
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 instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true

alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true

原型链继承主要有以下两个问题:
1.当原型包含引用类型值,一个实例对变量的修改会影响到其他变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function SuperType(){
this.colors = ["red", "blue", "green"];
}

function SubType(){
}

//inherit from SuperType
SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green,black"

2.在创建子类型的实例时,没有办法在不影响所有实例的情况下向超类构造函数传递参数。

借用构造函数

核心:在子类型内部调用超类型构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SuperType(){
this.colors = ["red", "blue", "green"];
}

function SubType(){
//inherit from SuperType
SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"

存在的问题:
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
26
27
28
29
30
31
32
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

原型式继承

在给出一个想要作为基础的(即被继承)的对象的条件下,可以使用这种方式实现继承。但其问题和原型链继承类似,不同实例会共享属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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"

ES5的Object.create(obj)方法对此做出了规范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"

寄生式继承

该种方式和原型式继承紧密相关,思路与寄生构造函数和工厂模式类似。

1
2
3
4
5
6
7
function createAnother(original) {
var clone = object(original);
clone.sayHi = function() {
alert("hi");
}
return clone;
}

和构造函数模式类似,函数无法复用会降低效率。

寄生组合式继承

组合继承的问题在于,会调用两次超类型的构造函数:
1.创建声明子类型原型的时候,这时候会在子类型原型上具有一组超类型的实例变量。
2.调用子类型构造函数的时候,这时候会在子类型实例上具有一组超类型的实例变量,他们会屏蔽掉超类上的实例变量(原型链查找机制)
下面的寄生组合式继承可以避免这个问题:

1
2
3
4
5
function inheritPrototype(subType, superType ) {
var prototype = object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}

解释:
1.创建超类型的一个副本
2.添加constructor属性,从而弥补因为重写原型而失去的默认的prototype属性
3.将新创建的超类副本赋值给子类原型

好处:
只在var prototype = object(superType.prototype);时获取了超类型的属性,避免了在子类实例化时候重写这些不必要的属性。