手记

谈一谈我所了解的原型-原型链

说起原型和原型链接,着实让我这个前端菜鸟胡搞了好一阵子。虽然有点绕口的缘故,但是更多的还是自己比较浮躁带来的后果,这一块据说是前端的基础,看了很多遍才差不多有点头目。分享一下我领悟到的武林秘籍,希望能给您带来一点启迪,如果存在任何问题,请及时指正我,谢谢。

一、 浅谈数据属性和访问器属性

1. 创建对象:

通常创建对象一般都会有两种方法:

//利用object来创建对象
var person = new Person();
person.name = "klivitam";
person.age  = 23;

person.sayName = function(){
  alert(this.name)
}
// 对象字面量法,推荐使用这种方法
var person = {
  name:"klivitam",
  age:23,

  sayName: function(){
    alert(this.name);
  }
}

2. 属性类型

在javascript中,对象的属性一共分为两种:数据属性和访问器属性。

  • 数据属性
    configurable:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或能否把属性修改为访问器属性,默认为true
    enumerable:表示能否通过for-in循环返回属性
    writable:表示能否修改属性的值
    value:包含该属性的数据值。默认为undefined
    数据属性包含一个数据值的位置,在这个位置可以读取和写入值。以上就是描述数据值行为的四个特性。
    okey,可能全凭着口述概念无法了解这个意思,现在就实操代码吧。
    // "use strict"
    var worker = {}
    Object.defineProperty(worker, "job", {
    writable: false,
    value: "码农"
    })
    console.log(worker.job)
    worker.job = "教师"
    console.log(worker.job)

当把writable的属性改成true的时候,
从上面的代码可以看出来writable是用来控制是否能修改属性的值。另外当writable为false的时候,并且使用严格模式下,会发生:
// "use strict"
var worker = {}
Object.defineProperty(worker, "job", {
    // writable: false,
    configurable:false,
    value: "码农"
})
console.log(worker.job)
delete(worker.job)
console.log(worker.job)
当configurable为false的时候,使用delete方法会失效,并且在严格模式下,delete会报错。同理改成true的时候,则为undefined,说明删除成功了。
注意:当value的值没初始化的时候,默认放置undefined

至于最后一个我觉得就没必要代码进行演示了,同理可得。

  • 访问器属性
    configurable:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或能否把属性修改为访问器属性,默认为false
    enumerable:表示能否通过for-in循环返回属性,默认为false
    Get:在读取属性时调用的函数,默认值为undefined
    Set:在写入属性时调用的函数,默认值为undefined
    访问器属性不包含数据值,包含的是一对get和set方法,在读写访问器属性时,就是通过这两个方法来进行操作处理的。并且访问器属性不能直接定义,要通过Object.defineProperty()这个方法来定义。
    直接上代码吧:
    var worker = {
    _job:"码农",
    age: 23
    }
    Object.defineProperty(worker,"job",{
    get:function(){
        return this._job;
    },
    set:function(newJob){
        if(newJob!==this._job){
            this._job = newJob;
            this.age ++
        }
    }
    })
    console.log(Object.getOwnPropertyDescriptor(worker,"job"));
    console.log(worker.job)
    worker.job = "教师"
    console.log(worker.job)
    console.log("更换职业就变老一年,5555~:"+worker.age)

二、 js设计模式

1、 工厂模式

工厂模式是一个很基础的一个模式吧,反正我在学java (android)的时候经常会遇到这种模式,主要是抽象了创建对象的具体过程。具体的代码如下:

//屌丝程序员,只能偶尔意淫一哈 = l =,别喷我 
function addBeatiGrilWx(name,age,job){
    var gril = new Object();
    gril.name = name;
    gril.age = age;
    gril.job = job;
    gril.sayHi = function(){
        console.log("hi! " + this.name)
    };
    return gril
}
var lyf = addBeatiGrilWx("liuyifei",18,"actor");
lyf.sayHi();
console.log(lyf);

效果如下工厂模式虽然解决了创建多个相似对象的问题,但是却没有解决对象识别的问题(即不清楚一个对象的类型),于是出现了构造函数模式。

2、 构造函数模式

构造函数可以用来创造特定类型的对象。具体的代码如下

function BeautiGril(name,age,photo){
    this.name = name;
    this.age = age;
    this.photo = photo;
    this.sayHi = function(){
        console.log("hi! "+this.name)
    }
}
var lyf = new BeautiGril("liuyifei",18,"baidu");
lyf.sayHi();
console.log(lyf)
console.log(lyf.constructor == BeautiGril)
console.log(lyf instanceof BeautiGril)
console.log(lyf instanceof Object)

具体的效果如下:创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,这也是前面提到他相较于工厂模式的优势。
构造函数模式看似很好,但是也存在一个问题,就拿上面的代码来说如果我创建多个实例,不止lyf(毕竟还是想多要几个小姐姐的 咳咳)一个。而此时sayHi这个公共的方法就会被多次重复创建。这样其实不太可取的,如果把sayHi方法放置出去,

function BeautiGril(name,age,photo){
    this.name = name;
    this.age = age;
    this.photo = photo;

}
function sayHi(){
    console.log("hi! "+this.name)
}

那么相当新建了一个全局方法,这样岂不是更加的没有必要了么?此时就要引入到原型模式了。

3. 原型模式

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

function Worker(){}
Worker.prototype.name = "programmer";
Worker.prototype.work = "programming...";
Worker.prototype.heartSound = function(){
    console.log(this.name+" want rest,but he still "+this.work)
}

var xiaoZhang = new Worker();
xiaoZhang.heartSound();

var xiaoWang = new Worker();
xiaoWang.heartSound();

console.log(xiaoZhang.heartSound ==xiaoWang.heartSound)

这里我还是说一下,我将heartSound()方法和所有的属性直接添加到了Woker的原型属性中,然后通过new创建对象,在原型模式中这些属性和方法对于所有的实例是共享的。
但是这里存在有点问题--那就是并不是所有的worker都是程序员。这就引发了最后一种模式的混用。

3. 原型模式+构造器模式

这个模式在我的理解上来说,主要是为了避免单独用原型模式所带来弊病,就拿上一份代码来说,并不是所有的worker都是程序员,如果想一个医生想去复用这个类的时候,就必须改变其原型上的值,如果改变其原型的值,那么整个都会乱套了。于是我想到构造器模式

function Worker(name,work){
    this.name = name;
    this.work = work;
}
Worker.prototype.heartSound = function(){
    console.log(this.name+" want rest,but he still "+this.work)
}

var xiaozhang = new Worker("programmer","programmer....");
xiaozhang.heartSound(); // programmer want rest,but he still programmer....

//医生也需要休息
var xiaomei = new Worker("doc","sos");
xiaomei.heartSound(); // doc want rest,but he still sos
三、 原型链

前面也差不多谈到了原型这个概念,什么叫原型呢?其实我有一个不太好,但是又很恰当的例子来描述这些个概念(看嗯哼家小狗、小猫想到的):

  • 小狗是小狗妈妈生的、小猫是小猫妈妈生的。小狗和小猫被称为对象的实例,狗妈妈、猫妈妈被称为对象的原型
    • 狗妈妈和狗爸爸能通过交配生出一大堆小狗出来,其中交配就被称为构造函数
    • 狗妈妈有很多狗宝宝,但是狗宝宝却只有一个狗妈妈,这可以被称之为原型的唯一性
    • 我们可以通过狗宝宝找到狗妈妈,狗妈妈也可以找到狗外婆,以此类推 这就是相当于原型链
    • 大家都知道狗有很多品种,很多品种里面也有发育好的,发育差的、胖的瘦的...例如胖的泰迪也是泰迪=>泰迪也是狗=>狗=>哺乳动物=>动物=>生物。总之一切的一切都有一个起点,这条链的终点将会被指向同一处,这就好比原型链最终指向null
    • 小泰迪生下来之后,它的样貌会跟泰迪妈妈大同小异,这就类比于原型的继承。
    • 小泰迪的主人领养小泰迪之后将其打扮成另外的模样,这就类比于对象属性可以覆盖原型属性。但是小泰迪的模样并不会改变小泰迪弟弟的模样,这就类比于对象属性的改变不会影响原型的改变。

其实有了上面的一个基本的了解之后,我们再来一步一步写代码就会比较容易了。

{
    function Dog(name){
        this.name = name
    }
    Dog.prototype.action = function(){
        console.log(this.name+" wang..");
    }

    let xiaogou1 = new Dog("xiaogou1");
    xiaogou1.action(); // xiaogou1 wang..

    let xiaogou2 = new Dog("xiaogou2")
    xiaogou2.action(); // xiaogou2 wang..

    let xiaogou3 = new Dog("xiaogou3");
    xiaogou3.action(); // xiaogou3 wang..

}

如上面所示 xiaogou1、xiaogou2、xiaogou3被称为对象实例而Dog被称为这群小狗的原型。可以通过构造方法来创建出1,2,3三只小狗。

{
    function Cat(name){
        this.name = name;
    }
    Cat.prototype.action=function(){
        console.log(this.name+" miao!!!")
    }

    function Dog(name){
        this.name = name
    }
    Dog.prototype.action = function(){
        console.log(this.name+" wang..");
    }
    let xiaogou = new Dog("xiaogou");
    xiaogou.action(); // xiaogou wang..

    let xiaomao = new Cat("xiaomao");
    xiaomao.action(); // xiaomao miao!!!
}

上面的代码中可以瞧出来:小狗能继承小狗原型上面的action方法去“wang...”,小猫会继承小猫的原型方法“miao!!!”。

{
    function Dog(name){
        this.name = name
    }
    Dog.prototype.action = function(){
        console.log(this.name+" wang..");
    }
    let taidi= new Dog("taidi");
    taidi.action = function(){
        console.log(this.name+ " miao!!!");
    }
    taidi.action() // taidi miao!!!

    let others = new Dog("other dog");
    others.action(); // other dog wang..

}

看上面的代码可以看出:当我们去强行让taidi的action方法改变的话,我们再进行访问的时候会先访问到实例上面的属性“ taidi miao!!!”,但是此时我们再用原型去创造实例的时候,我们并不会改变新增实例的action方法,这个说明了实例属性改变会覆盖原型属性,但是不会原型上面的额属性。

    delete taidi.action

    taidi.action() // taidi wang..

如上所示,当我们将泰迪action删除掉,再访问action方法则会重新显示原型上面的方法。如果我们重复调用上面的方法,却发现无法删除action方法,这进一步说明对象属性不能改变原型的属性。

四、 原型链的继承和彻底了解原型链

谈到面向对象呢?首先就会想到的是继承。我在这里呢?也就来触类旁通,希望用继承起手彻底搞清楚这一个东西。

{
    //这个是java入门继承的最好的例子,拿来讲解一哈
    function Animal(name){
        this.name = name
    }
    Animal.prototype.action = function(){
        console.log(this.name+" have running...")
    }
    Animal.prototype.need = function(){
        console.log(this.name+" need breathing")
    }

    function Fish (name){
        Animal.call(this,name)
    }
    Fish.prototype = Object.create(Animal.prototype);
    // Fish.prototype = new Animal() // 如果构造函数有值的时候,这里就不知道该填写什么了,就很尴尬
    // Fish.prototype = Animal.prototype; // 如果Fish想重写父类方法的时候,父类方法也会变化
    Fish.prototype.constructor = Fish;
    Fish.prototype.action = function(){
        console.log(this.name+" have Swimming...")
    }
    let fish = new Fish("fish");
    fish.name = "鲤鱼";
    console.log(fish.name) // 鲤鱼
    fish.action();
    fish.need();
    fish.best(); // es5:undefined,es6:报错
    console.log(fish.toString())

}

上面是我手写的一个js继承,结合我下面手绘的一张结构图来看一下(我找了好多画图工具,并没有发现好用,希望读者能推荐一款好用的mac画图工具)。

  • 实例鱼调用name属性,发现原型里面存在,就会直接输出。
  • 实例鱼在调用action方法的时候首先会在实例的属性里面去查找,然后发现实例的属性表里面并没有action属性,于是就会向上查找,找到鱼的原型,然后在父类的原型中找到action就会执行其方法。
  • 实例鱼在实例属性中没有发现need方法,于是就会向上查找。但是发现Fish的原型中也没有need方法就会再向上查找,发现在Animal的实例中发现有need方法,于是便执行输出。
  • 实例鱼在实例属性、Fish原型、Animal原型上面都没有发现toString方法,于是再向上查找,终于在object原型中找到toString方法,于是便执行输出
  • 实例鱼在所有的原型中都没有找到best方法,而Object的原型向上查找会返回null,于是便执行返回undefined。(es6中默认开启严格模式,而在严格模式下面未定义的值会报错)
四、说在最后

其实这篇文章写了很久,不知道是因为最近状态低迷的缘故 还是时间唤起了我的懒散。我原本是想着上周末的时候就写完这篇文章,但是一直拖到了现在。好说歹说这篇文章是出来了。希望自己还是能多写点东西。

4人推荐
随时随地看视频
慕课网APP