基于原型的JavaScript继承 - 面向对象的JavaScript - JavaScript核心

源码: https://github.com/RobinQu/Programing-In-Javascript/blob/master/chapters/JavaScript_Core/Object_Oriented_Javascript/Javascript_Prototypal_Inheritance.mdjavascript

原文: http://pij.robinqu.me/JavaScript_Core/Object_Oriented_Javascript/Javascript_Prototypal_Inheritance.htmlhtml

  • 本文须要补充更多例子
  • 本文存在批注,但该网站的Markdown编辑器不支持,因此没法正常展现,请到原文参考。

基于原型的JavaScript继承

坐卧不安的写下这篇文章。用JavaScript实现继承模型,已是很是成熟的技术,各类大牛也已经写过各式的经验总结和最佳实践。在这里,我只能就我所能,写下我本身的思考和总结。java

在阅读以前,咱们先假设几个在面向对象编程中的概念是你们熟悉的:git

  • 类, Class
  • 构造函数, Constructor
  • 继承, Inheritance
  • 实例, Instance
  • 实力化, Instantiation
  • 方法, Method
  • 多态, Polymorphism
  • 接口, Interface

因为讲解这些概念是十分复杂的,因此还请参阅其余资料。github

了解原型

面向对象是当代编程的主流思想。不管是C++仍是Java,都是面向对象的。严格上来说,JavaScript并非面向对象的,而是“基于对象的”(Object-based),由于它的确缺少面向对象里的不少特性,例如:编程

  • 继承
  • 接口
  • 多态
  • ...

但再另外一方面,JavaScript是基于原型(Prototype)的对象系统。它的继承体系,叫作原型链继承。不一样于继承树形式的经典对象系统,基于原型的对象系统中,对象的属性和方法是从一个对象原型(或模板)上拷贝或代理(Delegation)的。JavaScript也不是惟一使用这种继承方法的编程语言,其余的例子如:浏览器

  • Lisp
  • Lua
  • ...

那么,prototype在哪里呢?app

访问构造函数的原型

// 访问Array的原型
Array.prototype
// 访问自定义函数Foo的原型
var Foo = function() {}
Foo.prototype

访问一个实例的原型

__proto__不是标准属性,可是被大多数浏览器支持框架

var a = {}
a.__proto__;

使用ES5的Object.getPrototypeOf:编程语言

Object.getPrototypeOf([]) === Array.prototype;

再来点绕弯的:

[].constructor.prototype === Array.prototype

new关键字

大多数面向对象语言,都有new关键字。他们大多和一个构造函数一块儿使用,可以实例化一个类。JavaScript的new关键字是殊途同归的。

等等,不是说JavaScript不支持经典继承么!的确,其实new的含义,在JavaScript中,严格意义上是有区别的。

当咱们,执行

new F()

其实是获得了一个从F.prototype继承而来的一个对象。这个说法来自Douglas的很早以前的一篇文章1。在现在,若是要理解原型继承中new的意义,仍是这样理解最好。

若是咱们要描述new的工做流程,一个接近的可能流程以下:

  1. 分配一个空对象
  2. 设置相关属性、方法,例如constructorF.prototype上的各式方法、属性。注意,这里执行的并非拷贝,而是代理。后文会讲解这点。
  3. 将这个新对象做为构造函数的执行上下文(其this指向这个对象),并执行构造函数
  4. 返回这个对象

原型继承

咱们来定义一个简单的“类”和它的原型:

var Foo = function() {};
Foo.prototype.bar = function() {
    console.log("haha");
};
Foo.prototype.foo = function() { console.log("foo"); };

咱们在原型上定义了一个bar方法。看看咱们怎么使用它:

var foo = new Foo();
foo.bar(); // =>  "haha"
foo.foo(); // => "foo"

咱们要继承Foo:

var SuperFoo = function() {
    Foo.apply(this, arguments);
};
SuperFoo.prototype = new Foo();
SuperFoo.prototype.bar = function() {
    console.log("haha, haha");
};
var superFoo = new SuperFoo();
superFoo.foo(); // => "foo"
superFoo.bar(); // => "haha, haha"

注意到几个要点:

  1. SuperFoo中,咱们执行了父级构造函数
  2. SuperFoo中,咱们让然能够调用foo方法,即便SuperFoo上没有定义这个方法。这是继承的一种表现:咱们能够访问父类的方法
  3. SuperFoo中,咱们从新定义了bar方法,实现了方法的重载

咱们仔细想一想第二点和第三点。咱们新指定的bar方法到底保存到哪里了?foo方法是如何找到的?

原型链

要回答上面的问题,必需要介绍原型链这个模型。相比树状结构的经典类型系统,原型继承采起了另外一种线性模型。

当咱们要在对象上查找一个属性或方法时:

  1. 在对象自己查找,若是没有找到,进行下一步
  2. 在该对象的构造函数本身的prototype对象上查找,若是没有找到进行下一步
  3. 获取该对象的构造函数的prototype对象做为当前对象;若是当前对象存在prototype,就能继续,不然不存在则查找失败,退出;在该对象上查找,若是没有找到,将前面提到的“当前对象”做为起始对象,重复步骤3

这样的递归查找终究是有终点的,由于:

Object.prototype.__proto__ === null

也就是Object构造函数上,prototype这个对象的构造函数上已经没有prototype了。

咱们来看以前FooSuperFoo的例子,咱们抽象出成员查找的流程以下:

superFoo自己 => SuperFoo.prototype => Foo.prototype => Object.prototype

解读原型链的查找流程:

  • superFoo自己意味着superFoo这个实例有除了可以从原型上获取属性和方法,自己也有存储属性、方法的能力。咱们称其为own property,咱们也有很多相关的方法来操做:

    • obj.hasOwnProperty(name)
    • Object.getOwnPropertyNames(obj)
    • Object.getOwnPropertyDescriptor(obj)
  • SuperFoo.prototype

    • 回忆一下这句SuperFoo.prototype = new Foo();,也就是说SuperFoo.prototoye就是这个新建立的这个Foo类型的对象
    • 这也就解释了为啥咱们能访问到Foo.prototype上的方法和属性了
    • 也就是说,咱们要在这个新建的Foo对象的本地属性和方法中查找
  • Foo.prototype:

    • 查找到这一次层,纯粹是由于咱们制定了SuperFoo.prototype的值,回想上一条
  • Object.prototype

    • 这是该原型链的最后一环,由于Object.prototype这个对象的原型是null,咱们没法继续查找
    • 这是JavaScript中全部对象的祖先,上面定义了一个简单对象上存在的属性和方法,例如toString

那么,当在SuperFoo上添加bar方法呢?这时,JavaScript引擎会在SuperFoo.prototype的本地添加bar这个方法。当你再次查找bar方法时,按照咱们以前说明的流程,会优先找到这个新添加的方法,而不会找到再原型链更后面的Foo.prototype.bar

也就是说,咱们既没有删掉或改写原来的bar方法,也没有引入特殊的查找逻辑。

模拟更多的经典继承

基本到这里,继承的大部分原理和行为都已经介绍完毕了。可是如何将这些看似简陋的东西封装成最简单的、可重复使用的工具呢?本文的后半部分将一步一步来介绍如何编写一个大致可用的对象系统。

热身

准备几个小技巧,以便咱们在后面使用。

beget

若是要以一个对象做为原型,建立一个新对象:

function beget(o) {
    function F() {}
    F.prototype = o;
    return new F();
}
var foo = beget({bar:"bar"});
foo.bar === "bar"; //true

理解这些应该困难。咱们构造了一个临时构造函数,让它的prototype指向咱们所指望的原型,而后返回这个构造函数所建立的实例。有一些细节:

  • 咱们不喜欢直接作A.prototype = B.prototype这样的事情,由于你对子类的修改,有可能直接影响到父类以及父类的全部实例。大多数状况下这不是你想看到的结果
  • 新建F的实例,建立了一个本地对象,能够持有(own)自身的属性和方法,即可以支持以后的任意修改。回忆一下superFoo.bar方法。

若是你使用的JavaScript引擎支持Object.create,那么一样的事情就更简单:

Object.create({bar:"bar"});

要注意Object.create的区别:

  • 咱们能够建立没有原型的对象: Object.create(null)
  • 咱们能够配置建立的对象,参阅Object.create的文档2
  • 咱们没必要去运行一遍父类构造函数,这样能够避免不须要的反作用

函数的序列化、解义

JavaScript的函数能够在运行时很方便的获取其字符串表达:

var f = function(a) {console.log("a")};
f.toString(); // 'function(a) {console.log("a")};'

这样的能力其实时很强大的,你去问问Java和C++工程师该如何作到这点吧。

这意味着,咱们能够去分析函数的字符串表达来作到:

  1. 了解函数的函数列表
  2. 了解函数体的实际内容
  3. 了解一个函数是否有别名
  4. ...

动态的this

JavaScript中的this是在运行时绑定的,咱们每每须要用到这个特性,例如:

var A = function() {};
A.methodA = function() {
    console.log(this === A);
};
A.methodA();// => true

以上这段代码有以下细节:

  • A.methodA()运行时,其上下文对象指定的是A,因此this指向了A
  • 咱们能够用这个来模拟“类的静态方法或类方法”
  • 咱们可以经过这里的this引用到类(构造函数)自己

若干版本

最简单版本

单纯实现一个extend方法:

var extend = function(Base) {
  var Class = function() {
    Base.apply(this, arguments);
  }, F;
  if(Object.create) {
    Class.prototype = Object.create(Base.prototype);
  } else {
    F = function() {};
    F.prototype = Base.prototype;
    Class.prototype = new F();
  }
  Class.prototype.constructor = Class;
  return Class;
};

var Foo = function(name) {
  this.name = name;
};
Foo.prototype.bar = function() {
  return "bar";
};

var SuperFoo = extend(Foo);
var superFoo = new SuperFoo("super");
console.log(superFoo.name);// => "super"
console.log(superFoo.bar());// => "bar"

因为过于简单,我就不作讲解了。

更复杂的例子

  • 咱们须要一个根对象XObject
  • 根对象有各类继承方法,并能传入一些子类的方法和属性
  • 咱们要复用上个例子里的extend,可是会有修改
var extend = function(Base) {
  var Class = function() {
    Base.apply(this, arguments);
  }, F;
  if(Object.create) {
    Class.prototype = Object.create(Base.prototype);
  } else {
    F = function() {};
    F.prototype = Base.prototype;
    Class.prototype = new F();
  }
  Class.prototype.constructor = Class;
  return Class;
};

var merge = function(target, source) {
  var k;
  for(k in source) {
    if(source.hasOwnProperty(k)) {
      target[k] = source[k];
    }
  }
  return target;
};

// Base Contstructor
var XObject = function() {};

XObject.extend = function(props) {
  var Class = extend(this);
  if(props) {
    merge(Class.prototype, props);
  }

  // copy `extend`
  // should not use code like this; will throw at ES6
  // Class.extend = arguments.callee;
  Class.extend = XObject.extend;

  return Class;
};


var Foo = XObject.extend({
  bar: function() { return "bar"; },
  name: "foo"
});

var SuperFoo = Foo.extend({
  name: "superfoo",
  bar: function() { return "super bar"; }
});

var foo = new Foo();
console.log(foo.bar()); // => "bar"
console.log(foo.name); // => "foo"

var superFoo = new SuperFoo();
console.log(superFoo.name); // => "superfoo"
console.log(superFoo.bar()); // => "super bar"

上面的例子中,

  • XObject是咱们对象系统的根类
  • XObject.extend能够接受一个包含属性和方法的对象来定义子类
  • XObject的全部子类,都没有定义构造函数逻辑的机会!真是难以接受的:

    • 咱们偏好一个类上的init方法来初始化对象,而将构造函数自己最简化

      • 绕开工厂方法的实现过程当中,参数传递如何传递到构造函数的问题
    • 能够支持更多新的特性,例如super属性、mixin特性等

总结,而后呢?

咱们解决了一部分问题,又发现了一些新问题。但本文的主要内容在这里就结束了。一个更具实际意义的对象系统,实际随处可见,EmberAngular中的根类。他们都有更强大的功能,例如:

  • Ember中的binding,setter、getter
  • Angular中的函数依赖注入
  • ...

可是,这些框架中对象系统的出发点都在本文所阐述的内容之中。若是做为教学,John Resig在2008年的一篇博客中3,总结了一个现代JavaScript框架中的对象系统的雏形。我建立了docco代码注解来当即这段代码,本文也会结束在这段代码的注解。

还有一些更高级的话题和技巧,会在另一篇文章中给出。

相关文章
相关标签/搜索