木灵鱼儿

木灵鱼儿

阅读:145

最后更新:2022/05/01/ 22:10:05

第五章 代码复用模式

代码复用是一个既重要又有趣的话题。如果你面对自己或者别人已经写好的代码,而这些代码又是经过测试的、可维护的、可扩展的、有文档的,这时候你只想写尽量少且可以被复用的代码就是一个再自然不过的想法。

当我们说代码复用的时候,第一件想到方式就是继承,为此你可能看到JavaScript很多方式用于实现“继承”

什么是类?
如果有一个过程,这个过程能产生一个实例,实例是对象,那这个过程就是类。

JavaScript是基于原型面向对象程序设计的,那么它的继承方式也是基于原型实现的,而JavaScript是没有类的,只有构造函数,且支持new用法,所以看上去就和类的用法相似,但是本质还是一个函数,而继承也是一样,因为与类继承相似,继承的方式被称为“类式继承”,但是这仅仅是一种继承方式。

那么多继承方式,其实都是为了模拟,利用prototype和JS特性来模拟别的语言的类继承

原型继承

原型继承也被称为“无类继承”;表示不需要创建一个“父类”也可以实现继承,这种方式非常灵活,比如系统里只有猫和狗两种动物的话,没必要再为它们构造一个抽象的“动物类”

原型本质上是一种认知模式,可以用来复用代码,那我们为什么还要模拟“类继承”呢?

这里我们先看一段原型继承的代码:

//父类
function Test() {
    this.name = "test";
}
Test.prototype.getName = function() {
    return this.name;
};

//子类
function Age() {
    this.age = 16;
}
//继承父类
Age.prototype = new Test();

或者不用构造函数,我们直接使用字面量对象

//父类
var Test = {
    name: "test",
    getName: function() {
        return this.name;
    },
};

//子类
function Age() {
    this.age = 16;
}
//继承父类
Age.prototype = Test;

可以看到,原型的继承是非常简单灵活的,虽然可以继承到可复用的代码,但是明显会有一些问题,比如我们修改Age.prototype.getName明显会改动到它的父类Test

临时构造函数

我们可以通过一个临时的构造函数来解决这个问题,只要他们的原型不是同一个就行了。

function object(parent) {
  var F = function() {};
  F.prototype = parent;
  return new F();
};

//子类
var a = object(Test);

基于Test生成一个新的对象,甚至于这个函数还能添加带二个参数,用于表示新对象的属性。

在ESMAScript5中,这种原型继承的模式成为了标准的一部分,为此有了一个新的api语法:Object.create()

但是原型继承还是会有一些问题,于是为了解决这些问题,对于类继承的模拟方式层出不穷:_原型链,借用构造函数,组合继承,原型式继承,寄生式继承,寄生组合式继承_等等

类式继承

默认模式(原型继承为一切的起点)

//父类
function Test() {
    this.name = "test";
}
Test.prototype.getName = function() {
    return this.name;
};

//子类
function Age() {
    this.age = 16;
}
//继承父类
Age.prototype = new Test();

//实例
var age = new Age();
console.log(age.getName()); //test

通过子类的原型继承父类的实例实现继承,但是也有几个缺点:

  1. prototype中丢失了constructor指针
  2. 子类无法真的继承到父类的属性和静态属性
  3. 子类新增的原型方法一定要在继承父类之后
  4. 每新增一个子类,就需要实例化一个父类

构造函数继承

为了能将父级中this绑定的属性转移到子类中来,我们利用构造函数是一个函数的特性,通过call、apply的方式,改变this的指向,从而让子类获取到父类绑定的属性。

//父类
function Test() {
    this.name = "test";
}

//子类
function Age() {
    Test.apply(this);
    this.age = 16;
}
//实例
var age = new Age();
console.log(age);  //{name:"test",age:16}

此时属性已经转移到子类上自身上了,不会像上面那样name存在于原型上。

那么此时我们子类的原型就不能直接等于new Test()了,我们需要只继承父类的原型,因为Age.prototype = new Test();会导致name存在两份,一份在子类自身属性,一份在prototype,这显然不是我们想要的。

共享原型

不通过new去继承父类的原型模式。

//父类
function Test() {
    this.name = "test";
}
Test.prototype.getName = function() {
    return this.name;
};

//子类
function Age() {
    Test.apply(this);
    this.age = 16;
}
//继承父类
Age.prototype = Test.prototype;
//子类自己的原型方法
Age.prototype.getAge = function() {
    return this.age;
};

//实例
var age = new Age();

将子类的原型赋值为父类的原型,这样虽然可以,但是又会产生一个问题,就是子类原型的方法会被添加到父类上,因为prototype是共享的。

但是一个父类是可以拥有多个子类的,子类一多,方法重名问题啥的,这显然也不是很符合我们的需求。

临时构造函数

为了斩断子类与父类prototype的链接关系,从而解决共享原型所带来的问题,而又能受益于继承原型链带来的好处,那么只有一个办法,让子类在拥有自己的原型情况下,让原型的原型做继承。

//继承用的函数
function myExtends(child, parent) {
    var F = function() {};
    F.prototype = parent.prototype;
    child.prototype = new F();
}

//父类
function Test() {
    this.name = "test";
}
Test.prototype.getName = function() {
    return this.name;
};

//子类
function Age() {
    Test.apply(this);
    this.age = 16;
}
//继承父类
myExtends(Age, Test);
Age.prototype.getAge = function() {
    return this.age;
};

//实例
var age = new Age();

此时子类与父类的原型没有直接链接,而是通过一个F函数中介了,F构造函数的原型关联父类,当F构造函数被new出来时,会生成一个新的对象,这个对象成为了子类的原型对象。

此时子类拥有一个属于自己的原型对象,这个“原型对象”的prototype与父类相关联。

原型它也是个对象,但不能是函数

此时还有一个小问题,就是子类原型上的constructor指针没有了。

重置constructor函数指针

constructor属性并不会影响任何JavaScript中的内部属性,比如instanceof是不会受其影响的。

constructor其实没有什么用处,只是JavaScript语言设计的历史遗留物。由于constructor属性是可以变更的,所以未必真的指向对象的构造函数,只是一个提示。不过,从编程习惯上,我们应该尽量让对象的constructor指向其构造函数,以维持这个惯例。

我们针对上面的代码小小改动一下就可以了

//继承用的函数
function myExtends(child, parent) {
    var F = function() {
        this.constructor = child;
    };
    F.prototype = parent.prototype;
    child.prototype = new F();
}

继承父类的静态属性

目前我们还没有实现对父类的静态属性继承,在java这种语言中,子类是可以继承父类的静态属性和静态方法的,但是却不能重写父类的静态属性和方法。

继承静态属性也非常简单,我们做个for循环就可以了。

//继承静态属性的函数
function myExtendsStatic(child, parent) {
    var key,
        hasOwnProperty = Object.prototype.hasOwnProperty;

    for (key in parent) {
        if (hasOwnProperty.call(parent, key)) {
            child[key] = parent[key];
        }
    }
}

完整代码:

//继承用的函数
function myExtends(child, parent) {
    myExtendsStatic(child, parent);
    var F = function() {
        this.constructor = child;
    };
    F.prototype = parent.prototype;
    child.prototype = new F();
}

//继承静态属性的函数
function myExtendsStatic(child, parent) {
    var key,
        hasOwnProperty = Object.prototype.hasOwnProperty;

    for (key in parent) {
        if (hasOwnProperty.call(parent, key)) {
            child[key] = parent[key];
        }
    }
}

//父类
function Test() {
    this.name = "test";
}
Test.testFn = function() {
    console.log("我是一个静态方法");
};
Test.prototype.getName = function() {
    return this.name;
};

//子类
function Age() {
    Test.apply(this);
    this.age = 16;
}
//继承父类
myExtends(Age, Test);
Age.prototype.getAge = function() {
    return this.age;
};

//静态
Age.testFn(); //我是一个静态方法
//实例
var age = new Age();
console.log(age.getName()); //test

这部分实现我们可以在ts转换中看到类似的实现。当然ts做了一些兼容,可能在不存在直接获取prototype的情况下的特殊处理。

复制属性实现继承

这是另一种继承模式,在这种模式中,对象将从另一个对象中获取功能,其方法是仅需要将其复制即可。

function extend(parent, child) {
    var key,
        child = child || {};

    for (key in parent) {
        if (parent.hasOwnProperty(key)) {
            child[key] = parent[key];
        }
    }

    return child;
}

这是一种浅复制的方式,还有一种深度复制的方式,具体就不贴代码了,就是一种深度克隆。

这种方式不涉及原型和类继承,由于是复制,所以修改功能也不会影响到父级对象。

为此这种方式在后期有了一种新的用法:混入

混入

混入模式支持一个或者多个对象的功能进行复制,意在于重用,因为有时需要继承的可能不止一个对象,如果按照类式继承或者原型继承相对来说会非常麻烦,我们将多个需要继承对象的属性或方法复制到原型链上,达到功能的注入。

function mix() {
    var i,
        key,
        child = {};

    for (i = 0, len = arguments.length; i < len; i++) {
        for (key in arguments[i]) {
            if (arguments[i].hasOwnProperty(key)) {
                child[key] = arguments[i][key];
            }
        }
    }

    return child;
}

原型链上的注入就不写了,vue2也用到了混入,但是后续vue3就放弃了,因为混入会带来一个问题:将功能注入对象原型中会导致原型污染和函数起源方面的不确定性,功能可能会被覆写而无法快速定位根源

借用方法

借用方法也是一种代码的复用,且不用支付继承带来的影响。等于不用继承,也能得到继承的好处!

常见的两个方法:call、apply

function test() {
  return [].slice.call(arguments);
}

this的绑定

有时候我们可能并不会直接使用call、apply来进行复用,而是直接将对象中的某个方法单独保存,或者作为参数传递,如果方法里使用了this,这就会导致一些问题。

var a = {
    name: "a",
    getName: function() {
        return this.name;
    },
};

var getName = a.getName;
console.log(getName()); //<empty string>

显然这些时候,我们可能需要明确绑定this以避免这个问题。

function bind(that, fn) {
    return function() {
        return fn.apply(that, [].slice.call(arguments));
    };
}

var a = {
    name: "a",
    getName: function() {
        return this.name;
    },
};

var b = {
    name: "b",
};

var getName = bind(b, a.getName);
console.log(getName()); //b

绑定this后,我们也可以借用别人的方法,代价则是额外的闭包开销。

这个bind方法后续也被纳入了ECMAScript5的标准,api为:Funtion.prototype.bind

Polyfill

if (typeof Function.prototype.bind === "undefined") {
    Function.prototype.bind = function(context) {
        var fn = this,
            slice = Array.prototype.slice,
            args = slice.call(arguments, 1);

        return function() {
            return fn.apply(context, args.concat(slice.call(arguments)));
        };
    };
}

版权申明

本文系作者 @木灵鱼儿 原创发布在木灵鱼儿 - 有梦就能远航站点。未经许可,禁止转载。

关于作者

站点职位 博主
获得点赞 0
文章被阅读 145

相关文章