JavaScript深入系列之原型与原型链

前言

每次遇到新的概念,我们可能会想到三个问题,它是什么呢(what)?为什么会出现呢(why)?又能解决什么呢(how)?这次我们来看看原型与原型链这个概念,要解决这个3W(what, why, how),首先得从js的面向对象说起...

此文需要准备好电脑,大脑,一杯咖啡

面向对象

什么是面向对象?应该不需要我多说吧,任何编程语言都有面向对象,然后它们都有类(class) 的概念,但在ECMAScript(以下用es表示)中却没有,除了es6之外(其实上是语法糖,这样是为了更像面向对象编程的语法而已),但不代表因为es6有了类,我们就可以把原型与原型链的概念一笔带过,因为es6一般是在es5的基础上优化来的,而且现在面试对js的基础扎实要求比较高,上次被问到过有几种继承方法,回答没完全正确...🙈

那么问题来了,既然没有类的概念,那es又是如何实现的呢?起初是用工厂模式实现,这个又是什么玩意呢?继续看下面👇

工厂模式

正如字面的意思,就拿制造娃娃(Doll)的🌰来说吧,工厂要制造一个娃娃的过程:首先制造娃娃的对象(new Object()),然后赋给娃娃的属性,比如颜色,多高,多重,最后就制造出来,代码如下:

function createDoll(color,height,weight){ //制造工厂模式的函数
  let o = new Object();//创建一个娃娃的对象
  o.color = color;//颜色
  o.height = height;//多高
  o.weight = weight;//多重
  o.sayColor = function(){//报告颜色
    alert(o.color);
  }
  return o;
}
//为了显示是工厂模式,变量名后面加F(工厂:Factory)
let dollF1 = createDoll('orange',20,10);//要制造一个娃娃,就调用上面的函数
let dollF2 = createDoll('red',30,10);//同上

把一个对象的所有属性和方法(所谓本身的信息)封装在一个工厂模式的函数,这样就能制造多个对象

  • 好处:

    减少大量重复的代码

  • 缺点:

    无法识别对象的类型问题,换句话说,就是无法判断一个对象来自于哪个函数呢?这个稍后再说,正是因为这个缺点,所以才会出现构造函数模式~

构造函数模式

什么是构造函数呢?写法其实和普通函数差不多的,但只要被new调用就称为构造函数,代码如下:

function Doll(color,height,weight){//本来是普通函数
  this.color = color;
  this.height = height;
  this.weight = weight;
  this.sayColor = function(){
    alert(this.color);
  }
}
//为了表示是构造函数模式,变量名后面加C
let dollC1 = new Doll('orange',20,10);//用new调用了,上面的函数就称为构造函数
let dollC2 = new Doll('red',30,10);

为了更好的识别构造函数,函数名应该以一个大写字母开头,比如上面的大写字母D,然后与工厂模式相比,去掉创建对象( let o = new Object();),直接把属性和方法赋给this对象,去掉return返回语句。

这个有啥优点,之前提到过工厂模式无法识别对象的类型问题,我们先看看调用构造函数和调用工厂模式的打印结果

我们不光要看文章,还要动手打code,这样比较容易理解~

调用工厂模式的结果如下:

调用工厂函数的结果

调用构造函数的结果如下:

调用构造函数的结果

我们可以发现,调用构造函数比工厂模式多了一个constructor属性,这个属性指向Doll,为了证明这两个是相等,我们可以打印出看看:

dollC1.constructor === Doll; //true
//然后我们再来看看工厂模式的又是如何呢
dollF1.constructor === createDoll; //false,因为constructor属性找不到creteDoll,所以无法判断来自于createDoll函数,这就是工厂模式的缺点

为什么非要判断对象类型,别急,这个要说的原因很长,要一个一个解释,
就好比说吃美食,要慢慢咬嚼,才能知道美食是什么味道,相反吃的太快,就说不出味道的,也就没办法向别人(面试官或者写代码)解释这个本质~

这个constructor属性可以用来标识对象类型,但小红书推荐用instanceof来判断,这个是表示可以是Object的实例,也可以是构造函数的实例,看看上面调用工厂模式和构造函数的图片,工厂模式中的dollF1__proto__下面有一个constructor,指向Object;而构造函数中的dollC1__proto__下面有两个constructor,分别指向Object和Doll,然后我们运用instanceof来判断:

//构造函数模式
dollC1 instanceof Doll; //true
dollC1 instanceof Object; //true
//工厂模式
dollF1 instanceof createDoll; //false
dollF1 instanceof Object; //false

所以这就是工厂模式和构造函数模式的优劣比,至于为什么会有指向Object,凡是创建的实例对象都有Object构造函数,至于详细,可能比较长,有个知乎大佬写的比较有意思,推荐看看JavaScript世界万物诞生记

然而不要太天真了,任何事物绝对没有完美的,构造函数也是如此的,不然我就不会讲原型与原型链,那么构造函数有什么缺点呢?我们先看构造函数中的sayColor方法,这个方法在多个实例对象上应该是一样,然而看下面👇的代码:

dollC1.sayColor === dollC2.sayColor; //false

结果打印出false,哈?为什么呢?调用构造函数后,会生成不同作用域的多个实例,相当于开辟多个实例的内存,拥有自己的属性可以理解的,但拥有自己的方法这就没必要,就好比说两个人要去旅行,需要准备东西,由于卫生问题,带上自己的毛巾(属性)可以理解的,然后你和朋友都要带上沐浴液,带的越多,提的就越重,换个角度想,如果不带自己的沐浴液,就借朋友的沐浴液来用,是不是可以减少自己的容量呢?在构造函数也是一样的道理,带自己的属性和方法越多,开辟的内存就越大,所以出现原型模式这个概念(终于到这一步了🤪)

原型模式(也可以叫原型)

什么是原型模式呢?原型模式也可以这么叫共享模式(个人理解哈),这几年不是很流行共享嘛?共享单车,共享充电宝,共享汽车等等,一个单车可以被所有人共用,相当于一个原型模式可以被所有的实例共用,区别在于单车要花钱哈~

之前讲到的构造函数的属性和方法,这次在原型模式中,把属性和方法移到构造函数的原型对象,用prototype实现:

function Doll(){};
Doll.prototype.color='orange';
Doll.prototype.height=12;
Doll.prototype.weight=6;
Doll.prototype.sayColor=function(){
    console.log(this.color);
}
//为了表示是原型模式,变量名后面加P
let dollP1 = new Doll();
dollP1.sayColor();  //orange;
let dollP2 = new Doll();

这就是原型模式,之前说的构造函数有个缺点的,多个实例对象的方法不一致,这次我们来看看多个实例对象的方法在原型模式又是如何呢?

dollP1.sayColor === dollP2.sayColor; //true

一样的耶,就能省下好多内存的,关系图如下:

原型模式

然后我们再看看dollP1的打印结果,对比上面的关系图:

dollP1

dollP1.proto指向Doll的原型对象(Doll.prototype),如果要读取color的值,就会沿着这个方向去原型对象里面找,找到color,就返回color的值,找不到的话,则继续往上Object的原型对象,还是找不到的话,则会返回"undefined",下面我们试试dollP1的color和nama属性:

dollP1.color; //orange
dollP1.name; //undefined

然而如果我要给dollP1这个实例增加自己的属性,比如color,那直接在dollP1上增加这个属性就好了,代码如下:

dollP1.color='red';
dollP1.color; //red

关系图如下:

实例对象增加一个属性

实例上多了一个属性,这样读取color的值,首先会在自己的实例中搜索,找到了,就返回‘red’,就不会再往上原型对象里搜索。

注意:在实例对象上即使没赋值,比如undefined,也仍然会返回undefined,也就是说实例对象只要有该属性,不管有没有赋值还是null,也会直接返回该属性的值

看起来很完美的,先别这么想的,我们再来试试引用类型,比如数组,在原型对象上加一个数组arr的属性:

Doll.prototype.arr=[1,2,3];
//然后在实例对象dollP1修改原型对象的数组,会发生什么呢?
dollP1.arr.push(4); //4,返回数组的长度
Doll.prototype.arr; //[1, 2, 3, 4],唔,原型对象的arr也发生变化
dollP2.arr; //[1, 2, 3, 4]
一定要动手打code,不然就没办法体会到呢

就因为实例对象修改原型对象的数组属性,结果实例对象,原型对象都被改变了,这样原型对象就失去自己的原则,想看看如果多个实例对象调用一个原型对象,如果修改原型对象的数组属性,多个实例对象就都拥有同样的值,那创建多个实例对象有什么意义呢?所以这也就是原型模式的缺点,所以出现组合模式~

组合模式

组合模式又是什么呢?就是利用构造函数模式和原型模式的各自特点组合成一个模式,构造函数模式特点是能创建独立的实例,而原型模式的特点是能被共享,所以就把属性放进构造函数模式,而方法则放进原型模式,代码如下:

function Doll(color,height,weight){
    this.color = color;
    this.height = height;
    this.weight = weight;
    this.arr=[1,2,3];
};
Doll.prototype.sayColor=function(){
    console.log(this.color);
}
let doll1 = new Doll('orange',14,6);
dollP1.sayColor();  //orange;
let doll2 = new Doll('red',12,4);

doll1.arr.push(4); //4
doll1.arr; //[1,2,3,4]
doll2.arr; //[1,2,3];
doll1.sayColor === coll2.sayColor; //true

这样每个实例就拥有自己属性的副本,还能共享原型对象的方法,就能省下很多内存的。

这下我们解决了原型的3W问题,然后面向对象还有一个特点的,那就是继承,在es中又是通过什么方式解决继承的问题?答案就是原型链,看下面👇

原型链

什么是原型链呢?我们可以把这个概念拆分成“原型”+“链”,原型,就是之前讲到过的原型,而链呢,就是在原型模式上产生指向的链条,之前我们讲了三个构造函数,原型对象,实例对象,对吧?假如再创建另一个构造函数,然后让这个新的构造函数的原型对象等于原来构造函数的实例化对象?怎么理解的?比如说创建一个娃娃(目前只能制造人偶娃娃,Doll默认为人偶娃娃),现在有一个新的需求,要制造动物娃娃,动物娃娃和人偶娃娃差不多,可以直接借助Doll构造函数来实例化,说白就是继承Doll的属性和方法,不需要自己再创建同样的属性,代码如下:

function Doll(color,height,weight){ //默认人偶娃娃
    this.color = color;
    this.height = height;
    this.weight = weight;
};
Doll.prototype.sayColor=function(){
    console.log(this.color);
}

//制造动物娃娃的构造函数
function AnimateDoll(name){
    this.name = name;
}
//借助Doll构造函数来实例化AnimateDoll的原型对象
AnimateDoll.prototype = new Doll();
AnimateDoll.prototype.sayName=function(){
    return this.name;
}

let animate1=new AnimateDoll('pig');
animate1.name; //pig
animate1.sayName(); //pig
animate1.color; //undefined, 因为没传值,这个稍后再说呢~
animate1.sayColor(); //同上

假如要制造新的抱枕娃娃,这个抱枕娃娃也可以是动物,也可以是人偶,那么就借用动物娃娃的构造函数实例化抱枕娃娃的原型对象,这样就能继承动物娃娃和人偶娃娃的所有属性和方法,这三个之间的关系就成为原型链,当然不只有三个~

之前说到animate.color结果打印出undefined,因为没有传值的,这就需要利用call继承Doll的属性,然后把参数赋值给对应的属性,代码如下:

function Doll(color,height,weight){
    this.color = color;
    this.height = height;
    this.weight = weight;
};
Doll.prototype.sayColor=function(){
    console.log(this.color);
}

function AnimateDoll(name,...args){
    Doll.call(this, ...args); //新增一行
    this.name=name;
}

AnimateDoll.prototype = new Doll();
AnimateDoll.prototype.sayName=function(){
    return this.name;
}


let animate1=new AnimateDoll('pig','pink',20,10);
animate1.name; //pig
animate1.sayName(); //pig
animate1.color; //pink
animate1.sayColor(); //pink
animate1.height; //20
animate1.weight; //10

继承方法不只有这个原型链,还有其他的,有兴趣,可以自己去看看小红书或者google~

之前提到过一个问题“为什么非要判断对象类型”,假如没有对象类型,我们就没办法判断一个对象是指向哪个,也就无法实现继承的方法。

到此为止吼~觉得不错,求个赞~觉得有不足的,求个建议~

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