Object & prototype & inheritance
对象的创建
在我们创建一个对象时,我们可以利用构造器函数的方式,在这种方式下,我们会使用new
操作符,在这之前我们声明一个用来当做构造器函数的函数时会经历如下过程:
- 声明一个
function
function
自动获得属性prototype
,它指向原型对象
原型对象
自动获得一个constructor
属性,它指向prototype
所在的函数 (constructor是原型对象唯一自动获得的属性)
在使用new
操作符之后发生了这些事情:
- 创建一个基于
constructor
的实例
- 实例身上具有
__proto__
指针,指向构造函数的prototype
- 将构造函数的作用域赋给新对象(因此this指向了这个新对象)
- 执行构造函数中的代码(为这个对象添加新属性)
- 返回新对象(隐式返回)
接下来我们来看看对象创建的几种典型的模式:
工厂模式
Just Tell me what do you want.
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('kylewh',29,'Web Developer');
|
- 优点:解决了创建多个相似对象的问题
- 缺点:没有解决对象识别的问题,见如下代码
console.log( person1 instanceof createPerson ); console.log ( person1.prototype.constructor );
|
构造函数模式
So I can recognize where you from.
相较于工厂模式,我们看到这种模式下:
- 没有在内部显式的创建对象
- 属性和方法都赋给了this对象
- 并没有return语句
而实际上new操作符的所作所为恰恰解释了上面的区别,当使用new时,会经历以下4个步骤:
- 创建一个对象
- 将构造函数的作用域赋给新对象(this指向这个对象)
- 执行构造函数中的代码(为这个新对象添加属性方法)
- 返回新对象(隐式的返回this)
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function () { alert(this.name); } } var person1 = new Person('kyle', 29, 'webdever'); console.log( person1 instanceof Person )
|
- 优点:解决了工厂模式的无法识别对象的问题
- 缺点:对于每一个对象,都创建了同样的方法的副本,这些方法不具有复用性,观察如下代码:
var p1 = new Person(); var p2 = new Person(); console.log( p1.sayName == p2.sayName );
|
原型
原型模式
Finally I don’t have to keep everything.
prototype是每个函数都具有的属性,它是一个指针,指向一个对象,这个对象叫做原型对象,特定类型的所有实例都将共享这个原型对象上的属性与方法。
function Animal(name) { this.name = name; } Animal.prototype.run = function() { console.log ( this.name + ' is runing' ); } var a1 = new Animal('cat'); var a2 = new Animal('dog'); a1.run(); a2.run(); console.log( a1.run == a2.run );
|
缺陷: 我们发现function在原型对象上被共享,而我们知道function类型是一种引用类型,如果一个引用类型被共享,那么结果就是无论某个特例如何更改它原型对象上的引用类型数据,最后所有的其他实例也将得到同样的结果。
Animal.prototype = { constructor: Animal, skills: ['run', 'jump', 'sleep'], run: function() { console.log ( this.name + ' is runing' ); } } var a3 = new Animal('cat'); var a4 = new Animal('cat'); console.log( a3.skills ); console.log( a4.skills ); a3.skills.push('climb'); console.log( a3.skills ); console.log( a4.skills );
|
组合使用构造函数模式和原型模式
Just Keep your characteristic.
将私有引用类型通过构造函数模式进行添加,公有引用类型通过原型模式添加。
function Animal () { this.skills = ['run', 'jump', 'sleep']; } Animal.prototype = { constructor: Animal, run: function() { console.log ( this.name + ' is runing' ); } }
|
这种模式是使用最广泛、认同度最高的一种创建自定义类型的方法,可以说算是一种默认模式。
动态原型模式
Let’s make it into one piece.
通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; if ( typeof this.sayName != 'function' ) { Person.prototype.sayName = function () { alert(this.name); }; } } var friend = new Person("Nicholas", 29, "SoftWare Engineer");
|
非常完美的一种方法。
继承
原型链继承
让子类的原型指向父类的实例。
function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function () { return this.property; } function SubType() { this.subproperty = false; } SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function () { return this.subproperty; }; var instance = new SubType(); alert(instance.getSuperValue());
|
如果子类重写了超类中的方法,将会屏蔽超类里的同名方法。
问题: 同样对于超类中的引用类型属性,子类所有实例都会共享这一个属性,一旦一个实例修改了这个属性,其他实例也会共享这个“改动”
借用构造函数
我喜欢把它记做:【内置超类call法】,做法是在子类的构造函数调用超类型构造函数,相当于将超类构造函数的属性拷贝一份。
function SuperType() { this.colors = ['red', 'blue', 'green' ]; } function SubType() { SuperType.call(this); } var instance1 = new SubType(); instance1.colors.push('black'); alert(instance1.colors); var instance2 = new SubType(); alert(instance2.colors);
|
优点:
可以在子类的构造函数中向父类构造函数传递函数(因为call的使用)
function SuperType(name) { this.name = name; } function SubType() { SuperType.call(this, "kylewh"); this.age = 29; } var instance = new SubType(); alert(instance.name);
|
缺陷:
本质上构造函数模式的移植版,方法都是在构造函数里定义,不满足函数复用的原则,而且没有进行原型链接操作,那么父类的原型里的方法对于子类也是不可见的。
组合继承
使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
function SuperType(name) { this.name = name; this.color = ['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.constructor = SubType; SubType.prototype.sayAge = function() { alert(this.age); } var instance1 = new SubType('kylewh', 25); instance1.colors.push('black'); console.log(instance1.colors); instance1.sayName(); instance1.sayAge();
|
最常用的继承模式,避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为javascript中最常用的继承模式。
缺陷: 调用了两次父类构造函数,而且子类的实例具有通过父类构造函数拷贝而来的属性,而它的原型也是父类的实例,这样它原型上的同名属性都被实例上的属性给屏蔽了。 原型身上的属性成了冗余属性。
原型式继承
借助已有的对象创建新对象,同时还不用创建自定义类型,借助这个函数:
function object(o) { function F() {}; F.prototype = o; return new F(); } var person = { name: "kylewh", friends: ['john', 'edwin'] }; var anotherPerson = object(person); anotherPerson.name = 'greg'; anotherPerson.friends.push('rob'); alert(person.friends);
|
ES5新增的Object.create()规范化了原型式继承,这个方法接受两个参数:一个用作新对象原型得对象和(可选的)一个为新对象定义额外属性的对象。
注意:使用Object.create()创建对象的的效率并不高,通常要比使用构造函数创建对象更慢
var person = { name: 'kylewh', friends: ['edwin', 'john'] } var anotherPerson = Object.create(person, { name: { value: "Gred" } }); alert(anotherPerson.name)
|
依然逃不过引用类型共享的问题。
寄生式继承
与工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象。
function createAnother(original) { var clone = object(original); clone.sayHi = function () { alert('hi'); } return clone; } var person = { name: 'kylewj', friends: ['edwin', 'john'] } var anotherPerson = createAnother(person); anotherPerson.sayHi();
|
缺陷: 不能做到函数复用,不断的进行拷贝。
寄生组合式继承
为了解决组合继承的屏蔽&冗余问题,我们首先不需要为了指定子类型的原型而调用超类型的构造函数,使用寄生式继承来继承超类型的原型,再将结果指定给子类型的原型。
function inheritPrototype(subType, superType) { var prototype = Object(superType.prototype); prototype.constructor = subType; subType.prototype = prototype; }
|
1.借用构造函数模式继承父类属性
2.寄生模式继承父类原型
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; } inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function() { alert(this.age); }
|
只调用了以此superType的构造函数,足够高效,并且因此避免了在subType的原型上创建不必要的,多余的属性。与此同时,原型链还是保持不变,也能正常的使用instanceof和isPrototypeOf()。
开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。