我来重新学习 javascript 的面向对象(part 2)

续上一集内容,通过构造函数的方式,成功地更新了生产技术,老板笑呵呵,工人少奔波,只是问题总比办法多,又遇到一个新问题,就是会造成一些资源的重复和浪费,那么经过工程师们的智慧交流,他们产生了一个新技术,原型模式

一、使用原型模式

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();
// 创建无限多的 food 。。。。。。

console.log(food1.sayName == food2.sayName); // 返回 true
  • 将所有属性和方法,包括sayName 方法都放到原型Food的原型上去
  • 跟之前构造函数创建新对象的方式一样,使用new来创建

这样就完成了原型模式的使用了,能够将函数进行共享,不用每次都重复创建不同的函数实例了,而且所有的属性共享,也能够很方便节省代码和简化结构,其他人也可以很方便地进行使用。

但是比较懵逼,为什么这样就可以了呢?原型是个什么东西?怎么起作用的呢?

1.1 理解什么是原型

在《javascript 高级程序设计》里面是这样说的,我们创建的每个函数都有一个 prototype,这个属性是一个内存指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

换句大白话来说:

  • 原型就是根,所有东西都有根,是来自于哪里,是被谁创造出来的,并且能够通过这个根去追溯父辈祖辈的信息。
  • 全部东西都会由原型创造,所以都会带有一个原型属性,只是不同的原型创造出来的东西带有不同的原型属性。
image

例如图1,可以粗犷地理解为蛋是鸡生的,所以蛋的原型是鸡。

image

图片引用来自:https://hackernoon.com/understand-nodejs-javascript-object-inheritance-proto-prototype-class-9bd951700b29

或者用亲属关系来理解,原型就是你的父辈祖辈。

image

图片引用自http://china.findlaw.cn/info/hy/hyfwsfb/qita/1062124.html

例如图2,这是一个类树状结构的组织:

image

图片引用自:http://rohitnsit08.blogspot.com/2011/06/javascript-object-system.html

  • 这里的global object 的意思在后面有解释。
  • 从这个图可以看到,各种不同过的对象,都会有不同的原型,string 的原型就是 string.prototypefunction 的原型就是 string.prototype 等等,而这些原型的原型就是Object.prototype了,所以就有那么一句话,在 javascript 里面,所有的东西都是对象

在 javascript 里面,global object 有4种:

  1. 在浏览器里面,windows 被称作是 global object
  2. 在 nodejs 里面,nodejs 的运行本身也是一个 global objec
  3. 在 Worker 线程下, WorkerGlobalScope 也叫 global object
  4. 在一般 javascript 运行过程中,在所有对象被创建之前,会预先创建一个 global object,里面包含了所有这个 javascript 引擎里面拥有的属性和方法,这个也叫做 global object,并且 javascript 的对象系统都是基于这个 global object 建立的。

1.2 理解什么是 constructor 和构造函数的 prototype 和 [[prototype]]

image

其实原型是很好理解的东西,就是原来的形态,例如string 的原型就是 string.prototype,字符串的原来的形态就是字符串原型,但是还有一些比较影响理解和学习的东西,例如constructor prototype [[prototype]] 这些。

① 先来说constructor

其实构造函数也有constructor,原型对象有constructor,实例有constructor 也有,或者更加笼统的说,所有对象都是有constructor的。

// 构造函数的constructor,默认构造函数的原型对象是Function对象
function Food() {}
console.log(Food.constructor); // 返回[Function: Function]

// 实例的constructor是Food
var food1 = new Food("苹果");
console.log(food1.constructor); // 返回 [Function: Food]

// 构造函数的原型对象Food.prototype 也有constructor,指向构造函数
console.log(Food.prototype.constructor); // 返回 [Function: Food]
  • 构造函数也是函数,他的 constructor默认会指向[Function: Function],也就是函数原型对象是他的原型对象。
  • 当使用 new 来实例化对象的时候,实例的constructor会默认指向构造函数,证明是哪一个来自于哪一个构造函数,但是仅此而已,只是一个标识,所以是 Food
  • 构造函数 的原型对象Food Prototype上的constructor会指向构造函数 Food

可以看出,通过 constructor 可以看到他们之间的关系,但是通过constructor连接的关系是很脆弱的(容易变化,不可靠),也因为 javascript 在设计当初并没有太多考虑这个情况,所以constructor比较鸡肋。

另外虽然有这么多constructor,但是我们一般讨论的比较多的是,原型对象的constructor,他会默认指向构造函数的 prototype 属性,仅此而已,如果他没有被改变的话,也可以充当一种标志,代表通过这个构造函数生成的实例是来自哪个原型的,从而判断对象的类型是哪一种(但不可靠)

举例说明:

function Food() {}
console.log(Food.constructor); // 返回[Function: Function]

// 当重写原型的时候,实例的constructor 就变成了[Function: Object]
Food.prototype = {
  name: "苹果"
};
var food2 = new Food("苹果");
console.log(food2.constructor); // 返回 [Function: Object]

这里实例的constructor就被改变了,所以一般我们可以看到重写原型的时候(原型链被切断,会默认指向默认的原型对象),会手动加入一个 constructor 属性来指定它的值,以方便识别。

function Food() {}
console.log(Food.constructor); // 返回[Function: Function]
// 我们需要主动标记 constructor属性
Food.prototype = {
  constructor: Food,
  name: "苹果"
};
var food2 = new Food("苹果");
console.log(food2.constructor); // 返回 [Function: Food]

constructor其实没有什么用处,他是JavaScript语言设计的历史遗留物。由于constructor属性是可以变更的,所以未必真的指向对象的构造函数,只是一个提示。不过,从编程习惯上,我们应该尽量让对象的constructor指向其构造函数,以维持这个惯例。--by 贺师俊

② 再来说prototype

在 javascript 里面,只要是函数,都会有这样一个属性prototype,这个属性指向函数的原型对象,在默认情况下,所有原型对象都会自动获得一个 constructor 属性,指向prototype 所在的函数。

function Food() {}
console.log(Food.prototype) // 返回Food {}

var food1 = new Food("苹果");

console.log(food1.prototype) // 返回 undefined
  • 构造函数 Food 的prototype属性是一个内存指针,最终是指向 Food 的原型对象Food Prototype 的,这里需要注意我的写法,是Food Prototype ,这里虽然很相似,但其实不是。
  • 实例对象上的 prototype 属性没办法直接查看,所以返回 undefined,需要用别的方法来查看。

③ 最后说说[[prototype]]__proto__

这是存在于实例身上的 prototype 属性,但是没办法直接查看,只能通过某些方式来获取和判断。不同的浏览器有不同的叫法,的属性名字也可能是[[prototype]] 或者_proto_

// Object.getPrototypeOf会返回原型对象,但看起来跟普通构造函数没区别
console.log(Object.getPrototypeOf(food1)); // 返回 Food {}

// 所以一般会使用这个方式来判断原型对象是否一致
console.log(Object.getPrototypeOf(food1) === Food.prototype); // 返回 true
// 或者这个方式,isPrototypeOf会直接判断
console.log(Food.prototype.isPrototypeOf(food1)) // 返回 true

1.3 为什么能够通过原型模式来解决问题呢?

原型的作用主流程:

  1. 在 javascript 里面,创建一个新函数(对象),都会在创建过程里面增加一个prototype属性,也就是原型属性,这个属性指向构造函数的原型对象,例如food1prototype属性指向Food的原型对象Food prototype
  2. 而这个被指向的原型对象里面也会自动获得一个constuctor构造函数属性,这个属性里面包含了一个指向,指向之前被创建的对象的prototype属性的所在位置,相当于原型对象是母体,被创建的对象会关联到母体身上,
  3. javascript 解析器读取到对象之后,会执行一次搜索,如果在当前对象上没有搜索到目标属性的话,就会继续搜索指针指向的原型对象,会不断逐级查找(原型对象),直至找到为止。

参考前面所说的原型,constructor 和 prototype 的内容来理解

在《javascript 高级程序设计》第三版里面的有一幅图:


image
  • person1 和 person2 都是实例, Person 是构造函数,Person Prototype 是 Person 构造函数的原型对象。
  • person1或者 person2 的[[Prototype]]都指向了Person Prototype

问什么这里会有一条线指向了构造函数?

image

因为Person 构造函数的prototype 指向了Person Prototype,而Person Prototypeconstructor 也指向了Person 构造函数,他们之间通过这样来互相确认“关联状态”,但仅仅互相确认关系而已(因为 constructor 容易被改变)。

类比到我们的 Food 例子里面去,food1和 FoodFood Prototype的关系就跟 person1和 person2和PersonPerson Prototype 的关系是一样的。

二、 对于原型的一些使用技巧

① 如果需要查找这个实例对象的原型的话,可以使用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.prototypeconstructor就不再指向 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); // 返回 苹果

在 new 创建完实例之后,实例的原型对象是构造函数的原型对象,如果在这时候重写了构造函数的原型对象的话,那么原来实例跟原来构造函数的原型对象的链接就会被切断,就无法使用原型对象上的数据了。

三、文末我们又遇到了新问题了

用了原型模式之后,虽然解决了遇到的一系列问题,但也带来了一些新的副作用(怎么副作用那么多。。。。。),原型模式的共享特性带来了方便之余,也造成了一些困扰,如果我们需要一些不想共享的信息,例如 food1 的原产地是巴西,印度,非洲,food2的原产地是巴西,印度,俄罗斯,他们之间有一些区别,不能完全共享,那么怎么办呢?

会通过组合使用构造函数模式和原型模式或者动态原型模式来解决,下回分解。

参考内容

  1. 红宝书,《javascript 高级程序设计第三版》

版权信息

作者: 怂如鼠
网站: https://www.whynotbetter.com
本作品著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容