js 原型、原型链、继承

js 原型、原型链、继承

JavaScript中,原型也是一个对象,通过原型可以实现对象的属性继承,JavaScript的对象中都包含了一个[[Prototype]]内部属性,这个属性所对应的就是该对象的原型。
[[Prototype]]作为对象的内部属性,是不能被直接访问的。所以为了方便查看一个对象的原型,Firefox和Chrome中提供了__proto__这个非标准(不是所有浏览器都支持)的访问器(ECMA引入了标准对象原型访问器Object.getPrototype(object))。在JavaScript的原型对象中,还包含一个constructor属性,这个属性对应创建所有指向该原型的实例的构造函数

原型对象,对象原型

构造函数,实例,原型对象三者的关系可以使用一张图来理解

原型对象prototype

每一个构造函数都有一个属性prototype,我们也称之为原型对象,原型对象另外开辟一个内存空间,用来存放方法,作用是为了共享方法,从而达到节省内存

function Person(uname, age) {
    this.uname = uname;
    this.age = age;
};

Person.prototype.eat = function () {
    console.log('吃饭');
};
let jerry = new Person('jerry', 18);
jerry.eat();

// 实例对象有一个__proto__属性,可以指向该实例对象的构造函数的原型对象
console.log(jerry.__proto__);
console.log(Person.prototype);
console.log(jerry.__proto__ === Person.prototype);      // true

原型对象属性constructor

原型对象上有个属性constructor,用来指向该原型对象的构造函数

function Person(uname, age) {
    this.uname = uname;
    this.age = age;
};

// 这里指向的就是Person构造函数
console.log(Person.prototype.constructor === Person);   // true

可以通过Person.prototype.run = function(){}的方式添加方法
以这种方式创建方法的话,原型对象上其中有个自带的属性constructor,用来指向原型对象的构造函数

function Person(uname, age) {
    this.uname = uname;
    this.age = age;
};
Person.prototype.sing = function () {
    console.log('sing');
}
Person.prototype.jump = function () {
    console.log('jump');
}
console.log(Person.prototype);

另外一种创建方法的方式,这种方式创建会覆盖原有的原型对象,且原型对象上的constructor属性也会没有,所以必须重新将constructor指向构造函数

function Person(uname, age) {
    this.uname = uname;
    this.age = age;
};
Person.prototype = {
    rap: function () {
        console.log('rap');
    },
    basketball: function () {
        console.log('basketball');
    },
    // 重新指向构造函数
    constructor: Person,
}
console.log(Person.prototype);

对象原型__proto__

实例对象有一个属性,可以指向该实例对象的构造函数的原型对象

function Person(uname, age) {
    this.uname = uname;
    this.age = age;
};

let jerry = new Person('jerry', 18);

// 实例对象有一个属性,可以指向该实例对象的构造函数的原型对象
console.log(jerry.__proto__);
console.log(Person.prototype);
console.log(jerry.__proto__ === Person.prototype);      // true

原型链

当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
如果没有就查找它的原型(也就是__proto__指向的prototype原型对象)。
如果还没有就查找原型对象的原型(Object的原型对象)。
依此类推一直找到Object 为止(null)。
__proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。

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

let jerry = new Person('jerry', 18);

// Person.prototype指向Person构造函数的原型对象
// jerry.__proto__也指向Person构造函数的原型对象

// 原型对象也是对象,也有__proto__方法,原型对象的原型对象是Object
console.log(Person.prototype.__proto__);

// Object原型对象的构造函数是Object()构造函数
console.log(Person.prototype.__proto__.constructor);

// 在往上找Object原型对象的原型对象,是null,这个就是原型链的顶端,并不是无限下去的
console.log(Person.prototype.__proto__.__proto__);

对象的原型链

构造函数的原型链

继承

原型链继承

// 原型链继承,定义一个父类
function Father(name) {
  this.fatherName = [name]
}

// 父类定义一个方法
Father.prototype.printFatherName = function () {
  console.log(this.fatherName);
}

// 定义一个子类
function Son(name) {
  this.sonName = name
}

// 把子类的原型对象指向父类的实例
Son.prototype = new Father('Father')

// 由于覆盖了原有的prototype,需要将它上面的constructor指向子类,否则原型链就会混乱
Son.prototype.constructor = Son

Son.prototype.printSonName = function () {
  console.log(this.sonName);
}

// 实例化子类,此时子类就继承了父类的printFatherName方法(原型链方法会向上查找)
let s = new Son('Son')

s.printFatherName()             // ["Father"]
s.printSonName()                // Son
console.log(s.fatherName);      // ["Father"]
console.log(s.sonName);         // Son


// 原型链继承的缺陷,对于引用数据类型,如果Son有多个实例,其中一个实例对继承的父类属性做了修改,那么所有实例都会随之修改
let s1 = new Son('Son1')
s1.printFatherName()            // ["Father"]
s1.printSonName()               // Son1
console.log(s1.fatherName);     // ["Father"]
console.log(s1.sonName);        // Son1

s.fatherName.push('newName')    // s 对继承的属性做了修改,此时 s1 也会随之修改
console.log(s.fatherName);      // ["Father", "newName"]
console.log(s1.fatherName);     // ["Father", "newName"]

构造函数继承

使用call改变了this指向的继承方式,称为构造函数继承

function Father(name) {
  this.fatherName = [name]
}

Father.prototype.printFatherName = function () {
  console.log(this.fatherName);
}


function Son(name) {
  // 使用call改变了this指向,
  // 调用Father函数就相当于是把函数执行了一遍,在子类里面创建了一个和父类相同的属性,但是this是指向的子类实例
  Father.call(this, name)
}

let s = new Son('Son')
console.log(s.fatherName);

let s1 = new Son('Son1')
console.log(s1.fatherName);

// 解决了多个子类修改父类属性造成同步更改的问题
s.fatherName.push('newName')
console.log(s.fatherName);      // ["Son", "newName"]
console.log(s1.fatherName);     // ["Son1"]

// 但是又有个问题就是子类无法继承父类的方法
s.printFatherName()             // 报错  Uncaught TypeError: s.printFatherName is not a function

组合继承

把原型链继承和构造函数继承结合使用,就是组合继承

function Father(name) {
  this.name = name
  this.arr = [1, 2, 3]
}

Father.prototype.printName = function () {
  console.log(this.name);
}


// 构造函数继承父类的方法和属性
function Son(name, age) {
  Father.call(this, name)
  this.age = age
}

// 原型链继承父类原型上的方式和属性
Son.prototype = new Father()
Son.prototype.constructor = Son

Son.prototype.printSonAge = function () {
  console.log(this.age);
}


let s = new Son('son', 18)
s.printSonAge()
s.printName()
console.log(s.arr);

s.arr.push(4)


// 可以调用父类原型上面的方法,实例的属性修改也不会影响其他实例的属性
let s1 = new Son('son1', 20)
s1.printSonAge()
s1.printName()
console.log(s.arr);       // [1, 2, 3, 4]
console.log(s1.arr);      // [1, 2, 3 ]

// 缺点:会调用两次父类的构造函数,第一次是在 new Faher() 的时候, 第二次是在new Son()的时候(new Son() 里面会执行 Father.call)
// 因此实例的原型对象上会有两份相同的属性,解决这个问题可以用寄生组合继承

原型式继承

let Father = {
  name: 'Father',
  friends: ['A', 'B', 'C'],
  printName: function () { console.log(this.name); }
}

// 原型式继承原理是,创建一个中间实例,中间实例的原型指向父类(这个父类此时是一个对象的实例)
// 这个函数是Object.create函数的模拟实现
function createObj(obj) {
  function F() { }
  F.prototype = obj
  return new F()
}

let Son1 = createObj(Father)
let Son2 = createObj(Father)

// 使用下面Object.create()函数是一样的
// let Son1 = Object.create(Father)
// let Son2 = Object.create(Father)

// 这里修改name值,并不是修改了原型上的name,而是给自己身上加了一个name属性
Son1.name = 'Son1'
Son2.name = 'Son2'

Son1.printName()
Son2.printName()

// 缺点:原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
Son1.friends.push('D')
console.log(Son1.friends);  // ["A", "B", "C", "D"]
console.log(Son2.friends);  // ["A", "B", "C", "D"]

寄生式继承

let Father = {
  name: 'Father',
  friends: ['A', 'B', 'C'],
  printName: function () { console.log(this.name); }
}

// 寄生式继承主要用来增强对象
function createObj(obj) {
  var clone = Object.create(obj)
  clone.printAge = function () { console.log(this.age); }
  return clone
}

let son1 = createObj(Father)
son1.name = 'jerry'
son1.age = 18
son1.printName()
son1.printAge()

let son2 = createObj(Father)
son2.age = 20
son2.printName()
son2.printAge()

寄生组合继承

寄生组合继承应该算是es5中最完美的继承方式

function Father(name) {
  this.name = name
  this.friends = ['A', 'B']
}

Father.prototype.printName = function () {
  console.log(this.name);
}

function Son(name, age) {
  Father.call(this, name)
  this.age = age
}

// 创建一个中间函数
let tempFunc = function () { }

// 中间函数的prototype指向Father的原型对象
tempFunc.prototype = Father.prototype

// 子类的prototype指向中间函数的实例
Son.prototype = new tempFunc()

// 修正constructor指针
Son.prototype.constructor = Son


// 子类增加一个方法,对其他继承Father的子类没有影响
Son.prototype.printAge = function () { console.log(this.age); }


let son1 = new Son('jerry', 18)
son1.printName()  // jerry
son1.printAge()   // 18

let son2 = new Son('tom', 20)
son2.printName()  // tom
son2.printAge()   // 20


son1.friends.push('D')
console.log(son1.friends);  // ["A", "B", "D"]
console.log(son2.friends);  // ["A", "B"]

// 有人要问,为什么不能直接Son.prototype = Father.prototype,
// 这是因为如果多个子类,都指向同一个父类的prototype,子类的独立性就没有了,
// 如果其中一个子类对原型上面的东西做了修改,其他所有子类都会发生变化


// 其他子类
function Child(name, age) {
  Father.call(this, name)
  this.age = age
}

let F = function () { }
F.prototype = Father.prototype
Child.prototype = new F()
Child.prototype.constructor = Child

// 子类的原型上添加属性或方法,不会影响其他子类
Child.prototype.printAge = function () { console.log('child age', this.age); }

let c1 = new Child('child1', 22)
c1.printName()  // child1
c1.printAge()   // child age 22

ES6 class 继承

先看一个简单的例子

class Father {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    run() {
        console.log('I\'m running, by father');
    }
}

class Son extends Father {
    constructor(name, age, birth) {
        super(name, age);
        this.birth = birth;
    };
    run() {
        super.run();
    }
    fly() {
        console.log('I\'m flying');
    }
}

let son = new Son('jerry', 18, '2000');
console.log(son.name);  // jerry
console.log(son.age);   // 18
son.run();              // I'm running, by father
son.fly();              // I'm flying

一个稍微复杂的例子

class fatherCalculator {
    constructor(num1, num2) {
        this.num1 = num1;
        this.num2 = num2;
    };
    sum() {
        return this.num1 + this.num2;
    }
    multiply() {
        return this.num1 * this.num2;
    }
    log() {
        return [this.num1, this.num2];
    }
}

let cal = new fatherCalculator(1, 2);
console.log(cal.sum());;    // 3


// super关键字用于访问和调用对象父类上的函数。可以调用父类的构造函数,也可以调用父类的普通函数
// super关键字指向当前对象的原型对象, sonCalculator.prototype 指向的是 fatherCalculator
class sonCalculator extends fatherCalculator {
    constructor(num1, num2) {
        super(num1, num2);
    };
    sum() {
        return super.sum()
    };
    reduce() {
        return this.num1 - this.num2;
    }
}

let cal2 = new sonCalculator(3, 4);
console.log(cal2);
console.log(cal2.sum());    // 7
console.log(cal2.reduce()); // -1

// 如果子类中没有方法,会向父类中去找,就近原则
console.log(cal2.multiply());  // 12

// 当子类没有constructor的时候可以随意用父类的,但是如果子类也含有的话,constructor会返回实例,this的指向不同,不可以再直接使用父类的东西
class son extends fatherCalculator { };
let cal3 = new son(10, 10);
console.log(cal3.sum());    // 20

class son1 extends fatherCalculator {
    constructor(a, b, c) {  // 前两个参数是父类构造函数中的参数,无法在子类中拿到,但是可以调用父类的方法来获取到
        super(a, b)         // 子类constructor中必须先使用super,否则报错继承 Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
        this.c = c
    }
    log() {
        // 在子类中无法直接拿到父类中的属性,但是可以在父类中暴露api,子类中进行获取
        console.log(super.log());               // [1, 2]

          // 这里无法拿到a, b参数,只能拿到自己的属性c
        console.log(this.a, this.b, this.c);    // undefined undefined 33

        return [super.sum(), this.c]  
    }
}
let cal4 = new son1(1, 2, 33)
console.log(cal4.log());    // [3, 33]

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com