一篇就夠-JS繼承的多種方式和實(shí)現(xiàn)

原型鏈繼承

方法:子構(gòu)造函數(shù)的prototype指向?yàn)楦笜?gòu)造函數(shù)的實(shí)例,因?yàn)樵玩準(zhǔn)?strong>proto的鏈表,父構(gòu)造函數(shù)的實(shí)例的proto指向父構(gòu)造函數(shù)實(shí)例的原型。

function Parent(){
    this.name = 'johe'
}

Parent.prototype.getName = function(){
    console.log(this.name)
}

function Child(){

}

//原型必須是對(duì)象,所以為Parent的實(shí)例
Child.prototype = new Parent()

var child1 = new Child()

child1.getName()

問題:

  1. 引用類型的屬性被所有實(shí)例共享
  2. 創(chuàng)建Child的實(shí)例時(shí),不能向parent傳參
function Parent () {
    this.names = ['kevin', 'daisy'];
}

function Child () {

}

Child.prototype = new Parent();

var child1 = new Child();

child1.names.push('yayu');

console.log(child1.names); // ["kevin", "daisy", "yayu"]

var child2 = new Child();

console.log(child2.names); // ["kevin", "daisy", "yayu"]

這是因?yàn)镻arent在實(shí)例化之后,成為了Child的原型,原型上的屬性和方法是共享的。

借用構(gòu)造函數(shù)(經(jīng)典繼承)

調(diào)用父構(gòu)造函數(shù)

function Parent(){
    this.names = ['kevin','daisy'];
}

function Child(){
    Parent.call(this);
}

var Child1 = new Child()

Child1.names.push('johe')

var Child2 = new Child()

//['kevin','daisy']
Child2.names

優(yōu)點(diǎn):

  1. 避免了引用類型的屬性被所有實(shí)例共享
  2. 可以在Child中向Parent傳參
    缺點(diǎn):
  3. 方法都在構(gòu)造函數(shù)中定義,每次創(chuàng)建實(shí)例都會(huì)創(chuàng)建一遍方法
  4. instanceof檢驗(yàn)為false
function Parent(name){
    this.name = name;
}

function Child(name){
    Parent.call(this,name)
}

var Child1 = new Child('johe')

//johe 
Child1.name

組合繼承(原型鏈繼承和經(jīng)典繼承)

優(yōu)點(diǎn):借用構(gòu)造函數(shù)繼承解決了傳參問題和實(shí)例屬性被共享的問題,原型鏈繼承能夠滿足共享方法不被重復(fù)創(chuàng)建。
缺點(diǎn):調(diào)用了兩次父構(gòu)造函數(shù)

function Parent(name){
    this.name = name
    this.names = ['johe']
}

Parent.prototype.getName = function(){
    return this.name
}

function Child(name,age){
    //調(diào)用父級(jí)的構(gòu)造方法,實(shí)現(xiàn)實(shí)例屬性
    Parent.call(this,name)
    this.age = age
}

//這里不用參數(shù)是因?yàn)樽訕?gòu)造函數(shù)調(diào)用父構(gòu)造函數(shù)時(shí)已實(shí)現(xiàn)實(shí)例屬性,有實(shí)例屬性的情況下不會(huì)從原型鏈中查找
Child.prototype = new Parent()

var child1 = new Child('johe',18)
child1.names.push("johe2")
console.log(child1.name);//johe
console.log(child1.age);18
//["johe","johe2"]
console.log(child1.names);

var child2 = new Child('child2',19)
console.log(child2.name);//child2
console.log(child2.age);19
//["johe"]
console.log(child1.names);

原型式繼承(Object.create)

Object.create的模擬實(shí)現(xiàn)

function createObj(o){
    function F();
    F.protoype = o;
    return new F()
}

缺點(diǎn):
包含引用類型的屬性值始終都會(huì)共享響應(yīng)的值,這點(diǎn)跟原型鏈繼承一樣。

寄生組合式繼承(組合繼承優(yōu)化)

組合繼承的最大缺點(diǎn)就是會(huì)調(diào)用兩次父構(gòu)造函數(shù)
一次是設(shè)置子類型實(shí)例的原型:

Child.protoype = new Parent()

一次是創(chuàng)建子類型實(shí)例的時(shí)候:

function Child(name,age){
    Parent.call(this,name)
}

var child1 = new Child('johe',18)

這個(gè)時(shí)候Child.prototype這個(gè)對(duì)象內(nèi)的屬性其實(shí)是沒用的,因?yàn)樽宇愋蛯?shí)例已經(jīng)調(diào)用了父構(gòu)造函數(shù)進(jìn)行了屬性實(shí)例化。

所以就用到了寄生組合式繼承,讓Child.prototype間接的訪問到Parent.prototype

function Parent(name){
    this.name = name
    this.names = ['johe']
}
Parent.prototype.getName = function(){return this.name}

function Child(name,age){
    Parent.call(this,name)
    this.age = age
}

function F(){}

F.prototype = Parent.prototype

Child.prototype = new F()

var child1 = new Child('johe',18)

封裝繼承方法:

function Parent(name){
    this.name = name
    this.names = ['johe']
}
Parent.prototype.getName = function(){return this.name}

function Child(name,age){
    Parent.call(this,name)
    this.age = age
}

function createObject(o){
    function F(){}
    F.prototype = o
    return new F();
}

function setPrototype(child,parent){
    var prototype = createObject(parent)
    prototype.constructor = Child
    Child.protoype = prototype
}

setPrototype(Child,Parent)

這種方式的高效率體現(xiàn)它只調(diào)用了一次 Parent 構(gòu)造函數(shù),并且因此避免了在 Parent.prototype 上面創(chuàng)建不必要的、多余的屬性。與此同時(shí),原型鏈還能保持不變;因此,還能夠正常使用 instanceof 和 isPrototypeOf。開發(fā)人員普遍認(rèn)為寄生組合式繼承是引用類型最理想的繼承范式

這里為什么不直接使用Child.prototype=Parent.prototype,是因?yàn)镻arent.constructor應(yīng)該指向Parent,并且如果我們需要給Child實(shí)例的原型設(shè)置方法和屬性時(shí),會(huì)影響到Parent的實(shí)例,這明顯是不合理的。

ES6的繼承

ECMAScript6 引入了一套新的關(guān)鍵字用來實(shí)現(xiàn) class。使用基于類語言的開發(fā)人員會(huì)對(duì)這些結(jié)構(gòu)感到熟悉,但它們是不同的。JavaScript 仍然基于原型。這些新的關(guān)鍵字包括 class, constructor,static,extends 和 super。

class Fruits{
    constructor(type,size){
        this.type = type;
        this.size = size;
    }
    static sayHello(){
        console.log("hello");
    }
    sayType(){
        console.log('my type is '+this.type);
        return this.type;
    }
}

class Banana extends Fruits{
    constructor(type,size,color){
        super(type,size);
        this.color = color;
    }
    sayColor(){
        console.log('my color is '+ this.color);
        return this.color;
    }
}
let fruit = new Fruits("fruit","small");
let banana = new Banana("banana","small","yellow");
 //parent: Fruits {type:'fruit',size:'small'}
console.log('parent:',fruit);
//child: Banana {type:'banana',size:'small',color:'yellow' }
console.log('child:',banana);

//hello
console.log(Fruits.sayHello());
console.log(Banana.sayHello());

//true
console.log(Fruits.hasOwnProperty("sayHello"));
//false
console.log(Banana.hasOwnProperty("sayHello"));

//false
console.log(fruit.hasOwnProperty("sayType"))
//true
console.log(Fruits.prototype.hasOwnProperty("sayType"));
//my type is banana
console.log(banana.sayType());
//false
console.log(banana.hasOwnProperty("sayType"));
//Fruits {}
console.log(banana.__proto__);

首先查看class語法糖做了什么:

  • 將構(gòu)造函數(shù)內(nèi)的this屬性實(shí)例化
  • 將構(gòu)造函數(shù)外的非static函數(shù)給設(shè)置到構(gòu)造函數(shù)的原型對(duì)象中,共享屬性
  • static函數(shù)設(shè)置為構(gòu)造函數(shù)的屬性

用ES5實(shí)現(xiàn)就是組合方式來創(chuàng)建對(duì)象:

function Fruits(type,size){
    this.type = type;
    this.size = size;
}
Fruits.sayHello = function(){
    console.log('Hello');
}
Fruits.prototype = {
    constructor:Fruits,
    sayType:function(){
        console.log('my type is' +this.type);
        return this.type;
    }
}

再看看extends做了什么:

  • 繼承父類的實(shí)例屬性(調(diào)用父類的構(gòu)造函數(shù))
  • 繼承父類的共享屬性(原型鏈上有父類的原型對(duì)象)
  • 子類構(gòu)造函數(shù)的proto指向父類構(gòu)造函數(shù)(兩個(gè)都是對(duì)象)(繼承靜態(tài)函數(shù))
//true
console.log(Banana.__proto === Fruits);
//true
console.log(banana instance of Fruits);

es6繼承的es5實(shí)現(xiàn)

知道extends做了什么之后,我們可以知道其轉(zhuǎn)化成es5就是寄生組合式繼承(組合=原型鏈繼承+借用構(gòu)造函數(shù)),并且設(shè)置子類構(gòu)造函數(shù)的proto為父類構(gòu)造函數(shù).


function Fruits(type,size){
    this.type = type;
    this.size = size;
}
Fruits.sayHello = function(){
    console.log('Hello');
}
Fruits.prototype = {
    constructor:Fruits,
    sayType:function(){
        console.log('my type is' +this.type);
        return this.type;
    }
}

function Banana(type,size,color){
    Fruits.call(this,type,size);
    this.color = color;
}

function createObject(Parent){
    function Empty(){};
    Empty.prototype = Parent.prototype;
    return new Empty();
}

Banana.prototype = createObject(Fruits);
Banana.prototype.constructor = Banana;
Banana.prototype.sayColor = function(){
    console.log(this.color);
}

Banana.__proto__ = Fruits;

通過babeljs轉(zhuǎn)碼成ES5來查看,更嚴(yán)謹(jǐn)?shù)膶?shí)現(xiàn)。

//es6版本
class Parent{
    constructor(name){
        this.name = name;
    }
    static sayHello(){
        console.log('hello');
    }
    sayName(){
        console.log('my name is ' + this.name);
        return this.name;
    }
}
class Child extends Parent{
    constructor(name, age){
        super(name);
        this.age = age;
    }
    sayAge(){
        console.log('my age is ' + this.age);
        return this.age;
    }
}
// 對(duì)轉(zhuǎn)換后的代碼進(jìn)行了簡(jiǎn)要的注釋
"use strict";
// 主要是對(duì)當(dāng)前環(huán)境支持Symbol和不支持Symbol的typeof處理
function _typeof(obj) {
    if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
        _typeof = function _typeof(obj) {
            return typeof obj;
        };
    } else {
        _typeof = function _typeof(obj) {
            return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
        };
    }
    return _typeof(obj);
}
// _possibleConstructorReturn 判斷Parent。call(this, name)函數(shù)返回值 是否為null或者函數(shù)或者對(duì)象。
function _possibleConstructorReturn(self, call) {
    if (call && (_typeof(call) === "object" || typeof call === "function")) {
        return call;
    }
    return _assertThisInitialized(self);
}
// 如何 self 是void 0 (undefined) 則報(bào)錯(cuò)
function _assertThisInitialized(self) {
    if (self === void 0) {
        throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    }
    return self;
}
// 獲取__proto__
function _getPrototypeOf(o) {
    _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
        return o.__proto__ || Object.getPrototypeOf(o);
    };
    return _getPrototypeOf(o);
}
// 寄生組合式繼承的核心
function _inherits(subClass, superClass) {
    if (typeof superClass !== "function" && superClass !== null) {
        throw new TypeError("Super expression must either be null or a function");
    }
    // Object.create()方法創(chuàng)建一個(gè)新對(duì)象,使用現(xiàn)有的對(duì)象來提供新創(chuàng)建的對(duì)象的__proto__。 
    // 也就是說執(zhí)行后 subClass.prototype.__proto__ === superClass.prototype; 這條語句為true
    subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {
            value: subClass,
            writable: true,
            configurable: true
        }
    });
    if (superClass) _setPrototypeOf(subClass, superClass);
}
// 設(shè)置__proto__
function _setPrototypeOf(o, p) {
    _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
        o.__proto__ = p;
        return o;
    };
    return _setPrototypeOf(o, p);
}
// instanceof操作符包含對(duì)Symbol的處理
function _instanceof(left, right) {
    if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
        return right[Symbol.hasInstance](left);
    } else {
        return left instanceof right;
    }
}

function _classCallCheck(instance, Constructor) {
    if (!_instanceof(instance, Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}
// 設(shè)置共享屬性和靜態(tài)屬性到不同的對(duì)象上
function _defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
        var descriptor = props[i];
        descriptor.enumerable = descriptor.enumerable || false;
        descriptor.configurable = true;
        if ("value" in descriptor) descriptor.writable = true;
        Object.defineProperty(target, descriptor.key, descriptor);
    }
}
//將共享屬性設(shè)置到原型對(duì)象上,靜態(tài)屬性設(shè)置到構(gòu)造函數(shù)上
function _createClass(Constructor, protoProps, staticProps) {
    if (protoProps) _defineProperties(Constructor.prototype, protoProps);
    if (staticProps) _defineProperties(Constructor, staticProps);
    return Constructor;
}

// ES6
var Parent = function () {
    function Parent(name) {
        _classCallCheck(this, Parent);
        this.name = name;
    }
    _createClass(Parent, [{
        key: "sayName",
        value: function sayName() {
            console.log('my name is ' + this.name);
            return this.name;
        }
    }], [{
        key: "sayHello",
        value: function sayHello() {
            console.log('hello');
        }
    }]);
    return Parent;
}();

var Child = function (_Parent) {
    _inherits(Child, _Parent);
    function Child(name, age) {
        var _this;
        _classCallCheck(this, Child);
        // Child.__proto__ => Parent
        // 所以也就是相當(dāng)于Parent.call(this, name); 是super(name)的一種轉(zhuǎn)換
        // _possibleConstructorReturn 判斷Parent.call(this, name)函數(shù)返回值 是否為null或者函數(shù)或者對(duì)象。
        _this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name));
        _this.age = age;
        return _this;
    }
    _createClass(Child, [{
        key: "sayAge",
        value: function sayAge() {
            console.log('my age is ' + this.age);
            return this.age;
        }
    }]);
    return Child;
}(Parent);

var parent = new Parent('Parent');
var child = new Child('Child', 18);
console.log('parent: ', parent); // parent:  Parent {name: "Parent"}
Parent.sayHello(); // hello
parent.sayName(); // my name is Parent
console.log('child: ', child); // child:  Child {name: "Child", age: 18}
Child.sayHello(); // hello
child.sayName(); // my name is Child
child.sayAge(); // my age is 18


從babel轉(zhuǎn)譯我們可以認(rèn)識(shí)到幾點(diǎn):

  • 盡量使用Object.create來創(chuàng)造父構(gòu)造函數(shù)的實(shí)例
//創(chuàng)建一個(gè)superClass的實(shí)例,constructor屬性指向subClass
subClass.prototype = Object.create(superClass&&superClass.prototype,{
    constructor:{
        value:subClass,
        writable:true,
        configurable:trues
    }
});
//替代方案
function Empty(){};
Empty.prototype = superClass.prototype;
subClass.protoype = new Empty();
subClass.protoype.constructor = subClass;
  • 盡量使用Object.setPrototypeOf而不是proto
Object.setPrototypeOf(subClass,superClass);
//替代方案,不推薦
subClass.__proto__ = superClass.__proto__

模擬一下轉(zhuǎn)換實(shí)現(xiàn):

class Fruit{
    constructor(name){
        this.name = name;
    }
    static sayHello(){
        console.log("hello");
    }
    sayName(){
        console.log(this.name);
    }
}
class Banana extends Fruit{
    constructor(name,color){
        super(name);
        this.color = color;
    }
    sayColor(){
        console.log(this.color);
    }
}

//轉(zhuǎn)換實(shí)現(xiàn):
function createPropertiesByObject(target,props){
    for(var i =0;i<props.length;i++){
        let descriptor = props[i];
        //省略
        Object.defineProperties(target,descriptor.key,descriptor);
    }
}
function createProperties(target,protoProps,staticProps){
    createPropertiesByObject(target.prototype,protoProps);
    createPropertiesByObject(target,staticProps);
}

function createPolyFill(Parent){
    function Empty(){};
    Empty.protoype = Parent.prototype;
    return new Empty();
}

function inherit(Child,Parent){
    if(typeof Child!=='function'||typeof Parent!=='function'){
        throw new Error("");
    }
    Child.prototype = Object.create ? Object.create(Parent&&Parent.prototype,{
        constructor:{
            value:Child,
            writable:true,
            configurable:true
        }
    }) : createPolyFill(Parent);
    if(!Object.create){
        Child.prototype.constructor = Child;
    }
}

function setPrototypeOf(Child,Parent){
    setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf : function(C,P){
        C.__proto__ = P
    }
    setPrototypeOf(Child,Parent); 
}

var Fruit = function(){
    function Fruit(){
        this.name = name;
    }
    createProperties(Fruit,[{
        key:"sayName",
        value:function(){
            console.log(this.name)
        }
    }],[{
        key:"sayHello",
        value:function(){
            console.log("hello");
        }
    }])
    return Fruit;
}()

var Banana = function(Parent){
    inherit(Banana,Parent);
    function Banana(name,color){
        Fruit.call(name);
        this.color = color;
    }
    createProperties(Banana,[{
        key:"sayColor",
        value:function(){
            console.log(this.color)
        }
    }])
    setPrototypeOf(Banana,Parent);
    return Banana;
}(Fruit)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 繼承6種套餐 參照紅皮書,JS繼承一共6種 1.原型鏈繼承 核心思想:子類的原型指向父類的一個(gè)實(shí)例 Son.pro...
    燈不梨喵閱讀 3,251評(píng)論 1 2
  • 繼承 Javascript中繼承都基于兩種方式:1.通過原型鏈繼承,通過修改子類原型的指向,使得子類實(shí)例通過原型鏈...
    LeoCong閱讀 402評(píng)論 0 0
  • 原文鏈接:zhuanlan.zhihu.com (一) 原型鏈繼承: function Parent(name) ...
    越努力越幸運(yùn)_952c閱讀 332評(píng)論 0 2
  • 知道你在,一直都在,就在這片熟悉的土地上。就如同水底靜臥無數(shù)的石頭,卻不知要挪動(dòng)哪一塊,才能找到你藏身的地方? 城...
    布谷鳥_ee85閱讀 346評(píng)論 2 8
  • 新的言雨的言語 我不想選擇 我愿意判斷 執(zhí)行
    言雨的言語閱讀 131評(píng)論 0 0

友情鏈接更多精彩內(nèi)容