原型与继承(整理)

99 阅读13分钟
/**
 * 读完需要 明白的问题
  为什么 typeof 判断 null 是 Object 类型?
  Function 和 Object 是什么关系?
  new 关键字具体做了什么?手写实现。
  prototype 和__proto__是什么关系?什么情况下相等?
  ES5 实现继承有几种方式,优缺点是啥
  ES6 如何实现一个类
  ES6 extends 关键字实现原理是什么
1. 原型一把梭
  先上经典神图
    如图'经典神图'
  *function Foo 就是一个方法,比如JavaScript中的Array String等
  *function Object 就是一个Object
  *function Function 就是 Function
  *以上都是function 所以 .__proto__ 都是Function.prototype
  *再次强调,String Array Number Function Object 都是function
  接下来梳理概念
  1.1 函数对象和普通对象
  // function Obj(props = {}) {
  //   this.name = props.name
  //   this.age = props.age
  // }
  // const obj2 = new Obj({name: 'jj'})
  // const obj1 = {}
  // obj2.__proto__
  // obj1.__proto__
  先不管上面的代码意义,都是对象却存在着差异
  其实在JavaScript中,我们将对象分为函数对象和普通对象,所谓函数对象就是JavaScript用函数模拟的类 实现。 JavaScript中的Object和 Function 就是典型的函数对象
  关于函数对象和普通对象,直接看代码
  function fun1() {}
  const fun2 = function() {}
  const fun3 = new Function('name', 'console.log(name)')

  const obj1 = {}
  const obj2 = new Object()
  const obj3 = new fun1()
  const obj4 = new new Function()

  console.log(typeof Object) // function
  console.log(typeof Function) // function
  console.log(typeof fun1) // function
  console.log(typeof fun2) // function
  console.log(typeof fun3) // function

  console.log(typeof obj1) // object
  console.log(typeof obj2) // object
  console.log(typeof obj3) // object
  console.log(typeof obj4) // object
  上述代码,obj1 obj2 obj3 obj4 都是普通对象, fun1 fun2 fun3 都是Function 函数对象
  所以看出,所有的Function的实例,都是函数对象,其他的均为普通对象,其中包括Function实例的实例
  JavaScript中万物皆对象,而对象皆出自构造函数
  上图中,你疑惑的点是不是 Function 和 new Function 的关系。其实是这样子的:
  Function.__proto__ === Function.prototype // true

  1.2 __proto__
    【首先我们明确两点 __proto__ 和 constructor 是对象独有的 prototype 属性是函数独有的】
    但是在JavaScript中,函数也是对象,所以函数也拥有__proto__ 和 constructor属性
    function Person() {}
    let renzhen = new Person()
    如图: 'Object 和 Function关系'
    我们讲解下__proto__ ,这个说起来很复杂 是个历史问题
    ECMAscript 规范描述,prototype是一个隐式引用,但是之前的浏览器已经私自实现了__proto__ 这个属性,使得可以通过 obj.__proto__ 这个显式的属性访问,访问到被定义隐式属性的prototype
    因此情况是这样的,ECMAscript规范说prototype 应当是一个隐式引用
    *通过Object.getPrototypeOf(obj) 间接访问 指定对象的 prototype 对象
    *通过 Object.setPrototypeOf(obj, anthoerObj) 间接设置 指定对象的prototype 对象
    * 部分浏览器提前开了 __proto__ 的口子,使得可以通过 obj.__proto__ 直接访问原型 通过obj.__proto__ = antherObj 直接设置原型
    * ECMAscript2015 规范只好屈服, 将__proto__ 属性纳入规范

    所以从浏览器的打印结果我们可以看出,上图对象a 存在一个__proto__属性,而事实上,它只是开始开发者工具方便开发者查看原型的故意渲染出来的一个虚拟节点,虽然可以查看,但事实并不存在该对象上
    __proto__属性不能被 for in 遍历出来, 也不能 Object.keys(obj) 查找出来
    访问对象的 obj.__proto__ 属性,默认走的是 Object.prototype 对象的 __proto__ 属性的get/ set方法
    Object.defineProperty(Object.prototype, '__proto', {
      get() {
        console.log('get')
      }
    })
    ({}).__proto__
    cosnole.log((new Object()).__proto__) // get  get
    关于更多的__proto 更深入的介绍,可以观看 深入理解JavaScript原型 文章
    这里我们需要的是,__proto__ 是对象所独有的,并且 __proto__是一个对象指向另一个对象,也就是他的原型对象。我们也可以理解为父类对象,他的作用就是当你访问一个对象属性的时候,如果该对象内容没有存在这个属性,那么他就会去他的 __proto__属性所指向的对象(父类对象)属性中查找,如果父类对象依旧不存在这个属性,那么就会在这个对象的父类__proto__ 属性所指的父类去查找。以此类推,直到找到 null 这个查找的链子也就是原型链

  1.3 prototype
    object that provids shared properties for other objects
    在规范里,prototype 被定义为:给其它对象提供共享属性 的对象, prototype自己也是对象,只是用来承担某个职能
    所有对象,都可以作为另一个对象 prototype 来用
    如图 'prototype' 
    修改 __proto__ 的关系图,我们添加了 prototype, prototype 是【函数所独有的】【他的作用就是包含 可以给特定类型的所有实例提供 共享的属性和方法,它的含义就是函数的运行对象】也就是说这个函数所创建的实例的运行对象, 正如上图 renzhen.__proto__ === Person.prototype 任何函数在创建的时候,都会默认给函数添加prototype 属性

  1.4 constructor
    constructor 属性也是对象所独有的,它是一个对象指向一个函数,这个函数,就是该对象的构造函数
    【注意】每个对象都有其对应的构造函数,本身或者继承而来,但从constructor这个属性来讲,只有prototype对象才有, 每个函数在创建的时候,JavaScript 会同时创建一个 该函数对应的 prototype对象,而函数创建的对象.__proto__ ==== 该函数的prototype
    该函数的.prototype.constructor === 该函数本身。 故通过函数创建的对象即使自己没有constructor属性, 它也能通过 __proto__ 找到对应的constructor, 所以任何对象最终都可以找到其对应的构造函数
    【唯一特殊的是】开篇中提出的问题,JavaScript 原型的老祖宗: Function 它是自己的构造函数, 所以 Function.prototype ===  Function.__proto__
    为了理解,我们在 原来两个图里加上constructor
    如图 '加上constructor的'
    其中 constructor 属性,虚线表示继承而来的 constructor 属性。
    __proto__介绍的原型链,我们在图中直观的标出来的话就是如下这个样子
    如图 '加上constructor的'
  

2. typeof && instanceof 原理浅析
  2.1 typeof 基本用法
    MDN 文档点击这里:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/typeof
    2.1.1 基本用法
    typeof 的用法 比较熟悉判断一个变量类型,typeof可以判断 string boolean number function undefined symbol object 这七种数据类型,但是在判断object有问题,不能明确的告诉你 object是哪种object
    let s = new String('ab')
    typeof s === 'object' // true
    typeof null // object
    2.1.2 原理浅析
    typeof null 为object 其实需要从js如何存储变量类型来说,js设计的一个bug
    在JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型 的标签和实际数据值表示的,对象的类型标签是0,由于null 代表的是空指针,因此null 的类型标签也是0,typeof null 返回了object 曾经有个提案 typeof null === null 
    js在底层变量存储的时候,会在变量的机器码的低位 1-3 位存储类型信息
    **1: 整数
    **110 布尔
    **100 字符串
    **010 浮点数
    **000 对象
    但是对于undefined 和null来说 这两个值的存储有特殊的
    null 所有机器码都为0
    undefined 用-2^30整数来表示
    所以在用typeof 来判断变量类型的时候,我们需要注意,最好是用typeof 来判断基本数据类型 避免对null 的判断

  2.2 instanceof 基本用法
    istanceof 和typof 相似  instanceof 运算符来监测 constructor.prototype 是否存在于参数 object 原型链上, 与typeof方法不同的是 instanceof方法 要求开发者明确的 确认对象为某特定类型
    function C() {}
    function D() {}
    
    var o = new C()
    o.instanceof C // true 因为 Object.getPrototypeOf(o) === C.prototype
    o instanceof D; // false,因为 D.prototype 不在 o 的原型链上
    o instanceof Object // true 因为 Object.prototype.isPrototypeOf(o) 返回true
    C.prototype instanceof Object // true,同上

    C.prototype = {}
    var o2 = new C()
    o2 instanceof C // true
    o instanceof C // false  C.prototype 指向了一个空对象 这个空对象不在o的原型链上

    D.prototype = new C() // 继承
    var o3 = new D()
    o3 instanceof D // true
    o3 instanceof C; // true 因为 C.prototype 现在在 o3 的原型链上
    
    instanceof 原理浅析
      经过上述的分析,相比大家对这种经典神图已经不那么陌生了吧,那咱就对着这张图来聊聊 instanceof
      '经典神图'
      这里将规范定义 翻译为 JavaScript代码 如下
      function instance_of(L, R) {
        var O = R.prototype // 取R的显示原型
        L = L.__proto__ // 取L的隐式原型
        while(true) {
          if (L === null) {
            return false
          }
          if(O === L) { // 这里中的概念当O 严格等于 L 时,返回
            return true
            L = L.__proto__
          }
        }
      }
      所以如上原理, 再加上上文解释的原型知识,我们解释下 为什么Object 和 Function, instanceof 自己等于true
      ** Object instanceof Object
      // 为了方便表述,首先区分左侧表达式和右侧表达式
      ObjectL = Object, ObjectR = Object
      // 下面根据规范逐步推
      O = ObjectR.prototype = Object.prototype
      L = ObjectL.__proto__ = Function.prototype
      // 第一次判断
      O != L
      // 循环查找 L 是否还有__proto__
      L = function.prototype.__proto__ = Object.prototype
      // 第二次判断
      O == L
      // 返回true

      **Function instanceof Function
      // 为了方便表述,首先区分左侧表达式和右侧表达式
      FunctionL = Function, FunctionR = Function
      // 下面根据规范逐步推离
      O = FunctioinR.prototype = Function.prototype
      L = FunctionL__proto__ = Function.prototype
      // 第一次判断
      O == L
      // 返回true

      Foo instanceof Foo
      // 为了方便表述,首先区分左侧表达式和右侧表达式
      FooL = Foo, FooR = Foo; 
      // 下面根据规范逐步推演
      O = FooR.prototype = Foo.prototype 
      L = FooL.__proto__ = Function.prototype 
      // 第一次判断
      O != L 
      // 循环再次查找 L 是否还有 __proto__ 
      L = Function.prototype.__proto__ = Object.prototype 
      // 第二次判断
      O != L 
      // 再次循环查找 L 是否还有 __proto__ 
      L = Object.prototype.__proto__ = null 
      // 第三次判断
      L == null 
      // 返回 false


3.ES5 中的继承实现方式
  3.1 new 关键字
  一个例子看下new 关键字都干了什么
  function Person(name, age) {
    this.name = name
    this.age = age
    this.sex = 'male'
  }
  Person.prototype.inHandsome =  true
  Person.prototype.sayName = function() {
    console.log(`Hello, my name is ${this.name}`)
  }
  let handsomeBoy = new Person('renzhen', 24)
  console.log(handsomeBoy.name) // renzhen
  console.log(handsomeBoy.sex) // male
  cosole.log(handsomeBoy.isHandsome) // true
  handsomeBoy.sayName() // Hello my name is renzhen
  从上面的例子我们可以看到
    * 访问到Person 构造函数里的属性
    * 访问到Person.prototype 里的属性
    3.1.1 new 手写版本一
    function objectFactory() {
      const obj = new Object() // 从Object.prototype 克隆一个对象
      Constructor = [].shift.call(arguments) // 取得外部传入的构造器
      const F = function() {}
      F.prototype = Constructor.prototype
      obj = new F() // 指向正确的原型
      Constructor.apply(obj, arguments) // 借用外部传入的构造器给obj设置属性
      return obj // 返回obj
    }
    **用new Object() 的方式新建一个对象 obj
    ** 取出第一个参数,就是我们要传入的构造函数,此外因为shift会修改原数组,所以arguments 会被去除第一个参数
    ** 将obj 的原型指向构造函数,这样obj就可以访问到构造函数原型中的属性
    ** 使用 apply 改变构造函数 this 的指向到新建的对象,这样obj就可以访问到构造函数的属性
    ** 返回obj

    下面我们测试下
    function Person(name,age){
      this.name = name;
      this.age = age;
      this.sex = 'male';
    }
    Person.prototype.isHandsome = true;
    Person.prototype.sayName = function(){
      console.log(`Hello , my name is ${this.name}`);
    }

    function objectFactory() {
      const obj = new Object()
      Constructor = [].shift.call(arguments)
      const F = function(){}
      F.prototype = Contructor.prototype
      obj = new F()
      Constructor.apply(obj, arguments)
      return obj
    }

    let handsomeBoy = objectFactory(Person,'Nealyang',25);
    console.log(handsomeBoy.name) // Nealyang
    console.log(handsomeBoy.sex) // male
    console.log(handsomeBoy.isHandsome) // true
    handsomeBoy.sayName(); // Hello , my name is Nealyang
    // 注意上面我们没有直接修改 obj 的__proto__隐式挂载。

    3.1.2 new 手写版本二
    如果构造函数返回一个对象,那么我们也返回这个对象
    如上否则,就返回默认值
    function objectFactory() {
      var obj = new Object()
      Constructor = [].shift.call(arguments)
      var F = function() {}
      F.prototype = Constructor.prototype
      obj = new F()
      var set = Constructor.apply(obj, arguments)
      return typeof ret === 'object' ? ret : obj
    }
  3.2 类式继承
    function SuperClass() {
      this.superValue = true
    }
    SuperClass.prototype.getSuerValue = function() {
      return this.superValue
    }
    function SubClass() {
      this.subValue = false
    }
    SubClass.prototype = new SuperClass()
    SubClass.prototype.getSubValue = function() {
      return this.subValue
    }
    var instance = new SubClass()
    console.log(instance instanceof SuperClass) //true
    console.log(instance instanceof SubClass) //true
    console.log(SubClass instanceof SuperClass) //false
    从我们之前介绍的 instanceof 的原理我们知道,第三个 console 如果这么写就返回 true 了console.log(SubClass.prototype instanceof SuperClass)
    虽然实现起来清晰简单,但是有两个缺点
    ** 由于子类通过其原型prototype对父类进行实例化,继承了父类,所以说父类如果有共有属性是引用类型,就会在子类中被所有的实例共享,因此一个子类的实例更改子类型原型从父类的构造函数中继承的共有属性,就会直接影响到其它子类
    ** 由于子类实现的继承靠 原型prototype 对父类进行实例化实现的,因此在创建父类的时候,是无法向父类进行传参,因此实例化父类的时候,也无法对父类的构造函数内的属性进行初始化。

  3.3 构造函数继承
    function SuperClass(id) {
      this.books = ['js', 'css']
      this.id = id
    }
    SuperClass.prototype.showBooks = function() {
      console.log(this.books)
    }
    function SubClass(id) {
      SuperClass.call(this, id)
    }
    var instance1 = new SubClass(10)
    var instance2 = new SubClass(11)

    instance1.books.push('html')
    console.log(instanc1)
    console.log(instance2)
    instance1.showBooks() // TypeError

    SuperClass.call(this, id)当然是构造函数,的核心语句,由于父类中给this绑定属性,因此父类自然继承 父类的共有属性,由于这种类型的继承没有涉及到原型prototype, 所以父类的原型方法不会被子类继承,而如果想被子类继承,就必须放构造函数中,这样创造出来的每一个实例都会单独的拥有一份而不能共用, 这样就违背了代码复用的原则, 综合以上两个,我们提出组合式继承方法

  3.4 组合式继承
    function SuperClass(name) {
      this.name = name 
      this.books = ['js', 'css']
    }
    SuperClass.prototype.getBooks = function() {
      console.log(this.books)
    }
    function SubClass(name, time) {
      SuperClass.call(this, name)
      this.time = time
    }
    SubClass.prototype = new SuperClass()
    SubClass.prototype.getTime = function() {
      console.log(this.time)
    }
    如下,我们解决了之前说的问题,但是从代码上看,这个SuperClass 的构造函数执行了两遍就感觉非常不妥

  3.5 原型式继承
    function inheritObject(o) {
      function F() {}
      F.prototype = o
      // 返回过度对象的实例,该对象的原型继承了父对象
      return new F()
    }
    原型式继承大致实现方法如上,是想到了new 关键字模拟实现
    其实这种方式和类式继承非常相似,它只是对类式继承的一个封装,其中的过滤对象就相当于类式继承的子类,只不过在原型继承中作为一个普通的过渡对象的存在,目的是为了创建要返回的新的实例对象
    var book = {
      name: 'js book',
      likeBook: ['js', 'css']
    }
    var newBook = inheritObject(book)
    newBook.name = 'renzhenbook'
    newBook.liekBook.push('vue')
    var otherBook = inheritObject(book)
    otherBook.name = 'react'
    otherBook.likeBook.puhs('nodebook')
    console.log(newBook, otherBook)
    如上代码可以看出,原型式继承和类式继承 一样样子,对于引用类型的变量,还是存在子类实例共享的情况

  3.6 寄生式继承
    var book = {
      name: 'js book',
      liekBook: ['html', 'css']
    }
    function createBook(obj) {
      // 通过原型方式创建模板的对象
      var o = new inheritObject(obj)
      // 拓展新对象
      0.getName = function(name) {
        consoel.log(name)
      }
      return o;
    }
    其实寄生式继承就是对原型继承的拓展,一个二次封装的过程,这样新创建的对象不仅仅有父类的属性和方法,还新增了别的属性和方法

  3.7 寄生组合式继承

4. ES6 类的实现原理
  关于 ES6 中的 class 的语法糖包括继承的一些实现方式
  基础类
  如图'基础类'
  function _instanceof(left, right) {
    if(
      right !== null &&
      typeof Symbol !== 'undefined' &&
      right[Symbol.hasInstance]
    ) {
      return !!right[Symbol.hasInstance](left)
    } else {
      return left instanceof right
    }
  }
  function _classCallCheck(instance, Contructor) {
    if(!_instanceof(instance, Contructor)) {
      throw new TypeError('Cannot call a class as a function')
    }
  }
  var Person = function Person(name) {
    _classCallCheck(this, Person)
    this.name = name
  }
  _instanceof 就是用来判断实例关系的,上述代码就比较简单了,_classCallCheck的作用就是检查Person这个类,是否是通过new关键字调用的, 毕竟被编译成ES5之后,function 可以直接调用,但是如果直接调用的话,this就指向window对象 就是throw Error

  添加属性
  function _instanceof(left, right) {

  }
  function _classCallCheck(instance, Constructor) {

  }
  function _defineProperty(obj, key, value) {
    if (key in obj) {
      Object.defineProperty(obj, key, {
        value: value,
        enumerable: true,
        configurable: true,
        writable: true
      })
    } else {
      obj[key] = value
    }
    return obj
  }
  var Person = function Person(name) {
    _classCallCheck(this, Person)
    _defineProperty(this, 'shili', '实例属性')
    this.name = name
  }
  _defineProperty(Person, 'jingtai', '静态属性')

  添加方法
  function _instanceof(left, right) {}
  functioin _classCallCheck(instance, Contructor) {}
  function _defineProperty(obj, key, value) {}
  function _defineProperties(target, props) {
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i]
      descriptor.enmuerable = descriptor.enmuerable || false
      descriptor.configuable = true
      if("value" in descirptor) descriptor.writable = true
      Object.defineProperty(target, decriptor.key, descriptor)
    }
  }
  function _createClass(Constructor, protoProps, staticProps) {
    if(protoProps) _defineProperties(Constructor.prototype, protoProps)
    if (staticProps) _definePropperties(Constructor, staticProps)
    return Contructor
  }
  var Person = {
    function() {
      function Person(name) {
        _classCallCheck(this, Person)
        _defineProperty(this, 'shili', '实例属性')
        this.name = name
      }
      _createClass(Person, [{
        key: 'sayName',
        value: function sayName() {
          return this.name
        }
      }, {
        key: 'name',
        get:function get() {
          return 'renzhen'
        },
        set: function set(newName) {
          console.log('new name is' + newName)
        }
      }], [{
        key: 'eat',
        value: function eat() {
          return ' eat food'
        }
      }])
      return Person
    }()
  }
  看起来代码不少,其实就是一个_createClass函数和_defineProperties 函数
  首先看_createClass 这个函数的三个参数,第一个是构造函数,第二个是需要添加到原型上的函数数组,第三个是添加到类本身的函数数组,其实这个函数的作用非常简单,就是加强一下构造函数,所谓的加强构造函数就是给构造函数或者原型上加上一些函数
  而 _defineProperties 就是多个_defineProperty (感觉是废话,但是的确如此),默认enumerable 为false, configurable为true
  其实如上就是 es6 class的实现原理

  5. extend 关键字
  先上图 '关键字图片'
  function _instanceof(left, right) {...}
  function _classCallCheck(instance, Constructor) {...}
  var Parent = function Parent(name) {...}
  function _typeof(obj) {
    if (typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol') {
      _typeof = function _typeof(obj) {
        return typeof obj
      }
    } else {
      _typeof = function _typeof(obj) {
        return obj && typeof Symbol === 'function' && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj
      }
    }
    return _typeof(obj)
  }

  function _possibleConstructorReturn(self, call) {
    if(call && (_typeof(call) === 'object' || typeof call === 'function)) {
      return call
    }
    return _assertThisInitialized(self)
  }

  function _assertThisInitialized(self) {
    if (self === void 0) {
      throw new ReferenceError("this hasn not been initialised - super() has not been called ")
      return self
    }
  }

  function _getPrototypeOf(o) {
    
  }

    _inherits
    _possibleConstructorReturn
5. 最后练习

 */