不积跬步之我们来聊一下原型和原型链模式

首先我们要明确一个概念,JavaScript是一门面向对象的语言。既然是面向对象的语言,那么面向对象的一些基本的属性就会有,比如继承。在谈到继承的时候,我们总会想到javaextends,在没有ES6的年代里,我们应该怎么去实现继承呢?我总是把java的继承的做法来套用到这里。后来想明白了一件事儿,那就是 思想是相同的。而具体的实现做法却不尽相同,后来碰到所有差异的部分,我都会用这个思想来指导自己。去记住那些思想,实现反而是次要。比如设计模式,比如算法,比如排序,换一门语言同样。而原型和原型链模式就是为了实现继承而生的。但是原型模式并不是继承,它们有很大的不同。要说原型,我们就要从JavaScript的对象开始。那我们开始吧。

我错了,思想也不相同

创建对象

我们先从创建对象开始吧,创建对象的简单方式就是创建一个Object的实例,然后为它添加属性和方法。

var person = new Object();
person.name = "张三";
person.age = 21;
person.job = "程序员";
person.sayName = function(){
    alert(this.name)
}

上面我们创建了一个对象的实例,然后添加了三个属性和一个方法。后来我们把创建对象改为了更加简单的对象字面量的方式:

var person = {
    name:"张三",
    age:21,
    job:"程序员",
    sayName:function(){
        alert(this.name)
    }
}

上面创建的对象虽然简单但是不够服用,如果我要造很多类似的对象呢?那就需要用到工厂模式了!

工厂模式来造对象

工厂模式是常见设计模式之一,它把构造的细节做了抽象,让我们直接调用就好,无序关注细节。

function createPerson(name,age,job){
    var person = new Object();
    person.name = name;
    person.age = age;
    person.job = job;
    person.sayName = function(){
        alert(this.name)
    }
    return person
}

工厂模式虽然解决了创建对象的问题,但是它却没有办法识别一个对象的类型。怎么办呢?那就是使用构造函数模式。

是构造函数模式

废话不多说,上代码:

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        alert(this.name)
    }
}
var person1 = new Person("张三",21,"程序员");
var person2 = new Person("李四",21,"程序员");

构造函数模式和普通的函数其实没有什么什么不同。只是为了区分是构造函数模式,把函数的首字母大写。
同时它还和之前的工厂模式有一下的不同:

  • 没有显示地创建对象
  • 直接将属性和方法付给了this对象
  • 没有return 语句

当我们需要创建对象的时候使用new操作符来创建一个。(程序员,没有对象怎么办,自己new一个呗。)它的具体步骤如下:

  1. 创建一个新对象
    2.这个新对象内部的[[Prototype]]特性被赋值为构造函数prototype属性
  2. 将构造函数的作用域付给新对象(因此this就指向了这个新对象)
  3. 执行构造函数中的代码(为这个对象添加属性)
  4. 返回新对象

在上面的例子里,两个对象都有一个constructor(构造函数)属性,该属性指向了Person.这个属性一开始是用来标识对象类型的。但是,它并不靠谱,为啥不靠谱?后面聊,但是还是instanceof更加靠谱一点:

alert(person1.constructor === Person);//true
alert(person2.constructor === Person);//true 

alert(person1 instanceof Person)//true
alert(person1 instanceof Object)//true

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数胜过工厂模式的地方。

将构造函数当作函数

大家不要以为构造函数和函数是两个物种,其实他们一样,只是调用的方式不同而已。构造函数也是函数,并没有定义特殊的构造函数语法。

任何函数通过new操作符来调用,那么它就是构造函数。

任何函数是不通过new来调用,都是普通函数。

//当作构造函数使用
var person = new Person("张三",21,"程序员");
person.sayName(); //张三

//当作普通函数调用
 Person("张三",21,"程序员"); //添加到window中
 window.sayName();//张三
 
 //在另一个对象的作用域中调用
 var o = new Ojbect();
 Person.call(o,"张三",21,"程序员");
 o.sayName();//张三

上面例子我们谈一下下面的两点:
当作普通函数时候时,函数中的this指向window,所以属性和函数都添加到了全局对象中
通过call()或者apply()两个方法在特殊的对象的作用域中调用的Person函数,则特殊对象就拥有了这些属性和方法。

但是构造函数也有问题:

构造函数的问题

构造函数的问题其实非常明显,就是每个方法都要在每一个对象重实例上重新创建一遍。在前面的例子中就是这样。person1person2在创建的时候,sayName会重新创建一遍。为啥?因为JavaScript中的函数其实是一个对象。

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function("alert(this.name)");//与声明函数在逻辑上是等价的
}

不同的实例包含了不同的函数实例。这有啥问题,这样会导致不同的作用域链和标识符解析。也就是不同实例上的同名函数是不相等的。

alert(person1.sayName === person2.sayName) //false

那我们可以通过引用外部的函数来实现呀。

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}
function sayName(){
    alert(this.name)
}

这样确实是解决了上面的问题,但是这个函数却放在了全局里,那你的方法还封装不封装了,全都暴露在外面了,那,这个时候就到了原型模式出场了.

原型模式

说一下概念,这个概念就已经说清楚了什么是原型模式:

我们创建的每个函数都有一个prototype(原型属性),这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

那么按照字面意思来理解,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象。

使用原型对象的好处是可以让所有对象实例共享它包含的属性和方法。这样我们不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。

function Person() {
}
Person.prototype.name = "张三";
Person.prototype.age = 29;
Person.prototype.job = "程序员";
Person.prototype.sayName = function () {
   console.log(this.name);
};
let person1 = new Person();
person1.sayName(); //张三

1.理解原型对象

对照我们一开始给出的概念,

只要创建了一个新的函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性.这个属性是一个指向prototype属性所在函数的指针。就比如前面的例子

Person.prototype.constructor ===  Person

通过这个这个构造函数,我们还可以继续为原型对象添加其他的属性和方法。

浏览器的protytype.png

我们看上图,我们创建了一个普通函数,可以看到在初始化的时候,这个函数就已经有了prototype这个属性,这个属性指向函数的原型对象,而原型对象有一个constructor属性,这个属性指向了prototype所在函数的指针,也就是在哪个函数上,可以看到上面是指向f函数。其他的那些属性都是继承于Object

对象的__proto__.png

而使用构造函数new出来的实例则有一个属性可以查看原型对象。官网说法是[[prototype]],而我们可以再浏览器里通过__proto__这个属性来查看,如上图。

QQ图片20190921212456.png

具体的原理可以通过上图来查看,Person的原型对象prototype指向原型对象,原型对象的constructor又指向了Person。实例的[[Prototype]]则指向原型对象。可以发现这个[[Prototype]]标识的是实例和原型对象之间的关系,而不是实例和构造函数之间的关系。

剩下的一些api这里简略的说一下:

  • hasOwnProperty来检查一个属性是存在于实例中还是存在于原型中,只有属性存在于实例中时,才会返回true.
  • in操作符会在通过对象能顾访问给定属性时返回true,无论该属性存在于实例中还是原型中。
  • Object.getOwnPropertyNames,通过该方法可以得到所有实例属性,无论是否都可以枚举。

2.更简单的原型方法

每一次我们添加属性和方法时都要敲一遍Person.prototype。可以这样操作:

Person.prototype = {
    name:"张三",
    age:21,
    job:"程序员",
    sayName:function(){
        alert(this.name)
    }
};

我们把Person.prototype指向了一个新的对象,那么,这个新的对象的constructor就不会在指向Person,我们都知道,当我们创建一个函数时,这个函数就会同时初始化它的Prototype,这个对象会自动获得constructor属性,这个属性指向Person。现在我们的做法,就相当于完全重写了prototype属性。也就造成它的constructor不在指向Person。因为它是新的对象啊。此时,尽管instanceof操作符还能返回正确的结果,但是通过constructor已经无法确定对象的类型了。这就是为啥constructor不靠谱的原因。

但是这样会切断原型链之间的关系。

var friend = new Person();

console.log(friend instanceof Object); //true
console.log(friend instanceof Person);//true
console.log(friend.constructor == Person); //false
console.log(friend.constructor == Object);//true

如果constructor的值很重要,可以这样玩。

Person.prototype = {
    constructor:Person,
    name:"张三",
    age:21,
    job:"程序员",
    sayName:function(){
        alert(this.name)
    }
};

可是这样操作的话会造成[[Enumerable]]设置为true。造成可枚举,所以我们可以这样:

function Person(){
    
}
Person.prototype = {
    constructor:Person,
    name:"张三",
    age:21,
    job:"程序员",
    sayName:function(){
        alert(this.name)
    }
};
Object.defineProperty(Person.prototype,"constructor",{
    enumerable:false,
    value:Person
})

3.原型的动态性

知道为啥实例的[[Prototype]]是指向原型对象,而不是构造函数吗?
因为构造函数是死的,而原型对象时活的,对象会改变,而构造函数却不会去改变。

比如我们创建了一个实例,然后修改了它的原型对象,立马会反映出来。

const friend = new Person();

Person.prototype.sayHi = function(){
    alert("Hi");
}
friend.sayHi()//Hi

没有毛病。当我们创建了一个实例,然后接着修改它的原型对象,为其添加新的方法sayHi。当我们调用这个sayHi时,首先会从自己作用域里找,然后发现没有找到,接着就向上找,去它的原型对象里找,然后找到了,进行调用。

实例和原型对象之间是指针的关系,可是当我们把他们之间的关系切断时,也就找不到了。

function Person(){
    
}
const friend = new Person();

Person.prototype = {
    constructor:Person,
    name:"张三",
    age:21,
    job:"程序员",
    sayName:function(){
        alert(this.name)
    }
};

friend.sayName(); //error

在这个例子里,我们先创建了一个Person的实例,然后又重写了其原型对象,然后在调用friend.sayName时发生了错误,因为friend指向的原型中不包含以该名字命名的属性。

QQ图片20190922082417.png

我们可以从上图看出,重写原型对象之后,实例中保存原型的指针被打断了,指向了一个新的。从而造成它找不到sayName这个方法。

原型我们就聊这么多,想要了解更多大家可以翻JavaScrit高级程序设计之面向对象的程序设计这一章,下面我们说一下继承。

原型链

继承是面向对象语言一个非常重要的概念,许多OO语言都支持两种继承方式:接口继承和实现继承,接口继承只继承方法前面,而实现继承则继承实际的方法。而我们的JavaScript由于函数没有签名,无法实现接口继承,所以只支持实现继承。而且继承主要是通过原型链来实现的。

实际上原型链并没有和我们前面的原型脱节。它的基本思想是:利用原型让一个引用类型继承另一个引用类型的属性和方法。

简单的回顾一下构造函数,原型和实例的关系:每一个构造函数都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针(constructor),而实例都包含一个指向原型对象的内部指针([[prototype]])。那么,假如,让我们的原型对象指向另一个类型的实例,结果会是什么呢?此时的原型对象将包含一个指向另一个原型对象的指针[[prototype]],相应的,另一个原型中也包含着一个指向两一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念

来,看个代码:

//现有父类
function SuperType(){
   this.property = true;
}
//给父类添加原型方法
SuperType.prototype.getSuperValue = function () {
   return this.property;
}
//创建子类
function SubType() {
   this.subproperty = false;
}
//实现继承
SubType.prototype = new SuperType();
//给原型对象添加方法
SubType.prototype.getSubValue = function () {
   return this.subproperty;
}

const instance = new SubType();

console.log(instance.getSuperValue()) //true
console.log(instance.getSubValue())   //false

上面的代码定义两个类:SuperTypesubType.他们实现了继承的关系,而继承是通过把SubType的原型对象完全重写为新的SuperType的实例,这样存在于SuperType的原型对象中的方法同样也存在于它的实例中,也就是存在于SubType.prototype中,在建立了继承关系之后,我们为SubType.prototype添加了新的方法getSubValue

QQ图片20190922183804.png

通过上面的图我们可以直观的看到原型链的实现。
我们从下往上看,instance指向的原型对象和构造函数的原型当然是一个。由于我们没有使用自带的,而是把它完全重写,所以这个原型对象SubType[[prototype]]同样也指向了它自己的原型对象也就是SuperType
我们在调用instance.getSuperValue()的时候,依据从近到远的法则,层层递进的向上找,自己没有去原型里找,然后原型里也没有,那么就去原型的原型,一直找到了SuperType中的getSuperValue和它对应的值。

我们中间给SubType.prototype添加了一个方法记得不,因为这个时候我们已经把它指向了SuperType的实例,所以这个方法添加到了这个实例对象上了。

还记得默认原型不?

还记得默认原型不,当我们建立函数时都会有一个默认原型对象,这个原型对象就是Object的实例,当我们把SubType的原型对象完全重写为另一个对象时,原来的默认实例就已经被切断,但是我们新继承的实例的默认实例是Object的实例,或者是它的上一个是,这样的话,我们原型链的顶点就永远是Object的实例。而那些toString,valueOf等方法,你永远都能找到,下图可以直观的看出来。

QQ图片20190922185325.png

确定原型和实例的关系

有两种方法可以判断:instanceofisPrototypeOf

instance instanceof Object;//true
instance instanceof SuperType;//true
instance instanceof SubType;
Object.prototype.isPrototypeOf(instance); //true
SuperType.prototype.isPrototypeOf(instance); //true
SubType.prototype.isPrototypeOf(instance); //true

原型和原型链模式我们就到这里吧...

over...

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

推荐阅读更多精彩内容