关于构造函数、原型、继承、原型链你须要知道的一切

关于构造函数、原型、继承、原型链你须要知道的一切

1.构造函数与普通函数

构造函数的写法其实和普通函数并无区别,区别只是调用的时候,构造函数能够经过new建立对象,为了加以区分,咱们一般约定构造函数首字母大写:javascript

function Person(name){
  this.name=name
  this.say = function() {
    console.log(this.name)
  }
}

2.原型

构造函数都会有一个属性prototype,能够打印下Person.proptotype,打印出能够发现是一个对象,这就是原型对象了。每一个原型对象上都会有一个属性constructor,这个属性指向的是构造函数。java

接下来经过new Person建立一个实例对象,打印这个对象,会发现他有一个__proto__属性,这个属性指向的是原型对象,但这个属性不是一个标准属性,只是实例对象和原型之间的一个桥梁,是实例对象内部找原型对象使用的,代码中不使用,总结一下构造函数、原型、实例对象的三者关系:web

3.实例对象

接下来,咱们建立两个实例对象(new 构造函数出来的对象就是实例对象):svg

var per1 = new Person("张三")
var per2 = new Person("李四")
per1.say()
per2.say()

new的意思其实就是建立一个空对象,再把构造函数中的属性和方法,放到这个空对象中,而后返回这个对象,上述代码可以正常执行,可是有个问题,函数属于复杂数据类型,会开辟内存空间存放,上述那种写法会致使每建立一个对象就多一个say函数的空间,这实际上是一种浪费,由于函数功能都同样,咱们只须要一块空间就行,因此一般方法咱们都定义在原型上:函数

Person.prototype.say = function() {
    console.log(this.name)
  }

定义在原型上,实例对象为何可以访问到呢?缘由在于咱们上文说过的__proto__属性,执行的时候会先寻找per对象上有没有say方法,发现没有,就会去原型对象上找,而__proto__属性就是找到原型的桥梁。this

如此一来,原型上可能有不少方法,例如:spa

Person.prototype.say1 = function() {
    console.log(this.name)
  }
Person.prototype.say2 = function() {
    console.log(this.name)
  }
Person.prototype.say = function() {
    console.log(this.name)
  }
...

每一个方法都这样定义,可能比较繁琐,咱们能够简化写法,由于prototype自己就是对象,因此能够写成:prototype

Person.prototype = {
  say1: function () {},
  say2: function () {},
  say: function () {
    console.log(this.name)
  }
}

看上去好像没问题,可是执行 per.say3() 发现并无效果。缘由在于咱们前面说过,每一个原型对象都有一个constructor属性指向构造函数,因此做出以下修改:code

Person.prototype = {
  constructor: Person,
  say1: function () {},
  say2: function () {},
  say: function () {
    console.log(this.name)
  }
}
var per = new Person("xz")
per.say3()

如今执行上述代码,可以正常运行。orm

4.继承

继承的意思,就是孩子可以使用父亲的属性和方法,同时又有本身独特的属性和方法。先定义一个Child类:

function Child() {}

可能有同窗会想,继承直接让Child.prototype=Person.prototype不就能直接使用父类的原型上的方法了吗,是能够用,但这有个问题,若是Child想有本身的方法,这会致使Person的原型会跟着同步改变,这显然是不合理的。

咱们上文说过,实例对象的__proto__指向的是原型对象,那利用这点咱们可让

Child.prototype = new Person("张三")
var child1 = new Child()
child1.say() // 张三
var child2 = new Child()
child2.say() // 张三

能够发现经过这种方法,虽然继承了父类的方法,可是全部name都是“张三”,这不合理,因此属性的继承通常使用call方法实现继承:

function Child(name) {
  Person.call(this, name)
}
Child.prototype = new Person("张三")
var child1 = new Child("老王")
child1.say() // 老王
var child2 = new Child("小吴")
child2.say() // 小吴

js中的继承通常是属性使用call 方法使用父类实例对象这种组合继承。Child也能够有本身的属性和方法:

function Child(name, sex) {
  Person.call(this, name)
  this.sex = sex
}
Child.prototype = new Person("张三")
Child.prototype.say = function () {
  console.log(this.sex)
}
var child = new Child("老王", "male")
child.say() //male

虽然是同名方法,但咱们能够发现,child的say是调用的本身的say方法,由于js的机制是先在本身的对象上找,找不到才会经过__proto__一层层往下找,建议打印child观察一下结构。

同理,Child也能够有本身的子类,他们之间也是经过__proto__继承,这样就造成了原型链。调用方法会沿着这个原型链一直找,这是咱们为何能够调用.toString()之类方法的缘由,由于String.prototype上定义了toString方法。

原型链

既然这样,咱们能够扩展内置对象,例如:

String.prototype.say = function () {
  console.log("hello")
}
var str = "xz"
str.say() // hello

这个扩展方法固然没有任何意义,只是演示用,一般处理Date对象时,能够扩展format方法。