chiwent / blog

个人博客,只在issue内更新
https://chiwent.github.io/blog
8 stars 0 forks source link

JavaScript面向对象小结 #13

Open chiwent opened 5 years ago

chiwent commented 5 years ago

JavaScript面向对象小结

面向对象的语言有三大特征:封装继承多态
  所谓封装,就是将客观事物封装成抽象的类,并且类可以把自己的数据和方法让指定的类或对象操作,比如有些属性和方法是私有的,不能被外界访问。通过封装,可以对对象内部数据提供不同级别的保护。
  所谓继承,就是可以让某个对象获得另外一个对象的属性或方法。其概念的实现方式可分为两类:实现继承和接口继承:实现继承是指直接使用基类的属性和方法而无需额外编码能力;接口继承是指仅使用属性和方法的名称,但子类必须提供实现的能力。
  所谓多态,就是指一个类实例的相同方法在不同情形有不同的表现形式,使得具有不同内部结构的对象可以共享相同的外部接口。

在JavaScript,同样支持以上的的三个特性。但是,由于在ES5及之前的标准中没有引入class关键字,所以其面向对象的功能需要借助原型链来实现。

封装

通过构造函数封装:
构造函数和普通函数别无二致,它用来初始化对象。在命名规范中,它的首字母需要大写。
通过构造函数添加属性和方法,实际上就是通过this添加属性和方法,因为函数内的this总是指向当前对象。在实例化对象时,都会复制一份属性和方法。

function Dog(name, weight) {
    this.name = name;
    this.weight = weight;
    this.eat = function() {
        console.log('骨头');
    }
}

var dog = new Dog('Tom', 20);

如果构造函数内的属性和方法不是通过this添加的,那么该属性和方法就是私有属性和私有方法,其实例无法直接访问。而通过this添加的方法又称为特权方法,通过它可以让实例访问私有属性和私有方法。

function Dog(name, weight) {
    var _name = name; // 私有属性
    this.weight = weight;

    function _getName() {
        console.log(_name);
    }
    this.getName = function() {
        console.log(_name);
        _getName();
    }
}
var dog = new Dog('Tom', 20);

dog.getName();
dog._getName(); // 报错

继承

ES5继承

虽然没有class,但是由于JavaScript的函数作用域(函数外部无法当问函数内部的变量),我们可以借此模拟class,将属性和方法都保存在一个函数中。

原型链继承

将方法绑定在原型链中,该方法就是对象的公有方法,可以被子对象引用。

function Parent() {
    this.hobby = ['sing', 'dance'];
}
Parent.prototype.getHobby = function () {
    console.log(this.hobby);
}

function Child() {}
// 子类原型对象 指向 父类原型对象
Child.prototype = new Parent();
Child.prototype.constructor = Child;
var child = new Child();
child.getHobby();

使用原型链继承,每次创建一个子类实例,都需要重复地执行new操作。并且,由于原型链继承里面使用的都是同一个内存的值,假如修改其中一个子类实例继承的属性,将会影响到其他的子类实例:

var child2 = new Child();
child2.hobby.push('rap');

child.getHobby();  //['sing', 'dance', 'rap']
child2.getHobby(); //['sing', 'dance', 'rap']

构造函数继承

通过构造函数继承,可以避免实例对共享数据的影响,同时可以在子类中向父类传参:

function Parent(name) {
    this.hobby = ['sing', 'dance'];
    this.getHobby = function () {
        console.log(name + "'s hobby:", this.hobby);
    }
}
function Child(name) {
    Parent.call(this, name);
}

var child1 = new Child('Tom');
child1.hobby.push('rap');
child1.getHobby();  // Tom's hobby:["sing", "dance", "rap"]

var child2 = new Child('Tony');
child2.getHobby();  // Tony's hobby:["sing", "dance"]

组合继承

由上两种继承方式可知,原型链实现的继承都是复用同一个属性和方法,构造函数实现的继承都是独立的属性和方法。我们可以同时结合这两种继承方式,在原型上定义方法实现函数复用,通过构造函数有使得每个实例都有自己的属性:

function Parent(name, age) {
    this.name = name;
    this.age = age;
    this.hobby = ['sing', 'dance'];
    /*
    this.getHobby = function () {
        console.log(name+ "'s age", this.age , this.name + "'s hobby:", this.hobby);
    }
    */
}
Parent.prototype.getHobby = function () {
        console.log(this.name+ "'s age:", this.age ,'and', this.name + "'s hobby:", this.hobby);
    }
function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

var child1 = new Child('Tom', 20);
child1.hobby.push('rap');
child1.getHobby(); // Tom's age: 20 and Tom's hobby: ["sing", "dance", "rap"]

var child2 = new Child('Tony', 21);
child2.getHobby(); // Tony's age: 21 and Tony's hobby:["sing", "dance"]

原型式继承

这种方式是模拟Object.create,将传入的对象作为创建的对象的原型:

function createObj(obj) {
    function F(){}
    F.prototype = obj;
    return new F();
}

和原型链继承方式一样,不同实例也是使用同一内存的数据,可能会造成污染:

var Person = {
    age: 18,
    hobby: ['sing', 'dance']
}
var person1 = createObj(person);
var person2 = createObj(person);

person1.age = 20;
console.log(person2.age); // 18

person1.hobby.push('rap');
console.log(person2.hobby); // ["sing", "dance", "rap"]

在上述代码中,修改了实例person1age属性,但是不会影响到person2age属性,这是因为person1.age = 20的操作并未改变原型上的age。在查找对象上的属性时,总是优先查找实例对象,没有找到的情况下再查找原型对象上的属性,实例对象和原型对象上如果有同名属性,优先取实例对象上的值。

寄生式继承

创建一个仅用于封装继承的函数,其内部辅以属性和方法,最终返回对象:

function createObj(obj) {
    var o = Object.create(obj);
    o.hobby = ['sing', 'dance'];
    o.getHobby = function() {
        console.log(o.name, "'s hobby:", o.hobby);
    }
    return o;
}
var Person = {
    name: 'Tom'
}
var person = createObj(Person);
person.getHobby(); // Tom 's hobby: ["sing", "dance"]

寄生组合继承

function Parent(name) {
    this.name = name;
    this.hobby = ['sing', 'dance'];
}
Parent.prototype.getHobby = function() {
    console.log(this.name+ "'s age:", this.age , 'and', this.name + "'s hobby:", this.hobby);
}
function Child(name, age) {
    Person.call(this, name);
    this.age = age;
}

var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;

var child1 = new Child('Tom', 18);
child1.getHobby();

稍微封装一下:

function extend(subClass, superClass) {
    var F = function() {};
    F.prototype = superClass.prototype;
    subClass.prototype = new F();
    subClass.prototype.constructor = subClass;

    subClass.superclass = superClass.prototype;
    if (superClass.prototype.constructor === Object.prototype.constructor) {
        superClass.prototype.constructor = superClass;
    }
}

ES6继承

在ES6中,引入了classextends概念,用来实现对象继承:

class Parent {
    constructor() {
        this.food = 'meat';
        this.hobby = ['sing', 'dance']
    }
    getHobby() {
        console.log(this.hobby);
    }
}

class Child extends Parent {
    constructor(name, skill) {
        super();
        this.name = name;
        this.skill = skill;
    }
}

let child = new Child('Tom', 'Konfu');
child.getHobby();

子类必须要在constructor中调用super,否则创建实例时会报错,因为子类没有自己的this对象,而是继承自父类的。在调用super后,子类才能使用this。这样就体现出和ES5的不同:

ES6的静态方法和静态属性

在ES6中,假如我们用static来修饰对象内的方法,那么该方法就是静态方法,它只能通过直接调用类来使用,而不能被实例调用,同时它是可以被子类继承使用的:

class Parent {
    static staticMethod() {
        console.log('static');
    }
}
Parent.staticMethod();  // static
let parent = new Parent();
parent.staticMethod(); // 报错

class Child extends Parent {
    constructor() {
        super();
    }
}
Child.staticMethod(); // static

甚至,还可以通过super来调用父类的静态方法:

class Parent {
    static staticMethod() {
        console.log('static');
    }
}
class Child extends Parent {
    static staticMethod() {
        super.staticMethod();
    }
}
Child.staticMethod(); // static

静态属性是指类本身的属性,而不是定义在实例对象(this)上的属性,所以我们可以这样:

class Parent {}
Parent.name = 'Tom'

当然,在ES6中,可以通过static来修饰以实现静态属性:

class Parent {
    static name = 'Tom';
}

ES6本身没有私有方法和私有属性的具体实现标准,我们可以通过某种方式来达到想要的效果,详情可以参考: ECMAScript6入门ES6 系列之私有变量的实现

当我们new了一个实例,前后发生了什么?

比如var child = new Child()

模拟new

const NEW = function () {
    let fn = Array.prototype.shift.call(arguments);
    let obj = Object.create(fn.prototype);
    let o = fn.apply(obj, arguments);
    return typeof o === 'object' ? o : obj;
}

多态

从一个父类继承出来的子类有不同的形态:

function Person() {
    this.food = 'meat';
}
function Chinese() {
    this.skill = 'Kongfu';
}
function Japanese() {
    this.skill = 'Ninjutsu';
}
function American() {
    this.skill = 'Boxing';
}
Chinese.prototype = Japan.prototype = American.prototype = new Person();

var C = new Chinese();
var J = new Japanese();
var A = new American();
console.log(C.food, C.skill);
console.log(J.food, J.skill);
console.log(A.food, A.skill);