Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript面向对象小结 #13

Open
chiwent opened this issue May 29, 2019 · 0 comments
Open

JavaScript面向对象小结 #13

chiwent opened this issue May 29, 2019 · 0 comments

Comments

@chiwent
Copy link
Owner

chiwent commented May 29, 2019

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的不同:

  • ES5的继承实质是先创造子类的实例对象this,然后将父类方法添加到this上
  • ES6的继承实质是先创造父类实例对象this,然后再用子类的构造函数修改this

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()

  • 1.新建一个对象child
  • 2.child.__proto__ = Child.prototype,将新创建的对象的__proto__指向构造函数的prototype
  • 3.将this指向新创建的对象
  • 4.返回新对象

模拟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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant