深入探究JS的原型链继承的本质

73 阅读6分钟

总结

  1. 在大多数浏览器的ES5实现中,每个对象都有__proto__属性,指向对应的构造函数的prototype。
  2. Class作为构造函数语法糖,同时有prototype属性和__proto__属性
  3. 子类的__proto__属性表示构造函数的继承,总是指向父类
  4. 子类的prototype属性的__proto__属性表示方法的继承,总是指向父类的prototype

抛一个问题

  • 都说JS继承特殊,到底特殊在哪里?与传统的面向对象编程语言(如 Java、C++ 等)的继承有啥不同?

深入JS继承 看看内置的类型的指向

Function.__proto__ === Function.prototype //true   特殊

// 其实Function.prototype就是一个对象,而对象实例的__proto__指向Object.prototype
Function.prototype.__proto__ ===  Object.prototype //true

Object.__proto__ === Function.prototype  //true
Array.__proto__ === Function.prototype   //true
String.__proto__ === Function.prototype  //true
Number.__proto__ === Function.prototype  //true
Boolean.__proto__ === Function.prototype //true
Symbol.__proto__ === Function.prototype  //true
BigInt.__proto__ === Function.prototype  //true
Error.__proto__ === Function.prototype   //true
// 除了上述内置函数 还有Math、Set、Map、WeakSet、WeakMap、Date、RegExp、JSON、Promise...

({}).__proto__ === Object.prototype        //true
([]).__proto__ === Array.prototype         //true
("123").__proto__ === String.prototype     //true
(123).__proto__ === Number.prototype       //true 会形成包装类,和new Number(123)一样
(true).__proto__ === Boolean.prototype     //true
Symbol(123).__proto__ === Symbol.prototype // true
BigInt(123).__proto__ === BigInt.prototype // true
Error(123).__proto__ === Error.prototype //true
// Note: Error(123).__proto__ === new Error(123).__proto__

Object.prototype.__proto__ === null               //true
Array.prototype.__proto__ === Object.prototype    //true
String.prototype.__proto__ === Object.prototype   //true
Number.prototype.__proto__ === Object.prototype   //true
Boolean.prototype.__proto__ === Object.prototype  //true
Symbol.prototype.__proto__ === Object.prototype   //true
BigInt.prototype.__proto__ === Object.prototype   //true
Error.prototype.__proto__ === Object.prototype    //true

TypeError.prototype.__proto__ === Error.prototype

// Note: JavaScript 中的内置错误类型有以下七种:
// 1.  Error(错误):所有错误类型都继承自 Error 类型。
// 2.  EvalError(eval 错误):eval() 函数执行时发生错误时抛出的错误类型。
// 3.  RangeError(范围错误):当一个值超出有效范围时抛出的错误类型。
// 4.  ReferenceError(引用错误):当尝试访问未定义的变量时抛出的错误类型。
// 5.  SyntaxError(语法错误):当代码存在语法错误时抛出的错误类型。
// 6.  TypeError(类型错误):当尝试使用错误的数据类型时抛出的错误类型,比如使用一个非函数类型的值进行函数调用。
// 7.  URIError(URI 错误):当在使用全局函数 encodeURI() 或 decodeURI() 时,传入的参数无效时抛出的错误类型。

那到底特殊在哪里

// 有了上面基础做铺垫来看看class 
class Object{}   // function Object(){}
class Function extends Object{} // function Function(){} 
// class默认
Function.__proto__ === Object
Function.Prototype.__proto__ === Object.prototype
// 实际上
Function.__proto__ === Function.prototype === Object.__proto__
Function.Prototype.__proto__ === Object.prototype
// 但是 Object === Object.__proto__  // ???  
// 答案肯定是false  这也是我在全文第一行代码标志特殊的原因

很显然上述的class写法肯定是错的,因为JS内置的方法(Object,String,Number...)的继承是ES6之前的写法,此时都没有class,不要搞混。
Object.prototype = {...}
Function.prototype = Function.__proto__ = function(){}
function Object{} 
function Function{}
Function.prototype.__proto__ === Object.prototype

// 但是它并不影响下层的class的指向
// 除了继承Function的函数(Object,String,Number...)比较特殊外 下层都遵循class的规则(语法糖)
// function Error{}    内置方法
// class TypeError extends Error{}  内置class TypeError继承
TypeError.__proto__ === Error
TypeError.prototype.__proto__ === Error.prototype

// 至于下层遵循class的规则(语法糖)到底是啥?感兴趣的话可以用babel编译一下
// 我这里可以简单贴一段代码
// 编译前
function A{}   // 看清楚这里是函数 就算是class 最后还是会被转化为function
class B extends A {}
// 编译后
function A() {}
var B = /*#__PURE__*/function (_A) {
  _inherits(B, _A);                      // 这个函数里面实现继承             
  var _super = _createSuper(B);      
  function B() {
    _classCallCheck(this, B);        
    return _super.apply(this, arguments);
  }
  return _createClass(B);
}(A);
function _inherits(subClass, superClass) {
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function")
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { value: subClass, writable: true, configurable: true },
  })
  // B.prototype.__proto__ = A.prototype
  Object.defineProperty(subClass, "prototype", { writable: false })
   // B.__proto__ = A
  if (superClass) _setPrototypeOf(subClass, superClass)
}

总结一下: JS继承特殊的原因就是内置的方法扩展功能都是基于Function 去扩展的,es6之前什么原型链继承 构造函数继承 组合继承 原型式继承 寄生式继承 寄生组合式继承...这些都是围绕着基于函数的原型链特性({proto => prototype}),这使得继承在 JavaScript 中更加灵活和自由, 尽管es6有了class的,本质上还是通过函数去实现继承的。

一图胜千言

16a900cb6edae35b~tplv-t2oaga2asx-jj-mark_3024_0_0_0_q75.webp

基础用法

class Foo{
    constructor(){
        //return this  默认
    }
}
class Point{
    constructor (x,y,color){
        this.x = x;
        this.y = y;
        this.color = color;
    }
}
class ColorPoint extends Point{
    constructor (x,y,color){
        // this.color = color; //不写super会报错
        super(x,y,color);
        console.log(this);   //ColorPoint {x: 25, y: 8, color: "green"}
    }
    toString(){
        // return this.color + " " + super.toString;
        console.log(this);
    }
}
let cp = new ColorPoint(25,8,'green');

//实例cp
cp instanceof ColorPoint   //true
cp instanceof Point        //true

Object.getPrototypeOf 方法可以用来从子类上获取父类
Object.getPrototypeOf(ColorPoint) === Point;   //true;
Object.getPrototypeOf(cp)   //Point {constructor: ƒ, toString: ƒ}

// super关键字
// super这个关键字既可以当做函数用,也可以当做对象用,两种情况的用法不同
// 第一种情况,super作为函数调用时代表父类的构造函数。ES6要求,子类构造函数必须执行一次super函数
  class A{
    constructor(){
            console.log(this)
            console.log(new.target.name);  //new.target指向当前所new的函数
    }
  }
  class B extends A{
    constructor(){
            super();    //这里的super相当于A.prototype.contructor.call(this);
    }				//				  A.prototype.contructor.call(B);
  }
  new A();  //A
  new B();  //B    这里说明了super函数内部的this指向的是B

  第二种情况,super作为对象时在普通方法中指向父类的原型对象,在静态方法中指向父类
  子类B中的super.P()就是讲super当做一个对象使用,这时super在普通方法之中指向A.prototype
  class A{
    P(){
            return 2;
    }
  }
  class B extends A{
    constructor(){
            console.log(super());   		 // super执行返回实例 B{}
            console.log(super.P());      // 这里的super.P()相当于 A.prototype.P()
            // console.log(super);//报错
    }
  }
  let b = new B();         //2

  // 由于super指向父类的原型对象,所以定义在父类的实例上的方法或属性是无法通过super调用的
  class pittle{
    constructor(){
            this.y = 1;
    }
    print(){
            console.log(this.x);
    }
  }
  class ann extends pittle{
    constructor(){
            super();
            this.x = 2;
            console.log(super.y);//undefined
    }
  }
  let a = new ann();

  // 如果属性定义在父类的原型对象上,super就可以取到
  class A{}
  A.prototype.x = 2;
  class B extends A{
    constructor(){
            super();
            console.log(super.x); //2
    }
  }

  // ES6规定,通过super调用父类的方法时,super会绑定子类的this
  class A{
    constructor(){
            this.x = 1;
    }
    print(){
            console.log(this.x);
    }
  }
  class B extends A{
    constructor(){
            super();
            this.x = 2;
    }
    m(){
            super.print();   //实际上执行的是 super.print.call(this)
    }
  }
  let b = new B();
  b.m() //2

  // 由于super绑定子类的this,因此通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例属性
  class A{
    constructor (){
            this.y = 1;
    }
  }
  class B extends A{
    constructor(){
        super();
        // this.x = 2;
        super.x = 3;	//赋值时等同于this.x = 3
        console.log(super.x);  //undefined 读取时等同于A.prototype.x
        console.log(this.x);   //3
    }
  }
  let b = new B();

  super作为对象用在静态方法中指向父类,而不是父类原型对象
  class Parent{
    static myMathod(msg){
            console.log("static" + msg);
    }
    myMethod(msg){
            console.log('instance' + msg);
    }
  }
  class Child extends Parent{
    static myMethod(msg){
            super.myMathod(msg);
    }
    myMethod(msg){
            super.myMathod(msg);
    }
  }
  Child.myMathod(1); //static1

  // prototype && __proto__
  // 在大多数浏览器的ES5实现中,每个对象多有__proto__属性,指向对应的构造函数的prototype属性。
  // Class作为构造函数语法糖,同时有prototype属性和__proto__属性
  // 子类的__proto__属性表示构造函数的继承,总是指向父类
  // 子类的prototype属性的__proto__属性表示方法的继承,总是指向父类的prototype
  class A{}

  class B extends A{}

  B.__proto__ = A //true
  B.prototype.__proto__ = A.prototype

  A.__proto__ === Function.__proto__;//true
  A.__proto__ === Function.prototype //true
  Function.prototype === Function.__proto__ //true 函数比较特殊

  Array.__proto__ === Array.prototype   //false
  Array.__proto__ === Object.__proto__  //true

  A.prototype === Function.prototype //false
  A.prototype === Function.__proto__ //false

  A.__proto__  //ƒ () { [native code] }
  Function.prototype  //ƒ () { [native code] }
  Function.__proto__  //ƒ () { [native code] }
  A.prototype  //{constructor: ƒ}

  A.prototype.__proto__ === Object.prototype //true

  Object.__proto__ === Object.prototype  //false;

  Function.__proto__.__proto__ === Object.prototype  //true
  Function.prototype.__proto__ === Object.prototype  //true
  Function.__proto__ === Object.__proto__            //true
  Object.__proto__.__proto__ === Object.prototype    //true

  函数实例化对象指向这个函数的prototype
  ({}).__proto__ === Object.prototype //true
  ([]).__proto__ === Array.prototype //true

  Array.__proto__ === Object.__proto__ 				  //true
  String.__proto__ === Object.__proto__         //true
  Array.prototype.__proto__ === Object.prototype      //true

  // class静态属性

  // 旧写法
  class Foo(){
    //...
  }
  Foo.prop = 1;

  // 新写法
  class Foo{
    static prop = 1;
  }


  // Class 实例属性
  // 旧写法
  class MyClass{
    constructor(){
            this.myProp = 42;
            console.log(this.myProp);//42
    }
  }
  // 新写法
  class MyClass{
    myProp = 42;
    constructor(){
            console.log(this.myProp);//42
    }
  }

  // B继承A 那么如果B没写constructor 默认就是constructor(a,b){super(a,b)}
  class A {constructor(a,b){this.a = a;this.b = b}}
  class B extends A{}
  var b = new B(123,234)
  console.log(b);//B {a: 123, b: 234}

  Object.toString === Object.prototype.toString //false
  Number.toString === Object.toString //true

  数组重写了toString
  Array.toString === Object.toString  //true
  Array.prototype.toString === Array.toString //false