构造函数的写法其实和普通函数并没有区别,区别只是调用的时候,构造函数可以通过new创建对象,为了加以区分,我们通常约定构造函数首字母大写:
function Person(name){
this.name=name
this.say = function() {
console.log(this.name)
}
}
构造函数都会有一个属性prototype,可以打印下Person.proptotype,打印出可以发现是一个对象,这就是原型对象了。每个原型对象上都会有一个属性constructor,这个属性指向的是构造函数。
接下来通过new Person创建一个实例对象,打印这个对象,会发现他有一个__proto__属性,这个属性指向的是原型对象,但这个属性不是一个标准属性,只是实例对象和原型之间的一个桥梁,是实例对象内部找原型对象使用的,代码中不使用,总结一下构造函数、原型、实例对象的三者关系:
接下来,我们创建两个实例对象(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()
现在执行上述代码,能够正常运行。
继承的意思,就是孩子能够使用父亲的属性和方法,同时又有自己独特的属性和方法。先定义一个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方法。