给程序员看的Javascript攻略 - Prototype (下)

743 阅读4分钟



原文发表在: holmeshe.me , 本文是汉化重制版。

本系列在 Medium上同步连载。

还记得早先用ajax胡乱做项目的时候踩过好多坑,然后对JS留下了“非常诡异”的印象。最近换了一个工作,工作语言就是JS。然后发现这个语言真不得了,前面后面都能干,基本成了全栈的同义词。所以刚好就趁这个机会系统学习一下这个语言。因为是给程序员看的,这个系列不会讲基本的“if-else”,循环,或者面向对象。相反,我会关注差异,希望能给您在Pull Request里走查代码式的学习体验!

类继承在JavaScript里面可以用原型链实现。任何一个对象都可以用和自己绑定的原型,像亲子鉴定一样在原型链回溯到自己的亲爹,以及亲爷爷等等。原型链也天然支持继承的最终目的,代码复用:当访问一个当前对象里不存在的成员时,JS引擎会从当前对象开始,在原型链上,向上查找集成(子)树的成员信息。

简单而不容易

光用说的不太好理解,接下来我会一步一步实现这个原型链,所以这个阅读更像一个模拟的真实开发过程,我觉得搞成这样可能会比较容易记。

打上码:

var LaoWang = function() {
  this.aproperty = 'a';
  this.amethod = function () {
    return 'b';
  };
};


var XiaoMing = function() {};

XiaoMing.prototype = LaoWang.prototype; //let's make the chain
var subobj = new XiaoMing();

alert(subobj instanceof LaoWang);
alert(subobj.aproperty);
alert(subobj.amethod());


运行结果:

true
undefined

Uncaught TypeError: subobj.amethod is not a function


虽然类型是正确识别了,但是成员访问还有问题。所以按一般经验,我猜是父类的构造函数没调。试一下:

  var LaoWang = function() {
    this.aproperty = 'a';
    this.amethod = function () {return 'b';};
  };


  var XiaoMing = function() {
++  LaoWang();
  };

  XiaoMing.prototype = LaoWang.prototype;
++XiaoMing.prototype.constructor = XiaoMing;
  var subobj = new XiaoMing();

  alert(subobj instanceof LaoWang);
  alert(subobj.aproperty);
  alert(subobj.amethod());


还是不行。嗯嗯嗯,这次应该是父类的构造函数里面this指针又指错了。如果你没猜到,请 ⬅到我之前的文章有讲。


正如上面提到的文章里面说的,要修改这个问题,我们可以:

var tmpf = LaoWang.bind(this);
tmpf();

也可以:

LaoWang.apply(this);

我用第二种,因为可以少写一行代码:

var LaoWang = function() {
    this.aproperty = 'a';
    this.amethod = function () {return 'b';};
  };

  var XiaoMing = function() {
++  LaoWang.apply(this);
  };

  XiaoMing.prototype = LaoWang.prototype;
  XiaoMing.prototype.constructor = XiaoMing;
  var subobj = new XiaoMing();

  alert(subobj instanceof LaoWang);
  alert(subobj.aproperty);
  alert(subobj.amethod());

运行结果:

  true
  a
  b

搞定了!吗?

  var LaoWang = function() {
    this.aproperty = 'a';
    this.amethod = function () {return 'b';};
  };

  var XiaoMing = function() {
    LaoWang.apply(this);
  };

  XiaoMing.prototype = LaoWang.prototype;
  XiaoMing.prototype.constructor = XiaoMing;

++XiaoMing.prototype.another_property = 'c';
  var subobj = new XiaoMing();
++var superobj = new LaoWang();

++alert(subobj.another_property);
++alert(superobj.another_property);

运行结果:

c
c

事实上我们刚才搞出来的完全不是继承,而是其它不存在的东西。我姑且叫他“结对”吧。

方向完全错了。。。虽然方向错了,但其实大部分的努力还是正确的。正如我有时听到一类故事,主角虽然创业失败了,但是某一天发现,这个看似失败的经历积累的经验和资源直接关系到后面的成功。所以我们继续。

原型解耦

正常的做法是用两个实体,一个中间类(其实也是一个对象,第一等)和一个中间对象(普通),来解耦我们刚才写出来不知所谓的东西,关系我画一下:

Subclass.prototype
          |---intermediate object
               |---.__proto__
                     |---IntermediateClass.prototype === Superclass.prototype

在这个里面,中间对象是这个中间类的一个实例。

那我们回到上篇最开始的一段代码:

function inherits(ChildClass, ParentClass) {
  function IntermediateClass() {}
  IntermediateClass.prototype = ParentClass.prototype;
  ChildClass.prototype = new IntermediateClass;
  ChildClass.prototype.constructor = ChildClass;
  return ChildClass;
};

在看过具体问题和原理图以后,是不是就读得懂了呢?我们验证一下:

  var LaoWang = function() {
    this.aproperty = 'a';
    this.amethod = function () {return 'b';};
  };

  var XiaoMing = function() {
    LaoWang.apply(this);
  };

--XiaoMing.prototype = LaoWang.prototype;
++inherits(XiaoMing, LaoWang);
  XiaoMing.prototype.another_property = 'c';
  var subobj = new XiaoMing();
  var superobj = new LaoWang();
  
  alert(subobj instanceof LaoWang);
  alert(subobj instanceof XiaoMing);
  alert(subobj.aproperty);
  alert(subobj.amethod());
  alert(subobj.another_property);
  alert(superobj instanceof LaoWang);
  alert(superobj instanceof XiaoMing);
  alert(superobj.aproperty);
  alert(superobj.amethod());
  alert(superobj.another_property);

事实上,一个更好版本的inherits() 可以用Object.create() 来实现:

function inherits(ChildClass, ParentClass) {
--  function IntermediateClass() {}
--  IntermediateClass.prototype = ParentClass.prototype;
--  ChildClass.prototype = new IntermediateClass;
++  ChildClass.prototype = Object.create(ParentClass.prototype);
    ChildClass.prototype.constructor = ChildClass;
    return ChildClass;
};

写了这么多,验证的时候还有点小紧张:)

true
true
a
b
c
// this line is artificial
true
false
a
b
undefined

好了,可以按时下班了!

老乡留步,还有最后一点没说:

prototype.constructor

你可能已经留意到了,我在这里偷偷加了一段代码但是没说明。我是故意的,因为可以把上面的逻辑搞流畅一点。下面我们来看看这个。。。好吧,一言难尽,我再画个图。

class <----------------|
     |------.prototype.constructor
             |------......(I have drawn this part)

prototype.constructor其实是一个特殊的方法(最后再啰嗦一遍,第一类对象),这个方法指向的是这个类本身。正常情况下这个指向是由JS引擎在运行时完成的:

var LaoWang = function() {
  this.aproperty = 'a';
  this.amethod = function () {return 'b';};
};


alert(LaoWang.prototype.constructor === LaoWang);

运行结果:

true

但是呢,我在这个函数

function inherits(ChildClass, ParentClass)

里面做原型链的时候,把

ChildClass.prototype.constructor

这个指针的原始值给弄乱了,所以我们需要把这个值给设回去:

ChildClass.prototype.constructor = ChildClass;

好了,今天先写到这。如果您觉得这篇不错,可以关注我的公众号 - 全栈猎人。


也可以去Medium上随意啪啪啪我的其他文章。感谢阅读!👋