前面的文章已经介绍过类的三种模式-函数类,原型类和伪类。
一起来聊聊JavaScript核心中的"函数类""
来聊聊JavaScript核心之"原型类
来聊聊JavaScript核心之"伪类"
这篇文章谈论稍微高级一点的代码共享技术子类以及如何使用伪类模式实现相同的目标。
首先看一下为什么要引入父类和子类。
一个类可以工作的很好,当你想制造一批相似的对象。但是假如你想要第二类对象它只是和Car类依稀类似会怎么样?还看这个列子
var Car = function(loc) {
var obj.loc = {loc: loc
};
obj.move = function() {
obj.loc++;
};
return obj;
};
var amy = Car(1);
amy.move();
var ben = Car(1);
ben.move();
假如你需要创建一辆警车,它有一个呼叫功能call();我们不会把这个方法写进函数类里,因为非警车的普通汽车不会拥有这个方法,那我们实现这个相同类的幼稚的一个方法就是从Car类中拷贝所有代码到第二个类中,然后重构这个类
var Van = function(loc) {
var obj.loc = {loc: loc};
obj.move = function() {
obj.loc++; };
obj.grab = function{ /*...*/};
return obj;
};
var Cop = function(loc) { var obj.loc = {loc: loc};
obj.move = function() { obj.loc++; };
obj.call = function{ /*...*/}; return obj;};
我们为这两个类取不同的名字以区别他们。这里的grab方法是仅有敌人车才会用到的方法。而call方法仅仅对警车是有用的。看一下内存中的情况
这种场景下,我们有两个不同的构造函数,都可以生产出基本相同的对象,但是都有各自的新方法grab或者call.这种方法可以解决问题,但是存在大量重复代码,需要优化。
假设我们在这之前构造一个函数做大部分的工作,之后让Van和Cop类做剩下少量的定制,使得他们区别与普通汽车,我们把这个新的函数成为父类。重构后代码如下
var Car = function(loc) {
var obj.loc = {loc: loc};
obj.move = function() { obj.loc++; };
return obj;
};
var Van = function(loc) {
var obj = Car(loc);
obj.grab = function{ /*...*/};
return obj;
};
var Cop = function(loc) { var obj = Car(loc);
obj.call = function{ /*...*/}; return obj;};
此时我们有了两个完整的子类函数和一个父类函数。
此时内存里情况
实现了相同的功能,但是用了更少的代码。
当然,我们使用函数类来创建子类很容易。那么现在可以想一下如果用伪类模式应该怎么做。应该怎样写伪类模式的子类呢?我们从头开始分析,用一个新的例子
还是Van是Car的子类,zed是Car的实例对象,amy是Van的实例对象。到此,父类Car我们已经编写完毕,下面继续编写子类。我们先为子类写一个构造函数,内存里应该构建了一个新的子类,并且这个子类应该从父类中继承很多属性
var Van = function(loc){ /.../};
当然可以写成和父类一样,即:this.loc=loc;
var Van = function(loc){ this.loc = loc;};
但是这样做的话没什么意义,一旦Car有什么修改,Van也得跟着修改。所以这样不好。或者我们也可以在Van中调用Car函数,即这样
var Van = function(loc){ new Car(loc);};
但是这样仍然有问题,首先在内存里,它将产生一个新的实例对象(但不会作为该函数返回值),而代码 Var amy = new Van(9);将会创建新的对象作为调用new Van()的结果。
这里想一下使用关键字new运行函数时,函数里会自动运行额外的代码
其中比较特殊的是,它将会设置关键字this等于一个全新创建的对象。正是我们因为使用了new创建Van(),它将创建另一个新的对象,但是并不意味着这个Car函数会运行这个新对象作为this的值。事实上两个不同的object.create语句被插入到两个不同的函数里,会有两次使用关键字new的结果。如果这么写,不管你的Van有没有用new关键字来创建实例,在内存里都会创建一个全新的委托到Car类上的对象。如果你用new关键字调用Van,那么你已经创建了一个新对象,并且你并不需要另一个对象。或许有人会想直接将这个使用Car构造器构造的对象赋值给关键字this,比如这样
var Van = function(loc){ new Car(loc);};
但是在代码给关键字this赋值是不允许的,即使解释器允许做这样的事情。因此它仍旧解决不了创建了两个不同对象的问题。那么或许也可以去掉关键字new,直接调用Car函数呢
var Van = function(loc){ Car(loc);};
这样做看似行,那么仔细想想Car函数中的this会指向什么呢?答案是Car函数将会在全局作用域的执行环境中去运行,this指向global。因为这个Car函数被作为一个自由函数调用。如果这样写调用var amy = new Van(9);实际上将会绑定一个指向数字9的全局变量loc
你的Van实例将完全不会被Car函数里的代码影响。
那么如果我们调用Car函数,我们只需要确定它运行在正确的执行环境中,我们有方法可以指定函数被调用时的执行环境,.call方法允许我们在调用函数时指定一个特定的执行环境。这意味着Car函数里参数this表现得更像一个传入函数的参数。这样写
Car.call(this,loc);
就可以使Car函数的执行环境与Van函数相同。那么这就是基于目前分析的解决方案
Var Car = function(loc){
this.loc = loc;};
Car.prototype.move = function(){ this.loc++;};
Var Van = function(loc){ Car.call(this,loc);};
此时我们运行
console.log(amy.loc);
是可以成功输出"9"的。但是此时amy.move()并不能运行,当执行到这一句时,代码会终止。因为amy对象没有move方法,根据原型链原理,它会在它的构造函数的原型上去查找,即Van.prototype,但是Van.prototype里面也没有,Van.prototype的原型是Object.prototype,但是也没有move方法。
到此,我们已经实现让子类的特定代码调用父类的特定代码,但由于事实上我们使用原型链实现继承的效果,我们需要将子类原型链到父类原型,是父类中相似的代码能被继承。amy确实委托到Van.prototype,但是它和Car.prototype并没有一毛钱的关系,因此我们需要将Van.prototype委托到Car.prototype。理情况下,我们可以修改Van.prototype,以便它能直接委托到Car.prototype
var.prototype._proto_ = Car.prototype;
内存中情况是这样,我们希望他这样工作,Van.prototype对象一直向上委托到Car.prototype。这样它就会和Car实例变得很类似,因为它们都能把字段查询委托给Car的prototype属性。
但是官方版本的语言里这种写法是不允许的,取而代之我们将不得不用我们的新创建的对象重写这个原型对象。
人们常常期望Van.prototype只是简单的等于Car.prototype
Van.prototype = Car.prototype;
但是注意当你把一个变量或者属性赋值给另一个时JavaScript不会做任何拷贝操作,因为它们是同一个对象。
如果是这样的话,加在Car原型上的方法,一定在Van原型中存在,反之亦然。因此如果我们加了一个Van.prototype。grab方法,那么会使Car.prototype也拥有一个.grab方法,简而言之,我们不希望Van.prototype和Car.prototype是同一个对象,所以我们还有找一种替代方法来替代Van.prototype和Car.prototype相等。
这种替代方法就是Object.create方法,如果我们想把Van.prototype委托给Car.prototype,我们只需要把它传递给Object.create生成一个委托该对象的新对象就好。
Van.prototype = Object.create(Car.prototype)
现在如果我们需要amy有一个新的方法grab,那么我们之间将次方法添加到构造函数的的原型对象上就行了
Van.prototype.grab = function(){/*...*/};
到此,我们似乎完成了整个功能,但是本属于Van.prototype的一个属性丢失了,就是.constructor。
因此需要添加这行代码
Van.prototype.contructor = Van;
这就是伪类模式下的子类,完整代码
Var Car = function(loc){ this.loc = loc;};Car.prototype.move = function(){ this.loc++;};Var Van = function(loc){ Car.call(this,loc);};Van.prototype = Object.create(Car.prototype);Van.prototype.contructor = Van;Van.prototype.grab = function(){/*...*/};
到此,如果你从我的前几篇文章一路读来,那么你关于不同代码的复用模式已经了解很多,包括函数模式、修饰器模式和JavaScript原生提供的所有主要的类模式以及它们相应的子类技术。
完。
作者:长梦未央
图片来源:优达学城付费课程
个别文字摘自教学视频老师的讲解