本文介绍Javascript中面向对象编程。
面向对象编程的四个支柱
面向对象的四个支柱是:封装、抽象、继承、多态。Bob大叔曾说过,最好的函数是那些没有参数的函数。利用面向对象,可以接近达到bob说的好函数的要求。
- 封装。减少复杂性+增加可重用性
- 抽象。减少复杂生+隔离变化的影响,只暴露抽象,不暴露细节。
- 继承。消除多余的代码
- 多态性。重构,避免丑陋的选择性代码如 switch/case等。
这里不展开说明,准备在计划中新开的主题“设计模式”中详解。
私有属性和方法
对象的构造函数不加 this 作为成员属性,而是用let,相当于局部变量,这样外部就不能访问这个内部变量了。
function Circle(radius) {this.radius = radius;//属性,外部可以访问let color = ‘red’;//内部变量,外部访问不了// …
}
Javascript没有类
有两种继承:类继承classical Inheritance、原型继承Prototypical Inheritance。
javascript使用原型继承,它没有类 class,后面章节会谈到ES6的类,但实际就是个语法糖,不是真正的类。
原型继承
任何对象都有原型,简单建立一个空对象也是。
可以将原型理解成父母。
对象都有一个到其他对象的链接,那个对象就是原型。只有元对象没有父母(原型)。
let x = {};
let y = {};
// x和y都有属性 __proto__,指向原型对象。这两个对象的原型是元对象
// 这个__proto__属性在代码中是不可访问的
console.log(Object.getPrototypeOf(x) === Object.getPrototypeOf(y)
);
// 这个为true
// 在内存中只有一个元对象
对象执行一个方法,Javascript会在对象中找,找不到的话会到对象的原型中去找,一直找到元对象。
原型对象就是一个普通的对象。
多层继承
来看一下代码这两个对象:
let myArray = [];//数组
let circle = new Circle();//用构造函数生成对象
myArray对象是数组,而circle对象是用自定的构造函创建。他们继承关系就是:
- myArray–>数组基对象ArrayBase–>元对象objectBase
- circle–>Circle构造原型–>元对象objectBase
属性性质
我们建立一个对象:
let person = {name:’kelemi’};
如果用 for…in遍历时,是不会显示原型的方法的,因为原型的属性被设了不能遍历。
let objectBase = Object.getPrototypeOf(person);
let descriptor = Object.getOwnPropertyDescriptor(objectBase, “toString”);
console.log(descriptor)
// 查看原型的 toString 属性描述,能看到enumerable为false
当然,也可以设person的属性为不可 enumerable,如下:
let person = { name: ‘kelemi’ };
Object.defineProperties(person,”name”,{writable:false,enumerable:false,configurable:false,
});
// 默认情况下,属性都是可写、可枚举、可配置的,这里全改成了false
改了之后,我们再设置查看及配置属性将无效。以下语句将都出错:
person.name = ‘chenyongping’;//对应 writable
person.keys(person);//对应 enumerable
delete person.name;//对应configurable
构造函数原型
除了元对象(根),所有对象都有原型对象,通过
Object.getPrototypeof(myObj);
可以查看到。
在chrome控制台中,通过 myObj.__proto__ 得到原型对象。
如果对象是由构造函数生成的,构造函数有一个 prototype属性。
假设myObj的构造函数是 Contructor,那么Contructor的 prototype 与 myObj的__proto__ 是同一个对象。
原型成员与实例成员
有时候需要产生成千上万个实例,构造函数中的实现要重复生成,可以用原型成员简化。
function Circle(radius) {this.radius = radius;//实例成员,每一个实例这个值都不一样,用实例成员比较合理this.move = function () {
console.log(“move”);};
}
Circle.prototype.draw = function () {
//定义原型成员
console.log(“draw”);
};
Circle.prototype.toString = function () {
// 这里重新定义了toString属性,就近原则,先调用这个Circle构造原型的 // toString,元对象的 toString不调用
return “Circle with radius” + this.radius;
// 这个原型成员使用实例成员 radius,可以互相调用
};
属性的迭代
接着上面的例子,实例成员有radius、move();原型成员有 draw()。
用Object.keys(circle),只能列出 radius和move()。
而用 for…in ,则可以列出 radius、move()以及draw()等。
可以用 obj.hasOwnProperty(‘radius’)确认是否是实例成员。
避免扩展内建对象
比如内建对象构造函数Array,不建议使用
Array.prototype.shuffle=function(){
//…
}
记住:不要修改不属于你的对象和函数。
创建属于你的原型继承
比如有对象shape和circle,希望circle继承shape的方法。
shape的构造函数是 Shape(),circle的构造函数是 Circle(),修改Circle的ptototype即可
Circle.prototype=Object.create(Shape.prototype);
这样就使Circle的原型变成了Shape。
重设构造函数
上一节更改Circle.prototype后,最佳实践 是同时也更改构造函数。
Circle.prototype.constructor=Circle;
如果不更改的话,new Circle.prototype.constructor()相当于 new Shape().
更改了的话,相当于就是 new Circle()。
尽管一般都用 new Circle()来创建对象,但在一些自动化动态创建对象的案例中,可能需要调到constructor属性,所以最好在更改原型继承后,明确重新指定一下Circle.prototype.constructor。
调用父级构造函数
我们来看一段代码:
function Shape(color) {
this.color = color;
}
Shape.prototype.duplicate = function () {
console.log('duplicate');
}
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.draw = function () {
console.log('draw');
}
Circle.prototype = Object.create(Shape.prototype);
//原型继承
Circle.prototype.constructor = Circle;
//修改构造函数
const s = new Shape('red');
const c = new Circle(1);
在chrome中查看,可以看到创建的对象 c 中是没有 color属性的,我们需要修改Circle的构造函数。
...
function Circle(radius, color) {
Shape.call(this, color);
this.radius = radius;
}
...
注意Circle构造函数中传入两个参数,其中一个是父级的参数,父构造函数Shape调用call,并将当前的this传给它。
重构继承语句为一个函数
我们看到,每次继承都要写类似这样的语句。
Circle.prototype = Object.create(Shape.prototype);
//原型继承
Circle.prototype.constructor = Circle;
//修改构造函数
如果有多级继承,就显得非常丑了,也容易出错,需要重构。在代码前面添加一个函数。
function extend(Child, Parent) {
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
}
// 注意 Child和Parent首字母大写,表示需要传入的是构造函数
以后继承就只需调用类似 extend(Circle,Shape) 即可。
方法重写
可以重写父级的方法,比如在Circle中重写Shape的duplicate方法。
...
extend(Circle, Shape);
//重写方法必须在继承后声明,如果在extend语句前,则会失效
Circle.prototype.duplicate = function () {
Shape.prototype.duplicate();
//如果需要在重构方法调用父类的的方法,可以用Shape.prototype
Shape.prototype.duplicate.call(this);
//也可用这种方法,如果需要传入this的话
console.log('duplicate circle');
}
...
重写方法必须在继承的语句后。也可以在重构方法中调用父级的方法,如上面代码所示那样。
多态性
比如Square和Circle都继承Shape。Shape有个方法 duplicate(),而Square和Circle都重写了该方法。就可以用以下代码分别输出duplicate实现。
...
const shapes = [
new Circle(),
new Square(),
];
for (let shape of shapes)
shape.duplicate();
...
这是非常强大的,根据不同的对象,调用不同的方法。
何时使用继承
避免创建多层继承关系,这会增加复杂性。一般不超过一级继承。
实际上,用组合方式很可能要好于用继承方式。
比如有个对象Animal,它有eat()和walk()方法,Person和Dog继承于它可能还比较不错,但如果Goldfish也继承它的话,则不合适,因为Goldfish不会walk,新手可能会考虑增加一个继承层次。将Animal减至只有eat(),中间再加一个对象Fish继承Animal,并添加swim()方法,然后Goldfish继承Fish。你看,为了增加一个Goldfish对象,需要改动这么多,非常麻烦。
组合
直接看代码。
const canEat = {
eat: function () {
this.hunger--;
console.log("eating");
}
};
const canWalk = {
walk: function () {
console.log("walking");
}
};
const person = Object.assign({}, canEat, canWalk);
console.log(person);
代码先创建2个对象:canEat和canWalk,然后调用内置方法assign将两个对象组合在一起,这个方法第一个参数是目标对象,这里我们为空对象,后面可以跟多个参数,表示源对象。对象person就是canEat和canWalk两个对象的组合,增加了灵活性。如果后续需要增加goldfish对象的话,只需添加一个canSwim对象,然后将canEat和canSwim组合一下即可形成goldfish。
有些人可能更习惯用构造函数,也只需稍做一下改动即可:
...
function Person() { }
Object.assign(Person.prototype, canEat, canWalk)
//调用构造函数
const person = new Person();...
代码将目标对象设为Person.prototype实现了目的,调用构造函数与之前是一致的。
代码中总出现Object.assign有点丑,可以重构成函数。
function mixin(target, ...sources) {
Object.assign(target, ...sources)
}
//后面组合就可以用mixin函数
mixin(Person.prototype, canEat, canWalk)
上面代码重构了一个mixin函数,用于组合对象。
小结
本文介绍JavascriptJavascript中面向对象编程的相关知识。这里补充一下,之前用Child.prototype=Object.create(Parent.prototype)只继承了原型方法,如果需要同时继承实例方法的话,可以用Child.prototype=new Parent() 来实现。
下一篇计划介绍ES6中的类,敬请期待。