前言
JavaScript是一种面向对象(OO)的语言,但由于ECMAScript没有类的概念(ES6引入关键字class),所以在js中创建对象的方法也与其他基于类的语言有所不同。本篇总结在JavaScript中创建对象的7种方法;并在此基础上,总结实现继承的6种方式。
JavaScript中对象的基本概念
创建对象的2种基本方法
创建一个Object实例
1 | var person = new Object(); |
通过上述方法,创建了一个对象,并为其添加了3个属性,属性值有字符串,数字,函数。
对象字面量
1 | var person = { |
创建的对象与上述相同,只不过使用{}
的字面量方式创建。
属性与特性
关于属性和特性的细节,可以参考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
5var person = {};
Object.defineProperty(person, "name",{
configurable: false,
value: "Bob"
})
2.Object.defineProperties()
使用示例:1
2
3
4
5
6
7
8
9
10
11var 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
14var 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
13function 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
11function 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
5alert(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
13function 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 | function Person(){ |
原型模式的缺点:
1.所有实例在默认情况下都将取得相同的值。
2.由于共享属性,一旦一个对象修改,可能引发其他变量意外地受到影响。
如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function 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
13function 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
19function 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
21function 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
11function Person(){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(name);
};
return o;
}
实现继承的6种方式
原型链
1 | function SuperType(){ |
原型链继承主要有以下两个问题:
1.当原型包含引用类型值,一个实例对变量的修改会影响到其他变量:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function 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
15function 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
32function 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
20function 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
14var 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
7function createAnother(original) {
var clone = object(original);
clone.sayHi = function() {
alert("hi");
}
return clone;
}
和构造函数模式类似,函数无法复用会降低效率。
寄生组合式继承
组合继承的问题在于,会调用两次超类型的构造函数:
1.创建声明子类型原型的时候,这时候会在子类型原型上具有一组超类型的实例变量。
2.调用子类型构造函数的时候,这时候会在子类型实例上具有一组超类型的实例变量,他们会屏蔽掉超类上的实例变量(原型链查找机制)
下面的寄生组合式继承可以避免这个问题:
1 | function inheritPrototype(subType, superType ) { |
解释:
1.创建超类型的一个副本
2.添加constructor属性,从而弥补因为重写原型而失去的默认的prototype属性
3.将新创建的超类副本赋值给子类原型
好处:
只在var prototype = object(superType.prototype);时获取了超类型的属性,避免了在子类实例化时候重写这些不必要的属性。