详解javascript中的原型和原型模式创建对象

本文参考《 Javascript 高级程序设计(第3版 》

我们都知道,创建对象的方法有很多.其中有一种叫做原型模式创建,那么到底什么是原型呢,今天我们来说下.

原型模式

  1. 我们创建的每个函数都有一个 prototype(原型)属性 , 这个属性是一个 指针 ,指向一个 对象 , 这个对象的用途是 包含可以由特定类型的所有实例,共享的属性和方法 . 按照字面意思理解 , prototype就是通过 调用构造函数创建的 那个对象实例的 原型对象 .

  2. 使用原型对象的好处是 , 让 所有对象实例共享它所包含的属性和方法 .

理解原型

  • constructor

    无论什么时候,只要创建了一个新的函数,就会为该函数创建一个prototype属性 , 这个属性 指向函数的原型对象 , 默认情况下,所有原型对象都会 自动获得一个constractor(构造函数)属性 , 这个属性是 指向prototype属性所在函数的一个指针 .
    例如: function Person(){} , Person.prototype.constructor指向Person .

  • proto

    创建了自定义的构造函数之后 , 其 原型对象默认只会取得constractor属性 . 其他的方法都是 从Object继承过来 .

    当调用构造函数 创建一个新实例后 , 该实例的内部将 包含一个指针(内部属性),指向构造函数的原型对象 . ECMA-262第5版管这个指针叫做 [[Prototype]], 但在火狐 , 谷歌和safair在每个对象上都 支持一个__proto__属性 , 而在其他的实现中 , 这个属性对脚本是不可见的 . 不过要明确一点 , 这个连接存在于实例和构造函数的原型对象之间,而不是存在于实例和构造函数之间 , 也就是说这个指针指向的是构造函数的prototype , 而不是构造函数 .

    function Person(){};
    var p = new Person();
    
    Person.prototype;//{ constructor: ƒ Person() , __proto__: Object }
    p.__proto__ ;//{ constructor: ƒ Person() , __proto__: Object }
    
    p.__proto__ === Person.prototype; //true
    

    如上: __proto__就是实例和构造函数的原型对象的连接 , 注意是 构造函数的原型对象 , 而不是构造函数 .

  • 对象属性的读取

    每当代码 读取某个对象的某个属性时 , 都会 执行一次搜素 , 目标是具有给定名字的属性 . 搜索先从对象对象实例本身开始 , 如果 找到 给定名字的属性 , 则 返回该属性的值 ; 如果 没找到 , 则 继续搜索指针指向的原型对象 , 在 原型对象上找到 了这个属性 , 则 返回这个属性的值 .

    function Person(){};
    Person.prototype.name = "jack";
    
    var p = new Person();
    p;//Person {}
    p.name;//jack
    

    如上: p是构造函数Person的实例 , 打印出来是一个函数Person{}上面并没有name属性 . 但是p.name却能打印出来是jack . 这其实就是跟对象的读取机制有关系了,每次读取都会执行一次搜索 , 先从实例开始 , 如果在实例上找到该属性 , 那么就返回该属性的值 , 如果没找到 , 则继续搜索指针指向的原型对象 . 这个指针就是上面我们说的__proto__ , 最后在原型对象上找到了该属性 , 并返回该属性的值 .

  • 原型属性的操作

    虽然通过对象实例 可以访问 保存在 原型中的值 , 但是却 不能通过对象实例改写 原型中的值 .
    如果在实例中添加了 一个和实例原型中相同的属性 , 该属性会 屏蔽掉原型中的同名属性 . 这个属性只会 阻止我们访问原型中同名属性 , 但 不会修改那个属性 ,
    即使将这个属性的 值设为null , 也只会在实例中设置 , 而 不会恢复其指向原型的连接 . 不过 使用delete操作符 可以完全 删除实例属性 , 并且 能够重新访问原型中的属性

    function Person(){}
    Person.prototype.name = "jack"
    var p = new Person()
    
    p.name = "rose"
    
    console.log(p);//Person { name: 'rose' }
    console.log(Person.prototype);//Person { name: 'jack' }
    ————————————————————————————————————————————————————————————————————
    p.name = null
    
    console.log(p);//Person { name: null }
    console.log(Person.prototype);//Person { name: 'jack' }
    ————————————————————————————————————————————————————————————————————
    delete p.name
    
    console.log(p);//Person {}
    console.log(p.name)//jack
    console.log(Person.prototype);//Person { name: 'jack' }
    

    如上: p是构造函数Person的实例 , 先是设置p.name = “rose” , 再打印出来 , p.name的值确实变成了rose , 但是原型上的name属性并没有被覆盖 , 打印出来还是jack .
    再将p.name设置为null , 打印出来p.name为null, , 但是原型上name的值还是jack , 直到delete掉实例上的name属性后 , 打印出p.name才是jack . 所以说 在实例中添加了一个和原型对象上同名的属性后 , 这个属性只会屏蔽掉原型中的同名属性 , 这个属性阻止我们访问原型中同名属性 , 并不会覆盖掉原型中的同名属性 . 因为对象的查找机制 ,在实例中找到属性后就返回了该属性的值 , 不再继续查找了 .
    这里需要注意的是 , 即使将实例的属性设置为null( undefined同理 )也不会恢复 , 因为null也是一个值 . 并不会恢复其指向原型的链接 . 除非使用delete操作符将实例伤的属性完全删除 .

原型in操作符

  • in操作符的两种使用方式:
  1. 单独使用:通过对象能够 访问到给定属性时返回true , 无论该属性存在于实例还是原型中;

    function Person(){}
    Person.prototype.name = "jack"
    
    var p = new Person()
    console.log( 'name' in p );//true
    
    p.age = 24
    console.log( 'age' in p );//true
    
  2. for-in循环中使用:返回的是所有能通过 对象访问的、可枚举的(enumerate) 属性 , 其中既包括存在于实例中的属性 , 也包括存在于原型中的属性(但不包括原型中不可枚举的属性)

    function Person(){}
    Person.prototype.name = "jack"
    
    var p = new Person()
    
    p.age = 24
    
    for (let i in p) {
        console.log( i ); //name、age
    }
    
    ————————————————————————————————————————————————————————————————————
    
    //给实例p添加一个新属性job , 值为programmer
    Object.defineProperty( p , "job" , {
        value:"programmer"
    } )
    
    for (let i in p) {
        console.log( i )//age、name
    }
    console.log( p.job );//programmer
    console.log( "job" in p )
    

    如上: 实例上有一个age属性 , 原型上有name属性 , 没有疑问 , 都打印了出来 . 但是我们用defineProperty重新定义了一个job属性 , 值为programmer , 描述符emuberable( 是否可枚举 )不设置默认为false . 再for-in 循环,发现并没有打印出job . 但是直接取p.job却能够取出来 . 这就是for-in和in的区别 .

更简单的原型语法

  • 对象字面量方式重写prototype
    • 为了避免每添加一个属性和方法就在就要敲一遍Person.prototype , 更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象

      function Person(){}
      Person.prototype = {
          name:"jack",
          age:24,
          job:function(){
              return "programmer"
          }
      }
      
      var p = new Person()
      console.log(p.name);//jack
      

      如上: 将Person.prototype设置为等于一个对象字面量形式创建的新对象 , 最终结果相同.

  • 字面量创建prototype的缺点
    • 但是 , constructor属性不再指向Person了 . 之前我们说过 , 每创建一个函数 , 就同时会创建他的prototype对象 , 这个对象会自动获得constructor属性 . 但是我们在这里使用的语法 , 本质上 完全重写了默认的prototype , 因此constructor属性也就变成了新对象的constructor属性( 指向Object构造函数 ) , 不再指向Person函数了.

      //prototype未使用字面量方式创建
      function Person(){}
      Person.prototype.name = "jack"
      Person.prototype.age = 24
      var p = new Person()
      
      console.log( p instanceof Object );//true
      console.log( p instanceof Person );//true
      console.log( Person.prototype.constructor == Object );//false
      console.log( Person.prototype.constructor == Person );//true
      
      ————————————————————————————————————————————————————————————————
      //prototype使用字面量方式创建
      function Person(){}
      Person.prototype = {
          name:"jack",
          age:24,
          job:function(){
              return "programmer"
          }
      }
      var p = new Person()
      
      console.log( p instanceof Object );//true
      console.log( p instanceof Person );//true
      console.log( Person.prototype.constructor == Object );//true
      console.log( Person.prototype.constructor == Person );//false
      

      如上: 在未使用字面量方式创建prototype时 , Person.prototype.constructor指向了Person . 使用字面量方式创建prototype后 , Person.prototype.constructor指向了Object构造函数 .

    • 如果constructor属性真的很重要 , 可以用下面这种方式将constructor设置回适当的值 . 但是 , 这样设置会导致constuctor的[[enumerable]]特性被设置为true . 默认情况下 , 原生的constructor属性不可枚举 .

      function Person(){}
      Person.prototype = {
          constructor:Person,//加上这一行
          name:"jack",
          age:24,
          job:function(){
              return "programmer"
          }
      }
      var p = new Person()
      
      console.log( p instanceof Object );//true
      console.log( p instanceof Person );//true
      console.log( Person.prototype.constructor == Object );//false
      console.log( Person.prototype.constructor == Person );//true
      
      //使用for-in遍历发现constructor是可枚举的
      for( i in p ){
          console.log( i );//constructor、name、age、job
      }
      
      

原型的动态性

  • 实例和原型之间的关系
  • 由于在原型中查找值的过程是一次搜索 , 因此我们 读取原型对象所做的任何修改都能够立刻反映出来 ---- 即使是先创建了实例后修改原型 .
    其原因归结为 实例和原型之间的松散连接关系 . 实例和原型之间的 连接只不过是一个指针 , 而非副本.

    function Person(){}
    var p = new Person()
    
    console.log(p.name); //undefined
    
    Person.prototype.name = "jack"
    
    console.log(p.name);//jack
    

    如上:先创建实例 , 再在原型上添加属性 , 依然可以打印出来 . 主要原因就是实例和原型之间的联系 , 他们之间通过指针连接 , 实例并不是原型的副本.

  • 字面量创建prototype对于原型动态性的影响
  • 虽然可以随时为原型添加属性和方法 , 并且修改能立刻在所有对象实例中反映出来 . 但如果是 重写整个原型对象, 那么就不一样了 . 我们知道 , 调用构造函数时会为实例添加一个指向最初原型[[Prototype]] ( __ proto __ )的指针 , 而把 原型修改为另一个对象就等于切断了构造函数和最初原型之间的联系 . 请记住:实例中的指针仅仅指向原型 , 而不指向构造函数

    1// 先创建实例,再重写Person.prototype , 不在最初原型对象上添加方法. 
    function Person(){}
    var p = new Person()
    
    //动态重写prototype
    Person.prototype = {
        constructor:Person,
        name:"jack",
        age:24
    }
    console.log(p.name);//undefined
    console.log(p.age);//undefined
    
    ———————————————————————————————————————————————————————————————————
    2// 先重写Person.prototype , 再创建实例 , 不在最初原型对象上添加方法.
    function Person(){}
    
    //动态重写prototype
    Person.prototype = {
        constructor:Person,
        name:"jack",
        age:24
    }
    var p = new Person()
    console.log(p.name);//jack
    console.log(p.age);//24
    
    ———————————————————————————————————————————————————————————————————
    3// 先创建实例 , 再在最初原型上添加name属性  , 值为rose . 最后重写prototype
    function Person(){}
    var p = new Person()
    
    Person.prototype.name = "rose"
    
    Person.prototype = {
        constructor:Person,
        name:"jack",
        age:24
    }
    console.log(p.name);//rose
    console.log(p.age);//undefined
    console.log(p.__proto__);//Person { name: 'rose' }
    console.log(Person.prototype);//Person { constructor: [Function: Person], name: 'jack', age: 24 }
    
    ———————————————————————————————————————————————————————————————————
    4// 先创建实例 , 再重写prototype. 最后在最初原型上添加name属性  , 值为rose 
    function Person(){}
    var p = new Person()
    
    Person.prototype = {
        constructor:Person,
        name:"jack",
        age:24
    }
    
    Person.prototype.name = "rose"
    
    console.log(p.name);//undefined
    console.log(p.age);//undefined
    console.log(p.__proto__);//Person {}
    console.log(Person.prototype);//Person { constructor: [Function: Person], name: 'jack', age: 24 }
    ———————————————————————————————————————————————————————————————————
    5//先在最初原型上添加name属性 , 值为rose . 再重写prototype , 最后创建实例 .
    function Person(){}
    Person.prototype.name = "rose"
    Person.prototype = {
        constructor:Person,
        name:"jack",
        age:24
    }
    
    var p = new Person()
    console.log(p.name);//jack
    console.log(p.age);//24
    
    ———————————————————————————————————————————————————————————————————
    6//先重写prototype, 再在最初原型上添加name属性 , 值为rose . 最后创建实例 .
    function Person(){}
    
    Person.prototype = {
        constructor:Person,
        name:"jack",
        age:24
    }
    Person.prototype.name = "rose"
    var p = new Person()
    console.log(p.name);//rose
    console.log(p.age)//24
    

    如上:六种情况 :

  1. 第一种 : 先创建实例,再重写Person.prototype , 不在最初原型对象上添加方法 , p.name , p.age都是undefined;
  2. 第二种 : 先重写Person.prototype , 再创建实例 , 不在最初原型对象上添加方法.p.name , p.age分别为Jack和24 ;

对比1和2 ,不难发现.

重写prototype不能动态的反映到实例上
重写prototype必须在创建实例之前,实例才能拿到原型上的属性
  1. 第三种:先创建实例 , 再在最初原型上添加name属性 , 值为rose . 最后重写prototype, p.name和p.age分别为rose和undefined ;

根据1,2的结论 重写prototype并不能动态反映到实例上 , 所以 打印name取到了最初原型的上的值 . 虽然没有动态的反映 , 但是我们发现 , person.prototype已经被重写了.且 p.__proto__不指向Person.prototype

  1. 第四种:先创建实例 , 再重写prototype. 最后在最初原型上添加name属性 , 值为rose ;p.name和p.age均为undefined ;

三和四的共同点都是先创建实例再操作prototype , 不同点是三先在最初原型上添加属性 , 然后重写prototype . 而四是先重写prototype. 最后在最初原型上添加属性 .
那么我们发现.如果先重写prototype , 那么就会先切断构造函数和最初原型之间的关系 . 后面再给最初原型添加属性是不生效的

  1. 第五种:先在最初原型上添加name属性 , 值为rose . 再重写prototype , 最后创建实例 .p.name和p.age分别为jack和24 ;
  2. 第六种:先重写prototype, 再在最初原型上添加name属性 , 值为rose . 最后创建实例 .p.name和p.age分别为rose和24 ;

第五种和第六种其实道理一样 , 在创建实例之前操作prototype , 最难找自上而下的顺序 , 有同名属性就覆盖掉

原型对象的问题

原型对象的主要问题
  • 省略了为构造函数传递初始化参数 这一环节 , 结果所有实例在 默认情况下都会取得相同的属性值 . 但这还不是最大问题 . 最大的问题是由其 共享的本性 所导致的.
原型对象问题的表现
  • 原型中 所有属性是被很多实例共享的 , 这种共享对于函数非常合适 . 对于包含基本值的也说的过去 , 毕竟通过在实例上添加一个同名属性就可以隐藏原型中的对应属性 . 然而对于 包含引用类型值的属性 来说 , 问题就比较突出 .

    function Person(){}
    
    Person.prototype = {
        constructor:Person,
        name:"jack",
        age:24,
        firend:[ "rose" ]
    }
    
    var p1 = new Person();
    var p2 = new Person();
    
    console.log( p1.firend );//[ 'rose' ]
    console.log( p2.firend );//[ 'rose' ]
    
    p1.firend.push( "renzhch" );
    
    console.log(p1.firend);//[ 'rose', 'renzhch' ]
    console.log(p2.firend);//[ 'rose', 'renzhch' ]
    

    所以一般情况下,不建议单独使用原型模式创建对象

关于实例的几个方法

  • isPrototypeOf( 实例 )
    判断实例的__proto__是否指向构造函数的原型对象 isPrototypeOf(实例)
    Person.prototype.isPrototypeOf( p )

  • Object.getPrototypeOf( 实例 )
    这个方法返回 [[Prototype]] ( __ proto __ ) 的值
    Object.gePrototypeOf( person1 ) === Person.prototype // true
    Object.gePrototypeOf( person1 ).name // jack

  • hasOwnProperty( 属性 )
    检测一个属性是否存在于实例中,还是存在于原型中 . 属性存在与实例中 , 返回true , 否则返回false
    p.hasOwnProperty( ‘name’ )

  • Object.keys( 对象 )
    接收一个对象作为参数,返回一个 包含所有可枚举属性的字符串数组

  • Object.getOwnPropertyNames( 对象 )
    接收一个对象作为参数,返回一个 包含所有属性的字符串数组,无论是否可枚举

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值