原生JS专栏 - 原型/原型链, 五种继承模式

原生js专栏 - 原型/原型链, 继承模式

目录:

  • 原型

  • 原型链

  • 继承

    • 原型链继承

    • 借用构造函数

    • 共享原型

    • 圣杯模式

    • ES6 class

原型

原型这个东西呢, 我们可以把他理解为祖先, 拿我们人类来说, 我们的祖先生来就是有鼻子有眼, 有细胞, 所以我们继承了祖先的一些特质, 祖先也就是我们的原型, 而在程序中, 我们可以在创建对象的时候给对象设置原型从而让对象具有一些先天的特质, 这就是js中的原型, 我们来看看原型的基本概念

定义: 原型是Function对象的一个方法, 它定义了构造函数构造出来的对象的公共祖先, 通过构造函数构造出来的对象会继承该公共祖先(原型)的属性和方法, 原型也是一个对象, 在函数一声明的时候, 它身上就有一个属性叫做prototype, prototype就是原型

我们来屡屡这关系, 构造函数的原型就是构造函数构造出来的对象的爹(爹和祖先你随便想, 差不太离), 看个实例

// 构造函数Person, 它被我们创建的时候系统就给它加上了一个属性叫做prototype
// 这个prototype 就是原型, 然后只要是这个构造函数构造出来的对象, 他们的爹就是
// 这个prototype, 所以这个prototype上有些什么, Person构造出来的对象就有什么
// 如果我们不设置prototype, 则默认为一个{}
Person.prototype = {
    // 在原型中定义了一个lastName值为curry,这个时候所有由Person构造函数构造出来
    // 的对象身上都有lastName属性, 值为curry, 就好像你一出生你的姓就已经注定好了跟着你爹姓的感觉
    lastName: 'Curry', 
}

Person.prototype.sayHi = function() {
    console.log('hello, 你好')
}
function Person() {};
    
var fstPerson = new Person();
var secPerson = new Person();
console.log(fstPerson.lastName); // 毋庸置疑, 输出curry
console.log(secPerson.lastName); // 输出curry, 既然都是爹了那两个儿子势必都继承这个lastName
fstPerson.sayHi(); // 输出hello, 你好, 因为方法和属性都可以继承

小提示

如果构造函数自身有属性和原型上的属性重合了, 那么跟作用域链的关系一致, 可近的来


// 爹流传下来的姓是Curry
Person.prototype.lastName = 'Curry';

function Person() {
    // Person不高兴了, 直接自立门户, 就不跟着爹姓
    this.lastName = 'Kate';
}

var person = new Person();
console.log(person.name); // Kate, 可近的来 

小提示

我们可以通过原型来提取公有祖先

比如我现在有一个车间, 专门用来生产车子, 车子的主人和颜色都是可以选配的,但是高度长度品牌是固定的


function Car(owner, color) {
    this.owner = owner;
    this.color = color;
    this.lang = 4900;
    this.height = 1400;
    this.brand = 'BMW';
}

我们知道构造函数new的原理是, 在内部进行隐式三步

var this = Object.create(该构造函数的原型);
然后执行你写的代码this.xxx = xxx;
return this; //最后把this返回出去 

既然隐式三步是这样, 那代表即使我在Car车间中的lang, height, brand不会变化, 每new一次他们也会跟着this.xxx = xxx一次, 这样好像不太好, 而且每一个new出来的对象上的这些属性值都一模一样, 是公共属性,这种公共的玩意每次都创建一边感觉有点怪怪的, 所以我们用原型来抽取这些公共属性


Car.prototype = {
    lang: 4900,
    height: 1400,
    brand: 'BMW'
}
function Car(owner, color) {
    this.owner = owner;
    this.color = color; 
}

var fstCar = new Car('loki', 'black');
var secCar = new Car('thor', 'blue');
console.log(fstCar.lang); // 4900
console.log(secCar.brand); // BMW

重点

上面已经展示了很多对于原型属性的查, 我们来看看原型的增删改

  • 原型属性的增删改

    构造函数实例不允许增删改原型上的任何属性和方法, 如果想要增删改原型, 必须使用构造函数进行修改

    Person.prototype = {
        lastName: 'Curry'
    }
    function Person() {}
    
    var person = new Person();
    console.log(person.lastName); // 势必输出Curry吧 
    
    Person.prototype.lastName = 'Kate'; // 进行修改 
    var secPerson = new Person();
    console.log(secPerson.lastName); // 变成Kate
    console.log(secPerson); // {}
    
    // 如果我们强行在实例身上修改会出现什么后果呢
    
    Person.prototype = {
        lastName: 'Curry'
    }
    function Person() {}
    
    var person = new Person();
    console.log(person.lastName); // 势必输出Curry吧 
    
    var secPerson = new Person();
    secPerson.lastName = 'Kate'
    console.log(secPerson.lastName); // Kate
    console.log(secPerson); // {lastName: 'Kate'}
    
    // 我们发现如果你强行更改实例的lastName, 则会在实例上添加一个lastName属性, 
    // 原型不会改变, 从此访问也是可近的来
    

    增删和改的操作是一致…

    小提示

    我们是不能在实例中修改构造函数原型的值没错, 但是指的是构造函数中原型属性的地址不能改, 如果原型中某个属性值是个对象, 那么我们就可以操作了, 引用值只要地址不更改, 其中的数据怎么变化都无所谓

    Person.prototype = {
        address: {
            province: '湖南'
        }
    }
    function Person() {}
    
    var person = new Person();
    
    console.log(person.address.province); // 湖南
    person.address.province = '广东';
    console.log(person.address.province); // 广东
    console.log(person); // {}
    

重点

其实吧, 这个prototype确实是函数一被创建就有的, 他的初始值也是类似于一个空对象, 但是却不是空对象, 在他身上已经被初始化了两个属性, 分别是constructor和__proto__,来看个实例

function Person() {}; 
console.log(Person.prototype); //输出 {constructor: constructor: ƒ foo(), __proto__: Object}

输出结果如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QV0RsNFy-1583719207903)('..')]

也就是说prototype这哥们一出生就自带了constructor和__proto__属性, 我们仔细看会发现这两属性的颜色是浅紫色, 浅紫色的属性代表系统内置属性, 那么这两个内置属性是来干嘛的呢

  • constructor

    构造器, 访问该属性会返回构造该对象的构造函数, 如下

    function Person() {};
    
    var person = new Person();
    
    console.log(person.constructor); // ƒ Person() {}
    // person的构造函数就是Person, 所以他返回Person
    
    

    这个属性的功能主要也就是我们在开发中, 构造函数写的多了, new出来的实例也多了, 哪一天都迷失自己了, 不知道哪个实例是谁生的了, 我们就访问这个constructor属性来查看该实例的构造函数

    小提示

    prototype上的constructor属性是可以被修改的, 所以我们没事不要瞎整瞎改, 一改就会让构造函数构造出来的实例连自个是谁造出来的都搞错了, 简称六亲不认, 所以咱尽量不要修改constructor的值

    function Person() {};
    
    Person.prototype.constructor = 'hello? '
    
    var person = new Person();
    console.log(person.constructor); // 输出hello?
        
    // constructor本质上就是对象上的一个属性, 所以属性值可以为任何值
    
  • __proto__

__proto__这个熟悉只有一个作用: 存储原型

我们先来说说实例上的__proto__属性

当我们用构造函数new出一个实例的时候, 内部会进行隐式三段, 而这个隐式三段的第一步就会设定原型

var this = Object.create(构造函数的原型)

create完毕以后这个构造函数的原型总要有地方放吧, 所以干脆把构造函数实例的__proto__属性存入了构造函数的原型


function Person() {};

var person = new Person();

console.log(person.__proto__ === Person.prototype); // 输出true

重点

这个__proto__有什么用, 当在一个对象身上查找他没有的属性的时候, 他会通过__proto__属性往他的原型上找, 如果他的原型也没有的话, 会继续通过他原型(他原型也是对象, 所以也有__proto__属性)的__proto__属性继续往上找, 知道找到最顶头都没有就返回undefined, 由这些__proto__组合在一起的一层套一层的关系我们称作原型链, 而查找的方式叫做访问原型链, 关于原型链这里先提一嘴, 后面详细说

var obj = {}; // 这个obj上啥都没有

console.log(obj.toString); // 我查找obj上的这个属性 输出ƒ toString() { [native code] }

小提示

同样 这个__proto__属性也是可以进行手动更改的, 手动更改会造成原型链混乱, 不建议更改

Person.prototype = {
    name: 'loki'
}

function Person() {};

var person = new Person();

console.log(person.name); // loki

person.__proto__ = {name: 'thor'};

console.log(person.name); // thor

原型链

OK, 到了原型链, 我们先来看个栗子


Person.prototype = {
    lastName: 'thor'
}
function Person() {}; 

var person = new Person();

console.log(person.name); // 输出thor是跑不掉的吧
console.log(person.constructor); // 输出Person构造函数也是跑不掉的吧
console.log(person.toString); // ??? 输出了toString方法

上方例子, 输出person.name是因为person.__proto__指向了Person.prototype, 输出constructor是因为Person.prototype中本来就存在了constructor指向构造函数Person本身, 输出toString也能输出是为何呢?

function Person() {};

var person = new Person();

console.log(person); // {__proto__}
console.log(Person.prototype); // {constructor: f Person() {}, __proto__};

我们尝试着输出person实例和Person.prototype, 发现Person.prototype上也有个__proto__, 我们说过__proto__的作用只有一条, 就是存储prototype原型, 那么也就是说, Person.prototype也有原型喽, 还真是, 我们将Person.prototype.__proto__打开发现里面存在toString方法

重点

上面这种通过__proto__将不同的对象联系在一起形成的隐形链条叫做原型链

// 以上方的Person构造函数的简单链条表示关系
person.__proto__  -> Person.prototype, Person.prototype.__proto__ -> Object.prototype

我们可以自己也来写一条原型链


// 爷爷
Grand.prototype = {
    lastName: 'Curry'
}

console.log(Grand.prototype.__proto__ === Object.prototype); // true

function Grand() {}

// 我们要让Father连接Grand
Father.prototype = new Grand();
function Father() {}

// 让Son连接Father
Son.prototype = new Father();
function Son() {}

var son = new Son();
console.log(son.lastName); // 输出Curry

小提示

无特殊情况, Object.prototype是所有对象的最终原型, 唯一的特殊情况如下, 特殊情况就是Object.create,该方法是用来创建对象的, 该方法可以接收一个参数, 用来指定被创建对象的原型, 原型可以是一个对象或者是null

var obj = Object.create(null);
console.log(obj.toString); // 报错 obj.toString is not a function

继承(圣杯模式)

在js的历史长河中, 由于他不像其他静态语言一样与生俱来就有class和extends, 所以导致js的继承走的挺坎坷的, 从过去到现在也出现过了好几种继承模式, 也是一种漫长的演变和完善的过程

  • 原型链继承

  • 借用构造函数进行继承

  • 共享原型

  • 圣杯模式

  • Es6 class

我有一个需求, 子类需要继承父类的原型属性, 我分别用几种不同的方法来展现结果进行对比(借用构造函数实现继承呢, 其实不算真正意义的继承, 算是借尸还魂, 所以在这个方法上我会举另外一个例子)

  • 传统形式 => 原型链继承

        Father.prototype = {
            lastName: 'Curry'
        }
        function Father() {
            this.age = 40;
        }
    
        var father = new Father();
    
        Son.prototype = father;
        function Son() {}
    
        var son = new Son();
    
        console.log(son.lastName); // 输出Curry毋庸置疑
        console.log(son.age); // 输出40
    

    我们其实可以看出, 这个age属性我们是不想继承的, 但是没办法, 跑不掉的, 只要使用原型链继承的方式就会出现这种问题, 它会继承过多的没用的属性

  • 借用构造函数进行继承

    需求是, 我有一个车间工厂, 专门用来生产车, 然后有另一个保时捷车间工厂生产保时捷, 然后我用某种方式来让保时捷工厂可以借用车间工厂的功能, 如下

    function Car(lang, height) {
        this.lang = lang;
        this.height = height;
    }
    
    
    function Porsche(owner, color, lang, height) {
        // 我保时捷车间没有控制宽高的方法, 怎么办呢
        // 我找别人借用一下这个方法实现我的功能, 
        Car.call(this, lang, height);
        this.owner = owner;
        this.color = color;
    }
    
    var porsche = new Porsche('loki', 'black', 4900, 1400);
    console.log(porsche);// 输出 {lang: 4900, height: 1400, owner: 'loki', color: 'black'}
    

    上面这种使用call和apply实现借用他人构造函数实现我的功能的方法叫做借用构造函数继承, 这种继承的方式最大的短板就是每次都要多走一个函数, 对性能消耗比较大, 所以几乎没什么人会使用, 了解就好

  • 共享原型

    后来人们想到, 既然我通过原型链继承会继承过多的父类的属性, 那么我的prototype就不指向父类的实例了, 我直接指向父类的prototype, 子类和父类共享一个原型地址, 不就不会继承过多属性了吗

    Father.prototype = {
        lastName: 'Curry'
    }
    function Father() {
        this.age = 40;
    } 
    
    Son.prototype = Father.prototype; // 我们将子类的原型地址直接设置成父类的原型地址
    function Son() {}
    
    var son = new Son();
    console.log(son.lastName); // 输出Curry
    console.log(son.age); // 输出undefined
    
    // 我们会发现一些奇怪的事情, 由于父类和子类指向同一个原型地址, 所以导致
    // 子类和父类中任意一个人对该原型进行修改, 另一个都会被影响
    
    Son.prototype.habits = ['smoke', 'soccer']; // 儿子有些爱好抽烟和踢球
    var father = new Father();
    console.log(father.habits); // ['smoke', 'soccer']
    
    

    共享原型虽然不会继承父类过多的其他属性, 但是也造成了新的问题, 那就是因为父类和子类的原型指向同一个地址, 所以父类和子类任意一个对构造函数进行更改, 另一个都会被影响

  • 圣杯模式

    后来, 我们的开拓者前辈们研究出来了最终的继承方式 - 圣杯模式, 他不会有上面任意一种方式的弊端, 也同时在工作中被大规模的应用, 直接看个实例

    // 提取出来的公共圣杯模式方法, 可以实现target对origin的继承
    var grailInherit = (function() {
        var F = function() {}
        /*target: 需要继承的目标, origin: 被继承的目标*/
        return function(target, origin) {
            F.prototype = origin.prototype
            target.prototype = new F();
            target.constructor = target;
            target.superFather = origin;
        }
    } ())
    
    Father.prototype = {
        lastName: 'loki'
    }
    function Father() {
        this.age = 40;
    }
    
    function Son() {}
    
    grailInherit(Son, Father);
    
    Son.prototype.habits = ['smoke', 'soccer'];
    
    var father = new Father();
    var son = new Son();
    
    console.log(father.habits); // undefined
    console.log(son.lastName); // loki
    console.log(son.habits); // ['smoke', 'soccer']
    console.log(son.age); // undefined
    

    重点

    我们利用了另外一个构造函数F, 我们让F的prototype 直接共享Father的prototype, 这样导致new 出来的F实例 不会继承过多的Father的属性, 然后我们将Son.prototype指向该F实例, 所以F实例很干净没有任何多余的自己的属性, 所以导致Son也很干净没有继承到F的过多属性, 同时因为F实例的__proto__指向了Father的prototype, Son的实例__proto__指向了F实例, 所以Son实例是可以读取到Father身上的lastName的, 同时Son的原型进行任何修改跟Father都无关

  • ES6的继承

    在ES6中, 官方推出了class类和extends继承关键字, 用来帮助我们更好的实现继承, 这里不做太多的解释, 后面在ES6的文章中会具体写到, 可以先混个脸熟

    class Father {
        constructor() {
            this.age = 40; // 所有值钱写在构造函数内部的this.xxx = xxx的代码写入constructor中
        }
        // 所有原型属性和方法直接写在class大括号中
        lastName = 'Curry'
    
        // 所有静态属性和方法需要用static关键字声明, 也是直接写在class大括号中
        static isStatis = true
    }
    
    
    class Son extends Father {
        // 只要是继承的操作, constructor中必须执行super, 用来执行父级的构造函数, 必须填写,super是一个关键字, 不同的地方作用不同 
        constructor() {
            super();
        }
    }
    
    const son = new Son();
    
    console.log(son.lastName); // Curry
    console.log(son.age); // undefined
    

    class其实跟Java, C的class也不太一样, js的class类本质上就是构造函数的语法糖而已, 至于原理部分, ES6专栏见

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章