js基础系列(二)——原型和原型链

250 阅读7分钟

1、了解原型

先用来自MDN中的一段话来描述一下:每个实例对象( object )都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( __proto__ ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

每个实例对象的_proto_和这个实例对象的构造函数的prototype指向同一个对象,就是原型。而构造函数的prototype中的constructor属性就指向这个构造函数。(如下图所示)


2、instanceof的原理和手动实现

instanceof主要作用就是判断一个实例是否属于某种类型,用法如下所示:

let person = function(){
    
}
let no = new person()
no instanceof person//true

 instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。

利用原型链实现一个instanceof:

function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
    var O = R.prototype;
    L = L.__proto__;
    while (true) { 
        if (L === null) 
        return false; 
        if (O === L) // 这里重点:当 O 严格等于 L 时,返回true 
        return true; 
        L = L.__proto__; //继续往上查找左边变量的原型链
    } 
}
// 开始测试
var a = []
var b = {}

function Foo(){}
var c = new Foo()

function child(){}
function father(){}
child.prototype = new father() 
var d = new child()

console.log(instance_of(a, Array)) // true
console.log(instance_of(b, Object)) // true
console.log(instance_of(b, Array)) // false
console.log(instance_of(a, Object)) // true
console.log(instance_of(c, Foo)) // true
console.log(instance_of(d, child)) // true
console.log(instance_of(d, father)) // true

3、实现继承的方式

(1)原型继承

缺点:子类实例共享属性,造成实例间的属性会相互影响

function Parent1() {
  this.name = ['super1'] 
}
Parent1.property.reName = function () {
   this.name.push('super111')
}
function Child1() {

}
Child1.prototype = new Parent1()
var child11 = new Child1()
var child12 = new Child1()
var parent1 = new Parent1()
child11.reName()
console.log(child11.name, child12.name) // [ 'super1', 'super111' ] [ 'super1', 'super111' ], 可以看到子类的实例属性皆来自于父类的一个实例,即子类共享了同一个实例
console.log(child11.reName === child12.reName) // true, 共享了父类的方法

(2)构造函数继承

缺点:父类的方法没有被共享,造成内存浪费

function Parent2() {
  this.name = ['super1']
  this.reName = function () {
    this.name.push('super111')
  }
}

function Child2() {
  Parent2.call(this)
}

var child21 = new Child2()
var child22 = new Child2()
child21.reName()
console.log(child21.name, child22.name) // [ 'super1', 'super111' ] [ 'super1' ], 子实例的属性都是相互独立的
console.log(child21.reName === child22.reName) // false, 实例方法也是独立的,没有共享同一个方法

(3)组合继承

组合继承是将原型链继承和构造函数结合起来,从而发挥二者之长的一种模式

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

缺点: 父类构造函数被调用两次,子类实例的属性存在两份。造成内存浪费

function Parent3() {
  this.name = ['super3']
}
Parent3.prototype.reName = function() {
  this.name.push('super31')
}
function Child3() {
  Parent3.call(this) // 生成子类的实例属性(但是不包括父对象的方法)
}
Child3.prototype = new Parent3() // 继承父类的属性和方法(副作用: 父类的构造函数被调用的多次,且属性也存在两份造成了内存浪费)
var child31 = new Child3()
var child32 = new Child3()
child31.reName()
console.log(child31.name, child32.name) // [ 'super3', 'super31' ] [ 'super3' ], 子类实例不会相互影响
console.log(child31.reName === child32.reName) //true, 共享了父类的方法

(4)寄生继承

缺点:父类实例的构造函数指向,同时也发生变化(这是我们不希望的)

function Parent4() {
  this.name = ['super4']
}
Parent4.prototype.reName = function() {
  this.name.push('super41')
}
function Child4() {
  Parent4.call(this) // 生成子类的实例属性(但是不包括父对象的方法)
}
Child4.prototype = Object.create(Parent4.prototype) // 该方法会使用指定的原型对象及其属性去创建一个新的对象
var child41 = new Child4()
var child42 = new Child4()
child41.reName()
console.log(child41.name, child42.name) //[ 'super4','super41' ] [ 'super4' ], 子类实例不会相互影响
console.log(child41.reName === child42.reName) //true, 共享了父类的方法

(5)寄生组合继承

完美

function Parent(name) {
    this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
    this.arr = [1]; // (该属性,强调私有)
}
Parent.prototype.say = function() { // --- 将需要复用、共享的方法定义在父类原型上 
    console.log('hello')
}
function Child(name,like) {
    Parent.call(this,name,like) // 核心  
    this.like = like;
}
Child.prototype = Object.create(Parent.prototype) // 核心  通过创建中间对象,子类原型和父类原型,就会隔离开。不是同一个啦,有效避免了方式4的缺点。

<!--这里是修复构造函数指向的代码-->
Child.prototype.constructor = Child

let boy1 = new Child('小红','apple')
let boy2 = new Child('小明','orange')
let p1 = new Parent('小爸爸')

(6)class(ES6)

和寄生继承实现的效果一致

class Parent6 {
  constructor() {
    this.name = ['super6']
  }
  reName() {
    this.name.push('new 6')
  }
}

class Child6 extends Parent6 {
  constructor() {
    super()
  }
}

var child61 = new Child6()
var child62 = new Child6()
child61.reName()
console.log(child61.name, child62.name) // [ 'super6', 'new 6' ], 子类实例不会相互影响
console.log(child61.reName === child62.reName) //true, 共享了父类的方法

4、理解new()

首先给出new的定义:
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。 (MDN)

操作

  • 创建一个空的简单JavaScript对象(即{});
  • 设置该对象的构造函数到另一个对象 ;
  • 将步骤1新创建的对象作为this的上下文 ;
  • 如果该函数没有返回对象,则返回this。

模拟实现:

/**
 * 模仿new关键词实现
 * @param {Function} constructor 构造函数
 * @param  {...any} argument 任意参数
 */
const _new =  (constructor,...argument) => { 
    const obj = {} //创建一个空的简单对象 
    obj.__proto__ = constructor.prototype //设置原型
    const res = constructor.apply(obj, argument) //新创建的对象作为this的上下文传递给构造函数
    return (typeof res === 'object') ? res : obj //如果该函数没有返回对象,则返回this(这个this指constructor执行时内部的this,即res))。 
}


//开始测试
function Person(name,sex){
    this.name = name 
    this.sex = sex
}

const people = new Person('Ben','man');
const peopleOther = _new(Person,'Alice','woman');
console.info('people',people);// people Person { name: 'Ben', sex: 'man' }
console.info('peopleOther',peopleOther);// peopleOther Person { name: 'Alice', sex: 'woman' }
console.info('people.__proto__',people.__proto__);//people.__proto__ Person {}
console.info('peopleOther.__proto__',peopleOther.__proto__);//peopleOther.__proto__ Person {}

5、理解es6 class(类)

在es6的规范中,可以使用class语法,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法。

class Parent {
    static height = 12
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    speakSomething(){
        console.log("I can speek chinese");
    }
}
Parent.prototype.color = 'yellow'


//定义子类,继承父类
class Child extends Parent {
    static width = 18
    constructor(name,age){
        super(name,age);
    }
    coding(){
        console.log("I can code JS");
    }
}

var c = new Child("job",30);
c.coding()

经过babel转码之后:

"use strict";

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.configurable = 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;
    };
}();

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;
}

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 = Object.create(superClass && superClass.prototype, {
        constructor: {
            value: subClass,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}

function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}

var Parent = function () {
    function Parent(name, age) {
        _classCallCheck(this, Parent);

        this.name = name;
        this.age = age;
    }

    _createClass(Parent, [{
        key: "speakSomething",
        value: function speakSomething() {
            console.log("I can speek chinese");
        }
    }]);

    return Parent;
}();

Parent.height = 12;

Parent.prototype.color = 'yellow';

//定义子类,继承父类

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);

Child.width = 18;


var c = new Child("job", 30);
c.coding();

可以看到ES6类的底层还是通过构造函数去创建的。

转码中_createClass方法,它调用Object.defineProperty方法去给新创建的Parent添加各种属性。defineProperties(Constructor.prototype, protoProps)是给原型添加属性。静态属性,会直接添加到构造函数上defineProperties(Constructor, staticProps)。并添加了_inherits核心方法来实现继承。

以上实现继承的原理可以用下图来表示:


结语

关于原型和原型链的相关内容就介绍到这里啦,如果有错误的地方欢迎大家批评指正。

ps :以上内容是通过而来,借(chao)鉴(xi)了一些资料,如有雷同纯属我懒惰,请大家不要见怪!!!