关于JavaScript的原型链

193 阅读6分钟

关于JavaScript的原型链

JavaScript在ECMA6的class这些关键字出来之前,面向对象编程的方式是基于函数和原型链来实现的。这边涉及到两个东西:contructor和prototype。

本文首先从如何构造一个JavaScript的对象,再到构造函数,再到原型链的顺序来阐述。

如何构造一个JavaScript的object

在JavaScript中构造一个对象对便捷的方式如下:

var object = {
  property1: "value1",
  property2: "value2",
  method1: function () {
    console.log(this.property);
  }
}

上述代码能够直接创建一个对象。利用typeof可以得到object的类型:

console.log(typeof object); // "object"

这样做可以非常便捷的得到一个object,但是问题来了,如果需要创建多个属性和方法均一样的对象,使用上述的方法,就会出现问题了。代码就会变成如下这样:

var object1 = {
  property1: "value1",
  property2: "value2",
  method1: function () {
    console.log(this.property);
  }
}

var object2 = {
  property1: "value1",
  property2: "value2",
  method1: function () {
    console.log(this.property);
  }
}

var object3 = {
  property1: "value1",
  property2: "value2",
  method1: function () {
    console.log(this.property);
  }
}
...

如上代码所示,这样创建对象是非常灾难的,并且没有一个固定的模板可以用于创建对象,每次创建对象时需要从已经创建的对象中拷贝属性再进行修改,非常不科学。

于是,这时候就需要用到构建函数了。

构造函数

在别的语言中,有class这个概念,但是在JavaScript中没有(在ECMA6之前没有)。JavaScript的对象体系是基于构造函数(contructor)和原型链(prototype)的。构造函数可以作为对象的模板。

eg:

var User = function (username, description) {
  this.username = username;
  this.description = description;
}

var user1 = new User("Jack", "A man");
var user2 = new User("Rose", "A woman");
console.log(user1.username); // "Jack"
console.log(user2.username); // "Rose"

上述代码将User作为一个构造函数,然后通过new操作符创建了一个user1的对象。

可以看出构造函数的声明和普通函数一样。只是函数名的首字母作了大写,这个并不是硬性的,只是为了表示和普通函数的区别。因此普通函数也可以跟new操作符配合使用作为构造函数。当然,如果构造函数不和new操作符一起使用,则会被当做一个普通函数。此处不再赘述。

这边需要注意的是:
1.构造函数的内部用this来表示即将创建的对象。
2.操作符new用于根据构造函数创建一个对象

那么这边虽然实现了利用模板(构造函数)来创建对象的问题,但是仍然面临一个问题。
这边有这么个情况:

var User = function (username, description) {
  this.username = username;
  this.description = description;
  this.doSomething = function() {
    console.log(username + ' ' + description);
  }
}

var user1 = new User("Jack", "A man");
var user2 = new User("Rose", "A woman");

上述构造函数在执行构造函数代码的时候才将doSomething方法的方法体赋值给doSomething这个属性。那么,在每次执行构造函数代码的时候都会走这个逻辑。于是,每生成一个新的对象,都会生成一个新的doSomething方法。让我们测试一下:

user1.doSomething === user2.doSomething; // false

通过上述代码的运行结果,我们可以得知,user1和user2中的doSomething方法并不是同一个。在其他语言,比如Java中,我们的实例方法,都是指向同一个地址的方法的。那么在JavaScript中的这一现象,显然不是那么的友好。试想一下,如果有1000个User对象,那么就会有1000个doSomething方法,而doSomething的方法体都是一样的,这样造成了不必要的开销。

那么这时候,就需要用到JavaScript中的原型(prototype)了

原型

prototype

JavaScript的每个函数都会有一个prototype属性,这个属性将会指向一个对象。这个对象的所有属性和方法,都能被实例对象所共享。那么,如果将上一节中的doSomething方法放在prototype中,就可以处理掉不必要的开销:

var User = function (username, description) {
  this.username = username;
  this.description = description;
}
User.prototype.doSomething = function() {
  console.log(username + ' ' + description);
}
var user1 = new User("Jack", "A man");
var user2 = new User("Rose", "A woman");
user1.doSomething(); // Jack A man
user2.doSomething(); // Rose A woman
user1.doSomething === user2.doSomething; // true

这边使用了prototype之后,会有这么一个现象,就是对象在调用某个方法或使用某个属性的时候,会先查找自身有没有这个方法或者属性,如果有则直接使用,否则才回去原型对象中查找。如果原型对象没有这个方法或属性,则会再想原型对象的原型对象去查找,直到找到需要的方法或属性,或者一直追溯到了Object的原型。object的原型对象的原型对象是null的。

这个就是原型链了。然后这边如果追溯的层级过深的话是比较消耗性能的。

如果对象具有跟prototype所指向的对象名字一样的属性,那么在获取该属性时,会读取对象自身的属性值。

eg:

var object = {
  property1: "value1"
}

var Contructor = function() {
  this.property2: "value2"
}

Contructor.prototype = object;
var instance = new Contructor();
console.log(instance.property1); // value1

instance.property1 = "value2"
console.log(instance.property1); // value2

通过将构造函数的prototype属性指向一个对象,能够让通过该构造函数创建的实例对象,能够使用prototype属性所指向的这个对象的属性和方法。比如,如果将prototype指向一个数组,那么实例对象就可以调用数组的方法。

这里借鉴一下阮一峰老师的例子:

var MyArray = function () {};

MyArray.prototype = new Array();
MyArray.prototype.constructor = MyArray;

var mine = new MyArray();
mine.push(1, 2, 3);
mine.length // 3
mine instanceof Array // true

这边的这个操作,就相当于使MyArray继承了Array类了。这也就是后面要提的利用原型链做继承。

contructor

上述代码还提到了contructor属性。 这个属性用于指向构造函数。默认的情况下指向的是当前prototype指向的对象所在的构造函数。在修改了原型对象之后,一般会同时修改contructor属性。例如,上述代码中,在执行完MyArray.prototype = new Array();这句时,contructor会指向Array。之后我们将它指回MyArray。

contructor属性用于告知我们实例对象是由哪个构造函数创建的。

另外,有了contructor属性之后,就可以从一个实例对象直接创建另一个实例对象了:

var User = function (username, description) {
  this.username = username;
  this.description = description;
}

var user1 = new User("Jack", "A man");

var user2 = user1.contructor("Rose", "A woman");
console.log(user2 instanceof User); // true

构造函数的继承

让一个构造函数继承另一个构造函数:

function Super() {
  this.a = 'a';
}

function Sub() {
  Super.call(this);
  this.b = 'b';
}

Sub.prototype = Object.create(Super.prototype)
Sub.prototype.contructor = Sub;

1.上述代码,在Sub构造函数开始的时候调用Super, 执行完毕后走自己的逻辑。Super.call(this)表示将将传入的this作为执行函数中this的指向,然后调用Super的逻辑。也就是说这里是为了将Super中的this指向Sub中的this。这是因为JavaScript的this指向的是当前调用环境的this。如果不做这样的处理,Super在调用的时候,this可能会指向全局作用域,导致不必要的异常。

然后将Sub的prototype指向由Super为原型创建的对象,之后将contructor指向自己。在将Sub的prototype指向Super的时候,contructor会自动指向Super,所以之后需要将其赋值为自身,也就是Sub

到此,原型和原型链的东西差不多就说完了。这里总结一下构造函数,prototype,contructor和实例对象的关系:

function Super() {
  this.a = 'a';
}

function Sub() {
  Super.call(this);
  this.b = 'b';
}

Sub.prototype = Object.create(Super.prototype)

var sub = new Sub();
sub.__proto__ === Sub.prototype; // true
sub.contructor === Sub;          // true
Sub.prototype.contructor === Sub;// true
Sub.prototype.prototype.contructor === Super;// true

参考:阮一峰老师的JavaScript教程