引言
上面说了创建对象有字面量方式和工厂模式还有构造函数模式,结果发现他们都各自有缺点,所以下面再给大家介绍几种创建对象的方式,争取能找到一种无痛的模式?。
原型模式
下面会有一段非常晦涩难懂的内容,大家跟紧了别翻车。我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针(地址),指向一个对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
function Person () {}Person.prototype.name = '李小花';Person.prototype.age = '60';Person.prototype.sayName = function () { console.log(this.name);};var person1 = new Person();person1.sayName(); //李小花var person2 = new Person();person2.sayName(); //李小花
Person的prototype 属性中,构造函数,构造函数是空函数。新对象上的属性和实例是共享的。现在我们需要做一件事,就是理解原型对象。
理解原型对象
无论什么时候,只要创建了一个新的函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象,所有的原型都会获得一个constructor属性,指向prototype属性所在函数的指针。我们通过这个构造函数为原型对象添加其他方法和属性。创建对象后,其原型对象默认只有constructor属性。当构造函数创建一个新实例后,该实例的内部将包含一个指针,指向构造函数的原型。虽然在脚本中没有标准的方式访问[[Prototype]],但除IE外都支持一个叫_proto_。这个属性存在与实例与构造函数的原型对象上直接,而不存在于实例与构造函数之间。大家是不是看晕了?,我下面用一张图展示一下各个之间的对应关心,看好了,我要展示我高超的画图神功了。
灵魂图片已经展示,相信大家已经看懂了吧。需要注意的是,虽然这两个实例都不包含属性和方法 ,但我们可以在实例上调用原型上的方法,这是通过查找对象属性的过程来实现的。扩展一下,既然js无法实例的的[[Prototype]],但是可以通过isPrototype()方法来确定这层关系。
console.log(Person.prototype.isPrototypeOf(person1)); // true
在es5中新增了一个方法,叫Object.getPrototype(),这个方法返回[[Prototype]]的值,例如,
console.log(Object.getPrototypeof(person1) == Person.prototype) //trueconsole.log(Object.getPrototypeof(person1).name) //李小花
但是同样的,IE8以下的浏览器都不支持这个属性。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是给定名字的属性,搜索首先从实例本身开始,如果在实例本身找到了这个属性,则返回该属性的值,如果没有找到,则继续指针指向的原型对象,如果找到,则返回这个属性的值。
虽然可以通过对象实例访问保存在原型中的值,但确不能通过对象重写原型中的值。如果我们在实例中添加一个属性,而该属性与实例原型中的一个属性同名,这个属性会屏蔽原型中的那个属性。
function Person () {}Person.prototype.name = '李小花';Person.prototype.age = '29';Person.prototype.job = '班花';Person.prototype.sayName = function () { console.log(this.name);};var person1 = new Person();var person2 = new Person();person1.name = '张全蛋';console.log(person1.name); // 张全蛋console.log(person2.name); // 李小花
当为对象添加一个属性时,这个属性会屏蔽原型对象的同名属性,添加这个属性只会阻止我们访问原型的那个属性,但不会修改那个属性,使用delete属性能够让我们访问原型中的属性。
function Person () {}Person.prototype.name = '李小花';Person.prototype.name = '29';Person.prototype.job = '班花';Person.prototype.sayName = function () { console.log(this.name);};var person1 = new Person();var person2 = new Person();person1.name = '张全蛋';console.log(person1.name); // 张全蛋console.log(person2.name); // 李小花delete person1.nameconsole.log(person1.name); // 李小花
我们在遍历对象的的属性的时候,经常需要判断属性是否来自于对象的原型还是属性。
function Person () {}Person.prototype.name = '李小花';Person.prototype.name = '29';Person.prototype.job = '班花';Person.prototype.sayName = function () { console.log(this.name);};var person1 = new Person();var person2 = new Person();console.log(person1.hasOwnProperty('name')); // falseperson1.name = '张全蛋';console.log(person1.hasOwnProperty('name')); // trueconsole.log(person1.name); // 张全蛋console.log(person2.name); // 李小花delete person1.nameconsole.log(person1.name); // 李小花
我们有一个神器,hasOwnProperty方法,只有当属性是实例的属性时,才会返回true。
原型操作符中的in方法
我上面介绍了怎么通过hasOwnProperty方法判断属性是否来自对象属性,那怎么判断属性来自原型链呢?下面隆重介绍 in操作符,不过属性来自属性还是来自原型链的属性,都会返回true,我们通过这个属性结合hasOwnProperty方法判断属性是否来自于原型链。
function hasPrototypeproperty(obj, name) { return !object.hasOwnProperty(name) Person.prototype.name = '29';Person.prototype.job = '班花';Person.prototype.sayName = function () { console.log(this.name);};var person1 = new Person();console.log(hasPrototypeproperty(person, 'name')); // trueperson1.name = '张全蛋';console.log(hasPrototypeproperty(person, 'name')); // false
在使用for-in循环时,返回的是能够通过对象访问的,可枚举的属性,包括实例中的属性,也包含存在于原型中的属性,屏蔽原型中不可枚举的属性也会在for-in中返回。可以通过es5 中的Object.keys方法,返回一个包含所有可枚举属性的字符串数组。
function Person () {}Person.prototype.name = '李小花';Person.prototype.age = '29';Person.prototype.job = '班花';Person.prototype.sayName = function () { console.log(this.name);};var keys = Object.keys(Person.prototype);console.log(keys); //name age job sayNamevar p1 = new Person();p1.name = '全蛋';p1.age = 20;var p1keys = Object.keys(p1);console.log(p1keys); // name,age
很多时候我们不想那么麻烦的写,所以会像下面这样简便的申明原型模式。
function Person () {}Person.prototype = { name: '李小花', age: 30, job: '班花', sayName: function () { console.log(this.name) }}Object.defineProperty(Person.prototype, 'constructor', { enumerable: false, value: Person})
function Person() {}var friend = new Person();Person.prototype = { constructor: Person, name: '李小花', age: 30, job: '班花', sayName: function () { console.log(this.name) }};friend.sayName(); //Uncaught TypeError: friend.sayName is not a function
这段代码大家是不是看蒙了,前面的我都白学了?难道不应该输出李小花吗?记得我们之前说的吗,创建构造函数会为实例添加一个指向最初原型的[[Prototype]]指针,这段代码修改了切断了构造函数与原型的联系,所以请记住,实例中的指针仅指向原型,而不指向构造函数。为了增强大家的理解,我必须从操旧业,为大家画一幅流畅图。
重写原型对象之前的
重写原型对象之后
原生对象的问题
那么原声对象有缺点吗,如你所愿,当然是有缺点的,那么原声对象有缺点吗,如你所愿,当然是有缺点的,原型中的很多实例是被很多实例共享的,这种对于函数很合适,基本值的属性也是可以的,可以通过在实例中添加一个属性,隐藏原型中的对应属性,对于包含引用类型的值的属性来说,问题就比较突出了。
function Person () {}Person.prototype = { constructor: Person, name: '李小花', friends: ['全蛋', '大队长']}var person1 = new Person();var person2 = new Person();person1.friends.push('狗蛋');console.log(person1.friends) // ["全蛋", "大队长", "狗蛋"]console.log(person2.friends) // ["全蛋", "大队长", "狗蛋"]person1.friends = [1, 2, 3] console.log(person1.friends) // [1, 2, 3],切断了与原型的指针console.log(person2.friends) // ["全蛋", "大队长", "狗蛋"]
现在业内最广泛最认同的模式是组合使用构造函数模式和原型模式
function Person () { name: '李小花', friends: ['全蛋', '大队长']}Person.prototype = { constructor: Person, sayName: function () { console.log(1) }}
剩余的动态原型模式、寄生构造函数模式、稳妥构造函数模式因为很少用这里就不多介绍了。