原型和原型链

1,665 阅读8分钟

原型和原型链

1.理解原型设计模式以及JavaScript中的原型规则

  • 所有的引用类型(数组、对象、函数),都具有对象特征,即可自由扩展属性。

  • 所有的引用类型,都有一个属性下划线__proto__`属性(隐式原型),属性值是一个普通对象;

  • 所有函数,都具有一个prototype(显示原型),属性值是只有属性 constructor 的对象,属性 constructor 指向函数自身;

  • 所有的引用类型(数组、对象、函数),其隐式原型指向其各自的原型对象;

  • 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__属性(即它的构造函数的prototype)中去寻找;

    // 惯例,构造函数应以大写字母开头
    function Person(name) {
      // 函数内this指向构造的对象
      // 构造一个name属性
      this.name = name
      // 构造一个sayName方法
      this.sayName = function() {
        console.log(this.name)
      }
    }
    Person.prototype.drink = function() {
        console.log('喝东西')
    }
    // 使用自定义构造函数Person创建对象
    let personA = new Person('张三')
    personA.__proto__.eat = function() {
        console.log('吃东西')
    }
    let personB = new Person('李四')
    personB.eat() // 输出:吃东西
    personB.drink() // 输出:喝东西
    
    

    总结一下

    1. 对象有__proto__属性,函数有__proto__属性,数组也有__proto__属性,只要是引用类型,就有__proto__属性,指向其原型。
    2. 只有函数有prototype属性,只有函数有prototype属性,只有函数有prototype属性,指向new操作符加调用该函数创建的对象实例的原型对象。

2.instanceof的底层实现原理,手动实现一个instanceof

instanceof的作用:用于检测右侧构造函数的原型是否存在于左侧对象的原型链上。

const isObj = obj => ((typeof obj === 'object') || (typeof obj === 'function')) && obj !== null
function myInstanceOf(instance,Ctor){
   if (!isObj(Ctor)) // 右侧必须为对象
        throw new TypeError('Right-hand side of 'instanceof' is not an object')

    const instOfHandler = Ctor[Symbol.hasInstance]
    // 右侧有[Symbol.hasInstance]方法,则返回其执行结果
    if (typeof instOfHandler === 'function') return instOfHandler(instance)
        
    // 右侧无[Symbol.hasInstance]方法且不是函数的,返回false
    if (typeof Ctor !== 'function') return false
        
    // 左侧实例不是对象类型,返回false
    if (!isObj(instance)) return false
    
    // 右侧函数必须有原型
    const rightP = Ctor.prototype
    if (!isObj(rightP))
        throw new TypeError(`Function has non-object prototype '${String(rightP)}' in 			instanceof check`)
        
    // 在实例原型连上查找是否有Ctor原型,有则返回true
    // 知道找到原型链顶级还没有,则返回false
    while (instance !== null) {
        instance = Object.getPrototypeOf(instance)
        if (instance === null) return false
        
        if (instance === rightP) return true
    }
}

3.实现继承的几种方式和他们的优缺点

原型链继承:一句话用父类实例作为子类原型

function parent1{
  this.name=‘parent1’
  this.play=[1,2]
}
function child1(){
  this.type=‘child1’
}
child1.prototype=new parent1()
var ch1 = new child1()

优点:简单、可服用

缺点:无法传参、单一、共享父类属性改了就全变。

构造器继承:借用call让父类构造函数来增强子类实例

function parent2(){
  this.name=‘parent2’
  this.play=[1,2]
}
parent2.prototype.getName=function(){
  reutrn this.name
}
function child2(){
  parent2.call(this)
  this.type=‘child2’
}
child2.prototype.constructor=parent2

优点:可传参、可继承多个父、解决原型的引用篡改问题。

缺点:

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现复用
  • 每个子类都有父类实例函数的副本,影响性能

组合继承

优点:结合了两种模式的优点,传参和复用

缺点:调用了两次父类构造函数(耗内存),属性和方法也是生成两份,子类的构造函数会代替原型上的那个父类构造函数。

function parent3(){
  this.name=‘parent3’
  this.play=[1,2]
}

parent3.prototype.getName =function(){
  reutrn this.name
}
function child3(){
  parent3.call(this)
  this.type=“child3”
}
child3.prototype = new parent3()
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);  // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'

原型继承

let parent4 = {
    name: "parent4",
    friends: ["p1", "p2", "p3"],
    getName: function() {
      return this.name;
    }
  };

var chid4 = Object.create(parent4)

优点:类似于复制一个对象,用函数来包装。、

缺点:1、所有实例都会继承原型上的属性。    2、无法实现复用。(新实例属性都是后面添加的)

寄生继承

 let parent5 = {
    name: "parent5”,
    friends: ["p1", "p2", "p3"],
    getName: function() {
      return this.name;
    }
  };
  //寄生就是在原型础上创建一个克隆函数然后把相关的方法在写一下
 function clone(original) {
    let clone = Object.create(original);
    clone.getFriends = function() {
      return this.friends;
    };
    return clone;
 };

优点:没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成了创建的新对象。

缺点:没用到原型,无法复用。

寄生组合继承

  //组合
  function Parent6() {
    this.name = 'parent6';
    this.play = [1, 2, 3];
  }
   Parent6.prototype.getName = function () {
    return this.name;
  }
  function Child6() {
    Parent6.call(this);
    this.friends = 'child6';
  }
	//寄生继承
  function clone (parent, child) {
    // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;
  }

  clone(Parent6, Child6);
  Child6.prototype.getFriends = function () {
    return this.friends;
  }

  let person6 = new Child6();
  console.log(person6);
  console.log(person6.getName());
  console.log(person6.getFriends());

优点:比较完美的实现了继承,解决了组合继承的问题

4.至少说出一种开元项目中应用原型继承的案例:

Jquery 、vue

5.描述new一个对象的详细过程,手动实现一个new操作符

过程:

1.创建新对象

2.设置原型,将对象的原型设置为函数的prototype对象。

3.让函数的this指向新创建的对象,执行构造函数。

4.判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

function myNew(constructor, ...args) {
    // 1. 创建一个新对象
    const obj = {};
    // 2. 为新对象添加属性__proto__,将该属性链接至构造函数的原型对象
    obj.__proto__ = constructor.prototype;
    // 3. 执行构造函数,this被绑定在新对象上
    const res = constructor.call(obj, ...args);
    // 4. 确保返回一个对象
    return res instanceof Object ? res : obj;
}

6.理解es6 class和它的构造以及继承的底层实现原理

class的由来:

JS面向对象是通过构造函数实现的,这和其它语言差异很大。es6引入了class这个概念作为对象的模版,解决了这个差异。

constructor

ES6 的类,完全可以看作构造函数的另一种写法。

class Point {
  // ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true

上面代码表明,类的数据类型就是函数,类本身就指向构造函数。

class Point {
  constructor() {
    // ...
  }
  toString() {
    // ...
  }
  toValue() {
    // ...
  }
}

// 等同于
Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};
复制代码

上面代码中,constructor()toString()toValue()这三个方法,其实都是定义在Point.prototype上面。定义的内部方法是不可枚举的,而用ES5的方式去实现就是可枚举的。

class的实例

class的实例的行为大多数和ES5的保持一致。只是class不能直接像ES5构造函数那样直接使用,必须使用new关键字。

取值getter和setter

与ES5一致,可以使用get和set关键字。

属性表达式

类的属性可以使用表达式(变量)。

Class表达式

与函数一样,类也可以使用表达式的形式定义。

const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined

static关键字

加上static关键字后,本方法(属性)不会被实例继承。只能直接通过类调用。且静态方法包含this的话,这个this是指向class本身的而不是指向实例。

父类的静态方法(属性)可以被子类继承。静态方法可以使用super关键字调用super对象。

私有方法和私有属性

利用Symbol实例来实现属性的私有,或者#提案来实现。

in运算符

判断私有属性时,in只能用在定义该私有属性的类的内部。

子类从父类继承的私有属性,也可以使用in运算符来判断。

7.class的继承

Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。(ES5常用寄生组合继承)。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // 正确
  }
}

Object.getPrototypeOf()

Object.getPrototypeOf方法可以用来从子类上获取父类。

Object.getPrototypeOf(ColorPoint) === Point
// true
复制代码

因此,可以使用这个方法判断,一个类是否继承了另一个类。猜一下这个方法其实就是招它的原型是否的构造函数是否等于父class。

super 关键字

super既可以当函数使用又可以当对象使用。

super()代表父类的构造函数(可以和es5构造器函数继承的实现一起理解,我猜想就是一个东西),super内部指向的是子类B的实例。

super作为对象式,指向父类的原型对象。在静态方法中,指向父类。

class的prototype和--proto--属性

1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性

原生构造函数的基础(基础引用数据类型的继承)

ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。下面是一个继承Array的例子。

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1

arr.length = 0;
arr[0] // undefined

Mixin 模式的实现

Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。

下面是一个更完备的实现,将多个类的接口“混入”(mix in)另一个类。

function mix(...mixins) {
  class Mix {
    constructor() {
      for (let mixin of mixins) {
        copyProperties(this, new mixin()); // 拷贝实例属性
      }
    }
  }

  for (let mixin of mixins) {
    copyProperties(Mix, mixin); // 拷贝静态属性
    copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
  }

  return Mix;
}

function copyProperties(target, source) {
  for (let key of Reflect.ownKeys(source)) {
    if ( key !== 'constructor'
      && key !== 'prototype'
      && key !== 'name'
    ) {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}
复制代码

上面代码的mix函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。

class DistributedEdit extends mix(Loggable, Serializable) {
  // ...
}