Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

原型和原型链 —— 一名【合格】前端工程师的自检清单答案整理 #3

Open
akeymo opened this issue Dec 12, 2019 · 0 comments

Comments

@akeymo
Copy link
Owner

akeymo commented Dec 12, 2019

原型和原型链

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

原型规则

  1. 构造函数通过new操作符创建实例
  2. 实例对象的__proto__指向构造函数的prototype指向的原型对象
  3. 原型对象的constructor指向它的构造函数
  4. 所有对象都有__proto__属性
  5. 所有函数都有prototype属性,函数即可作为构造函数也可作为对象,因此也有__proto__属性
  6. 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会根据它的_proto_属性沿着它的原型链向上寻找

设计模式

  1. 工厂模式:在函数内创建对象,给对象赋予属性及方法,再将对象返回
    function Person() {
        const obj = new Object();
        obj.name = '张三';
        obj.sayHi = function () {
            console.log('hello world');
        }
        return obj;
    }
    
    let p1 = Person();
    p1.sayHi();
    
  2. 构造函数模式:无需在函数内部重新创建对象,而是用this指代
    function Person() {
        this.name = '张三';
        this.sayHi = function() {
            console.log('hello world');
        }
    }
    
    let p1 = new Person();
    p1.sayHi();
    
  3. 原型模式:函数中不对属性进行定义,利用prototype属性对属性进行定义,可以让所有对象实例共享它多包含的属性及方法
    function Person() {
        Person.prototype.name = '张三';
        Person.prototype.sayHi = function () {
            console.log('hello world');
        }
    }
    
    let p1 = new Person();
    p1.sayHi();
    
  4. 混合模式:构造函数模式+原型模式,构造函数模式用于定义属性,原型模式用于定义方法和共享属性
    function Person() {
        this.name = '张三';
    }
    Person.prototype.sayHi = function () {
        console.log('hello world');
    }
    
    let p1 = new Person();
    p1.sayHi();
    
  5. 动态原型模式:将所有信息封装在了构造函数中,而通过构造函数中初始化原型,这个可以通过判断该方法是否有效而选择是否需要初始化原型
    function Person() {
        this.name = '张三';
        if (typeof Person._sayHi === "undefined") {
            Person.prototype.sayHi = function () {
                console.log('hello world');
            }
            Person._sayHi = true;
        }
    }
    
    let p1 = new Person();
    p1.sayHi();
    

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

实现原理

  • instanceof是用来判断一个实例是否属于某个类型或者用来判断是否是其父类型或者祖先类型的实例
  • 利用判断左边实例的__prop__是否和右边构造函数的prototype指向同一个原型对象来判断,查找过程中会遍历左边变量的原型链,通过o.__proto一层一层往上找,直到找到与右边变量的prototype相等,返回true,否则返回false

实现

function new_instance_of(leftVaule, rightVaule) {
    let rightProto = rightVaule.prototype; // 取右表达式的 prototype 值
    let leftProto = leftVaule.__proto__; // 取左表达式的__proto__值
    while (true) {
        if (leftProto === null) {
            return false;
        }
        if (leftProto === rightProto) {
            return true;
        }
        leftProto = leftProto.__proto__;
    }
}

实现继承的几种方式以及他们的优缺点

原型链继承

利用Sub.prototype = new Super(),这样连通了子类-子类原型-父类。

function Super(){
    this.flag = true;
}

Super.prototype.getFlag = function(){
    return this.flag;
}

function Sub(){
    this.subFlag = true;
}

// 实现继承
Sub.prototype = new Super();
// 添加方法,需要在继承之后
Sub.prototype.getSubFlag = function(){
    return this.subFlag;
}

优点:

  • 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
  • 父类新增原型方法/原型属性,子类都能访问到
  • 简单,易于实现

缺点:

  • 要想为子类新增属性和方法,必须要在继承语句之后执行,不能放到构造器中
  • 无法实现多继承
  • 来自原型对象的引用属性是所有实例共享的
  • 创建子类实例时,无法向父类构造函数传参

构造函数继承

在构造子类构造函数时内部使用callapply来调用父类的构造函数

function Super() {
    this.flag = true;
}

function Sub() {
    //如果父类可以需要接收参数,这里也可以直接传递
    Super.call(this);
}

const obj = new Sub();
obj.flag = false;

const obj2 = new Sub();
console.log(obj2.flag); //依然是true,不会相互影响 

优点:

  • 解决了原型链继承中,子类实例共享父类引用属性的问题
  • 创建子类实例时,可以向父类传递参数
  • 可以实现多继承(call绑定多个父类对象)

缺点:

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

组合继承

使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承

function Super() {
    this.flag = true;
}

Super.prototype.getFlag = function () {
    return this.flag;
}

function Sub() {
    this.subFlag = true;
    // 继承实例属性,第一次调用Super
    Super.call(this);
}
//继承父类方法,第二次调用Super
Sub.prototype = new Super();
Sub.prototype.getSubFlag = function () {
    return this.subFlag;
}

优点:

  • 弥补了原型链继承和构造函数继承的缺点,既可以继承实例的属性和方法,也可以继承原型的属性和方法
  • instanceofisPrototypeOf( )也能用于识别基于组合继承创建的对象

缺点:

  • 调用了两次父类构造函数, 造成了不必要的消耗

寄生组合继承

寄生组合式继承就是为了降低调用父类构造函数的开销而出现的,不必为了指定子类型的原型而调用超类型的构造函数

function extend(subClass, superClass) {
    // 复制父类的原型
    const prototype = object.create(superClass.prototype);
    // 将复制的原型的constructor指向子类
    prototype.constructor = subClass;
    // 将子类的prototype指向复制的原型
    subClass.prototype = prototype;//指定对象
}

function Super() {
    this.flag = true;
}

Super.prototype.getFlag = function () {
    return this.flag;
}

function Sub() {
    this.subFlag = true;
    // 继承实例属性,第一次调用Super
    Super.call(this);
}

extend(Sub, Super);
Sub.prototype.getSubFlag = function () {
    return this.subFlag;
}

优点:

  • 弥补了组合继承调用两次父类构造函数的缺陷

Class继承

class Sub extends Super{}

缺点:

  • es6语法糖,需要兼容性

至少说出一种开源项目(如Node)中应用原型继承的案例

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

过程

  1. 创建一个新的空对象,继承构造函数的prototype指向的原型对象。
  2. 执行构造函数,执行过程中,相应的传参会被传入,同时将上下文(this)指定为新的对象实例。new foo等同于new foo()只能用在不传递任何参数的情况下。
  3. 如果构造函数的返回值是对象,则返回构造函数返回的对象,否则返回步骤1创建的对象。

实现

function create(Con, ...args) {
    let obj = {};
    Object.setPrototypeOf(obj, Con.prototype);
    let result = Con.apply(obj, args);
    return result instanceof Object ? result : obj;
}

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

构造

  1. 通过class关键字可以定义类,类的所有方法都是定义在类的prototype属性上,在类的实例上面调用方法,其实就是调用原型上的方法。
    class Animal {
      constructor() { ... };
      toString() { ... };
      getName() { ... };
    }
    
    // 等价于
    Animal.prototype = {
      constructor() {},
      toString() {},
      getName() {}
    }
    
  2. 一个类必须有constructor方法。如果没有显示定义,也会有一个空的constructor方法被默认添加。通过new命令生成对象实例时,自动调用该方法。
  3. 实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。
    //定义类
    class Point {
    
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
    
      toString() {
        return '(' + this.x + ', ' + this.y + ')';
      }
    
    }
    
    var point = new Point(2, 3);
    
    point.toString() // (2, 3)
    
    point.hasOwnProperty('x') // true
    point.hasOwnProperty('y') // true
    point.hasOwnProperty('toString') // false
    point.__proto__.hasOwnProperty('toString') // true
    
  4. 与ES5一样,类的所有实例共享一个原型对象
  5. super关键字既可以当作函数使用,也可以当作对象使用。
    • 当作函数使用时,代表父类的构造函数,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B,因此super()在这里相当于A.prototype.constructor.call(this)
    • 作为对象时,指向父类的原型对象。需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的
    class A {
      p() {
        return 2;
      }
    }
    
    class B extends A {
      constructor() {
        super();
        console.log(super.p()); // 2
      }
    }
    
    let b = new B();
    
    • 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();
      }
    }
    
    let b = new B();
    b.m() // 2
    
    • 由于绑定子类的this,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性
    class A {
      constructor() {
        this.x = 1;
      }
    }
    
    class B extends A {
      constructor() {
        super();
        this.x = 2;
        super.x = 3;
        console.log(super.x); // undefined
        console.log(this.x); // 3
      }
    }
    
    let b = new B();
    

继承的底层实现原理

ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this

  • 子类的__proto__属性,表示构造函数的继承,总是指向父类
  • 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性
     class A {
     }
     
     class B extends A {
     }
     
     B.__proto__ === A // true
     B.prototype.__proto__ === A.prototype // true
    

Object.getPrototypeOf方法可以用来从子类上获取父类,因此,可以使用这个方法判断,一个类是否继承了另一个类。

Object.getPrototypeOf(ColorPoint) === Point
// true

代码实现:

class Parent {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}

class Child extends Parent {
    constructor(name, age) {
        super(name, age);
    }

    coding() {
        console.log('I can code JS');
    }
}

/**
 * 收集公有函数和静态方法
 * 将方法添加到构造函数或者构造函数的原型中,并返回构造函数
 */
var _createClass = function () {
    function defineProperties(target, props) {
        for (var i = 0; i < props.length; i++) {
            var descriptor = props[i];
            descriptor.enumerable = descriptor.enumerable || false;
            descriptor.cofigurable = true;
            if ('value' in descriptor) {
                descriptor.writable = true;
                Object.defineProperty(target, descriptor.key, descriptor);
            }
        }
    }

    return function (Constructor, protoProps, staticProps) {
        // 给原型添加属性
        if (protoProps) {
            defineProperties(Constructor.prototype, protoProps);
        }
        // 静态属性,添加到构造函数上
        if (staticProps) {
            defineProperties(Constructor, staticProps);
        }

        return Constructor;
    }
}();

/**
 * 防止通过构造函数直接运行,判断是否是通过new调用
 */
function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError('Cannot call a class as a function');
    }
}

/**
 * 实现继承 
 */
function _inherits(subClass, superClass) {
    if (typeof superClass !== 'function' && superClass !== null) {
        throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass);
    }
    // 核心思想就是
    // subClass.prototype.__proto__ = superClass.prototype
    // subClass.__proto__ = superClass

    // 原型方法的继承,子类的prototype的__proto__指向父类的prototype
    // 翻译一下就是:
    // function F() { }
    // F.prototype = superClass.prototype
    // subClass.prototype = new F()
    // subClass.prototype.constructor = subClass
    subClass.prototype = Object.create(superClass && superClass.prototype, {
        constructor: {
            value: subClass,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });

    if (superClass) {
        // 继承构造函数,子类的__proto__指向父类
        Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
    }
}


function _possibleConstructorReturn(self, call) {
    if (!self) {
        throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    }

    return call && (typeof call === "object" || typeof call === "function") ? call : self;
}

// 子类继承父类
var child = function (Parent) {
    _inherits(Child, Parent);

    function Child(name, age) {
        _classCallCheck(this, Child);

        return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name, age));
    }

    _createClass(Child, [{
        key: 'coding',
        value: function coding() {
            console.log('I can code JS');
        }
    }]);

    return Child;
}(Parent);
@akeymo akeymo changed the title 原型和原型链 原型和原型链——一名【合格】前端工程师的自检清单答案整理 Dec 19, 2019
@akeymo akeymo changed the title 原型和原型链——一名【合格】前端工程师的自检清单答案整理 原型和原型链 —— 一名【合格】前端工程师的自检清单答案整理 Dec 19, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant