注意

这篇文章仅仅是我个人对于JavaScript对象的理解,并不是教程。这篇文章写于我刚了解js对象之后。文章肯定有错误之处,还望读者费心指出,在下方评论即可^-^

什么是JavaScript对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var person = {   //person就是对象,对象都有各种属性,每个属性又都对应着自己的值
//键值对形式
name: "Mofan",//可以包含字符串
age: 20,//数字
parents: [ //数组
"Daddy",
"Mami",
]
sayName: function(){ //函数
console.log(this.name);
},
features: { //甚至是对象(很少用,我是没见过)
height: "178cm",
weight: "60kg",
}
}

js里除了基本类型外所有事物都是对象:

  • 函数是对象function sayName(){} ——sayName是函数对象
  • 数组是对象var arr = new Array() ——arr是数组对象

为什么JavaScript要这么设计呢?我觉得首先这样一来,统一了数据结构,使JavaScript成为一门编程风格非常自由化的脚本语言:无论定义什么变量,统统var;其次,JavaScript对象都有属性和方法,函数数组都是对象,调用引用就会非常灵活方便;再者,为了构建原型链?

创建对象的几种方式

  • Object()模式
    使用对象字面量:var obj={...}就像上面那样
    或者使用原生构造函数Object():
1
2
3
4
5
6
7
var person = new Object();
person.name = "Mofan";
person.sayName = function(){
console.log(this.name);
};
console.log(person.name);//Mofan
obj.sayName();//Mofan
  • 利用函数作用域使用自定义构造函数模式模仿类(构造器模式):
1
2
3
4
5
6
7
8
9
10
function Person(name,age){
this.name = name;
this.age = age;
this.print = function(){
console.log(this.name + this.age)
};
}
var person = new Person("Mofan",19);
console.log(person.name+person.age);//Mofan19
person.print();//Mofan19
  • 原型模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person(){}
//可以这样写
/*Person.prototype.name = "Mofan";
Person.prototype.age = 19;
Person.prototype.print = function(){
console.log(this.name+this.age);
}*/
//推荐下面这样写,但两种方式不能混用!因为下面这种方式实际上重写了
//Person原型对象,如果两者混用,后面赋值方式会覆盖前面赋值方式
Person.prototype = {
name:"Mofan",
age:19,
print:function(){
console.log(this.name+this.age);
}
}
var person = new Person();
console.log(person.name+person.age);//Mofan19
person.print();//Mofan19
  • 组合构造函数模式和原型模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(name,age){
//这里面初始化属性
this.name = name;
this.age = age;
...
}
Person.prototype = {
//这里面定义公有方法
print:function(){
console.log(this.name+this.age);
},
...
}
var person = new Person("Mofan",19);
console.log(person.name+person.age);//Mofan19
person.print();//Mofan19
  • 动态创建原型模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(name,age){
//初始化属性
this.name = name;
this.age = age;
//在创建第一个对象(第一次被调用)时定义所有公有方法,以后不再调用
if(typeof this.print !="function"){
Person.prototype.print =function(){
console.log(this.name+this.age);
};
Person.prototype.introduction=function(){
console.log("Hi!I'm "+this.name+",I'm "+this.age);
};
//如果采用对象字面量对原型添加方法的话,第一次创建的对象将不会有这些方法
};


}
var person = new Person("Mofan",19);
person.print();//Mofan19
person.introduction();//Hi!I'm Mofan,I'm 19

还有一些模式用的场景比较少

这些模式的应用场景

怎么会有这么多的创建模式?其实是因为js语言太灵活了,因此前辈们总结出这几种创建方式以应对不同的场景,它们各有利弊。

  • 第一种方式,使用字面量或者使用构造函数Object()常用于创建普通对象存储数据等。它们的原型都是Object,彼此之间没有什么关联。事实上,下面创建方式都是一样的:
1
2
3
4
5
6
var o1 = {};//字面量的表现形式
var o2 = new Object;
var o3 = new Object();
var o4 = new Object(null);
var o5 = new Object(undefined);
var o6 = Object.create(Object.prototype);//等价于 var o = {};//即以 Object.prototype 对象为一个原型模板,新建一个以这个原型模板为原型的对象
  • 第二种方式,利用函数作用域模仿类,这样就可以在创建对象时传参了,可以创建不同属性值得对象,实现对象定制。不过print方法也定义在了构造函数里面,如果要把它当做公有方法的话,这样每new一个对象,都会有这个方法,太浪费内存了。可以这样修改一下构造器模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//构造器方法2
function print(){ //定义一个全局的 Function 对象,把要公有的方法拿出来
console.log(this.name + this.age);
}

function Person(name,age){
this.name = name;
this.age = age;

this.print = print.bind(this);//每个 Person 对象共享同一个print 方法版本(方法有自己的作用域,不用担心变量被共享)
}
var person = new Person("Mofan",19);
console.log(person.name+person.age);//Mofan19
person.print();//Mofan19

然而这样看起来很乱,也谈不上类的封装性。还是使用原型吧

  • 第三种方式,纯原型模式,不管是属性还是方法都添加到原型里面去了,这样做好处是很省内存,但是应用范围就少了,更多的对象 内部的属性是需要定制的,而且一旦更改原型,所有这个原型实例都会跟着改变。因此可以结合构造函数方式来实现对对象的定制,于是就有了

  • 第四种方式——组合构造函数模式与原型模式,可以定制的放在构造器里,共有的放在原型里,这也符合构造器和原型的特性。

    “这是es5中使用最广泛、认同度最高的创建自定义类型的方法”`—《JavaScript高级程序设计》第三版

  • 第五种方式,动态原型模式,出现这种方式是因为有些面向对象开发人员习惯了类构造函数,于是对这种独立出来的构造函数和原型感到困惑和不习惯。于是,就出现了把定义原型也写进构造函数里的动态原型模式。
    上面在动态原型模式程序里面讲“如果采用对象字面量对原型添加方法的话,第一次创建的对象将不会有这些方法”这是因为在if语句执行以前,第一个对象已经被创建了,然后执行if里面的语句,如果采用对象字面量给原型赋值,就会导致原型在实例创建之后被重写,创建的第一个实例就会失去与原型的链接,也就没有原型里的方法了。不过以后创建的对象就可以使用原型里的方法了,因为它们都是原型被修改后创建的。

原型是什么

在JavaScript中,原型就是一个对象,没必要把原型和其他对象区别对待,只是通过它可以实现对象之间属性的继承。任何一个对象也可以成为原型。之所以经常说对象的原型,实际上就是想找对象继承的上一级对象。对象与原型的称呼是相对的,也就是说,一个对象,它称呼继承的上一级对象为原型,它自己也可以称作原型链下一级对象的原型。

一个对象内部的[[Prototype]]属性生来就被创建,它指向继承的上一级对象,称为原型。函数对象内部的prototype属性也是生来就被创建(只有函数对象有prototype属性),它指向函数的原型对象(不是函数的原型!)。
当使用var instance = new Class();这样每new一个函数(函数被当做构造函数来使用)创建实例时,JavaScript就会把这个原型的引用赋值给实例的原型属性,于是实例内部的[[Prototype]]属性就指向了函数的原型对象,也就是prototype属性。

原型真正意义上指的是一个对象内部的[[Prototype]]属性,而不是函数对象内部的prototype属性,这两者之间没有关系!对于一个对象内部的[[Prototype]]属性,不同浏览器有不同的实现:

1
2
3
4
5
6
7
8
9
10
 var a = {}; 

//Firefox 3.6+ and Chrome 5+
Object.getPrototypeOf(a); //[object Object]

//Firefox 3.6+, Chrome 5+ and Safari 4+
a.__proto__; //[object Object]

//all browsers
a.constructor.prototype; //[object Object]

之所以函数对象内部存在prototype属性,并且可以用这个属性创建一个原型,是因为这样以来,每new一个这样的函数(函数被当做构造函数来使用)创建实例,JavaScript就会把这个原型的引用赋值给实例的原型属性,这样以来,在原型中定义的方法等都会被所有实例共用,而且,一旦原型中的某个属性被定义,就会被所有实例所继承(就像上面的例子)。这种操作在性能和维护方面其意义是不言自明的。这也正是构造函数存在的意义(JavaScript并没有定义构造函数,更没有区分构造函数和普通函数,是开发人员约定俗成)。下面是一些例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = {}    //一个普通的对象
function fun(){} //一个普通的函数
//普通对象没有prototype属性
console.log(a.prototype);//undefined
console.log(a.__proto__===Object.prototype);//true

//只有函数对象有prototype属性
console.log(fun.prototype);//Object
console.log(fun.__proto__===Function.prototype);//true

console.log(fun.prototype.__proto__===Object.prototype);//true
console.log(fun.__proto__.__proto__===Object.prototype);//true
console.log(Function.prototype.__proto__===Object.prototype);//true
console.log(Object.prototype.__proto__);//null

当执行console.log(fun.prototype);输出为

1
2
3
// 一个对象, 对象包含constructor、__proto__两个属性
constructor:ƒ fun()
__proto__:Object

可以看到,每创建一个函数,就会创建prototype属性,这个属性指向函数的原型对象(不是函数的原型),并且这个原型对象会自动获得constructor属性,这个属性是指向prototype属性所在函数的指针。而__proto__属性是每个对象都有的。

接着上面再看:

1
2
3
4
5
6
7
8
9
10
11
12
function Person(){}//构造函数,约定首字母大写
var person1 = new Person();//person1为Person的实例

console.log(person1.prototype);//undefined
console.log(person1.__proto__===Person.prototype);//true
console.log(person1.__proto__.__proto__===Object.prototype);//true
console.log(person1.constructor);//function Person(){}

//函数Person是Function构造函数的实例
console.log(Person.__proto__===Function.prototype);//true
//Person的原型对象是构造函数Object的实例
console.log(Person.prototype.__proto__===Object.prototype);//true

person1和上面那个普通的对象a有区别,它是构造函数Person的实例。前面讲过:

当使用var instance = new Class();这样每new一个函数(函数被当做构造函数来使用)创建实例时,JavaScript就会把这个原型的引用赋值给实例的原型属性,于是实例内部的[[Prototype]]属性就指向了函数的原型对象,也就是prototype属性。

因此person1内部的[[Prototype]]属性就指向了Person的原型对象,然后Person的原型对象内部的[[Prototype]]属性再指向Object.prototype,相当于在原型链中加了一个对象。通过这种操作,person1就有了构造函数的原型对象里的方法。

另外,上面代码console.log(person1.constructor);//function Person(){}中,person1内部并没有constructor属性,它只是顺着原型链往上找,在person1.__proto__里面找到的。

可以用下面这张图理清原型、构造函数、实例之间的关系:
js原型关系图

继承

JavaScript并没有继承这一现有的机制,但可以利用函数、原型、原型链模仿。
下面是三种继承方式:

类式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//父类
function SuperClass(){
this.superValue = "super";
}
SuperClass.prototype.getSuperValue = function(){
return this.superValue;

};
//子类
function SubClass(){
this.subValue = "sub";
}
//类式继承,将父类实例赋值给子类原型,子类原型和子类实例可以访问到父类原型上以及从父类构造函数中复制的属性和方法
SubClass.prototype = new SuperClass();
//为子类添加方法
SubClass.prototype.getSubValue = function(){
return this.subValue;
}

//使用
var instance = new SubClass();
console.log(instance.getSuperValue);//super
console.log(instance.getSubValue);//sub

这种继承方式有很明显的两个缺点:

  • 实例化子类时无法向父类构造函数传参
  • 如果父类中的共有属性有引用类型,就会在子类中被所有实例所共用,那么任何一个子类的实例更改这个引用类型就会影响其他子类实例,可以使用构造函数继承方式解决这一问题

构造函数继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 //父类
function SuperClass(id){
this.superValue = ["big","large"];//引用类型
this.id = id;
}
SuperClass.prototype.getSuperValue = function(){
return this.superValue;

};
//子类
function SubClass(id){
SuperClass.call(this,id);//调用父类构造函数并传参
this.subValue = "sub";
}
var instance1 = new SubClass(10);//可以向父类传参
var instance2 = new SubClass(11);

instance1.superValue.push("super");
console.log(instance1.superValue);//["big", "large", "super"]
console.log(instance1.id);//10
console.log(instance2.superValue);["big", "large"]
console.log(instance2.id);//11
console.log(instance1.getSuperValue());//error

这种方式是解决了类式继承的缺点,不过在代码的最后一行你也看到了,没有涉及父类原型,因此违背了代码复用的原则。所以组合它们:

组合继承

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
function SuperClass(id){
this.superValue = ["big","large"];//引用类型
this.id = id;
}
SuperClass.prototype.getSuperValue = function(){
return this.superValue;

};
//子类
function SubClass(id,subValue){
SuperClass.call(this,id);//调用父类构造函数并传参
this.subValue = subValue;
}
SubClass.prototype = new SuperClass();
SubClass.prototype.getSubValue = function(){
return this.subValue;
}

var instance1 = new SubClass(10,"sub");//可以向父类传参
var instance2 = new SubClass(11,"sub-sub");

instance1.superValue.push("super");
console.log(instance1.superValue);//["big", "large", "super"]
console.log(instance1.id);//10
console.log(instance2.superValue);["big", "large"]
console.log(instance2.id);//11
console.log(instance1.getSuperValue());["big", "large", "super"]
console.log(instance1.getSubValue());//sub
console.log(instance2.getSuperValue());//["big", "large"]
console.log(instance2.getSubValue());//sub-sub

嗯,比较完美了,但是有一点,父类构造函数被调用了两次,这就导致第二次调用也就是创建实例时重写了原型属性,原型和实例都有这些属性,显然性能并不好。先来看看克罗克福德的寄生式继承:

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 createAnnther(original){
var clone = object(original);
clone.sayName = function(){
console.log(this.name);
}
return clone;
}
var person = {
name:"Mofan",
friends:["xiaoM","Alice","Neo"],
};
var anotherPerson = createAnnther(person);
anotherPerson.sayName();//"Mofan"
}

就是让一个已有对象变成新对象的原型,然后再在createAnother函数里加强。你也看到了,person就是一个普通对象,所以这种寄生式继承适合于根据已有对象创建一个加强版的对象,在主要考虑通过已有对象来继承而不是构造函数的情况下,这种方式的确很方便。但缺点也是明显的,createAnother函数不能复用,我如果想给另外一个新创建的对象定义其他方法,还得再写一个函数。仔细观察一下,其实寄生模式就是把原型给了新对象,对象再加强。

等等,写到这个地方,我脑子有点乱,让我们回到原点:继承的目的是什么?应该继承父类哪些东西?我觉得取决于我们想要父类的什么,我想要父类全部的共有属性(原型里)并且可以自定义继承的父类私有属性(构造函数里)!前面那么多模式它们的缺点主要是因为这个:

1
SubClass.prototype = new SuperClass();

那为什么要写这一句呢?是只想要继承父类的原型吗?如果是为什么不这么写:

1
SubClass.prototype = SuperClass.prototype;

这样写是可以继承父类原型,但是风险极大:SuperClass.prototype属性它是一个指针,指向SuperClass的原型,如果把这个指针赋给子类prototype属性,那么子类prototype也会指向父类原型。对SubClass.prototype任何更改,就是对父类原型的更改,这显然是不行的。

寄生组合式继承

但出发点没错,可以换种继承方式,看看上面的寄生式继承里的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
28
29
30
31
32
33
34
35
36
37
38
39
40
function object(o){
function F(){};
F.prototype = o;
return new F();
}
function inheritPrototype(subType,superType){
var proto = object(superType.prototype);
proto.constructor = subType;//矫正一下construcor属性
subType.prototype = proto;
}

function SuperClass(id){
this.superValue = ["big","large"];//引用类型
this.id = id;
}
SuperClass.prototype.getSuperValue = function(){
return this.superValue;

};
//子类
function SubClass(id,subValue){
SuperClass.call(this,id);//调用父类构造函数并传参
this.subValue = subValue;
}
inheritPrototype(SubClass,SuperClass);//继承父类原型
SubClass.prototype.getSubValue = function(){
return this.subValue;
}
var instance1 = new SubClass(10,"sub");//可以向父类传参
var instance2 = new SubClass(11,"sub-sub");

instance1.superValue.push("super");
console.log(instance1.superValue);//["big", "large", "super"]
console.log(instance1.id);//10
console.log(instance2.superValue);//["big", "large"]
console.log(instance2.id);//11
console.log(instance1.getSuperValue());//["big", "large", "super"]
console.log(instance1.getSubValue());//sub
console.log(instance2.getSuperValue());//["big", "large"]
console.log(instance2.getSubValue());//sub-sub

解决了组合继承的问题,只调用了一次父类构造函数,而且还能保持原型链不变,为什么这么说,看对寄生组合的测试:

1
2
console.log(SubClass.prototype.__proto__===SuperClass.prototype);//ture
console.log(SubClass.prototype.hasOwnProperty("getSuperValue"));//false

因此,这是引用类型最理想的继承方式。

总结

创建用于继承的对象最理想的方式是组合构造函数模式和原型模式(或者动态原型模式),就是让可定义的私有属性放在构造函数里,共有的放在原型里;继承最理想的方式是寄生式组合,就是让子类的原型的[[prototype]]属性指向父类原型,然后在子类构造函数里调用父类构造函数实现自定义继承的父类属性。

JavaScript对象总有一些让我困惑的地方,不过我还会继续探索。我在此先把我了解的记录下来,与各位共勉。错误的地方请费心指出,我将感谢您的批评指正。


本文为作者原创,转载请注明本文链接,作者保留权利。

参考文献:
[1] http://www.cnblogs.com/chuaWeb/p/5039232.html
[2] http://www.cnblogs.com/xjser/p/4962821.html
[3] https://javascriptweblog.wordpress.com/2010/06/07/understanding-javascript-prototypes/