《高程第六章---面向对象程序设计》小结---创建对象(该章节重点在于理解原型链)

《JavaScript高级程序设计》这本书比较厚,之前刚学JS的时候挑战过一次,结果止步第三章。现在工作了一段时间想想还是得回过头来补充一下基础知识的。目前刚看完第六章,就从第六章开始总结吧,再逐步补充之前的好了。

本章小结

创建对象章节主要讲解如何解决一个问题:使用对象字面量或者Object构造函数创建对象时会因此产生的大量冗余代码。
解决该问题的思路有很多,本章介绍的有①工厂模式,②构造函数,③原型模式,④原型与构造函数的组合,⑤动态原型,⑥稳妥构造函数。
并且在介绍这些方法的同时也介绍了这些方法的试用问题和优缺点,并提出了解决方案。
该博客会采用介绍方法->提出该方法的问题->如何解决该方法产生的问题这样的步骤来进行讲解,代码的展示有可能用截图的形式

工厂模式

  • 解决的问题:该模式主要解决的的就是用对象字面量和Object构造函数创建对象时会产生大量冗余代码这个问题;
  • 介绍和用法:
    • 介绍:工厂模式顾名思义,就像一个工厂的功能一样可以批量创建产品,可以通过创建一个工厂函数,再通过传参的方式创建出我们想要的对象
    • 用法示例:
// 例如我们要创建多个单车对象,先创建一个生产单车的工厂
        function factory(brand, weight, height) {
            let bike = new Object()
            bike.brand = brand
            bike.weight = weight
            bike.height = height
            bike.ride = function() {
                console.log('ride')
            }
            return bike
        }
// 然后通过设定单车的参数,让工厂生产出我们所需要的单车
        let bikeA = factory('oxc', '3kg', '1.1m')
        let bikeB = factory('mmp', '2kg', '1.2m')
// 这样我们就得到了两个单车
bikeA: {brand: "oxc", weight: "3kg", height: "1.1m", ride: ƒ} 
bikeB: {brand: "mmp", weight: "2kg", height: "1.2m", ride: ƒ}
// 也可以通过无数次的调用factory函数去创建无数个你想要的型号的单车
  • 产生的问题
    • 对象内方法重复创建:在上面的单车工厂中,创建出来的单车都有ride这个方法,并且这个方法的功能是一模一样的,但是在bikeA和bikeB两个单车中的ride方法却不是同一个,也就是说这个相同功能的方法被重复创建了
      image.png
    • 无法识别创建出来的对象的类型:假设我们在一个工厂函数中去创建一个字符串对象,那么返回出来的这个数组的数据类型无法准确识别,如下例子:
        function factory() {
            let str = new String('oxc')
            return str
        }
        let oxc = factory()
        console.log(typeof oxc)   // object
// 当然,可以通过instanceof这个方法判定创建出来的对象是不是string类型,但是这样也会有一个问题,普通赋值方法创建出来的字符串用instanceof是无法判断该字符串是不是属于string类型的,而我们平时创建字符串时也极少使用Sting构造函数去创建一个字符串
let a = 'oxc'
a instanceof String // false

随后,又出现了使用构造函数模式创建对象的方法

构造函数模式

  • 介绍:这个方法和工厂模式区别并不大,但是还是有区别的①不需要return对象出来,②使用了new操作符,③函数体内没有显式地去创建对象;
  • 示例:以上面的单车例子来看
        function Bike(brand, weight, height) {
            this.brand = brand
            this.weight = weight
            this.height = height
            this.ride = function() {
                console.log('ride')
            }
        }
        let oxc = new Bike('oxc', '1.2kg', '1.3m') //  {brand: "oxc", weight: "1.2kg", height: "1.3m", ride: ƒ}
// 虽然该方法和工厂模式区别不大,但是还需要理解一下new操作符在这中间做了什么事情
  • new操作符做的事情
    • 创建了一个新的对象; let o = new Object()
    • 设置该对象的原型链,让其指向Bike构造函数的原型对象; o.__proto__ = Bike.prototype
    • 执行函数,并设置构造函数内this的指向为创建的新对象; Bike.call(this)
    • 返回这个对象; 'return obj'
  • 备注
    • 使用这种方式创建的对象,可以使用constructor属性查看它的构造函数
      oxc.constructor === Bike // true
    • 构造函数的命名使用驼峰模式,不过首字母需要大写
    • 使用new操作符进行调用(注意这里,如果不用new操作符而直接调用,那么函数内的属性和方法将会被设置到外层对象上,例如在全局环境window下调用这个函数,因为此时this指向的是window对象,所以其内属性与方法都会被放到window对象上,当然啦,你也可以自己创建一个对象,然后在构造函数调用时使用call或者apply强制将this指向这个对象,但这是多此一举)
  • 构造函数的问题
    • 和工厂函数一样的重复创建对象内的方法

为了解决方法重复创建的问题,后来又出现了原型模式

原型模式

  • 介绍:原型模式基于函数的prototype属性而产生,prototype这个属性是一个指针,指向一个对象,这个对象包含所有由这个函数创建的实例都可以共享的方法和属性。那么在这个字面意思的理解上,其实就已经知道如何解决构造函数模式和工厂模式所产生的重复创建实例内方法的问题了,将在下面进行讲解:
  • 使用实例:以上面的单车为例
        // 将属性和方法都设置到Bike函数的prototype属性中去
        function Bike() {}
        Bike.prototype.brand = 'oxc'
        Bike.prototype.weight = '1.2kg'
        Bike.prototype.height = '1.2m'
        Bike.prototype.ride = function() {
            console.log('ride')
        }
        let bikeA = new Bike()
        let bikeB = new Bike()
        bikeA.ride()                 // ride
        bikeB.ride()                 // ride
        console.log(bikeA.ride === bikeB.ride)   // true   bikeA和bikeB对象的ride方法变成了同一个
  • 关于原型链

    • 原型模式和原型链:提到原型模式,就不得不提及原型链这个东西了,这也是原型模式能解决重复创建函数这个问题的关键。
    • 原型链介绍(以上面的Bike原型模式为例):
      1 . 在JS中,无论何时何地,只要创建了一个函数,那么这个函数就肯定会有一个prototype属性,prototype属性是一个指针,指向这个函数的原型对象,该对象存储着所有由该函数的所创建的实例对象所共享的方法和属性,此外,该原型对象中还存在一个叫做constructor的属性,这个属性指向这个原型函数,如下图所示:
      image.png

      2 . 这时候,我们使用new操作符创建了两个个Bike实例let bikeA = new Bike(),在这个操作中,结合上面的new操作符所做的事,创建一个空对象并使这个对象的__proto__指向Bike函数的prototype
      image.png

      image.png

      3 . 然后,在我们调用bikeA或者bikeB的ride方法时,它会先在自身的属性中寻找是否存在ride方法,如果不存在,则通过__proto__进入到上一层,也就是Bike构造函数的原型对象prototype中去寻找ride方法,找到后进行调用,这相当于bikeA.__proto__.ride(),bikeB调用的原理是一样的,他们调用的其实都是处于Bike.prototype对象中的ride方法。这个调用的查找链条就是原型链。
      4 . 另外值得一提的是,所有的对象都是Object构造函数的实例,bikeA和bikeB也可以通过原型链对Object构造函数的prototype里的方法和属性进行访问,而Objectprototype指向的就是null了(有木有一种道生一的感觉呢?),例如: bikeA.toString()
      image.png

      5 . 大家知道,在JS中,一切皆对象,那么如果将构造函数Bike当做一个对象来看的话,那么Bike对象(函数)的构造函数又是谁呢?答案是Function构造函数,而Function构造函数的prototype对象因为是对象,所以Function.prototype.__proto__指向的又是Object.prototype,最终的效果图是下面这样的,因为画图工具的问题所以线条可能会有交织
      image.png
  • 原型模式的一些特点

    • 在实例中重写原型中已经有的属性不会影响到原型中的属性,但是会通过该实例对这个属性进行访问的话,重写的属性会屏蔽, 例如
      image.png

      原因其实很简单,就是因为原型链由内向外查找这个特性造成的。
      那么如何分辨一个对象的属性是它自己的还是属于它原型链上的呢?答案是使用hasOwnProperty方法
      image.png
    • 在实例上使用delete操作符无法删除原型对象上的属性,但是可以通过__proto__去删除原型上的属性(尽量不要这么做,因为__proto__不是在所有浏览器都能用,所以尽量少用__proto__去进行操作)
      image.png
    • in操作符可以通过实例获取到原型上的属性(in操作符特性是只要能在对象上访问到的属性,他都会返回true),另外,for in循环也是可以拿到原型上的属性的
      image.png
  • 改良版原型构造函数(解决在创建原型函数的时候要写n次prototype的问题),注意:采用字面量的方法去创建构造函数的话需要将函数原型对象中的constructor重写回指向构造函数本身, 否则函数原型对象上的constructor指向Object构造函数, 另外还要注意将这个重写的constructor属性用Object.definedProperty设置其不可进行遍历(enumerable设置为false)

        function Bike() {}
        Bike.prototype = {
            brand: 'oxc',
            weight: '1.2kg',
            height: '1.2m',
            constructor: Bike,
            ride() {
                console.log('ride')
            }
        }
  • 原型模式的产生的问题
  1. 如果将引用类型放到构造函数的原型对象上去,会导致由该构造函数创建出来的实例全部共享这个引用类型(就像函数被共享了一样),比如下面这个例子:
        function A() {}
        A.prototype.oxc = {
            age: 123,
            name: 'oxc'
        }

        let b = new A()
        let c = new A()
// 这时候对b.oxc对象里面的属性进行修改,会影响到c.oxc对象,原因就在于oxc保存的是一个指向对象的地址,而b与c共享了这个地址,所以b与c持有同一个oxc对象
image.png
  1. 采用原型模式的话无法传递参数到构造函数中,导致所有创建出来的实例里面的属性都是一样的
  • 总结:原型模式虽然解决了构造函数模式和工厂模式的重复创建函数的问题,但是又因为解决该问题的特性导致多个实例间的引用类型被共享,并且所有创建出来的实例都是一样的。
    为了解决原型模式所导致的问题,又出现的原型模式和构造函数模式组合使用的方法。

原型模式和构造函数组合使用

  • 介绍:该模式将原型模式的聚合性和构造函数模式的分离性(我自创的词)组合在一起使用,完美的解决了原型模式所带来的引用类型共享和无法传参,以及构造函数模式重复创建函数的问题。
  • 原理:这种模式的原理很简单,无非是用构造函数的模式去设置属性,然后用原型模式去设置函数罢了
  • 实例
        function A(name) {
            this.oxc = {
                name: name,
                age: 123
            }
        }
        A.prototype.sayName = function() {
            console.log(this.oxc.name)
        }

        let b = new A('oxc')
        let c = new A('大春春')
image.png

动态原型模式

  • 介绍:该方法是原型模式的一个变种,会有这种模式可能因为有人觉得把构造函数中函数定义的部分,也就是Func.prototype.XXX = function(){}写到函数体的外面不好看,所以想到这么个方法(纯属个人猜测)
  • 原理:既然觉得把构造函数中的函数定义部分写到外边不好,那就写到里面去呗,但是这样又会因为构造函数的多次调用而多次创建相同的函数,那就加上一个条件判断吧,看下面例子:
  • 实例
        function A(name) {
            let mark = 0
            this.oxc = {
                name: name,
                age: 123
            }
            if (typeof this.sayName !== 'function') {
                mark++
                console.log(`已经创建了${mark}次`)
                A.prototype.sayName = function() {
                    console.log(this.oxc.name)
                }
            }
        }

        let b = new A('oxc')
        let c = new A('大春春')
// 该函数在创建第一次执行A的时候因为sayName方法是undefined,所以会在构造函数A的原型对象上设置sayName函数,而在第二次执行A的时候因为this.sayName已经被创建,所以就不执行了
image.png

寄生构造函数模式

  • 介绍和适用情况:该方法主要用于为JS的原声构造函数(Array,String等)添加自定义方法,在写法上和工厂模式除了会使用new操作符之外几乎没有任何区别
  • 实例:
        function S() {
            let value = new Array()
            value.push.apply(value, arguments)
            value.sum = function() {
                console.log(this.reduce((p, n) => p + n))
            }
            return value
        }
        let newArr = new S(1, 2, 3)
        newArr.sum()         //   6
  • 优势:
    这个方法针对为JS原生构造函数添加自定义方法的开发者来说非常友好,因为如果直接在原生构造函数的原型链上去添加自定义方法有可能会出现覆盖原生对象原型链上的原生方法的情况。
    而采用寄生构造函数模式,则是创建一个原生构造函数的实例,将其绑定到该实例本身上,对原生构造函数不会有任何影响;


    image.png

稳妥构造函数模式

  • 介绍:该方法只适用于特定的情况下(防止数据被改动),该模式特点是不适用this也不用new,但是由此创建的对象中的属性除了调用流出来的接口外,没有其它办法获取,因为场景见过的不多,所以这种模式本人在实际中没有用过也没见到有人用过;
  • 实例:
        function S(name) {
            let o = new Object()
            o.sayName = function() {
                console.log(name)
            }
            return o
        }
        let newObj = new S('oxc')
image.png

后记

然并卵,上面的模式其实工作中用得都比较少了,但是这些只是还是非常有意思的,现在工作中创建对象用得多是ES6推出Class了,当然了,Class那又是另外一个话题了,将在以后的博客中进行记录和学习.

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

推荐阅读更多精彩内容