JavaScript之原型

原型的介绍

在ES6之前,JavaScript是没有class关键字的,声明一个类是通过原生的构造函数来实现。下面的例子是使用function Animal()声明一个动物类,这样就可以通过new 获取到动物实例cat了:

   function Animal(name) {
    this.name = name;
   }
   let cat = new Animal('Tom');
   console.log(cat.name); // Tom

约定构造函数名称首字母需大写以区分普通函数,并且必须通过new关键字使用。new一个构造函数实例时内部隐式声明一个对象this,对其赋值后将原型指向构造函数的prototype,然后将其返回。构造函数默认没有return,如果加了return返回的是一个对象,该对象就会覆盖this对象,返回其他值则会忽略。

JavaScript中所有的对象都有一个隐藏的[[Prototype]]特性,所有的函数都具有一个prototype属性,它俩就称为原型,其实前者是一个指针指向后者,后者是一个对象属性。[[Prototype]]的值要么为null要么为另一个对象,不能直接获取[[Prototype]],可以通过console.dir(obj)来查看obj的结构,里面就能清晰的看见该属性以及相关的内容。让我们一起看一下上述cat实例的结构:

可以看到[[Prototype]]的值是一个Object类型,它指向的就是我们提到的Animal.prototype。在new一个cat的时候,cat实例的[[Prototype]]就自动指向了Animal.prototype,那我们就说cat的原型是Animal.prototype。我们可以查看Animal.prototype来验证这一点,并且 console.log(cat.__ proto__ === Animal.prototype) // true。

可以看到Animal.prototype的值和[[Prototype]]是一样的。这里说明一点就是所有的函数都具有prototype对象属性,它包含一个constructor属性(默认指向函数本身),对于构造函数声明类的情况,[[Prototype]]的指向就是这个对象,拿上面的cat举例来说,它们之间的关系如下图所示:

在不声明类的时候,任何对象的[[Prototype]]也能被改变。我们可以改变对象的原型,也就是[[Prototype]]的指向,指定原型后,对象就能使用原型上的属性及方法。一个方法就是上文用到的通过__ proto __属性来改变,它其实是[[Prototype]]的setter和getter,这种方式在ES6中被setPrototypeOf()getPrototypeOf()所取代,下面演示它的使用将dog对象的原型改为cat:

 let cat = {
      name: 'Tom',
      walk() {
        console.log('walking...');
      }
    }
    let dog = {
      name: 'Dog',
      __proto__: cat
    }
​
    console.dir(dog.__proto__ === cat); // true
    console.log(dog.name); // Dog
    dog.walk(); // walking...
    console.dir(dog);

它的执行结果如下图所示:

从dog打印的结构就可以看到dog的[[Prototype]]已经被修改为cat,这也是为什么它的内部没有声明walk()方法但是它能使用的原因,因为它使用的是原型cat里的walk()方法。简单说一下这个过程:当对象使用某个属性时,会先从对象内部开始找有没有该属性,发现有就直接使用该属性的值,如果没有就会沿着[[Prototype]]往原型上找,如果还没有找到又继续往原型的原型上找,这个过程就是一条原型链,找到后就使用,如果没找到那肯定就是报错未定义了。像我们使用的除Array直接调用的方法之外的数组方法都是定义在原型上的,其他的数据类型也是如此,所有的对象的根原型是Object.prototype

原型链继承

一般在开发中不会单独使用原型,会使用继承。上文提到在ES6之前想使用类是通过原生构造函数来实现的,而实现类的继承则是通过原型链来实现的,对象可以使用原型定义的属性和方法,通过这点就可以实现继承。我们举个例子来实现一下原生的继承:

    function SuperType() {
      this.property = true;
    }
    SuperType.prototype.getvalue = function (){ // 在原型上添加一个方法
      return this.property;
    }
​
    function SubType() {
      this.property = false;
    }
​
    SubType.prototype = new SuperType(); // 改变原型对象实现继承
    let instance = new SubType();
    console.log(instance.getvalue()); // false

其中SuperType是父类,SubType是子类,这里要说明一点,将子类的原型对象指向父类的实例从而实现了继承,就不能再改变子类原型对象,否则就切断原型链而无法继承了。让我们画图说明一下它们之间的联系:

可以看出,子类的实例instance依照原型链就访问到了方法getValue()。将父类的实例作为instance的原型就继承了父类的属性property,只不过子类本身就有property属性,所以打印出来的值是子类的false。我们说过prototype对象中默认有一个constructor指向函数本身,但是一旦将该prototype重新赋值可能就不存在constructor了(取决于赋值的对象中是否有constructor属性),就算有也不会再指向该函数本身了,如果需要用到constructor的话就得重新定义

盗用构造函数

上文提到的原型链继承有一个问题就是所有的子类共享一个实例数据,对于普通类型的数据倒也不影响,但是如果想独立使用引用类型的数据就不行了,一旦改了某个实例其他的也会受影响。这时候就出现了盗用构造函数,它可以解决原型链中出现的一些问题。

    function SuperType() {
      this.property = [1];
    }
​
    function SubType() {
      SuperType.call(this);      
    }
​
    let instance1 = new SubType();
    let instance2 = new SubType();
    instance1.property.push(2);
    console.log(instance1.property); // [1, 2]
    console.log(instance2.property); // [1]

将SuperType的执行上下文拿到SubType中来,这样instance1和instance2就有了property属性,并且它俩之间是独立的,改变其中一个不会影响其他的使用。但是这么看来,盗用构造函数也有一个问题,就是想使用的所有东西都得放在构造函数中,包括能共用的函数。原本将函数放在原型上,所有实例都能共享到它,但盗用构造函数只是实现了继承没有用到原型机制,所以实际盗用函数也不会单独使用。

组合继承

组合继承同时使用了原型链和盗用构造函数,保证了每个实例间的数据独立,并且也能共享在原型上声明的方法。

    function SuperType() {
      this.property = [1];
    }
​
    SuperType.prototype.getValue = function (){ // 原型上添加一个方法所有实例共用
      return this.property;
    }
​
    function SubType() {
      SuperType.call(this); // 第一次调用
    }
​
    SubType.prototype = new SuperType(); // 第二次调用
    let instance1 = new SubType();
    let instance2 = new SubType();
​
    instance1.property.push(2);
    console.log(instance1.getValue()); // [1, 2]
    console.log(instance2.getValue()); // [1]

这里原型链之间的关系和上文提到的是一样的,就不画图了,区别就是用到了盗用构造函数,因此实例的数据独立。property是引用类型数据,通过盗用函数实现继承,每个实例都是独立的,因此改变instance1的值不会影响instance2。getValue()方法作用是获取property值,将它放在Super的原型对象中,所有实例都能共享它返回自己的property值,不需要每个实例都去实现,提高了效率。虽然组合继承非常好用,但理解了上文那张原型链图就不难发现组合继承的一个问题,那就是子类实例里的数据在原型上也有,也即是父类的构造函数始终会被调用两次,这也是降低了效率。

原型式继承

原型式继承可以不直接通过(父类)构造函数实现继承,它的实现是这样的:

    function setPrototype(obj) {
      function F() {}; // 临时的构造函数
      F.prototype = obj; // 将其原型对象赋值为obj
      return new F(); // 此时返回的实例原型[[Prototype]]就是指向obj
    }
​
    let person = {
      name: 'Y',
      arr: [1],
      sayHello() {
        console.log('hello');
      }
    }
​
    let foo = setPrototype(person); // foo的原型为person,所以可以访问其属性和方法
    foo.name = 'W'
    foo.arr.push(2);
    console.log(foo.name); // W
    foo.sayHello(); // hello
​
    let bar = setPrototype(person); // bar的原型为person,所以可以访问其属性和方法
    console.log(bar.name); // Y
    console.log(bar.arr); // [1, 2] 

创建构造函数修改原型对象并返回实例是setPrototype()函数做了,这种情况就适用于无需关心构造函数而实现继承,但它们的引用类型数据依旧是共享的,和构造函数继承是一样的,可以理解为浅复制。通过这种方式就可以解决上文说到的那个问题了。

   function object(o) {
      function F() {};
      F.prototype = o;
      return new F();
    }
    
    function SuperType() {
      this.property = [1];
    }
​
    SuperType.prototype.getValue = function (){ // 原型上添加一个方法所有实例共用
      return this.property;
    }
​
    function SubType() {
      SuperType.call(this);      
    }
​
    SubType.prototype = object(SuperType.prototype); // 直接改变原型
​
    let instance1 = new SubType();
    let instance2 = new SubType();
​
    instance1.property.push(2);
    console.log(instance1.getValue()); // [1, 2]
    console.log(instance2.getValue()); // [1]
    console.dir(instance1.__proto__);

这样通过盗用构造函数实现继承属性,通过原型实现共用方法,就基本上没啥问题了。对于原型以及原生继承就讨论到这里了,虽然ES6增加了class关键字声明类,其实它内部还是使用了构造函数和原型,理解了原型再去使用class会更加清晰。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值