面向对象把构成问题的 transaction 分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为,意在写出通用代码,加强代码重用,屏蔽差异性。

一、什么是面向对象编程

js 是基于原型的,基于面向对象编程

面向对象就是把数据和对数据的操作方法放在一起,作为一个整体——对象。对同类对象抽象出其共性,形成类

1.面向过程程序设计

将一个项目(或者一个事件)从头到尾按顺序,一步一步完成,先做什么,后做什么,一直到结束,也是我们人做事的方法。

自上而下,先确定一个整体的框架,然后添砖加瓦,逐步实现想要得到的效果,适用于简单的系统,容易理解。但是难以应对复杂的系统,不易维护扩展,难以复用

面向过程是分析解决问题的步骤,然后用函数把这些步骤一步一步的实现,然后在使用的时候一一调用则可。强调的是完成这件事儿的动作,更接近我们日常处理事情的思维。

2.面向对象程序设计

将一个项目(或者一个事件)分成更小的项目,每一个部分负责一方面的功能,最后由这些部分组成一个整体,先设计组件,在完成拼装,适用于大型复杂的系统

面向对象把构成问题的 transaction 分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为,意在写出通用代码,加强代码重用,屏蔽差异性。

想要弄明白面向对象,需要先理解类和对象的概念

《什么是类和对象?》

二、创建对象的方法

1.创建字面量和实例

window.onload = function() {
    // 实例
    var person = new Object();
    person.name = '小明';
    person.age = 22;
    person.year = function() {
        console.log(this.name + '今年' + this.age + '岁了!')
    };
    person.year();

    // 字面量
    var student = {
        name: '小明',
        age: 22,
        year: function () {
            console.log(this.name + '今年' + this.age + '岁了!')
        }
    }
    student.year();
}

// 小明今年22岁了!

两者输出的结果是一样的,控制台输出:

缺点:重复实例化对象,代码冗余高

2.工厂模式

window.onload = function() {
    function createObj(name, age) {
        var obj = new Object();
        obj.name = name,
        obj.age = age,
        obj.year = function() {
            console.log(this.name + '今年' + this.age + '岁了!')
        }
        return obj;
    }
    var obj = createObj('小明', 22);
    obj.year();
}

// 小明今年22岁了!

优点:解决重复实例化对象的问题 缺点:无法识别对象的类型,因为所有的实例都指向一个原型

3.构造函数

window.onload = function() {
    function Person(name, age) {
        this.name = name;
        this.age = age;
        this.year = function() {
            console.log(this.name + '今年' + this.age + '岁了!')
        }
    }
    var student = new Person('小明', 22);
    student.year();
}

// 小明今年22岁了!

优点:可以识别对象的类型 缺点:多个实例重复创建方法,无法共享

4. 原型模式

window.onload = function() {
    function Par() {}
    Par.prototype = {
        constructor: 'Par',
        name: '小明',
        age: 22,
        year: function() {
            console.log(this.name + '今年' + this.age + '岁了!')
        }
    };
    var son = new Par();
    son.year();
}

// 小明今年22岁了!

缺点:所有实例共享他的属性和方法,不能传参和初始化属性值

5.混合模式 (推荐使用)

是构造函数和原型模式混合的写法,拥有各自的优点,构造函数共享实例属性,原型模式共享方法和想要共享的属性,可以传参和初始化属性值

先用构造函数定义对象的属性方法,然后用原型模式创建方法,使用的属性通过 prototype 获取,有一个 constructor 属性,可以指向要操作的函数对象(构造函数)

比如constructor: Par,就代表下面这个原型方法指向Par()对象(构造函数)

window.onload = function() {
    function Par(name, age) {
        this.name = name;
        this.age = age;
    }
    Par.prototype = {
        constructor: Par,
        year: function() {
            console.log(this.name + '今年' + this.age + '岁了!');
        }
    };
    var son = new Par('小明', 22)
    son.year();
}

// 小明今年22岁了!

三、原型,原型链

1.原型对象

  1. 函数对象都具有prototype属性,它指向函数的原型对象 (浏览器内存创建的对象),原型对象都具有constructor属性,它指向prototype属性所在的函数对象 (构造函数)
window.onload = function() {
    function Par(name, age) {
        this.name = name;
        this.age = age;
    }
    Par.prototype = {
    // constructor指向对象
        constructor: Par,
        year: function() {
            console.log(this.name + '今年' + this.age + '岁了!');
        }
    };
    var son = new Par('小明', 22)
    son.year();

/*********************************************/
    console.log(Par.prototype)
    console.log(Par.prototype.constructor)
/*********************************************/
}

通过控制台可以看到

构造函数的prototypr属性指向原型对象

原型对象的construcyor属性指向构造函数

  1. 当调用构造函数创建一个实例后,该实例会有一个隐藏属性__proto__ ,它指向构造函数的原型对象
console.log(son.__proto__ === Par.prototype)

// true
  1. 所有的构造函数的 prototype 都是 object 类型
console.log(typeof Par.prototype)

// object
  1. Function 的 prototype 是一个空函数,所有内置函数的__proto__属性都指向这个空函数
console.log(Math.__proto__)
  1. 如果构造函数实例和原型对象中同时定义了一个属性,在调用时,会屏蔽原型对象中的属性,如果想要访问原型对象中的属性值,需要通过delete方法将同名属性在实例(构造函数)中彻底删除
window.onload = function () {
    function Par(name) {
        this.name = name;
    }
    Par.prototype.name = "张三";
    var son = new Par("李四");
    console.log(son.name); // 李四
    console.log(son.__proto__.name); // 张三

    // 使用 delete 删除实例的同名属性值
    console.log(delete son.name);   // true
    console.log(son.name); // 张三
}
  1. 通过hasOwnProperty(属性名)可以判断一个属性存在于构造函数中,还是原型对象中

true表示存在构造函数中;false表示存在原型对象中

console.log(Par.hasOwnProperty(name));  // false
  1. 操作符in,可以判断一个属性是否存在(存在于构造函数和原型对象中皆可)
window.onload = function () {
    function Par(name, age) {
        this.name = name;
        this.age = age;
    }
    Par.prototype = {
        constructor: Par,
        year: function() {
            console.log(this.name + this.age)
        }
    };
    var son = new Par('xm', '22')
    son.year();
    console.log('name' in Par); // true
    console.log('age' in Par);  // false
}

同样的两个属性,判断其是否存在于实例或者原型对象中,输出的结果不一样

参考:《对象中是否有某一个属性 in》https://www.cnblogs.com/IwishIcould/p/12333739.html

2.__proto__和 prototype 的区别

  1. prototype属性只有函数对象上才有,而__proto__属性所有对象都有

  2. prototype是由函数对象指向原型对象,而__proto__是由实例指向函数对象的原型对象

  3. 原型链,将父类型的实例作为子类型的原型对象,这种链式关系叫做原型链

3.继承

  1. 原型链继承

优点:父类原型定义的属性和方法可以复用 缺点:子类实例没有自己的属性,不能向父类传递参数

function test1() {
    function SuperType() {
        this.city = [ "北京", "上海", "天津" ];
        this.property = true;
    }
    SuperType.prototype = {
        constructor : SuperType,     // 保持构造函数和原型对象的完整性
        age : 15,
        getSuperValue : function() {
            return this.property;
        }
    };
    function SonType() {
        this.property = false;
    }

    // 重写子类的原型指向父类的实例:继承父类的原型
    SubType.prototype = new SuperType();

    SubType.prototype = {
        constructor : SubType,
        getSonType : function() {
            return this.property;
        }
    };

    // 优点验证
    let son = new SubType();
    console.log(son.age); // 15
    console.log(son.getSuperValue()); // false

    // 缺点验证
    let instance1 = new SubType();
    instance1.city.push("重庆");
    console.log(instance1.city); // ["北京", "上海", "天津", "重庆"]

    let instance2 = new SubType();
    console.log(instance2.city); // ["北京", "上海", "天津", "重庆"]

}

// test1();
  1. 构造函数继承

优点:子类实例有自己的属性,可以向父类传递参数,解决原型链继承的缺点 缺点:父类原型的属性和方法不可复用

function test2() {
    function SuperType(name) {
        this.name = name;
        this.city = [ "北京", "上海", "天津" ]
    }
    SuperType.prototype = {
        constructor : SuperType,
        age : 18,
        showInfo : function() {
            return this.name;
        }
    };

    function SubType() {
        // 父类调用 call() 或者 apply() 方法和子类共用同一个 this,实现子类实例属性的继承
        SuperType.call(this, "张三");
    }

    // 优点验证
    let instance = new SubType();
    instance.city.push("重庆");
    console.log(instance.city); // ["北京", "上海", "天津", "重庆"]

    let instance1 = new SubType();
    console.log(instance1.city); // ["北京", "上海", "天津"]

    // 缺点验证
    console.log(instance.age); // undefined
    instance.showInfo(); // son.showInfo is not a function
}

// test2();
  1. 组合继承(推荐)

优点:原型的属性和方法可以复用,每个子类实例都有自己的属性 缺点:父类构造函数调用了两次,子类原型中的父类实例属性被子类实例覆盖

function test3() {
    function SuperType(name) {
        this.name = name;
        this.city = [ "北京", "上海", "天津" ]
    }
    SuperType.prototype = {
        constructor : SuperType,
        showInfo : function() {
            console.log(this.name + "今年" + this.age + "岁了");
        }
    };

    function SubType(name, age) {
        // 1. 通过构造方法继承实现实例属性的继承
        SuperType.call(this, name);
        this.age = age;
    }

    // 2. 通过原型链继承实现原型方法的继承
    SubType.prototype = new SuperType();

    // 优点验证
    let instance = new SubType("张三", 15);
    instance.showInfo(); // 张三今年 15 岁了

    let instance1 = new SubType();
    instance1.city.push("重庆");
    console.log(instance1.city); // ["北京", "上海", "天津", "重庆"]

    let instance2 = new SubType();
    console.log(instance2.city); // ["北京", "上海", "天津"]

}

// test3();
  1. 寄生组合继承(推荐)

优点:解决了组合继承的缺点,效率高 缺点:基本没有

function test4() {
    function inheritPrototype(subType, superType) {
        // 1. 继承父类的原型
        var prototype = Object.create(superType.prototype);
        // 2. 重写被污染的 construct
        prototype.constructor = subType;
        // 3. 重写子类的原型
        subType.prototype = prototype;
    }
    function SuperType(name) {
        this.name = name;
        this.city = [ "北京", "上海", "天津" ];
    }

    SuperType.prototype.sayName = function() {
        console.log(this.name);
    };

    function SubType(name, age) {
        SuperType.call(this, name);
        this.age = age;
    }

    // 将父类原型指向子类
    inheritPrototype(SubType, SuperType);

    SubType.prototype.sayAge = function() {
        console.log(this.age);
    }

    // 优点验证
    let instance = new SubType("张三", 15);
    instance.sayName(); // 张三

    let instance1 = new SubType();
    instance1.city.push("重庆");
    console.log(instance1.city); // ["北京", "上海", "天津", "重庆"]

    let instance2 = new SubType();
    console.log(instance2.city); // ["北京", "上海", "天津"]
}

// test4();

4.ES6 新方法--class

新的关键字class在 es6 开始被引入到 javascript 中来,class的目的就是让定义类更简单

用函数方法实现:

function Person(name) {
    this.name = name;
}

Person.prototype.hello = function () {
    console.log('Hello, ' + this.name + '!');
}

var son = new Person('xm')
son.hello();    // Hello, xm!

class来实现:

class Person {
    constructor(name) {
        this.name = name;
    }

    hello() {
        console.log('Hello, ' + this.name + '!');
    }
}

var son = new person('xm')
son.hello();    // Hello, xm!

可以在看到,在定义class中,直接包含了构造函数constructor属性,和原型对象上的函数hello()方法,省略掉了function关键字

需要注意:原来的写法是,构造函数和原型对象分散开来写,现在用class可以直接把两者串在一个对象中,只有最后传参和调用方法时写法是一样的

class 继承

class定义对象的另一个巨大的好处是继承更方便了。想一想我们从Person派生一个PrimaryPerson需要编写的代码量。现在,原型继承的中间对象,原型对象的构造函数等等都不需要考虑了,直接通过extends来实现:

class PrimaryPerson extends Person {
    constructor(name, grade) {
        super(name); // 记得用 super 调用父类的构造方法!
        this.grade = grade;
    }

    myGrade() {
        alert('I am at grade ' + this.grade);
    }
}

注意PrimaryPerson的定义也是通过 class 关键字实现的,而extends则表示原型链对象来自Person,子类的构造函数可能会和父类的不太相同

例如,PrimaryPerson需要namegrade两个参数,并且需要通过super(name)来调用父类的构造函数,否则父类的name属性无法正常初始化。

PrimaryPerson已经自动获得了父类Personhello方法,我们又在子类中定义了新的myGrade方法。

ES6 引入的class和原有的JavaScript原型继承有什么区别呢?

实际上它们没有任何区别,class的作用就是让 JavaScript 引擎去实现原来需要我们自己编写的原型链代码。简而言之,用class的好处就是极大地简化了原型链代码。

但是!

目前并不是所有的浏览器都支持class,所以在选择的时候一定要慎重!