《谈谈 JavaScript 中的 function constructor 和 new 关键字》这篇文章中我们说明了如何通过函数构造式(function constructor)搭配关键字 new 来建立对象,但其实这样只讲了一半,在这篇我们会补齐另一半,说明 function constructor 如何用来设定该对象的原型(prototype)。

在 JavaScript 中的函数也是一种对象,其中包含一些属性像是该函数的名称(Name)和该函数的内容(Code),但其实function这里面还有一个属性,这个属性就是prototype,这个属性会以空对象的型式呈现。

除非你是把function当做function constructor来使用,否则这个属性就没有特别的用途;但如果你是把它当做function constructor,通过new这个关键字来执行这个function的话,它就有特别的意义了。

要进入这个functionprototype属性只要直接通过 .prototype 就可以了。

然而,有一点很容易令人困惑的地方,我们会以为如果我使用 .prototype 时,就可以直接进入该函数的原型,但实际上并不是这样的!

函数当中prototype这个属性并不是这个函数的prototype,它指的是所有通过这个function constructor所建立出来的对象的prototype,听起来有点混乱吧…没关系,让我们来看一些代码来帮助我们理解这一概念。

说明函数中的 prototype 属性

1.function 中的 prototype 属性一开始是空对象

我们先执行上篇文章最后所写的代码:

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

var person1 = new Person("Jay", "chou");
console.log(person1);
var person2 = new Person("Jane", "chou");
console.log(person2);

到 Google Chrome 的 console 视窗中,我们输入 Person.prototype得到的结果会得到一个空对象,如下图:

2.通过 function constructor 所建立的对象会继承该 function 中 prototype 的内容

接着,让我们在Person.prototype里面增加一个getFullName的函数:

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

Person.prototype.getFullName = function () {
  return this.firstName + " " + this.lastName;
};

var person1 = new Person("Jay", "chou");
console.log(person1);
var person2 = new Person("Jane", "chou");
console.log(person2);

在上面代码的第 6 - 8 行中,我们为Person.prototype添加了一个函数,所以当我们在 Google Chrome 的 console 视窗中调用Person.prototype时,会多了这个函数在内:

刚刚,我们有提到很重要的一句话,「函数当中prototype这个属性并不是这个函数的prototype,它指的是所有通过这个 function constructor 所建立出来的对象的prototype」。

这句话的意思其实是说Person.prototype并不是Person.__proto__,但是所有通过Person这个function constructor所建立的对象,在该实例对象的__proto__中,会包含有Person.prototype的内容。

也就是说,当我们使用new这个运算符来执行function constructor时,它会先建立一个空对象,同时将该构造函数中prototype这个属性的内容(Person.prototype),设置到该实例对象的prototype中,即 person1.__proto__ === Person.prototype的结果为true

因此,当我们在 Google Chrome 的 console 中输入person1.__proto__时,我们就可以看到刚刚在Person.prototype所建立的函数getFullName已经继承在里面了:

实际运用

由于Person.prototype中的方法已经被继承到由Person这个function constructor所建立的实例对象person1中,所以这时侯,我们就可以顺利的使用 person1.getFullName 这个方法:

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

Person.prototype.getFullName = function () {
  return this.firstName + " " + this.lastName;
};

var person1 = new Person("Jay", "chou");
console.log(person1);
console.log(person1.getFullName());

可以正确的执行getFullName这个函数并得到如下的结果:

通过 function constructor 与 Prototype 的实用处

通过这样的方法,我们可以让所有根据这个函数构造器(function constructor)所建立的对象都包含有某些我们想要使用的方法。如果我们有 1000 个对象是根据这个函数构造器所建立的,那么我们只需要使用 .prototype这样的方法,就可以让这 1000 个物件都可以使用到我们想要执行的某个method,这样减少了代码的复用。

有的人可能会好奇问,为什么我们不把getFullName这个方法直接写在函数构造式当中呢?

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
  this.getFullName = function () {
    return this.firstName + " " + this.lastName;
  };
}

/*
Person . prototype . getFullName  =  function ( )  { 
  return  this . firstName +  ' '  +  this . lastName ; 
}
*/

注意!我们不该把方法放在function constructor 中。

把方法放在函数构造式中这么做虽然仍然可以正确执行并得到结果,但是这么做会有个问题,如果我们是把这个方法直接写在函数构造式中,那么每一个对象都会包含有这个方法,如果我们有 1000 个对象根据这个函数构造式所建立,那么这 1000 个对象都会包含这个方法在内,如此将会占据相当多的内存;但如果是建立在prototype 中,我们只会有一个这样的方法。

所以,为了性能上的考量,通常会把方法(method)放在构造函数的prototype 中,因为它们可以是通用的;把属性(property)放在构造函数当中,因为每一个对象可能都会有不同的属性内容,如此将能有效减少内存的问题。

最后,如果感觉当前缺少你要用的方法,可以自己通过这一方法去创建。

例如在 json2.js 源码中,为DateStringNumberBoolean方法添加一个toJSON的属性。

if (typeof Date.prototype.toJSON !== "function") {
  Date.prototype.toJSON = function (key) {
    return isFinite(this.valueOf())
      ? this.getUTCFullYear() +
          "-" +
          f(this.getUTCMonth() + 1) +
          "-" +
          f(this.getUTCDate()) +
          "T" +
          f(this.getUTCHours()) +
          ":" +
          f(this.getUTCMinutes()) +
          ":" +
          f(this.getUTCSeconds()) +
          "Z"
      : null;
  };

  String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function (
    key
  ) {
    return this.valueOf();
  };
}

如果你要添加内置方法的原型属性,最好做一步判断,如果该属性不存在,则添加。如果本来就存在,就没必要再添加了。

如果觉得文章对你有些许帮助,欢迎在我的 GitHub 博客点赞和关注,感激不尽!


JavaScript      JavaScript constructor prototype

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!