美文网首页
Javascript学习笔记-原型链

Javascript学习笔记-原型链

作者: Patrick浩 | 来源:发表于2017-08-28 22:19 被阅读0次
    Javascript原型链.png

    对于Javascript原型链,是Javascript中很重要的内容,要理解关键有三点:
    Javascript中原型链作用是为了实现Javascript中的继承机制。
    Javascript中原型链是利用对象关联的方式实现的(不同于一般的类复制)。
    Javascript中之所以能使用原型链来实现继承,关键是Javascript对象中属性检索机制
    于是从类基础开始说起。

    1. 面向对象

    1.1 类基础

    在面向对象语言中,经常会使用到类。类是一种设计模式,是某种事物的描述,类一般存在构造函数,构造函数是用于构建实例,实例是类的具体实现,类和实例的关系就类似于蓝图和建筑物的关系一样。

    1.2 继承和多态

    继承和多态是面向对象的两个重要的特性。对于类似Java的面向对象语言来说,继承是通过创建实例化的过程中,复制父类的属性和方法来实现,并且通过重写父类的方法(实现了多态)。

    2. Javascript中对象属性

    这里先提前引入原型链,主要是为了描述,具体原型链继承的方式在 3.2 原型链继承 中描述。

    2.1 属性获取

    Javascript的对象中属性获取的时候,会根据一定的步骤进行取值:
    首先判断对象中是否存在该属性,如果存在该属性则返回属性值。
    否则访问该对象的原型对象(__proto__),判断其是否存在该属性,如果存在,则返回该属性值。
    否则继续遍历原型对象,直到Object.prototype如果仍然没有找到该属性则返回undefined

    var o1 = {a: 1};
    console.log(o1.a); // 1, obj中存在a,直接返回
    function F(){};
    F.prototype.b = 2;
    var o2 = new F;
    console.log(o2.hasOwnProperty('b')); // false
    console.log(o2.b); // 2, obj的原型对象中存在属性b,返回该属性
    var o3 = {};
    console.log(o3.b); // undefined , 由于obj.__proto__中并不存在该属性,所以返回undefined
    

    2.2 属性设置

    属性设置也会遍历原型链,但是根据属性存在位置以及原型链上属性描述符的不同,可能会存在不同的设置结果:

    // 1. 对象中存在该属性 , 直接修改属性值
    var o1 = {a: 1};
    console.log(o1.__proto__.a); // undefined
    console.log(o1.a); // 1
    o1.a = 2; 
    console.log(o1.__proto__.a); // undefined ,原型链上并没有增加该属性
    console.log(o1.a); // 2,当前对象属性值被修改
    
    // 2. 对象中不存在该属性,原型链上也不存在该属性,则对象中增加该属性
    var o2 = {};
    console.log(o2.__proto__.a); // undefined
    console.log(o2.hasOwnProperty('a')); // false
    o2.a = 1;
    console.log(o2.__proto__.a); // undefined ,原型链上并没有增加该属性
    console.log(o2.hasOwnProperty('a')); // true
    
    // 3. 对象中不存在该属性,原型链上存在该属性且不为只读,则当前对象增加该属性,并屏蔽原型链属性值
    function F() {}
    F.prototype.a = 1;
    var o3 = new F;
    console.log(o3.__proto__.a); // 1
    console.log(o3.hasOwnProperty('a')); // false
    o3.a = 2;
    console.log(o3.__proto__.a); // 1, 原型链上属性没有发生变化
    console.log(o3.hasOwnProperty('a')); // true,对象增加属性
    console.log(o3.a); // 2,屏蔽原型链上属性
    
    // 4. 对象中不存在该属性,原型链上该属性为只读,则不会在当前对象中增加该属性,且该属性值不变
    function F() {}
    Object.defineProperty(F.prototype, 'a', {
        writable: false,
        value: 1
    })
    var o4 = new F;
    console.log(o4.__proto__.a); // 1
    console.log(o4.hasOwnProperty('a')); // false
    o4.a = 2;
    console.log(o4.__proto__.a); // 1, 原型链上属性没有发生变化
    console.log(o4.hasOwnProperty('a')); // false,对象中没有增加该属性值
    console.log(o4.a); // 1,获取原型链上属性
    
    // 5. 对象中不存在该属性,原型链上存在该属性的setter方法,则会调用该setter方法
    function F() {}
    Object.defineProperty(F.prototype, 'a', {
        set(){
            console.log('set a');
        }
    })
    var o5 = new F;
    console.log(o5.__proto__.a); // undefined
    console.log(o5.hasOwnProperty('a')); // false
    o5.a = 2; // 'set a'
    console.log(o5.__proto__.a); // undefined, 原型链上属性没有发生变化
    console.log(o5.hasOwnProperty('a')); // false,对象中没有增加该属性值
    console.log(o5.a); // unedefined,获取原型链上属性值并没有变
    

    3. 继承

    3.1 混入

    1.2 继承和多态 中所说,一般面向对象编程语言的继承都是在对象实例化的时候,采用复制的方式将父类的内容深度复制一份到实例中,由于Javascript中并没有实例化的过程,但是可以通过对象复制的方式来实现继承关系,这样的方式可以叫做混入。
    一般的显示混入,在混合对象的过程中,会将目标对象中不存在的属性进行复制添加。

      // 显示混入函数
      function mixin(target, source) {
        for(let key in source){
          if(!target.hasOwnProperty(key)){  // 不存在该属性,则添加该属性
            target[key] = source[key];
          }
        }
      }
    

    这样就可以将源对象的属性添加到目标对象属性中,类似复制的原理实现继承关系
    当然也可以直接创建一个对象包含所有属性,用新对象属性覆盖掉原对象属性并返回。

    3.2 原型继承

    3.2.1 Function.prototypeObject.__proto__

    Javascript中默认的继承机制并没有使用类似上面的复制机制实现,而是利用Javascript中的对象,通过对象关联的方式进行实现继承,也就是原型继承。
    首先Javascript的Function对象中,默认包含一个不可枚举的prototype属性,该属性的值为对应的原型对象,其结果为包含一个不可枚举属性constructor的对象

    function F() {}
    console.log(F.hasOwnProperty('prototype')); // true
    console.log(Object.propertyIsEnumerable(F.prototype)); // false
    console.log(F.prototoype); // { constructor: f}
    

    注意:这里我们可以使用new F的方式创建对象,这种方式类似Java等面向对象语言中的实例化,F类似构造函数,但是Javascript中不存在类,所以这只能理解为构造函数方法调用。且这里的constructor并不代表对象的构造关系。
    Javascript对象中存在__proto__属性,指向对象的原型对象

    function F() {};
    var f = new F();
    console.log(f.__proto__ === F.prototype); // true
    

    3.2.2 Javascript原型继承实现

    原型继承利用了Object.create()方法实现

    function F() {}
    F.prototype.a = 1;
    function G() {}
    G.prototype = Object.create(F.prototype);
    var g = new G;
    console.log(g.a); // 1,根据原型链机制获取到原型对象F.protoype中属性'a'的值
    console.log(g.__proto__ === G.prototype); // true
    console.log(g.__proto__.__proto__ === F.prototype); // true
    

    其中,Object.create()的实现原理:

    function create(o) {
      function F(){}
      F.prototype = o;
      return new F();
    }
    

    通过代码我们可以看到,利用Object.create()关联了对象,使得GF联系了起来,同样通过这个就可以知道为什么Object.create(null)创建出来的对象对象不在Object.prototype链上了

    3.3 行为委托

    对于Function的对象,利用Function.prototype原型链,创建了对象的关联,对于两个对象之间,根据Object.create()的原理,也可以直接创建关联,通过这样的方法,在属性获取不到的时候,当前对象会委托关联对象进行数据获取。

    var o1 = {a: 1};
    var o2 = Object.create(o1);
    console.log(o2.a); //1 , 获取o1对象上的值
    

    因为Javascript中继承本身就是对象之间的关联,所以比起使用原型继承的方式,需要使用new等看起来像构造函数的方式实现继承关系,利用行为委托更优秀。Javascript继承实现的本质对象关联

    // 原型链实现继承,通常子类含有父类相同方法并进行重写
    function F(width, height){
      this.width = width;
      this.height = height;
    }
    F.prototype.width = 1
    F.prototype.height = 1;
    F.prototype.square = function () {
      return this.width * this.height
    }
    function G(width, height){
        F.call(this, width, height); // 需要使用这种显示的方式来调用父类构造器实现初始化
    }
    G.prototype = Object.create(F.prototype);
    G.prototype.square = function () {
      return 1/2 * this.width * this.height
    }
    var g = new G(2, 1);
    g.square(); // 1;
    
    // 行为委托实现继承,子对象和父对象方法名一般不同
    var F = {
      init(width, height) {
        this.width = width;
        this.height = height;
      },
      rectSquare() {
        return this.width * this.height
      }
    }
    var G = Object.create(F);
    G.build = function(width, height){
      this.init(width, height); // 利用this来实例化对象
    };
    G.angelSquare = function() {
      return 1/2 * this.width * this.height;
    }
    G.build(2, 1);
    G.angelSquare(); // 1;
    

    4. 其他

    4.1 ES6中class

    ES6中引入了class关键字和extends关键字来实现Javascript中的继承,使得看起来更像一般的面向对象语言,但是实际上这里的class只是原型继承的语法糖,本质还是对象的关联,并非类的复制,所以当改变原型对象的内容会影响到对应的对象。

    class F {
      constructor(name) {
        this.name = name;
      }
      log() {
        return 'log:' + this.a
      }
    }
    var f = new F('patrick');
    F.prototype.log = function () {
      return 'new log:' + this.name
    }
    console.log(f.log()); // new log: patrick 修改了原型对象,影响了实际对象。
    

    4.2 hasOwnProperty

    由于我们所说的Javascript对象中属性获取和设置是需要在原型链上进行查找的,所以使用hasOwnProperty值来判断是否为当前对象属性,可以阻断原型链上的查找,急速性能

    4.3 Object.prototype

    对于所有的对象,最终原型对象都指向Object.prototype,而且Object.prototype的原型对象为null

    5. 参考:

    《你不知道的Javascript(上篇)》
    MDN Inheritance and the prototype chain

    相关文章

      网友评论

          本文标题:Javascript学习笔记-原型链

          本文链接:https://www.haomeiwen.com/subject/fcxfdxtx.html