原型模式
在 JavaScript 中,万物皆为对象,函数也不例外,每个函数在创建的同时,Object 对象都会为其创建一个原型对象,由函数的 prototype 属性指向,该对象包含应该由特定引用类型的实例共享的属性和方法。
使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享,举个栗子:
function Person(eye){
Person.prototype.name = '狐白吟';
Person.prototype.hobby = 'van游戏';
this.eye = eye;
};
const p1 = new Person('blue');
p1.eye; // 'blue'
p1.name; // '狐白吟'
const p2 = new Person('black');
p2.eye; // 'black'
p2.name; // '狐白吟'
console.log(p2.name === p1.name); // true
这里name 和 hobby 属性都直接添加到了 Person 的 prototype 属性上,构造函数体中没有它们,但这样定义后,调用构造函数创建的新的对象仍然拥有相应的属性,方法也不例外,可以自己手动试一下。
与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例所共享的。
理解原型
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象)。默认情况下,所有原型对象会自动获得一个名为 constructor 的属性,指回与之关联的构造函数,按照我上面给出的例子,Person.prototype.constructor指向Person。然后,因构造函数而异,可能会给原型对象添加其它属性和方法。
在自定义构造函数时,原型对象只会默认获得 constructor 属性,其它的所有方法都继承自 Object 。每次调用构造函数创建一个新的实例时,实例内部的[[Prototype]]指针就会被赋值为构造函数的原型对象,我们称之为隐式原型。按照《JavaScript高级程序设计》这本书上的说法,没有专门访问这个指针的标准方法,都是Firefox、Safari和Chrome在每个对象上暴露__proto__属性,可以通过这个属性可以访问对象的原型。
即实例与构造函数原型之间有直接的联系
前面提到,函数也是对象,那么函数也有隐式原型么?答案是肯定的。那么是谁创建了函数呢?----Function---- 注意这个大写的"F"
两种写法虽然效果相同,但绝对不推荐第二种写法。
根据上面说的一句话——对象的__proto__指向的是创建它的函数的prototype,就会出现:Object.__proto__ === Function.prototype,看下图
这里有的读者可能就有点懵了:
“自定义函数Foo.__proto__指向Function.prototype,Object.__proto__指向Function.prototype,唉,怎么还有一个……Function.__proto__指向Function.prototype?这不成了循环引用了?”
实际上就是一个环形结构,仔细想想,Function也是一个函数,函数是一种对象,也有__proto__属性。既然是函数,那么它一定是被Function创建。所以——Function是被自身创建的。所以它的__proto__指向了自身的Prototype。这也是JavaScript特殊处之一。
注意
既然Function是个函数,那么它的prototype指向的原型对象自然也有__proto__属性并且指向的是Object.prototype
说到这里,就得提到一个运算符——instanceof。
当我们对一个引用类型使用使用typeof时,我们只能获取到一个object/function,我们并不知道它具体是哪一种,这个时候就要请出instanceof
funcrion Foo(){}
let fn = new Foo()
console.log(fn instanceof Foo) // true
console.log(fn instanceof Object) // true
先不管为什么fn instanceof Object返回的是true,这里先告知大家instanceof的判断规则
instanceof运算符的第一个变量是一个对象,暂时称为A;第二个变量一般是一个函数,暂时称为B。
instanceof的判断队则是:沿着A的__proto__这条线来找,同时沿着B的prototype这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回true。如果找到终点还未重合,则返回false。看下图
根据这个规则,来看一下下面这个现象
console.log(Object instanceof Function) //true
console.log(Function instanceof Function) //true
console.log(Function instanceof Object) //true
很懵是吧,我一开始看到这里也是,但结合上面提到的:Function也是一个函数,Function是被自身创建的,每个函数在创建的同时,Object 对象都会为其创建一个原型对象。这三点来看,这个现象也很好解释了,将之前的那些图片结合起来:
不要怕!!!晕了没事,反复再看几遍,一定要把这个看懂,非常重要!
而这张图片,也将我们的重点内容——继承——原型链给引出来了
继承
继承是面向对象编程中讨论最多的话题,很多面向对象语言都支持两种继承:接口继承和实现继承,前者在JS中是不可能的,原因此处不表,所以实现继承是JS唯一支持的继承方式,而这就是通过原型链来实现的。
原型链
function Person(eyes){
this.eyes = eyes;
}
const laowang = new Person('green')
Person.prototype.age = 34
Person.prototype.money = 1145140
console.log(laowang.money) // 1145140
console.log(laowang.eyes) // green
laowang是Person函数new出来的一个对象,eyes是laowang对象的基本属性,那money是从哪里来的呢?答案是Person.prototype,因为laowang.__proto__指向的是Person.prototype
访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着__proto__这条链向上找
每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例内部有一个内部指针指向原型,如果原型是另一个类型的实例,这就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数,这样就在实例和原型之间构造了一条原型链
那么我们在实际应用中如何区分一个属性到底是基本的还是从原型中找到的呢?大家可能都知道答案了——hasOwnProperty,特别是在for…in…循环中,一定要注意。
诶?hasOwnProperty这个方法哪里来的呢?答案是Object.prototype
对象的原型链是沿着__proto__这条线走的,因此在查找laowang.hasOwnProperty属性时,就会顺着原型链一直查找到Object.prototype。
由于所有的对象的原型链都会找到Object.prototype,因此所有的对象都会有Object.prototype的方法。这就是所谓的“继承”。
当然这只是一个例子,你可以自定义函数和对象来实现自己的继承。
原型链继承是官方定义的JS的主要继承方式,其基本思想就是通过原型继承多个引用类型的属性和方法。
原型链继承的缺点
我们来看一个例子
function Person() {
this.hand = 2
}
function YellowRace() { }
YellowRace.prototype = new Person()
const hjy = new YellowRace()
console.log(hjy.head)
根据原型链的特性,当我们查找hjy实例的head和hand属性时,由于hjy本身并没有这两个属性,引擎就会去查找hjy的原型,还是没有,继续查找hjy原型的原型,也就是Person原型对象,结果就找到了。就这样,YellowRace和Person之间通过原型链实现了继承关系。
但这种继承是有问题的:
- 创建
hjy实例时不能传参,也就是YellowRace构造函数本身不接受参数。 - 当原型上的属性是引用数据类型时,所有实例都会共享这个属性,即某个实例对这个属性重写会影响其他实例。
针对第二点,我们通过一段代码来看一下:
function Person() {
this.colors = ['white', 'yellow', 'black']
}
function YellowRace() { }
YellowRace.prototype = new Person()
const hjy = new YellowRace()
hjy.colors.push('green')
console.log(hjy.colors) // ['white', 'yellow', 'black', 'green']
const laowang = new YellowRace()
console.log(laowang.colors) // ['white', 'yellow', 'black', 'green']
可以看到,hjy只是想给自己的生活增添一点绿色,但是却被laowang给享受到了,这肯定不是我们想看到的结果。
寄生式组合继承
JS中一共有6种继承方式,其余这里不会专门去讲,但会用到,感兴趣的可以去查一下
ES5新增了一个方法Object.create()将原型链继承的核心代码封装成了一个函数,但这个函数有了不同的适用场景:如果你有一个已知的对象,想在它的基础上再创建一个新对象,那么你只需要把已知对象传给该函数即可。
Object.create()可以接受两个参数,第一个参数是作为新对象原型的对象,第二个参数也是个对象,里面放入需要给新对象增加的属性(可选)。第二个参数与Object.defineProperties()方法的第二个参数是一样的,每个新增的属性都通过自己的属性描述符来描述,以这种方式添加的属性会遮蔽原型上的同名属性。这是原型式继承的封装函数,而我们寄生式组合继承用到的寄生式继承与之非常接近,是在此基础上增强对象,然后返回这个对象
寄生式组合继承通过在使用上述函数继承父类的原型对象,然后将返回的新对象复制给子类的原型对象,然后在子类的构造函数中调用父类的构造函数实现上下文的绑定,从而保证继承自父类的属性是自己的,对其修改不会影响到其它的实例
function inherit(Father, Son) {
const prototype = Object.create(Father.prototype) // 获取父类原型对象副本
prototype.constructor = Son // 将获取的副本的constructor指向子类,以此增强副本原型对象
Son.prototype = prototype // 将子类的原型对象指向副本原型对象
}
function Person(eyes) {
this.eyes = eyes
this.colors = ['white', 'yellow', 'black']
}
function YellowRace() {
Person.call(this, 'black') // 调用构造函数并传参
}
inherit( Person ,YellowRace) // 寄生式继承
const hjy = new YellowRace()
hjy.colors.push('green')
const laowang = new YellowRace()
console.log(hjy.colors)
console.log(laowang.colors)
console.log(hjy.getEyes())
上述寄生式组合继承只调用了一次Person构造函数,避免了在Person.prototype上面创建不必要、多余的属性。于此同时,原型链依然保持不变,效率非常之高效。
后记: 其实有很多大佬已经把原型链讲的十分透彻了,我在这写博客也只是为了加深一下自己的理解,很推荐王福朋先生写的这篇博客,附上链接:[https://www.cnblogs.com/wangfupeng1988/p/3977924.html](url)
本篇也有一部分是参考自这篇博客,图片也来自这篇博客,真的写的很好。图侵删