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

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

1.构造函数与普通函数

构造函数的写法其实和普通函数并没有区别,区别只是调用的时候,构造函数可以通过new创建对象,为了加以区分,我们通常约定构造函数首字母大写:

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

2.原型

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

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

关于构造函数、原型、继承、原型链你需要知道的一切_第1张图片

3.实例对象

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

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__属性就是找到原型的桥梁。

如此一来,原型上可能有很多方法,例如:

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本身就是对象,所以可以写成:

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

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

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

现在执行上述代码,能够正常运行。

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方法。

关于构造函数、原型、继承、原型链你需要知道的一切_第2张图片

既然这样,我们可以扩展内置对象,例如:

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

这个扩展方法当然没有任何意义,只是演示用,通常处理Date对象时,可以扩展format方法。

你可能感兴趣的:(JavaScript,javascript)