阅读 349

【译】关于 JavaScript 的原型你应该知道的所有事情

原文地址

大多数时候, JavaScript 原型让刚开始学习 JavaScript 的人困惑——尤其是有 C++ 或者 Java 背景的人。

在 JavaScript 中,相较于 C++ 和 Java,继承有一些不同的作用。JavaScript 继承是众所周知的 “原型继承”。

当你在 JavaScript 中遇到 时,事情就变得有点困难了。新的语法 class 看起来像 C++ 或者 Java,但实际上,它们的作用是不同的。

这篇文章中,我们将尝试理解 JavaScript 中的“原型继承”。我们也会看看新的语法 class 并且尝试理解它真正是什么。现在开始吧。

首先,我们用老派 JavaScript 函数和原型开始。

理解 prototype 的需要

如果你曾经跟 JavaScript 的数组,对象或者字符串打交道,你应该注意过默认地很多可用的方法。

举个例子:

var arr = [1,2,3,4];
arr.reverse(); // returns [4,3,2,1]
var obj = {id: 1, value: "Some value"};
obj.hasOwnProperty('id'); // returns true
var str = "Hello World";
str.indexOf('W'); // returns 6
复制代码

你曾经好奇过这些方法是哪里来的吗?你自定并未定义过它们。

你可以像这样定义自己的方法吗?你或许说可以像这样做:

var arr = [1,2,3,4];
arr.test = function() {
    return 'Hi';
}
arr.test(); // will return 'Hi'
复制代码

这是有效的,但是只对叫 arr 的变量有效。我们用另一个变量 arr2 调用 arr2.test() 将会跑出一个错误:“TypeError:arr2.test is not function”。

那么如何处理这些方法才能让每一个 array/string/object 的实例变得可用?你能创建自己的方法用同样的行为吗?答案是肯定的。你需要用正确的方法处理。要这样做,JavaScript 的原型就出现了。

首先看看那些方法来自哪里。考虑下面的代码:

var arr1 = [1,2,3,4];
var arr2 = Array(1,2,3,4);
复制代码

我们用两种不同的方式创建数组:arr1 使用数组字面量和 arr2 使用 Array 构造函数。它们两者是相等的,有一些不同,但不是这篇文章的问题。

现在看看构造函数 Array——在 JavaScript 中是预定义的构造函数。如果你打开 Chrome 开发这工具,然后在控制台输入 console.log(Array.prototype) 输入回车,你会看见下面这些内容:

在这里你可以看到所有的我们好奇的方法。这里就是我们得到函数方法的地方。自己试试 String.prototypeObject.prototype

我们创建一个自己的简单构造函数:

var foo = function(name) {
 this.myName = name;
 this.tellMyName = function() {
   console.log(this.myName);
 }
}
var fooObj1 = new foo('James');
fooObj1.tellMyName(); // will print James
var fooObj2 = new foo('Mike');
fooObj2.tellMyName(); // will print Mike
复制代码

你能找出上面代码的基本问题吗?问题在于我们在上述处理中浪费了内容。注意这个方法 tellMyName,在 foo 的实例中,每一个都是一样的。每次我们创建一个 foo 实例方法 tellMyName,都占用一部分系统内存。如果 tellName 对所有实例都是一样的,它最好保留在一个地方,并且所有我们的实例都来自这个地方。我们看看如何实现:

var foo = function(name) {
 this.myName = name;
}
foo.prototype.tellMyName = function() {
   console.log(this.myName);
}
var fooObj1 = new foo('James');
fooObj1.tellMyName(); // will print James
var fooObj2 = new foo('Mike');
fooObj2.tellMyName(); // will print Mike
复制代码

来比较下上面和之前的实现。在上面的实现中,如果你 console.dir() 这个实例,会得到以下内容:

注意实例的属性只有 myNametellMyName 是定义在 __prototype__ 之下。之后我们再讨论 __prototype__。更要注意的是,两个实例的 tellMyName 是相等的。如果它们的引用相同,在 JavaScript 中函数比较就相等。这说明 tellMyName 在多个实例中没有消耗额外的空间。

我们看看之前的例子:

注意这次 tellMyName 定义为实例的属性。它不再 __proto__ 下面。同样的,注意这次比较函数等价的结果是 false。这是因为他们在不同的内存位置,并且他们的引用也不相同。

我希望你现在理解了 __prototype 的必要性。

所有的 就JavaScript 函数都有一个 prototype 属性,是一个 object 类型。你可以在 prototype 下面定义自己的属性。当你使用函数作为构造函数时,所有的实例将会继承来自 object 的 prototype

现在我们来看看上面的 __prototype____prototype__ 是原型对象的简单引用,实例继承了原型对象。听起来很复杂?实际上一点都不。我们看一个可见的例子。

考虑下面代码。我们已经创建了一个数组,通过数组字面量来创建的,它的属性来自于 Array.prototype

var arr = [1, 2, 3, 4];
复制代码

上面我刚刚提到:“__prototype__ 是原型对象的简单引用,实例继承了原型对象。”所以,arr.__prototype__ 应该和 Array.prototype 是相同的。来证明看看:

我们不应当用 __proto__ 访问原型对象。根据 MDN 的参考,__proto__ 是非常不推荐的,并且不是在所以浏览器都支持。正确的方法如下:

var arr = [1, 2, 3, 4];
var prototypeOfArr = Object.getPrototypeOf(arr);
prototypeOfArr === Array.prototype;
prototypeOfArr === arr.__proto__;
复制代码

上面代码片段展示了 __proto__Ojbect.getPrototypeof 返回的东西一样。

现在来休息一下。喝点咖啡,试试上面的例子。等你准备好了,我们再继续。

原型链和继承

在上面第二张图中,注意到在第一个 __proto__ 对象里有另一个 __proto__ 了吗?如果没有回去看看第二张图。我们现在讨论它的实际意义。这就是著名的原型链。

在 JavaScript 中,我们通过原型链实现继承。

考虑下面的例子:我们都理解术语“机车”。公共汽车可以被叫做看做机车。小汽车也能被当做机车。公共汽车,小汽车和摩托车都有共同的属性,这就是为什么他们能被称作机车。举个例子,它们可以从一个地方移动到另一个地方。它们有轮子,有喇叭等等。

当然,公共汽车,小汽车和摩托有不同的类型,比如 Mercedes, BMW,Honda 等等。

上面的图表中,公共汽车从机车集成了一些属性。Mercedes Benz 从公共汽车继承了一些属性。类似的还有汽车和摩托车。

我们在 JavaScript 中建立这种关系。

首先,为了简单的缘故我们假定一些观点:

  1. 公共汽车有 6 个轮子
  2. 公共汽车,小汽车,摩托的加速和刹车不相同,所有的公共汽车,小汽车和摩托都一样。
  3. 所有的机车都能鸣笛。
function Vehicle(vehicleType) {  // 机车构造
    this.vehicleType = vehicleType;
}
Vehicle.prototype.blowHorn = function () {
    console.log('Honk! Honk! Honk!'); // 所有的机车可以鸣笛
}
function Bus(make) { // 公共汽车构造
  Vehicle.call(this, "Bus");
  this.make = make
}
Bus.prototype = Object.create(Vehicle.prototype); // 使公共汽车继承来自机车的属性
Bus.prototype.noOfWheels = 6; // 假设所有的公共汽车有 6 个轮子
Bus.prototype.accelerator = function() {
    console.log('Accelerating Bus'); // 公共汽车加速
}
Bus.prototype.brake = function() {
    console.log('Braking Bus'); // 公共汽车减速
}
function Car(make) {
  Vehicle.call(this, "Car");
  this.make = make;
}
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.noOfWheels = 4;
Car.prototype.accelerator = function() {
    console.log('Accelerating Car');
}
Car.prototype.brake = function() {
    console.log('Braking Car');
}
function MotorBike(make) {
  Vehicle.call(this, "MotorBike");
  this.make = make;
}
MotorBike.prototype = Object.create(Vehicle.prototype);
MotorBike.prototype.noOfWheels = 2;
MotorBike.prototype.accelerator = function() {
    console.log('Accelerating MotorBike');
}
MotorBike.prototype.brake = function() {
    console.log('Braking MotorBike');
}
var myBus = new Bus('Mercedes');
var myCar = new Car('BMW');
var myMotorBike = new MotorBike('Honda');
复制代码

来解释一下上述代码:

我们有个 Vehicle 构造器,它是机车类型。所有的机车都能鸣笛,在 Vehicle 原型上有个 blowHorn 属性。

作为 Bus,是一种机车,从 Vehicle 对象里继承属性。

我们假设所有的公共汽车有 6 个轮子,同时有加速和刹车程序。所以我们在 Bus 原型上定义有 noOfWheels,acceleratorbrake属性。

小汽车和摩托车类似。

来 Chrome 开发者工具的 console 里面执行代码。

执行以后,我们得到 3 个对象 myBus, myCarmyMotorBike

输入 console.dir(mybus),按下回车。点击三角形图标展开内容,你会看到如下:

myBus 之下,我们有 makevehicleType 属性。注意 Bus 的原型的 __proto__ 的值。它的原型的所有属性这里是可见的:acceleratorbrakenoOfWheels

现在我们看看第一个 __proto__ 对象。这个随想有另一个 __proto__ 作为它的属性。

在这之下,我们有 blowHornconstructor 属性。

Bus.prototype = Object.create(Vehicle.prototype);
复制代码

记着这行代码吗? Object.create(Vehicle.prototype) 将会创建一个空对象,这个对象的原型是 Vehicle.prototype。我们设置这个对象作为 Bus 的原型。对于 Vehicle.prototype 我们没有特殊定义任何原型,所以默认地继承来自 Object.prototype

我们来看看下面的魔法:

我们可以访问 make 属性作为 myBus 自己的属性。 我们可以访问 brake 属性,从 myBus 的原型中。 我们可以访问 blowHorn 属性 从 myBus 的原型的原型。 我们可以访问 hasOwnProperty 属性 从 myBus 的原型的原型的原型。:)

这叫做原型链。无论何时在 JavaScript 中我们访问一个对象的原型时,它首先检查是否这个属性在对象内部。如果不在就检查它的原型对象。如果在,就会得到这个原型值。否则,它会检查属性是否存在原型的原型上,如果也不在,那么检查原型的原型的原型,一直如此下去。

那么这种方式将会检查多久?如果属性在任何一个位置被发现或者任何位置上 __proto__ 的值是 null 或者 undefined 的时候就停止。接着会抛出一个错误,告诉你这个查找的值不存在。

这是在 JavaScript 中通过原型链的帮助,继承是如何运作的。

随便试试上面的例子,用 myCarmyMotorBike

正如我们知道的,JavaScript 中一切都是对象。在每个实例中你都能找到它,原型链结束于 Object.prototype

如果你通过 Objec.create(null) 创建一个对象,上面的规则就是个例外了。

var obj = Object.create(null)
复制代码

上面的 obj 代码,将是一个空对象,,没有任何原型。

更多关于 Object.create 的信息,请查阅 MDN。

你能改变一个已经存在的对象的原型吗?答案显而易见,通过 Object.setPrototypeOf() 就可以。具体信息参考 MDN。

想知道一个属性是否是对象自己的属性?你已经知道如何这么做了。 Object.hasOwnProperty 将会告诉你,是否这个属性来自对象自己或者来自它的原型链。具体信息参考 MDN。

注意 __proto__ 也作为 [[prototype]] 引用。

现在休息一下。我们将继续最后一部分内容。

理解 JavaScript 的类

根据 MDN:

JavaScript 类,发布于 ECMAScript 2015,是一种语法糖,覆盖了 JavaScript存在的基于原型的继承。对于 JavaScript 而言,类语法没有引进新的面向对象继承。

JavaScript 中的类提供更好的语法去实现我们在上面做的事情,这种语法要更清晰。我们来一睹为快。

class Myclass {
  constructor(name) {
    this.name = name;
  }

  tellMyName() {
    console.log(this.name)
  }
}
const myObj = new Myclass("John");
复制代码

constructor 方法是一种特殊的方法。无论何时,你创建了类的实例,它会自动执行。这类里只可能有一个constructor

你在类里定义的方法将会移动到原型对象上。

如果你想在实例里有一些属性你可以在构造器上定义它,正如我们做的那样 this.name = name

来看看我们的 myObj

注意我们在实例内部有一个 name 属性,同时在原型上有一个 tellMyName 方法。

考虑如下代码:

class Myclass {
  constructor(firstName) {
    this.name = firstName;
  }

  tellMyName() {
    console.log(this.name)
  }
  lastName = "lewis";
}
const myObj = new Myclass("John");
复制代码

我们看看输出:

lastname 移动到了实例而不是原型。只有方法,那些你声明在类体里的方法会移动到原型。尽管这有点意外。

考虑如下代码:

class Myclass {
  constructor(firstName) {
    this.name = firstName;
  }

  tellMyName = () => {
    console.log(this.name)
  }
  lastName = "lewis";
}
const myObj = new Myclass("John");
复制代码

输出:

注意 tellMyName 现在是一个箭头函数,同时它被移动到了实例而不是原型。所以记着箭头函数会总是移动到实例,请小心使用它们。

我们来看看静态类属性:

class Myclass {
  static welcome() {
    console.log("Hello World");
  }
}
Myclass.welcome();
const myObj = new Myclass();
myObj.welcome();
复制代码

输出:

静态属性是你可以不用创建类的实例就能访问的。另一方面,实例也不能访问类的静态属性。

那么静态属性是一个只在类中的新的概念,并且不在旧 JavaScript 中的吗?不,旧的JavaScript也支持。旧的JavaScript这样实现静态类:

function Myclass() {
}
Myclass.welcome = function() {
  console.log("Hello World");
复制代码

现在看看如何在类中实现继承:

class Vehicle {
  constructor(type) {
    this.vehicleType= type;
  }
  blowHorn() {
    console.log("Honk! Honk! Honk!");
  }
}
class Bus extends Vehicle {
  constructor(make) {
    super("Bus");
    this.make = make;
  }
  accelerator() {
    console.log('Accelerating Bus');
  }
  brake() {
    console.log('Braking Bus');
  }
}
Bus.prototype.noOfWheels = 6;
const myBus = new Bus("Mercedes");
复制代码

我们继承其他类使用 extends 关键字。

super() 将会简单地执行父类的构造器。如果你从其他类继承,同时在子类使用构造器,那么必须在子类的构造器中调用 super(),以免抛出错误。

我们已经知道在类体中,如果定义了不是常规函数的任何属性,它将被移动到实例中而不是原型链。我们我们在 Bus.prototype 上定义 noOfWheel

在类体中,如果你想去执行父类方法,你可以使用 super.parentClassMethod()

输出:

https://cdn-images-1.medium.com/max/800/1*62igbvXqzZZBvlH7_jNPBw.png
复制代码

上面的内容看起来跟我们的第七张图很像。

总结

那么你是否应该使用新的类语法或者旧的构造器语法呢?我觉得没有一定的答案。它取决于你的场景。

这篇文章中,类的部分我已经证明了你可以用原型的方式继承类。关于 JavaScript 类有更多的东西需要了解,但是超过了本文的范围。看看 MDN 上关于类的文档。或者以后我会写一篇关于类的文章。

如果这篇文章帮到了你,那么点个赞我会很感激。

感谢阅读 :)

pic

关注下面的标签,发现更多相似文章
评论