续上一集内容,通过构造函数的方式,成功地更新了生产技术,老板笑呵呵,工人少奔波,只是问题总比办法多,又遇到一个新问题,就是会造成一些资源的重复和浪费,那么经过工程师们的智慧交流,他们产生了一个新技术,原型模式。
function Food() {}
Food.prototype.name = "苹果";
Food.prototype.sayName = function() {
console.log("我是" + this.name);
};
var food1 = new Food();
food1.sayName();
var food2 = new Food();
food2.sayName();
console.log(food1.sayName == food2.sayName); // 返回 true
sayName
方法都放到原型Food
的原型上去new
来创建这样就完成了原型模式的使用了,能够将函数进行共享,不用每次都重复创建不同的函数实例了,而且所有的属性共享,也能够很方便节省代码和简化结构。
但是比较懵逼,为什么这样就可以了呢?原型是个什么?怎么起作用的呢?
javascript 的原型是一个属性,一般我们叫他原型属性 prototype
,这个属性是一个内存指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
换句大白话来说:
例如这里,可以粗犷地理解为蛋是鸡生的,所以蛋的原型是鸡。
图片引用来自:https://hackernoon.com/understand-nodejs-javascript-object-inheritance-proto-prototype-class-9bd951700b29
为了更方便理解,如下图,在 javascript 里面,所有的东西都是对象,这是一个类树状结构的组织:
图片引用自:http://rohitnsit08.blogspot.com/2011/06/javascript-object-system.html
global object
的意思在后面有解释,他会分出不同的对象,有 string object
、objecr object
、function object
、array object
、等等。Function.prototype
,而不只是Function.prototype
,还有其他的对象原型,又都来自于Object.prototype
,所以也这就是平时大家常说的,javascript 里面一切都是对象的原因了。在 javascript 里面,global object 有4种:1. 在浏览器里面,windows 被称作是 global object2. 在 nodejs 里面,nodejs 的运行本身也是一个 global objec3. 在 Worker 线程下, WorkerGlobalScope 也叫 global object4. 在一般 javascript 运行过程中,在所有对象被创建之前,会预先创建一个 global object,里面包含了所有这个 javascript 引擎里面拥有的属性和方法,这个也叫做 global object,并且 javascript 的对象系统都是基于这个 global object 建立的。
prototype
属性,也就是原型属性,这个属性指向函数的原型对象,例如food1
指向Food
constuctor
构造函数属性,这个属性里面包含了一个指向,指向之前被创建的对象的prototype
属性的所在位置,相当于原型对象是母体,被创建的对象会关联到母体身上,并且是一对多的关联,即一个母体对多个子体。这里有2个图帮助理解:
Person
是构造函数,Person Prototype
是 Person 是构造函数的原型属性。Person
是构造函数的prototype 指向了Person Prototype
,而Person Prototype
的 constructor 也指向了Person
是构造函数。Person Prototype
,如果有就返回,如果没有就报无法找到。类比到我们的 Food 例子里面去,food1和 Food 和 Food Prototype的关系。
这是一个图, 对象->动物->狗->bichong(狗的大种类)->Foo(狗的小种类)->foo(我家的狗)
,这就是所谓的原型的链图的一种情况,也是原型链的一个很形象的介绍。
图片来源于:https://hackernoon.com/understand-nodejs-javascript-object-inheritance-proto-prototype-class-9bd951700b29
prototype
属性有可能叫做[[prototype]]
或者_proto_
① 如果需要查找这个实例对象的原型的话,可以使用Object.getPrototypeOf
,他会返回整个原型对象
function Food() {}
Food.prototype.name = "苹果";
Food.prototype.sayName = function() {
console.log("我是" + this.name);
};
var food1 = new Food();
console.log(Object.getPrototypeOf(food1)) // 返回 Food { name: '苹果', sayName: [Function] }
② 只能通过对象实例访问保存在原型的值,不能通过对象实例来重写原型中的值③ 对象实例可以重写从原型对象中“继承”过来的同名属性,这时候会切断对象实例和原型对象的某个同名属性的联系,如果想恢复联系即恢复没改过的同名属性的话,可以使用delete
删除对象实例的某个属性④ hasOwnProperty()
方法可以检测一个属性是存在于实例中还是存在于原型中
function Food() {}
Food.prototype.name = "苹果";
Food.prototype.sayName = function() {
console.log("我是" + this.name);
};
var food1 = new Food();
console.log(food1.hasOwnProperty("name")); // 返回 false
food1.name = "bilibili"; // 设置 food1的 name 属性(也就是改写从原型对象继承过来的 name 属性)
console.log(food1.hasOwnProperty("name")); // 返回 true
console.log(food1.name); // 返回 bilibili
⑤ 更简单的原型写法
function Food() {}
Food.prototype = {
constructor: Food, // 这里需要注意
name: '苹果',
};
constructor
的话,Food.prototype
的constructor
就不再指向 Food
,这样就没办法通过constructor
来识别得到改对象实例是属于哪个原型对象了。constructor
需要设置,所以对象的[[Enumerable]]
可遍历属性就会被设置为 true,代表可以被遍历。⑥ 在原型对象上直接编辑修改,会即时反应到实例对象上,所以可以随时进行修改,很方便。⑦ 如果重写原型对象,要注意原型对象的指向问题:
function Food() {
}
var food1 = new Food("苹果"); // 继续指向原来的 Food.prototype(最初的那个原型对象)
// 重写Food.prototype
Food.prototype = {
constructor: Food,
name: '苹果',
};
console.log(food1.name); // 返回 undefined
function Food() {
}
// 重写Food.prototype
Food.prototype = {
constructor: Food,
name: '苹果',
};
var food1 = new Food("苹果"); // 指向新的被重写后的Food.prototype
console.log(food1.name); // 返回 苹果
用了原型模式之后,虽然解决了遇到的一系列问题,但也带来了一些新的副作用(怎么副作用那么多。。。。。),原型模式的共享特性带来了方便之余,也造成了一些困扰,如果我们需要一些不想共享的信息,例如 food1 的原产地是巴西,印度,非洲,food2的原产地是巴西,印度,俄罗斯,他们之间有一些区别,不能完全共享,那么怎么办呢?
会通过组合使用构造函数模式和原型模式或者动态原型模式来解决,下回分解。