本文首发于个人博客:www.wyb.plus
1. 原型模式
1.1 原型模式的核心概念
在构造函数的
辨析2
中了解到 , 把函数定义在构造函数内会造成内存浪费 , 把函数定义在构造函数外又有两个新问题 , 那么如何完美的解决这个问题呢?这就需要原型模式了.原型对象 >>>
- 我们创建的每个函数都有一个
prototype(原型)
属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。- 按照字面意思来理解,那么
prototype
就是通过调用构造函数而创建的那个对象实例的原型对象。简单来说 , 构造函数的prototype
就是实例对象的原型对象- 使用原型对象的好处是可以让所有对象实例
共享
它所包含的属性和方法根据公式可以得出
//例子 function fn() { console.log(this.name); } function Person(name, age) { this.name = name; this.age = age; this.sayName = fn() } let wyb = new Person("王雨波", 18) let wangyubo = new Person("xxx", 19) //公式 wyb.__proto__ === Person.prototype //true wangyubo.__proto__ === Person.prototype //true //得出 Person.prototype.sayName === wyb.__proto__.sayName Person.prototype.sayName === wangyubo.__proto__.sayName
//所以,我们可以重新改造一下例子 function Person(name, age) { this.name = name; this.age = age; } Person.prototype.sayName = function() { console.log(this.name); } let wyb2 = new Person("王雨波", 18) let wangyubo2 = new Person("xxx", 19)
此时 ,
wyb2.sayName
===wangyubo2.sayName
, 这说明他们共用一个内存地址 , 这就解决了内存浪费的问题 , 并且 , 这个方法也没有被定义在全局window中补充 >>> 在构造函数中 :
- 通过
this.属性
来定义的参数会在实例中直接看到- 通过
构造函数.prototype
定义的参数会在实例的._proto_
属性中看到, 这代表._proto_
里面列举的属性都是从构造函数的prototype
中获取的 , 这就反映了我们之前那个公式1:a.__proto__ === Array.prototype
, a代表实例对象
,Array
代表构造函数
可是这种方法又会有一个新的问题 >>>
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.sayName = function() { console.log(this.name); } //在构造函数的原型对象上添加一个属性 值为对象类型 Person.prototype.girlFriends = { first: "斋藤飞鸟", second: "迪丽热巴" } //new两个实例对象 let wyb2 = new Person("王雨波", 18) let wangyubo2 = new Person("xxx", 19) //修改其中一个实例对象的继承过来的属性 wyb2.girlFriends.first = "呆头鹅" //另一个实例继承过来的属性被修改了 console.log(wangyubo2.girlFriends.first);//-->"呆头鹅"
原型对象上的属性为引用类型 , 当实例对象修改这个继承过来的引用类型的值时,会导致原型对象被修改 , 再导致其他实例对象的值被修改 , 如果你希望它是这样那么自然没问题 , 但是大多数时候我们都是不希望他们相互影响的
解决思路1: 通过设置实例对象的对象属性 , 让他变成不可枚举,不可配置,不可写入
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.girlfriends = { first: "斋藤飞鸟", second: "迪丽热巴" } let wyb2 = new Person("王雨波", 18) let wangyubo2 = new Person("xxx", 19) console.log(wyb2.girlfriends, wangyubo2.girlfriends); //待修改对象设置成wyb2 Object.defineProperty(wyb2, "girlfriends", { value: {first:'关晓彤',second:'鞠婧祎'}, enumerable: false, configurable: false, writable: false }) console.log(wyb2.girlfriends, wangyubo2.girlfriends);
修改后 , 实例对象wyb的改变并没有导致原型对象被修改 , 所以另外一个实例对象也没有被修改
(这个思路其实是错误的 , 学到后面之后我发现其实它的输出结果跟使用
Object.defineProperty()
来改变对象的属性毫无关系 , 这个结果的原理其实是第一次输出的时候 , wyb2和wangyubo2自身都没有girlfriends属性 , 那么都在原型上找 , 原型上有这个属性所以输出了 , 而第二次输出的时候 , 又开始从自身开始找 , wyb2上自身已经有了girlfriends属性了 , 所以输出自身的girlfriends , 而wangyubo2自身没有 , 所以去原型上找)更正一下 :
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.girlfriends = { first: "斋藤飞鸟", second: "迪丽热巴" } let wyb2 = new Person("王雨波", 18) let wangyubo2 = new Person("xxx", 19) console.log(wyb2.girlfriends, wangyubo2.girlfriends); //待修改对象设置成wyb2 wyb2.girlfriends = { first: '关晓彤', second: '鞠婧祎' } console.log(wyb2.girlfriends, wangyubo2.girlfriends);
这个结果应该是这样的代码
出现错误的原因在于我对这个原型理解不到位 , 对那个新问题的认识不到位
重新描述一下这个问题 >>>
- 两个实例对象上自身没有某个引用类型的属性 , 而他们的原型上有
- 此时我通过修改某个实例对象的这个属性其实修改的是原型上的属性 , 而他们三个的这个属性现在都是共用的一个地址 , 所以另外一个实例对象的这个属性也会被改变
- 而当我给其中的一个实例对象设置上了这个属性时 , 它就会屏蔽原型上的这个属性 , 优先使用自己设置的这个属性
- 如果使用delete操作符删除实例对象的这个属性 , 那么它又会恢复指向原型 , 再次使用原型上的这个属性
思路1的衍生 : 既然可以把实例对象设置成私有属性 , 那么可不可以直接把原型对象的属性设置成这样的?
function Person(name, age) { this.name = name; this.age = age; } //待修改对象换成Person.prototype,并且换些主角 Object.defineProperty(Person.prototype, "girlfriends", { value: { first: '新垣结衣', second: '石原里美' }, enumerable: false, configurable: false, writable: false }) let wyb2 = new Person("王雨波", 18) let wangyubo2 = new Person("xxx", 19) console.log(wyb2.girlfriends, wangyubo2.girlfriends); wyb2.girlfriends.first = '关晓彤'; wyb2.girlfriends.second = '鞠婧祎' console.log(wyb2.girlfriends, wangyubo2.girlfriends);
不行!!! Person.prototype仍然被修改了
1.2 原型模式模型图
1.3 原型模式的操作
原型的检测 >>>
语法 :
构造函数.prototype.isPrototypeOf({待检测对象})
如果被检测对象的
[[Prototype]]
指向构造函数的prototype
属性的,那么这个方法就返回true辨析 >>>
- 注意
isPrototypeOf
左侧一定是原型对象 , 右侧是该原型对象的实例对象 , 不能放反了 ;- 而
instanceof
的左侧是实例对象 , 右侧是该实例的原型对象 ;- 这两个都跟层级无关
原型的获取 >>>
方法1:
实例.__proto__
方法2:
实例.constructor.prototype
方法3:
Object.getPrototypeOf(实例)
- 方法1兼容性不好 , 谷歌支持 ;
- 其他两个方法兼容性比较好
1.4 原型链的基本概念
每当读取某个对象的某个属性时 , 都会执行一次搜索 , 目标是具有给定名字的属性
- 搜索先从实例对象本身开始 , 如果在实例中找到了具有给定名字的属性,则返回该属性的值;
- 如果没有找到 , 则继续搜索指针指向的原型对象 , 在原型对象上查找 , 如果找到了则返回该属性的值 ,
- 如果没找到则继续沿着原型链往下找 , 直到顶层原型对象被找完 , 还没找到就返回undefined
2. 原型的覆盖问题
如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性(原型的属性依旧还在),其他继承原型属性的实例不受影响
示例 :
function Person(name, age) { this.name = name; this.age = age; this.hobby = [1, 2, 3] } let wyb = new Person("王雨波", 18) let wangyubo = new Person("xxx", 19) console.log(wyb, wangyubo); wyb.hobby.push(4) console.log(wyb, wangyubo);
当为实例对象添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;
简单来说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。
不过,使用
delete
操作符则可以完全删除实例属性 , 这样就可以恢复指向原型的链接 (老师是这样讲的 , 但是我实际测试出来不是这样的结果----其实是我的错)function Person(age) { this.name = null; this.age = age; this.hobby = [1, 2, 3] } let wyb = new Person(18) console.log(wyb); delete wyb.name console.log(wyb.name);
如果恢复了指向原型的链接 , 那我应该输出null而不是undefined???
我又想明白了 , 我并没有给原型对象定义name , 所以他自然是undefined
function Person(age) { this.name = null; this.age = age; this.hobby = [1, 2, 3] } //当我给wyb的原型对象定义一个name属性 Person.prototype.name = "wangyubo"; let wyb = new Person(18) console.log(wyb); delete wyb.name console.log(wyb.name);
这样就没问题了
前面例子中每添加一个属性和方法就要敲一遍
Person.prototype
。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象但是这样会有一个问题
function Person(age) { this.name = null; this.age = age; this.hobby = [1, 2, 3] } Person.prototype.name = "wangyubo"; let wyb = new Person(18)
当我们这样一条一条的给原型对象定义属性时 , 原型对象上还有一个
constructor
属性function Person(age) { this.name = null; this.age = age; this.hobby = [1, 2, 3] } Person.prototype = { name: "wangyubo", age: 28, hobby: [123, 456] }; let wyb = new Person(18)
而当我给原型对象定义一个对象时 , 原型对象上就没有
constructor
属性了//从等式4 Array.prototype.constructor === Array //可以得到解释
我们在这里使用的语法,本质上完全重写了
默认的prototype 对象
,constructor
属性也就变成了新对象的constructor
属性(指向Object 构造函数),不再指向Person 函数。尽管
instanceof
操作符还能返回正确的结果,但通过constructor
已经无法确定对象的类型了(尽管你爸爸不要你了 , 但他仍然是你爸爸)(这里其实又有问题了 , wyb的生成是在原型被修改之后 , 所以wyb是基于新的Person生成的 , 那么instanceof的结果自然是true , 如果wyb的生成是在原型被修改之前的话 , 那么instanceof的结果自然是false)
稍微更正一下
function Person(age) { this.name = null; this.age = age; this.hobby = [1, 2, 3] } let wyb = new Person(18) Person.prototype = { name: "wangyubo", age: 28, hobby: [123, 456] };
用
instanceof
操作符测试Object
和Person
仍然返回true
,但constructor
属性则等于Object 而不等于Person 了。如果
constructor
的值真的很重要,可以像下面这样特意将它设置回适当的值。function Person(age) { this.name = null; this.age = age; this.hobby = [1, 2, 3] } Person.prototype = { constructor: Person,//手动添加constructor name: "wangyubo", age: 28, hobby: [123, 456] }; let wyb = new Person(18)
但是此时此时的
constructor
是可枚举的 , 为了保证不会被枚举,可以同时定义数据特性(使用Object.defineProperty
)function Person(age) { this.name = null; this.age = age; this.hobby = [1, 2, 3] } Person.prototype = { constructor: Person, name: "wangyubo", age: 28, hobby: [123, 456] }; //设置对象属性 Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person }) let wyb = new Person(18)
3. 原型的动态性继承
3.1 原型的动态性问题
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此
function Person(name, age) { this.name = name; this.age = age; this.hobby = [1, 2, 3] } let wyb = new Person("xxx", 18) Person.prototype.sayName = function() { console.log(this.name); } wyb.sayName() // --> xxx
sayName方法在wyb实例对象之后创建 , 如果不是动态继承的话 , 那么wyb是没有这个方法的
3.2 原型重写时的动态性问题
尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。
我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。
请记住:实例中的指针仅指向原型,而不指向构造函数(这也是我在原型的覆盖问题这节出现的原因 , 我去构造函数里面找了)
function Person(name, age) { this.name = name; this.age = age; this.hobby = [1, 2, 3] } let wyb = new Person("xxx", 18) Person.prototype = { constructor: Person, name: "wangyubo", age: 18, sayName: function() { console.log(this.name); } } wyb.sayName()
和3.1中的示例相比 , 我们不过是把原型对象直接定义成了一个对象而已 , 此时再次调用wyb这个实例对象时 , 已经没有动态继承了
如果我在修改原型对象之后再new一个新的实例
// wyb.sayName() let wangyubo = new Person("yyy", 28)
详细比较两个实例对象的不同
这两个实例对象的原型对象已经截然不同了
还有一些有意思的问题 : 检测一下
//检测的代码 function Person(name, age) { this.name = name; this.age = age; this.hobby = [1, 2, 3] } let wyb = new Person("xxx", 18) Person.prototype = { constructor: Person, name: "wangyubo", age: 18, sayName: function() { console.log(this.name); } } let wangyubo = new Person("yyy", 28)
- 检测结果显示wyb已经不是Person的实例对象了 , 而wangyubo还是Person的实例对象
- wyb和wangyubo都是Object的实例对象
原因在于 >>>>
- 重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;
- 之前创造的对象引用的仍然是最初的原型。
打个比方 >>>
代码在执行到
let wyb = new Person("xxx", 18)
的时候创造了实例对象wyb , 那么此时wyb的原型是默认的没修改的Person.prototype
, 再往下执行时 ,Person.prototype
被修改了 , 那么此时的Person.prototype
已经不再是wyb的老爸了 , 代码再往下执行有创造了wangyubo实例 , 而wangyubo实例是基于新的修改后的Person.prototype
创造的所以总结来说就是 , wyb的老爸被修改了 , wyb自然不是新的老爸的亲儿子 , 但是wangyubo却是新老爸的亲儿子 . 且 wyb的初始老爸和新老爸都是基于爷爷Object创造的 , 所以wyb还是Object的亲孙子 , emmmm…感觉怪怪的样子~~
//注意 //这样写相当于修改初始原型对象的某个属性,并没有把这个原型对象给干掉 Person.prototype.sayName=function() { console.log(this.name); } //这样写相当于把初始原始对象给干掉了,重新生成了一个新的原型对象,之后的实例对象都会基于新的原型对象生成 Person.prototype = { constructor: Person, name: "wangyubo", age: 18, sayName: function() { console.log(this.name); } } //这就是上面的示例的原因
4. 原生对象原型
所有原生引用类型(Object、Array、Date,等等)都在其构造函数的原型上定义了方法。
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法
尽管可以这样做,但我们不推荐在程序中修改原生对象的原型。如果因某个实现中缺少某个方法,就在原生对象的原型中添加这个方法,那么当在另一个支持该方法(比如不同版本的浏览器)的实现中运行代码时,就可能会导致命名冲突
4.1 原生对象的问题
- 它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值(如果所有的属性都在prototype上定义的话)
- 原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。然而,对于包含引用类型值的属性来说,就有些问题