梳理清楚JS中的【创建对象的模式】+【原型&原型链】+【继承】

吐槽君 分类:javascript

面试官:谈谈对原型链和继承的理解

是不是每次被问到这个问题,都感觉自己懂一些,但是仍然把握不大呢?或者不知道该怎么描述。

这次跟着我一起,梳理一下JS中的【创建对象的模式】+【原型&原型链】+【继承】。

创建对象的模式

说到原型链,就不得不提到 JS 中创建对象的方式了。

最简单的,通过对象字面量,或者 new Object() 的方式。但是当我们想创建一组拥有差不多的属性的对象时,这时候就需要批量生产。

最开始,工厂函数诞生了。

工厂模式

function foo(name) {
    const obj = {};
    obj.name = name;
    obj.sayname = () => {
        console.log(obj.name);
    };
    return obj;
}

const foo1 = foo('z');
 

工厂函数确实解决了批量创建函数的问题,但是其没有为所创建的对象赋予一个是谁创建的标识。其创建的所有对象,仍然直接是 Object 的实例。也就是没有解决对象的识别问题。(我是谁?我在哪?我娘老子是谁?)

alert(foo1 instanceof foo); //false
alert(foo1 instanceof Object); //true
 

接着,构造函数解决了工厂模式的问题。

构造函数模式

function Person(name) {
    this.name = name;
    this.sayName = () => {
        console.log(this.name);
    };
}

const person1 = new Person('zl');
 

构造函数与工厂模式不同的是,其在函数内部使用了 this 对象,并且在创建对象的时候,使用的是 new 操作符。

new 操作符所做的操作就是,创建一个对象,将构造函数的作用域指向这个对象,this 也指向这个对象。执行构造函数里的语句。返回这个对象。

构造函数会在对象的原型链上新增该构造函数。也就解决了对象的识别问题。

然而,无论是工厂模式还是构造函数模式,都有一个问题,那就是不同的实例上,其拥有的同名的方法是不相等的。

每次执行工厂模式的函数或者构造函数里的语句时,都是创建了一个新函数。所以任意两各实例上的同名函数都不是同一个函数。这导致了内存的浪费。而且对于构造函数来说,由于有 this 的存在,便可以将函数在全局中定义好。这样也可以让所有实例的这个函数指向同一个函数。但是如果有很多函数的话,这样会导致全局作用域拥挤,全局作用域也名不副实了。

所以为了决绝这个问题,便有了原型模式

原型模式

纯粹的原型模式

function Person() {}

Person.prototype.name = ['zl'];
Person.prototype.sayName = () => {
    console.log(this.name);
};
 

纯粹的原型模式,直接往构造函数的原型上添加属性和方法。这样其创建的对象也会继承这些属性和方法。但是其问题便是,引用类型的值,都指向同一个堆。而且不能传递参数。

所以最常用的模式是 组合使用构造函数模式和原型模式。

构造函数里创建为对象添加属性,原型中为对象添加方法。

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

将其组合在一起

function Person(name) {
    this.name = name;
    if (typeof this.sayName !== 'function') {
        Person.prototype.sayName = () => {
            console.log(this.name);
        };
    }
}
 

说完了以上,便也理解了 JS 中原型和原型链的大概意思。寻找一个对象的属性时,如果其本身没有,那么便会往上层原型寻找,直到寻找到所有对象的原型 Object。直到 null,也就是原型链的最顶端。

那么,具体怎么寻找,需要谈到两个属性 prototype_proto_

只有函数才有 protoytpe 属性。一般的 对象只有 _proto_ 属性。并且对象的 _proto_ 属性指向创建这个对象的构造函数的 prototype 属性。

比如我们访问一个创建自 B 构造函数的对象 AtoString 方法的时候,这个对象本身并没有这个方法,于是通过其 _proto_ 属性访问构造函数 Bprototype 属性,结构发现构造函数的原型上也灭有这个方法,于是有通过构造函数 B_proto_ 属性访问 Object.prototype

说完了创建对象和原型和原型链。就需要说一下 JS 的继承了。

继承

首先是原型链继承。

原型链继承

function Super(name) {
    this.name = name;
}
Super.prototype.sayName = () => {};

function Me() {}
Me.prototype = new Super();
Me.prototype.constructor = Me;

Me.prototype.sayname = () => {};
 

将超类型的实例赋值给子类型的原型对象。

这样的问题依旧是继承而来的所有属性和方法都在原型对象上。一旦属性是引用类型值

构造函数继承,解决了引用类型值的问题。

构造函数继承

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

function Me(age) {
    Super.call(this, 'zl');
    this.age = age;
}
 

构造函数继承的问题就是继承的函数的问题。如果超类型将方法也放在构造函数里,那么创建的 子类型的对象,其方法都不想等。

组合使用构造函数模式和原型模式实现继承。

组合继承

function Super(name) {}
Super.prototype.sayName = () => {};

function Me(age) {
    Super.call(this, 'zl');
    this.age = age;
}

Me.prototype = new Super();
Me.prototype.constructor = Me;

Me.prototype.sayAge = () => {};
 

以上是按照基础的原型链和构造函数的思路,三种继承方式。

原型式继承

Object 本身提供了一种方法,叫做 Object.create();该方法也是一种继承方式。它接受一个对象,作为创建的对象的原型。其原理就是手动创建一个构造函数,将传入的 对象当作这个构造函数的 原型。并返回构造函数的 实例对象。也被称做原型式继承。

function foo(obj) {
    functioon Foo() {};
    Foo.prototype = obj;
    return new Foo();
}
 

不过这种继承方式,引用类型的值将会一致共享。(都在同一个原型对象上。)

最后说一种 ES6 采用的继承方式:寄生组合式继承

寄生组合式继承

前面说过,组合继承是 JS 最常用的继承方式。

回过头看下构造函数和原型模式组合的继承方式,会发现,这种模式有一种美中不足的地方:两次调用了超类型的函数。

一次是在子类型构造函数内部。这是为了继承超类型的构造函数里定义的属性。防止引用类型值的问题。

第二次是在为子类型原型对象赋值的时候。这个时候将超类型的实例赋值给了子类型的原型对象。这个时候,原型对象里面也拥有超类型构造函数里的属性。只不过,在子类型的对象读取这些属性的时候,由于其对象本身就有这些属性,会优先读取本身的属性,不会寻找到原型对象上来。

寄生组合模式就是优化了这一点,其实在为子类型原型赋值的时候,我们只需要超类型的原型上的属性。

function Super(name) {
    this.name = name;
}
Super.prototype.sayName = () => {};

function Me(age) {
    Super.call(this, 'zl');
    this.age = age;
}

let _prototype = Object(Super.prototype);
Me.prototype = _prototype;
Me.prototype.constructor = Me;

Me.prototype.sayAge = () => {};
 

回复

我来回复
  • 暂无回复内容