JavaScript即学即用教程[8]-原型及面向对象

类以及实例

在js中,一个函数在通过new来调用时,该函数就相当于其他语言中的类型了。new调用后,函数会自动创建一个新实例,自动return出来。

我们在函数体中,可以对this进行操作,相当于初始化新创建的实例。 return其他东西的话,会覆盖掉默认的return this, 所以如果要创建类型实例,尽量不要自己return东西。

JavsScript与其他语言比较不同的地方在于它是有点函数式的,所以并没有那么的 面向对象, 另外他的面向对象是通过原型实现的。这就导致他实现 面向对象中的继承就有很多种方式。

原型继承

1
2
3
4
var Person = function () {

}
var p = new Person

当p被new出来的时候,p对象的内部原型指针已经指向了Person.prototype, 因此p继承了Person.prototype对象上的所有内容。
所谓的继承,指的是当通过p访问某个属性或方法时,如果p自身没有,则会去寻找p原型链上的Person.prototype对象上有没有。

从表面上看,我们可以说p继承自Person。

当我们给p对象添加一个自有属性时,p对象自身拥有该属性,则访问时就不会去寻找原型链上的该属性,遵循了就近原则。

Note: 实例化对象之后,不要轻易重写类的原型对象,因为重写后,实例依然指向最初的那个原型对象。

1
2
var p = new Person()
Person.prototype = {a: 1}

上面这个例子,实例化p之后,又重写了Person.protypte, 其实p还在指向最初的Person.prototype, 所以重写变得没有意义。

父类子类的实例属性继承

在其他语言中,子类的构造函数构造时,一般要去调用父类的构造函数完成基类要求的相关属性的构造。在JavaScript进行类型继承时,也应该做好这样的操作,方能把父类的实例属性继承下来,从而让创建出来的实例既完成了父类的初始化任务,又完成了子类的初始化任务。

其最佳实践便是,在构造函数中调用父类构造函数。至于原型继承的东西,就通过原型链的继承来完成即可。

举例: Dog继承自Animal,Animal有年龄、颜色,狗有名字和住址,这些都是实例属性。 而动物会吃饭,狗会叫,这是他们分别具有的公共方法。 OK,实现吧:

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 Animal (age, color) {
this.age = age
this.color = color
}
Animal.prototype.eat = functdion () {}

// 狗类
function Dog(name, address, age, color) {
// 既可以做构造器,又可以做静态工厂函数. (因为这句代码就允许该函数使用非new的方式来调用,内部给转成new)
if (!(this instanceof Dog)) {
return new Dog(name, address);
}
// 先调用下父类的构造器,初始化一下父类所要求的实例属性
Animal.call(this, age, color);
this.name = name
this.address = address
}
// 利用inherits函数实现继承
var inherit = require('utils/inherit')
inherits(Dog, Animal);
// 继承完了之后,再给Dog.prototype添加原型方法,因为继承的过程中实际上在创建Dog.prototype
Dog.prototype.wang = function () {
console.log('汪汪汪')
}

为什么要用nodejs内置的的inherit来让子类继承父类,而不是直接 Dog.prototype = new Animal() 呢。

这里涉及到一个小问题,那就是如果我们这样简单的实现Dog继承Animal,会导致Dog.prototype上面拥有了Animal的实例属性,其实这不符合所谓继承的目标。

我们仅仅希望Dog.prototype继承Animal原型上的东西,而Animal构造函数里的东西,我们希望通过Dog构造函数内部调用的方式来创建为Dog的实例的自有属性。 所以inherit这个事情还是个小学问,那么问题来了:

utils/inherit是怎么实现的呢?

1
2
3
4
5
6
7
8
9
10
11
exports.inherits = function(ctor, superCtor) {
ctor.super_ = superCtor;
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
};

原来他利用了Object.create,来避免了执行Animal父类的构造函数,却达到了继承Animal.prototype的目的。而创建出来的Dog.prototype对象,虽然原型链指向了Animal.prototype, 但是缺少一个constructor属性,只有配置好这个属性,才能让所有的Dog实例知道自己是被Dog构建new出来的,所以inherit函数在Object.create的过程中,顺便设置了Dog.prototype的constructor属性。

同时,inherit函数还给子类类型添加了一个静态属性super_,来标示他的父类是谁。

这样,一个完整的继承函数就实现了。

原型判断

判断一个原型是不是某个对象的: Person.prototype.isPrototypeOf(xx)

获取某个对象的原型: Object.getPrototypeOf(xx)

相关方法

判断属性类型

obj.hasOwnProperty(‘att’)
可以判断一个对象是否拥有每个自有属性。换句话说,是判断att是不是对象obj的实例属性。

判断属性有无

in操作符,可以判断一个对象在整个原型链上是否拥有某个属性att, 如 att in obj, 如果obj对象上有att,或者obj对象的原型链上有att,则返回true,否则返回false。

遍历对象

For in 可以遍历一个对象自身及其原型链上的所有可枚举属性。 其遍历的目标是对象里所有 可枚举 的属性。 是否可枚举是属性的一种基本特征,一般情况下,开发人员通过简单方式定义的所有属性都是可枚举的。 js中固有的那些toString,toLocaleString, ValueOf, hasOwnProperty,都是不可枚举的。

1
2
3
for (var k in obj) {
console.log(obj[k])
}

如果希望只遍历对象自身属性,则需要遍历时通过obj.hasOwnProperty(k)来判断。 另外,ES5提供了Object.keys(obj)来获得对象上所有自有可枚举属性,可以满足这个需求。

甚至,我们还可以玩的更high,去获取一个对象上所有的自有属性(包括不可枚举),使用: Object.getOwnPropertyNames(),
例如,获取Object.prototype上的所有属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
[ '__defineGetter__',
'__defineSetter__',
'hasOwnProperty',
'__lookupGetter__',
'__lookupSetter__',
'propertyIsEnumerable',
'__proto__',
'constructor',
'toString',
'toLocaleString',
'valueOf',
'isPrototypeOf',
]

应用

判断一个属性是不是一个对象的原型属性

1
2
3
function hasPrototypeProperty (obj, name) {
return !obj.hasOwnProperty(name) && (name in object)
}

自己实现一个Object.create函数,在不调用父类构造函数的情况下来继承父类

1
2
3
4
5
6
7
if (!Object.create) {
Object.create = function (o) {
function F() {} //定义了一个隐式的构造函数
F.prototype = o;
return new F(); //其实还是通过new来实现的
};
}