原型与继承全面刨析

686 阅读5分钟

原型与继承全面刨析

1. 什么是原型、原型链

​ 原型就是一个叫做 prototype的对象。

​ 在js中每个数据类型都是一个个对象,即使像number、string这样的简单数据类型也是由Number、String这些构造函数实例出来的。

​ 这些个实例对象本身都有一个__proto__属性,这个属性保存的(一般情况下)是它构造函数原型的引用。

​ 函数也是对象,但它能作为构造函数生成实例对象。它的特殊在于它除了拥有__proto__之外还拥有一个Prototype 这个Prototype就是主角

从写一个构造函数开始:

function Foo(){}

这里Foo就是我们的构造函数,它被声明出来是编译器内部其实做了两件大事。

  1. 在堆内存中占了一片空间,作为构造函数的储存地址
  2. 又在堆内存中占了一片空间,作为此构造函数原型Prototype 的存储地址
  3. 构造函数中属性Prototype 指向2中原型地址,同时这个原型对象中也有一个叫做constructor的属性指回构造函数。即构造函数对象和它的原型对象就像一个双向链表(注意有时候我们会新指定原型对象,一定不要忘了与再与构造函数相关联)

new Foo()之后

知晓new做了什么:

  • 为这个新实例对象创建内存空间
  • 将构造函数的this指向新实例对象
  • 将新实例对象的__proto__指向构造函数的原型对象
  • 执行构造函数内部的逻辑,这里没有return,构造函数也会将创建的新实例创建出来

在这里插入图片描述

那什么是原型链呢?

​ 上面Foo构造函数的原型对象本身也是一个对象。即拥有__proto__指向它的构造函数的原型,一个对象的构造函数即ObjectObject 的原型也是对象,故也有__proto__,不过此时__proto__里面便只有null了。

在这里插入图片描述

这一条链子就叫做原型链。

2. 关于原型的一些操作

2.1 原型检测

instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

通过栗子来理解

在这里插入图片描述

isPrototype

isPrototypeOf() 方法用于测试一个对象是否存在于另一个对象的原型链上。

如下面Objec的原型是出现在foo的原型链中的

在这里插入图片描述

2.2 检测对象属性

in

会去找原型链

hasOwnProperty

只看本身

2.3 设置原型

2.3.1 Object.create

Object.create方法创建一个新的对象,其参数是可以指定这个新对象的原型。

创建一个新对象,使其原型为foo

在这里插入图片描述

2.3.2 __proto__

__proto__当然也是可以改变对象的原型的,但是这里是需要注意下面几点

  • __proto__它本质来说不是对象属性,可以理解为访问原型对象时的一个getter/setter,它的值只被允许是对象或null
  • 虽然__proto__可以获取和设置原型,但是一般对原型设置不建议使用。下面有专用的api

2.3.3 setPrototypeOf 和getProttoeypOf

在这里插入图片描述

2.3.4 最后需要注意的

在这里插入图片描述

3. 继承

class

es6以后,js增加了class语法。

class A {
    constructor(x) {
        this.x = x;
    }
    sayHello() {
        console.log(`hello ${this.x}`);

    }
}
class B extends A {
    constructor(x) {
        super(x)
    }

}
const b = new B('b');
b.sayHello()

​ 这使得我们可以和书写java一样的,书写js中的继承。但是请注意它现在虽然可以这样写,其实现继承的本质依然没有改变。继承的核心仍然是原型,class只是一个语法糖罢了。

回归

function A(x) {
    this.x = x;
}
A.prototype.sayHello = function() {
    console.log(`hello ${this.x}`);

}

function B(x) {
    A.call(this, x);
}

B.prototype.__proto__ = A.prototype;

const b = new B('b')
b.sayHello();

为什么属性写在构造函数里,方法写在原型对象中

​ 注意我们实例新对象时,主要执行的是构造函数。比如把构造函数中的一些属性放到新对象中。如上面的x。如果再往里面添加一些方法,那么新对象中又得需要保存它们的引用,同时令开辟内存保存它们的实体。那么我们每new一个就多一个这样的操作。那么多重复的方法即浪费了空间又无从谈起可复用。

4. 7大继承写法

4.1 原型链继承

原理:将父类的实例对象设置为子类的原型

function A(){}
function B(){}
B.prototype = new A();

优点:简单

缺点:重设了B的原型对象但是没有回指,故后面拿不到构造函数B。且A的实例属性变成了B的原型属性

4.2 借用构造函数

原理:子类构造函数中调用父类的构造函数

function A(x){
    this.x=x;
}
function B(x){
    A.call(this,x);
}

优点:可以向父类传参

缺点:东西都写在构造函数,函数复用无从谈起了。

4.3 组合继承

原理:4.1+4.2

function A(x){
    this.x=x;
}

function B(x){
    A.call(this,x);
}
B.prototype=new A();
B.prototype.constructor =B;

优点:即实现了方法复用又可以向父类传值

缺点:会调用两次构造函数A

4.4 原型式继承

原理:设置一个函数,里面新建一个构造函数,且函数的参数即使该内部构造函数的原型。最后返回该构造函数实例

function object(o){
    function A(){};
    A.prototype=o;
    return new A();
}

其实这个函数不就是Object.create()

缺点:回看4.1

4.5 寄生式继承

思想:4.4 + 再搞一个封装继承过程的函数

//寄生式继承
function obj(o) {
    function Foo() {};
    Foo.prototype = o;
    return new Foo();
}
function createAnoter(o) {
    let clone = obj(o)
    clone.sayHi = function() {};
    return clone;

}

优缺点:参考4.2

4.6 寄生组合式继承

思想:就是实现一个extend,完成原型链的链接及原型和构造函数的回链

//寄生组合式继承
function extend(fa, son) {

    son.prototype = Object.create(fa.prototype);
    son.prototype.constructor = son

}

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

function Son(name, age) {
    Fa.call(this, name, age)
}
extend(Fa, Son)

优缺点:参考4.3 。比4.3好一点的是这里的父类构造函数只被调用一次

4.7 Es6

第3节已有举例

总结

在这里插入图片描述