掌握Javascript(六)

本文介绍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中的类,敬请期待。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注