Javascript 的原型和继承

概念

  1. 在 JavaScript 中,函数是允许拥有属性的。所有的函数会有一个特别的属性 prototype,叫做函数的原型对象;同时原型对象中,有一个叫做constructor的属性,指向这个函数本身。
  2. 基于构造函数创建对象 (new) 后,每个实例对象都有一个私有的 __proto__ 属性,指向其构造函数的原型对象, es6 中利用Object.getPrototypeOf()来访问对象的原型。
  3. 原型对象又有自己的原型对象 __proto__ ,层层向上直到某个原型对象为 null。
  4. 几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例,Object 的原型对象是 null。

注意:如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用 constructor 指回原来的构造函数

屏幕快照 2021-06-28 下午8.43.24.png

原型链

原型链是由__proto__串联起来的链状结构。

当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型 __proto__ ,以及该对象的原型的原型 __proto__.__proto__ ,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

注意:如何区分一个属性到底是基本的还是从原型中找到的呢?答案:hasOwnProperty

for (let item in obj) { // 会列举出obj及其原型链上的所有可枚举属性
    if (obj.hasOwnProperty(item)) { ... } // hasOwnProperty是Object.prototype中的方法
}
复制代码

创建对象构造原型链

使用语法结构创建的对象

var o = {a: 1};
// 原型链: o ---> Object.prototype ---> null
// 继承 Object.prototype 的属性:hasOwnProperty 等

var a = [1];
// 原型链: a ---> Array.prototype ---> Object.prototype ---> null
// 继承 Array.prototype 的属性:forEach,indexof 等

function f(){ }
// 原型链: f ---> Function.prototype ---> Object.prototype ---> null
// 继承 Function.prototype 的属性:call,bind 等
复制代码

使用构造函数创建的对象

在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作符 来作用这个函数时,它就可以被称为构造方法(构造函数)。

function Foo() {
    this.value = 1;
}
Foo.prototype.showValue = function(){ // 公共方法定义在构造函数的原型上
    console.log(this.value);
};
var foo = new Foo();// 原型链: foo ---> Foo.prototype ---> Object.prototype ---> null
复制代码

屏幕快照 2021-06-28 下午9.28.18.png

注意:我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。

使用 Object.create 创建的对象

原型作为 Object.create 参数,生成一个新对象。

var fooProto = { 
    showValue: function(){ console.log(this.value); }
};
var foo = Object.create(fooProto, {
    value: {
        value: 1,
        writable: true,
        enumerable: true,
        configurable: true
    }
}); // 原型链:foo ---> fooProto ---> Object.prototype ---> null
复制代码

屏幕快照 2021-06-28 下午9.32.52.png

使用 class 关键字创建的对象

ECMAScript6 引入了一套新的关键字用来实现 class。使用基于类语言的开发人员会对这些结构感到熟悉,但它们是不同的。JavaScript 仍然基于原型。

这些新的关键字包括 class, constructor,static,extends 和 super。

class Foo {
  constructor(value) {
    this.value = value;
  }
  showValue(){ console.log(this.value); }
}
var foo = new Foo(1);
复制代码

屏幕快照 2021-06-28 下午9.38.38.png

继承

我们知道面对对象(OOP)的三大特点是:继承,封装,多态(重载,重写)

js并不是严格的面对对象的语言,因为js的面对对象也是基于原型链实现的。

首先定义需要继承的父类:

function Parent(name, height) {
    this.name = name;
    this.height = height;
    this.getName = function(){
        console.log('Name:', this.name);
    };
}
Parent.prototype.getHeight = function(){
    console.log('Height:', this.height);
};
复制代码

利用构造函数实现继承

关键: 在函数对象内调用父类的构造函数,使得自身获得父类的方法和属性。

function Child(name, height, age) {
    Parent.apply(this, [name, height]); // 改变 Parent 的 this 指向并运行,使得 Child 实例拥有了 Parent 的属性和方法
    this.age = age;
}
let ch = new Child('a', 170, 21); // 原型链:ch ---> Child.prototype ---> Object.prototype ---> null
复制代码

屏幕快照 2021-06-28 下午10.30.12.png

优点:

  • 子类实例属性不共享
  • 创建的子类实例可以向父类传递参数
  • 可以实现多继承,用 call / apply 改变父类的this

缺点:

  • 实例是子类的实例,不是父类的 ch instanceof Parent === false
  • 只能继承父类的实例属性和方法,不能继承父类原型上的方法(ch.getHeight()会报错)
  • 无法实现函数复用,每个子类都有父类函数的属性和方法的副本,占用很大的内存,而且当父类的方法发生改变了时,已经创建好的子类实例并不能更新方法

利用原型链实现继承

function Child(age) {
    this.age = age;
}
let pa = new Parent('a', 170);
Child.prototype = pa; // 关键:Child 的原型设为 Parent 的实例
Child.prototype.constructor = Child; //为了不破坏原型链,将constructor指向本身

Parent.prototype.show = function(){
    console.log('show');
} // 父类新加的方法,所有子类实例都可以访问
let ch = new Child(21); // 原型链:ch ---> pa ---> Parent.prototype ---> Object.prototype ---> null
复制代码

屏幕快照 2021-06-28 下午10.35.37.png

优点:

  • 子类与父类的关系为指向关系,实例是子类的实例,也是父类的实例
  • 父类新增的原型方法或属性,子类都能访问

缺点:

  • 原型属性上的引用类型值会被所有实例共享,实例之间相互影响
  • 给子类型原型添加属性和方法必须在替换原型之后】
  • 创建子类型实例时无法向父类型的构造函数传参

组合模式

结合构造继承和原型继承的各自优点来进行对父类的继承。用构造继承属性,而原型继承方法。

function Child(name, height, age) {
    Parent.apply(this, [name, height]);
    this.age = age;
}
Child.prototype = new Parent('a', 170);
let ch = new Child('a', 170, 21);
复制代码

屏幕快照 2021-06-28 下午10.38.48.png
优点:

  • 子类可向父类传参
  • 实例既是子类的实例,也是父类的实例
  • 多个实例之间不存在公用父类的引用属性的问题
  • 实例可以继承父类实例的属性和方法,也可以继承原型上的属性和方法

缺点:

  • 两次调用父类的构造函数,生成了两份实例,相同的属性既存在于实例中也存在于原型中

寄生组合继承

不必为了指定子类的原型而调用父类的构造函数,直接拿到父类的原型对象并继承。

与组合继承区别在于没有实例化父类对象。

几乎结合了构造、原型链、组合继承的所有优点。

function Child(name, height, age) {
    Parent.apply(this, [name, height]);
    this.age = age;
}

(function(){
    let Temp=function(){}; // 创建一个临时的类
    Temp.prototype=Parent.prototype; //子类的原型指向父类的实例
    Child.prototype=new Temp();
})()

let ch = new Child('a', 170, 21);
复制代码

屏幕快照 2021-06-28 下午10.41.53.png

es6 类的继承语法

语法更直观,书写更简便。

class Parent {
    constructor (name, height){
        this.name = name;
        this.height = height;
    }
    static show() {
        console.log('show');
    }
    getName() {
        return this.name;
    };
    getHeight() {
        return this.height;
    };
}
 
class Child extends Parent {
    constructor (name, height, age) {
        super(name, height);
        this.age = age;
    }
}
 
let ch = new Child('a', 170, 21);
复制代码

屏幕快照 2021-06-28 下午10.51.19.png
注意:当继承的函数被调用时,this 指向的是当前继承的对象,而不是继承的函数所在的原型对象。

参考

  1. 继承与原型链
  2. JavaScript 对象入门
  3. js中的原型链,6种继承方式及其案例
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享