JavaScript Fortnight-Diary:旧时代的妥协——创建对象与继承

创建对象

工厂模式

        function createPerson(name){
            let o = new Object()
            o.name = name
            return o
        }
        let person1 = createPerson('lpj')
        let person2 = createPerson('hjy')

缺点:不能共享、对象标识紊乱

  • 单纯构造函数模式
function Person(name){
            this.name = name;
            this.sayName = function(){console.log(this.name)}
        }
  • 确保实例被标识为特定类型(通过constructor 但一般使用instanceof操作符)
  • person1 instanceof Object==person2 instance of Person==true

缺点:this.sayName = function(){}方法其实相当于this.sayName = new function(){}也就是说方法和属性等价了,每个实例都有一个自己的方法
console.log(person1.sayName == person2.sayName)//false

原型模式

function Person(name){
            this.name = name;
        }
Person.prototype.sayName =  function(){console.log(this.name)}
Person.prototype.job = 'student'
  • 每个实例实际上都有[[prototype]]内部特性(一个指向构造器原型的指针) __ proto__实际上就用于访问本实例的[[prototype]]而不是直接访问外部
  • 任何自定义的构造器函数的原型都只是单纯的函数对象,由Object()构造器创造,因此原型链到最后Person.prototype.__proto__.__proto__ ===null
  • 相关方法
    • console.log(Person.prototype.isPrototypeOf(person1)//true
      直接确认原型
    • console.log(Object.getPrototypeOf(person1)== Person.prototype) console.log(Object.getPrototypeOf(person1).job)//student
      用于获取实例[[Prototype]]的值
    • setPrototypeOf
      可以重写实例proto所指向的原型,但具有性能问题
    • Object.create()
      也可以重写实例指向的原型,性能更好,在继承时可能会用到
  • 遮蔽(shadow)与确认
    实例如果有和原型上同名的属性,会遮蔽原型上的,使其搜索不到,可以通过delete该属性删除从而恢复原型属性的暴露
    hasOwnProperty用于确认属性在原型上还是实例上
    in用于确认可枚举是否属性在原型在实例上
    Object.keys()返回所有可枚举属性
    Object.getOwnPropertyNames()返回全部属性包括不可枚举的
let person1 = new Person()
let person2 = new Person()
console.log(person1.hasOwnProperty("name"))//false 不在实例上
console.log("name" in person1)//true 在原型上
person1.name = 'sb'//遮蔽
console.log(person1.hasOwnProperty("name"))//true 在实例上
console.log("name" in person1)//true 在实例和原型上都有
delete person1.name//取消遮蔽 
console.log(person1.hasOwnProperty("name"))//false
  • 一些点
    • construcor默认是不可枚举属性 要用Object.getOwnPropertyNames()才能返回
    • Object.keys()in在内部的枚举顺序是不确定的,取决于引擎,其余的方法顺序则是确定的
  • 原型修改与重写
    • Person.prototype.any = 'something'可以直接添加原型的属性,并且只要添加后,任何情况下都可以访问(原型的动态性)
    • 记住,实例只有指向原型的指针,没有指向构造器函数的指针
function Person(){}
let friend = new Person
Person.prototype = {
constructor :Person,//注意 如果不写的话,这里将会从Person变成Object
name:'sb',
say(){console.log('sb')}
}
friend.say()//错误

friend指向的原型仍是最初的原型

  • 尽管可以修改原生对象原型(如Array、String的),但不建议这么做,应使用es6的自定义类来继承原生类型

缺点:

  • 弱化了构造函数传递初始化参数的能力例如Person.prototype.job = 'student'会被所有实例继承,这对函数(方法)来说还可以接受,但属性往往不需要。
  • 共享了属性,导致如果你person1.job = 'teacher'的话,person2.job也会改变
    ↑这里是错误的,number和string都不会改变,这些属性是独立拷贝的,但如果是Array,显然由于引用类型的原因,你push一个新元素进去,会导致共享的全都被修改
    ↑注意:“独立拷贝是错的”,原型就是只有一份,这里之所以看起来person2.job不变是因为添加了person1的实例属性job,原型被遮蔽了,而person2未被遮蔽

总之,不能纯粹使用原型模式,该写构造器里就写构造器里
↑对于num和str来说,写在构造器里可以解决这个问题,但对于Array等引用来说,写在构造器里还是一样的,必须采用盗用构造器

↑草 写在构造器里还真不一样 其实很显然 因为写在构造器里就是new了新的

    function Animal(){
            this.foot = 4
            this.colors = ['white','black']
        }
        let cow = new Animal
        cow.colors.push("yellow")
        cow.foot = 2
        let cat = new Animal

        console.log(cow.foot+cow.colors)//2white,black,yellow
        console.log(cat.foot+cat.colors)//4white,balck

真正的缺点应该称作:原型包含引用值从而在所有实例间将会共享,这个缺点将在继承中显著体现

继承

为什么要继承?
为了让一个实例能得到多个引用类型(Person、Object等)的属性和方法,提升面向对象的能力、提高可读性、降低代码复杂度

  • 基本继承
        function Animal(){
            this.foot = 4
        }
        Animal.prototype.sayMyFoot = function(){
            console.log("my foot has "+this.foot)
        }
        function Cat(name){
            this.name = name
        }

        Cat.prototype = new Animal()//开始继承

        Cat.prototype.sayMyName = function(){
            console.log("my name is "+this.name)
        }

        let mimi = new Cat("mimi")
        mimi.sayMyName()
        mimi.sayMyFoot()
//实际上mimi还继承了所有Object()原型的方法        console.log(mimi.__proto__.__proto__.__proto__.__proto__===null)//ture

注意:Cat.prototype如果使用字面量重写的话,会导致原型链断开,因为构造器是Object了而不是Animal

Cat.prototype = {
            constructor:Cat,
            //constructor:Animal, constructor就是Cat啊 别乱来
            sayMyName : function(){
            console.log("my name is "+this.name)
        }
        }
        Cat.prototype.__proto__ = Animal.prototype//有了这句就能修复原型链

注意:别纠结,知道原型链断开就好。这里修复后只能继承sayMyFoot,如果要继承foot的话还要call才行

原型链的问题

  • 正如前面结尾所说的,原型包含引用值从而在所有实例间将会共享,而这里(通过new继承) 相当于给Cat.prototype添加了一个foot属性,这是num就没问题,如果是Array等引用值的话,就会产生问题
    mimi.colors===["white","black"]
    mimi.colors.push("yellow")
    miao.colors===["white","black","yellow"]//受影响
    mimi.foot = 2
    miao.foot===4//不受影响
  • 子类型在实例化时不能给父类型的构造器传参 Cat.prototype = new Animal()你传个p参对吧
  • 解决办法:盗用构造函数
    利用call把Animal的this指向Cat(即call里面的第一个参数this) 这样相当于把Animal的初始化代码都偷到Cat上执行了,相当于没有两层new了,该放到构造器里的属性就放进了Cat构造器里而非跑到prototype里
function Animal(foot){
            this.foot = foot
            this.colors = ['white','black']
        }
        Animal.prototype.sayMyFoot = function(){
            console.log("my foot has "+this.foot)
        }

        function Cat(name,foot){
            this.name = name
            Animal.call(this,foot)
        }

        //Cat.prototype = new Animal()//开始继承
        
        Cat.prototype.sayMyName = function(){
            console.log("my name is "+this.name)
        }       let mimi = new Cat("mimi",4)
        console.log(mimi.colors)

        mimi.colors.push("yellow")

        let miao = new Cat("miao",4)
        console.log(miao.colors)//不被影响
        console.log(miao.foot)//传参成功
        miao.sayMyFoot()//报错!

如cb所说:Cat.prototype.__proto__ = Animal.prototype等价于Objcet.setPrototypeOf(Cat.prototype, Animal.prototype)但我觉得后者会有性能问题
还可以Cat.prototype= Object.create(Animal.prototype)//(注意不是__proto__)这个方法自己就可以重新使[[prototype]]指过去但要补充一行Cat.prototype.constructor = Cat//因为是函数特有的默认属性

什么?这都有缺点?
是的

  • 必须在构造函数中定义方法,this.name = name Animal.call(this,foot)因此函数不能重用
  • 更致命的是,子类实例也访问不到父类(Animal、Object)原型上的方法
  • 组合式继承
    既然访问不到方法,那就用原型链继承方法,用盗用来继承属性
    这样既可以把方法定义在初始原型上实现 重用,又可以让每个实例拥有自己的属性
    完整代码如下:
    function Animal(foot){
            this.foot = foot
            this.colors = ['white','black']
        }
        Animal.prototype.sayMyFoot = function(){
            console.log("my foot has "+this.foot)
        }
        
        //盗用构造器继承属性
        function Cat(name,foot){
            this.name = name
            Animal.call(this,foot)
        }
        //第三种继承:组合继承
        //在盗用的基础上,使用__proto__来进行原型链继承而不使用new
        
        Cat.prototype= new Animal //不要用 尽管它是书里的方式
        
        Cat.prototype.sayMyName = function(){
            console.log("my name is "+this.name)
        }
        //你也可以选择覆写
/*      Cat.prototype = {
            //constructor:Animal, constructor就是Cat啊 别乱来
            sayMyName : function(){
            console.log("my name is "+this.name)
        }
        }
        Cat.prototype.__proto__ = Animal.prototype*/

        let mimi = new Cat("mimi",4)
        console.log(mimi.colors)

        mimi.colors.push("yellow")

        let miao = new Cat("miao",4)
        console.log(miao.colors)//不被影响
        console.log(miao.foot)//传参成功
        miao.sayMyFoot()//成功继承方法

注意:这里书里沙雕了,就硬要把Animal属性全部继承给Cat,从而使用Cat.prototype= new Animal,但其实没必要,反而会带来很多副作用

  • 存在效率问题,在Cat.prototype= new Animal调用了一次Animal
    Animal.call(this,foot)又调了一次
  • 由于上述问题,Cat.prototype的foot和colors属性和实例上的foot和colors是两个不同的重复
    console.log(Cat.prototype.colors===mimi.colors)//false
    解决方法
  • Cat.prototype.__proto__ = Animal.prototype
    • 区别是Cat.prototype搜索不到foot和colors 要能搜索到就必须new
    • 缺点是失去Object.create添加属性的特性以及寄生方式DIY方法的能力
  • Object.setPrototypeOf(Cat.prototype, Animal.prototype)
    跟上面一样,但是还会有性能问题
  • 标准方法: Object.create(Animal.prototype)然后设置constructor
    即寄生式组合继承
  • 原型式继承:为寄生式打基础
    解决了重复调用父类构造函数的问题(直接利用了原型而不用构造器)
    (直接用__ proto__重定位也是可以解决这个问题的)
    试想一种函数
        function object(o){
            function F(){}
            F.prototype = o
            return new F()
        }
        let person = {
            name : "lpj",
            girlFriends: ["hjy","cst"]
        }
        let anotherPerson = object(person)
        anotherPerson.name = "sb"//这里会添加name到实例上而不是原型上
        //anotherPerson.__proto__.name = "sb"
        anotherPerson.girlFriends.push("someone")//这里会直接访问原型的girlFriends
        let yetAnotherPerson = object(person)
        yetAnotherPerson.name = "KKP"

        console.log(anotherPerson)//name is sb
        console.log(yetAnotherPerson)//name is KKP
        console.log(anotherPerson.__proto__)//Array has 3
        console.log(yetAnotherPerson.__proto__)//Array has 3

借用 构造器简单地实现了对象之间的信息共享,不用专门去写构造器函数了
其实是对person进行浅拷贝

Objcet.create()第二个参数还允许添加新的属性到新的实例上,注意同名会产生遮蔽

  • 更进一步:寄生式继承
    寄生式继承的概念是比较广的,不仅用于继承

顾名思义,用Object.create()新建一个对象,然后增强它 再返回

  function createAnother(original){
  let clone = Object.create(original)
  clone.sayHi = function(){console.log('hi')}
  return clone
}
  let person = {
name : "lpj"
}
  let anotherPerson = createAnother(person)
  anotherPerson.sayHi() 

注意:这会使得createAnother类型的函数的复用性大幅下降

  • 来个大杂烩:寄生式组合继承
    利用create()把Animal的原型浅复制出来,增强一个constructor属性,赋给Cat的原型就完事了
        function Animal(foot){
            foot = 4
            this.foot = foot
            this.colors = ['white','black']
        }
        Animal.prototype.sayMyFoot = function(){
            console.log("my foot has "+this.foot)
        }
        
        //继承属性
        function Cat(name,foot){
            this.name = name
            Animal.call(this,foot)
        }

        //非标准最佳方式:Cat.prototype.__proto__ = Animal.prototype 缺点是失去Object.create添加属性的特性以及寄生方式DIY方法的能力
        //Cat.prototype= new Animal 原方式
        function inheritPrototype(child,father){
            let prototype = Object.create(father.prototype)
            prototype.constructor = child
            child.prototype = prototype
        }
        inheritPrototype(Cat, Animal)

        Cat.prototype.sayMyName = function(){
            console.log("my name is "+this.name)
        }

到这里,实现了方法共享、属性私有(伪)、传参、且只调用了一次构造器、不破坏任何原型链、且在必要时可以添加新方法和属性

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

推荐阅读更多精彩内容