1

面向对象

1. 面向对象的三大特性

封装
继承
多肽

1.1 原型链的知识

原型链是面向对象的基础,是非常重要的部分。有以下几种知识:

2. 创建原型的几种方法

2.1 方式一:字面量

var obj1 = {name:"江小白"}
var obj2 = new Object(name:"江小白")
上面的两种写法,效果是一样的。因为,第一种写法,obj11会指向Object。
  • 第一种写法是:字面量的方式。
  • 第二种写法是:内置的构造函数

2.2 方式二:通过构造函数

var M = function(name){
    this.name = name;
}
var obj3 = new M("asd asd)

2.3 方法三:Object.create

var p = {name:'lipiao'}
var obj3 = Object.create(p);
// 此方法创建的对象是原型链对象
第三种方法,这种方式里,obj3是实例,p是obj3的原型(name是p原型里的属性),构造函数是Objecet 。

3. 原型链

6uK7on.png

  1. 构造函数通过 new 生成实例
  2. 构造函数也是函数,构造函数的prototype指向原型。(所有的函数有prototype属性,但实例没有 prototype属性)
  3. 原型对象中有 constructor,指向该原型的构造函数

6ul0MT.png

  1. 实例的__proto__指向原型。也就是说,Foo.__proto__ === Foo.prototype。

声明:所有的引用类型(数组、对象、函数)都有__proto__这个属性。
Foo.__proto__ === Function.prototype的结果为true,说明Foo这个普通的函数,是Function构造函数的一个实例。

3.2 原型链

原型链的基本原理:任何一个实例,通过原型链,找到它上面的原型,该原型对象中的方法和属性,可以被所有的原型实例共享。

Object对象是原型链的顶端。

原型可以起到继承的作用。原型里的方法都可以被不同的实例共享:

//给Foo的原型添加 say 函数
  Foo.prototype.say = function () {
      console.log('');
  }

原型链的关键:在访问一个实例的时候,如果实例本身没找到此方法或属性,就往原型上找。如果还是找不到,继续往上一级的原型上找。

3.3 instanceof的原理

6u33Nj.png
instanceof的作用:用于判断实例属于哪个构造函数。
instanceof的原理:判断实例对象的__proto__属性,和构造函数的prototype属性,是否为同一个引用(是否指向同一个地址)。

    注意1:虽然说,实例是由构造函数 new 出来的,但是实例的__proto__属性引用的是构造函数的prototype。也就是说,实例的__proto__属性与构造函数本身无关。
    注意2:在原型链上,原型的上面可能还会有原型,以此类推往上走,继续找__proto__属性。这条链上如果能找到, instanceof 的返回结果也是 true。

比如说:

foo instance of Foo的结果为true,因为foo.__proto__ === M.prototype为true。
foo instance of Objecet的结果也为true,为Foo.prototype.__proto__ === Object.prototype为true。
但我们不能轻易的说:foo 一定是 由Object创建的实例`。这句话是错误的。我们来看下一个问题就明白了。

3.4 分析一个问题

问题:已知A继承了B,B继承了C。怎么判断 a 是由A直接生成的实例,还是B直接生成的实例呢?还是C直接生成的实例呢?

分析:这就要用到原型的constructor属性了。

foo.__proto__.constructor === M的结果为true,但是 foo.__proto__.constructor === Object的结果为false。
所以,用 consturctor判断就比用 instanceof判断,更为严谨。

4. new运算符

当new一个对象是发生了什么
  • 创建一个新的空对象实例。
  • 将此空对象的隐式原型指向其构造函数的显示原型。
  • 执行构造函数(传入相应的参数,如果没有参数就不用传),同时 this 指向这个新实例。
  • 如果返回值是一个新对象,那么直接返回该对象;如果无返回值或者返回一个非对象值,那么就将步骤(1)创建的对象返回。

5 类的定义、类的声明(继承的本质:原型链)

5.1方式一、用构造函数模拟类(es5)

function Animal(){
    this.name="xiaoqi";//通过this,表明这是一个构造函数
}

5.2方式二、用class声明(es6)的写法

class Animal{
    constructor(){//可以在构造函数里写属性
        this.name = name;
    }
}

5.3 类的实列化

类的实例化很简单,直接 new 出来即可。

new Animal();

5.4继承的几种方式

继承的本质是原型链,
继承是面向对象语言的基础概念,一般面向对象语言支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。ECMAScript中函数没有签名,因此无法实现接口继承。ECMAScript只支持实现继承,而其实现继承主要是靠原型链来实现。
5.4.1 原型链
原理:让一个引用类型继承另一个引用类型的方法和属性

具体实现如下:

// 父类
function Parent(){
    this.name = "xiaoqi";
    this.children = ["zhuzhu","chouzhu"]
}
Parent.prototype.getChildren = function(){
    console.log(this.childen);
}
// 子类
function Child(){

} 

Child.prototype = new Parent()
var child1 = new Child();
child1.children.push("hanhanzhu")
console.log(child1.getChildren())// Array ["zhuzhu", "chouzhu", "hanhanzhu"]

var child2 = new Child();
console.log(child2.getChildren())// Array ["hanhan", "chouzhu"]
优点
  • 父类新增原型方法/原型属性,子类都能访问到
  • 简单,易于实现
缺点
  • 无法实现多继承
  • 引用类型的属性被所有实例共享
  • 在创建Child的实例的时候,不能向Parent传参
5.4.2 盗用构造函数

原理:使用apply()和call()方法以新对象为上下文执行构造函数,子类构造函数内部调用超类构造函数

具体实现如下:

//盗用构造函数
// 父类 
function Parent(name){
     this.name = name;
     this.colors = ["red","yellow"];
     this.getName = function(){
         return this.name;
     }
 }

//  子类
function Child(name){
    Parent.call(this,name);
}

var child1 = new Child("xiaoqi");
child1.colors.push("xiaopiao");
console.log(child.colors);//["red","yellow","xiaopiao"]

var child2 = new Child("xiaopiao");
child2.colors.push("xiaoqi")
console.log(child2.colors)//["red","yellow","xiaoqi"] 
优点
  • 避免了引用类型的属性被所有实列共享,可以向父级传递参数
  • 解决了原型链继承中子类实例共享父类引用属性的问题

创建子类实例时,可以向父类传递参数
可以实现多继承(call多个父类对象)

缺点
  • 只能继承父类的实例属性和方法,不能继承原型属性和方法
  • 每次实列都会创建一遍方法,函数复用是一个问题
5.4.3 组合继承(原型链+借用构造函数的组合继承)

原理:通过借用构造函数实现对实例属性的继承。这样,既能够保证能够通过原型定义的方法实现函数复用,又能够保证每个实例有自己的属性

具体实现如下:

//  组合继承(原型链+借用构造函数的组合继承)
function Parent(name,age){
    this.name= name;
    this.age = age;
    this.colors = ['red','green']
    console.log("parent")
}
Parent.prototype.getColors = function(){
    console.log(this.colors);
}

// 子类
function Child(name,age,grade){
    Parent.call(this,name,age)//创建子类实列会折行一次
    this.grade = grade;
}

Child.prototype = new Parent();//指定子类原型会执行一次
Child.prototype.constructor = Child;//校正构造函数
Child.prototype.getName = function(){
    console.log(this.name)
}
var c = new Child("xiaoqi",88,99);
console.log(c.getName());
// 输出:“Parent”,"Parent","xiaoqi"
优点
  • 可以继承实例属性/方法,也可以继承原型属性/方法
  • 不存在引用属性共享问题
  • 可传参
  • 函数可复用
缺点
  • 创建子类时会调用两次超类的构造函数
5.4.4 原型式继承

原理:借助原型可以基于已有的对象创建新对象,同时还不比因此创建自定义类型

具体实现如下:

function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}
在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。本质上object()就是完成了一次浅复制操作
var person ={
    name:"xiaoqi",
    friends:["piaopiao","xiaopiao"]
}

var p1= object(person);
p1.name="xiaopiao"
p1.friends.push("heihei")

var p2=object(person);
p2.name = "xiaoqi"
p2.friends.push("haha")

console.log(p1.name)
console.log(person.friends)
//["piaopiao","xiaoxiao","heihei","haha"]
ECMAScript5通过新增Object.create()方法规范化了原型式继承,这个方法接收两个参数:一个用作新对象原型的对象和为新对象定义属性的对象
  • 注意Object.create()有两个参数,第二个与Object.defineProperties()的第二个参数一样
var person ={
    name:"xiaoqi",
    friends:["piaopiao","xiaopiao"]
}

var p1= Object.create(person);
p1.name="xiaopiao"
p1.friends.push("heihei")

var p2=Object.create(person);
p2.name = "xiaoqi"
p2.friends.push("haha")

console.log(p1.name)
console.log(person.friends)
//["piaopiao","xiaoxiao","heihei","haha"]
5.4.5 寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路,即创建一个仅用于封装继承函数过程的函数,该函数在内部以某种方式来增强对象,最后返回对象

具体实现如下:

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

function createAnother(original) {
    var clone = object(original); // 创建新对象
    clone.sayHi = function(){ 
        console.log('hello, world'); // 增强对象,添加属性或法,这里导致方法难以复用问题
    }
    return clone; // 返回新对象
}

var person = {
    name: 'alice',
    friends: ['Sherly', 'Taissy', 'Vant']
}

var p1 = createAnother(person);
p1.sayHi(); 

> "hello, world"
5.4.6 寄生组合式继承

合继承是 JavaScript最常用的继承模式,其最大的问题是不管在什么情况下都会调用两次超类构造函数:一次是在创建子类原型时,一次是在子类型构造函数内部。子类型最终会包含超类的全部实例属性。
所谓寄生组合式继承,即通过构造函数来继承属性,通过原型链继承方法,背后的基本思路是:不必为了指定子类的原型而调用超类的构造函数,我们所需要的无非就是超类原型的一个副本而已

具体实现如下:

function Parent(name,age){
    this.name = name;
    this.age = age;
    console.log('parent')
}

Parent.prototype.getName = function(){
    return this.name;
}

function Child(name,age,grade){
    Parent.call(this,name,age);
    this.grade = grade;
}

// 寄生组合的方式
// 复制父类的原型对象
function create(original){
    function F(){};
    F.prototype = original;
    return new F();
}

//创建父类的原型副本,改变子类的原型,同时纠正构造函数
function inherit(subClass,superClass){
    var parent = create(superClass.prototype);
    parent.constructor = subClass;
    subClass.prototype = parent;
}

inherit(Child,Parent);

var child = new Child("xiaoqi",99,99)
// ‘parent’
寄生组合继承的高效率在于它只调用了一次超类构造函数,同时还能够保持原型链不变,能够正常使用 instanceof 和 isPrototypeOf() 寄生组合继承被普遍认为是引用类型最理想的继承方式
5.4.7 增强型寄生组合继承
寄生组合式继承能够很完美地实现继承,但也不是没有缺点。inherit() 方法中复制了父类的原型,赋给子类,假如子类原型上有自定的方法,也会被覆盖,因此可以通过Object.defineProperty的方式,将子类原型上定义的属性或方法添加到复制的原型对象上,如此,既可以保留子类的原型对象的完整性,又能够复制父类原型
function Parent(name, age){
    this.name = name;
    this.age = age;
}

Parent.prototype.getName = function(){
    console.log(this.name)
}

function Child(name, age, grade){
    Parent.call(this, name, age);
    this.grade = grade;
}


function inherit(child, parent){
    let obj = parent.prototype;
    obj.constructor = child;
    for(let key in child.prototype){
        Object.defineProperty(obj, key, {
            value: child.prototype[key]
        })
    }
    child.prototype = obj;
}

inherit(Child,Parent);

var child = new Child("xiaoqi",99,99)
// ‘parent’
5.4.8 ES6中class 的继承

S6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

  • class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的
class Person {
            //调用类的构造方法
            constructor(name, age) {
                this.name = name
                this.age = age
            }
            //定义一般的方法
            showName() {
                console.log("调用父类的方法")
                console.log(this.name, this.age);
            }
        }
        let p1 = new  Person('kobe', 39)
        console.log(p1)
        //定义一个子类
        class Student extends Person {
            constructor(name, age, salary) {
                super(name, age)//通过super调用父类的构造方法
                this.salary = salary
            }
            showName() {//在子类自身定义方法
                console.log("调用子类的方法")
                console.log(this.name, this.age, this.salary);
            }
        }
        let s1 = new Student('wade', 38, 1000000000)
        console.log(s1)
        s1.showName()
继承方式优点缺陷
原型链继承能够实现函数复用1.引用类型的属性被所有实例共享;2.创建子类时不能向超类传参
借用构造函数1. 避免了引用类型的属性被所有实例共享; 2. 可以在子类中向超类传参方法都在构造函数中定义了,每次创建实例都会创建一遍方法,无法实现函数复用
组合继承融合了原型链继承和构造函数的优点,是Javascript中最常用的继承模式创建子类会调用两次超类的构造函数
原型继承在没有必要兴师动众地创建构造函数,而只是想让一个对象与另一个对象保持类似的情况下,原型式继承完全可以胜任引用类型的属性会被所有实例共享
寄生式继承可以增强对象使用寄生式继承来为对象添加函数,会由于不能做到函数复用造成效率降低,这一点与构造函数模式类似;同时存在引用类型的属性被所有实例共享的缺陷
寄生组合继承使用寄生式继承来为对象添加函数,会由于不能做到函数复用造成效率降低,这一点与构造函数模式类似;同时存在引用类型的属性被所有实例共享的缺陷

@不负代码不负漆
16 声望9 粉丝

专注于前端开发,对前沿技术有高度关注


下一篇 »
New的原理