前端系统化学习【JS篇】:(十七)原型和原型链

870 阅读16分钟

前言

  • 细阅此文章大概需要 20分钟\color{red}{20分钟}左右
  • 本篇中讲述了:
      1. 三句话总结原型和原型链
      1. 原型链查找机制
      1. prototype原型对象上的属性方法"相对论"
      1. 自定义类的prototype原型和proto原型链
      1. new 构造函数的实现
          1. 实现构造函数
          1. 兼容性优化【Object.carate([OBJECT])】
          1. 手动实现create方法
      1. 向内置类的原型对象上扩展自定义的方法
      1. Function 和 Object的特殊之处
      1. 类的原型重定向问题"
          1. 重定向后的原型对象存几个严重的问题
          1. 解决constructor和原始属性方法的丢失的问题的几种方案
  • 如果有任何问题都可以留言给我,我看到了就会回复,如果我解决不了也可以一起探讨、学习。如果认为有任何错误都还请您不吝赐教,帮我指正,在下万分感谢。希望今后能和大家共同学习、进步。
  • 下一篇会尽快更新,已经写好的文章也会在今后随着理解加深或者加入一些图解而断断续续的进行修改。
  • 如果觉得这篇文章对您有帮助,还请点个赞支持一下,谢谢大家!
  • 欢迎转载,注明出处即可。

内置类的prototype原型和__proto__原型链

三句话总结原型和原型链

  1. 每一个函数(包括构造函数(类))都天生具有一个属性:prototype(原型),属性值是一个对象,对象当中存储的是当前类供实例调用的公共属性和方法。

  1. 在原型对象上,有一个内置的属性“constructor(构造函数)”,存储的值是当前函数本身,所以把类称为构造函数

  1. 每一个对象(包括实例)都天生具备一个属性,“proto(隐式原型/原型链)”,属性值是 【自己所属的类的原型对象】 【实例.__proto__===所属类.prototype】


上面所说的函数和对象的范围

  • 函数类型
    • 普通函数
    • 构造函数(类)
    • 内置类(如Number,String,Object....)
  • 对象类型
    • 普通对象/数组对象/正则对象/日期对象
    • prototype原型对象/__proto__原型链对象(因为里面存的是【实例所属类的原型对象的地址】)
    • 类的实例也是对象(排除基本数据类型值的特殊性)
    • 函数也是对象 【既然函数中可以具有自己的属性prototye,说明函数也是一个对象】
      • 函数当中的常见属性
        • arguments:实参 类数组
        • name:变量名
        • length:形参个数
        • from:[ARRAY]类的属性,是把类数组转换为数组
        • isArray:[ARRAY]类的属性,检测当前值是不是一个数组
        • prototype:原型属性
    • 万物皆对象

原型链查找机制

  1. 访问对象的某成员,首先看是否为私有的属性,如果是私有的则找到的就是私有的

  2. 如果没有找到私有的,就通过【原型链__proto__】找【所属类的prototype原型对象】上的公共属性和方法

  3. 如果还是没有找到,就找【所属类的prototype原型对象】上【原型链__proto__】所指向的【内置类的prototype原型对象】上的公共属性和方法,直到找到Object基类为止(因为Object的prototype原型对象的__proto__属性为null)


  • Object作为所有类的'基类',它的prototype原型对象上的__proto__属性理论上应该指向 Object类的prototype原型对象,也就是指向自己,但是这样做没有任何意义,所以 【Object类的prototype原型对象上的__proto__属性的值为null】


  • 【重要】如果一个实例要执行的方法,是存在【类中或更上一级类中的原型上存储的】公共方法,则其原理是:【真正执行是基于this】

    1. 先通过【原型链__proto__】找【所属类的prototype原型对象】上的公共属性和方法
    2. 并使方法执行
    3. 但是方法执行的THIS是【调用该方法的那个实例】,从而实现实例通过原型链查找机制调用公共方法
  • 【重要】 对象是没有prototype属性的,只有函数有,当【对象(除函数).prototype】时,只会顺着原型链向上找,最终找到Object.prototype原型对象,发现其也没有prototype属性,则返回undefined。

    let arr1 = [10,20,30];
    arr.length;
    // 1.先找实例对象arr1自己的私有属性,找到了length私有属性
    arr1.push();//相当于arr1.__proto__.push.call(arr1)相当于Array.prototype.push.call(arr1)
    // 1.先找实例对象arr1自己的私有属性,找不到
    // 2.于是通过【原型链__proto__】向【自己所属类的prototype原型对象上】找【所属类】的公共属性和方法,
    //在Array类的prototype原型对象上找到了push公共方法
    arr1.hasOwnproperty();
    // 1.先找实例对象arr1自己的私有属性,找不到,
    // 2.于是通过【原型链__proto__】向【自己所属类的prototype原型对象上】找【所属类】的公共属性和方法,仍然找不到
    // 3.于是 通过【Array类的prototype原型对象】上的【__proto__原型链指向】找到了【基类Object类的prototype原型对象】,
    //在这里找到了hasOwnproperty公共方法

prototype原型对象上的属性方法"相对论"

  • 相对于该类的实例、自己的子类及其实例来说是公有的
  • 相对于该类prototype原型对象自己来讲是私有的
    Array.prototype.hasOwnProperty('push');//true
    // Array.prototype上没有hasOwnProperty,通过__proto__向上查找,
    //找到了,执行方法,this是Array.prototype,而对于它来说push是自己的私有属性,则为true

自定义类的prototype原型和__proto__原型链

    function fn(){
        //给实例添加两个属性一个方法
        this.x = 100;
        this.y = 200;
        this.getX = function(){
            console.log(this.x);
        }
    }
    //给fn的prototype原型对象上添加两个方法
    fn.prototype.getX = function(){
        console.log(this.x);
    };
    fn.prototype.getY = function(){
        console.log(this.y);
    };
    //创建两个实例
    let f1 = new fn;
    let f2 = new fn;

    console.log(f1.getX === f2.getX);//false
    //虽然两个实例当中的各个属性和方法内容相同,但都是各自的私有属性,所以并不相等
    console.log(f1.getY === f2.getY);//true
    //各自私有找不到,去原型上找,找到公有getY
    
    console.log(f1.__proto__.getY=== fn.prototype.getY);//true
    //相当于fn原型对象上的getY和自己对相比,
    console.log(f1.__proto__.getX=== f2.getX);//false
    //相当于fn原型对象上的getX和f2实例私有的getX方法相对比
    console.log(f1.getX === fn.prototype.getX);//false
    //相当于fn原型对象上的getX和f1实例私有的getX方法相对比
    console.log(f1.constructor);//fn
    //f1私有中找不到constructor属性,通过__proto__向上查找,fn原型上有constructor,指向函数本身
    console.log(fn.prototype.__proto__.constructor);//object
    //fn原型的__proto__向上查找,找到Object的原型上的constructor,指向object函数本身

    f1.getX();//100
    //先找方法,是私有的,执行,this是f1,
    f1.__proto__.getX();//undefined
    //先找方法,是fn原型上公有方法,执行,this指向fn原型,原型上没有x属性,
    f2.getY();//200
    //先找方法,实例私有上没有,__proto__向上查找fn原型上公有方法,找到了,执行getY,this是f2,
    fn.prototype.getY();//undefined
    //先找方法,是fn原型上公有方法,执行,this指向fn原型,原型上没有y属性,

new 构造函数的实现

  • 实现构造函数

    1. 创建一个实例对象
      • 改变新建实例对象中原型链的指向
    2. 执行函数
    3. 改变函数的this指向
    4. 判断返回值类型,并且返回实例或其他
      • 分析函数执行返回值
        • 没有返回值或者返回的是基本类型值则默认都返回创建的实例
        • 否则以函数自身的返回值为准
    function Cat(name){
    this.name = name;
    }
    Cat.prototype.bark = function (){
        console.log('miaomiao');
    };
    Cat.prototype.sayName = function(){
        console.log('my name is'+this.name);
    };

    function _new(Func,...args){
        //实现构造函数
        //1.创建一个实例对象
            //1.改变新建实例对象中原型链的指向
        //2.执行函数
        //3.改变函数的this指向
        //4.判断返回值类型,并且返回实例或其他
            // 分析函数执行返回值(没有返回值或者返回的是基本类型值则默认都返回创建的实例,否则以函数自身的返回值为准)
            
        //创建实例对象,并改变实例的原型链指向
        // let obj = {};
        // obj.__proto__ = Func.prototype;//修改原型链指向的方法1
        let obj = Object.create(Func.prototype);

        //执行函数并改变函数this指向
        let result = Func.call(obj,...args);
        //判断返回值
        if(result!==null&&/^(object|function)$/.test(typeof result)){
            //如果【返回引用类型值】且【不为null】,则以返回内容为准
            return result;
        }
        return obj;
        //否则默认返回实例
    }

    let mimi = _new(Cat,'咪咪');
    mimi.bark();
    mimi.sayName();
    console.log(mimi instanceof Cat);


  • 对于IE11-进一步优化【Object.carate([OBJECT])】

  • 所有浏览器实现面向对象都是通过原型链机制来实现的,我们可以操作__proto__,修改原型链是由于浏览器将该属性暴露出来供我们使用,但是在IE11-版本中【为了防止原型链错乱】,并没有把这个属性暴露出来【禁止使用,并没有提供该属性】,于是在IE11-版本中不能通过修改__proto__来实现修改实例对象的原型链指向
  • 解决的办法就是利用Object.carate创造一个空对象,再和有原型链指向的对象合并,从而达到修改原型链指向的目的
  • Object.carate([OBJECT])
    • 创建一个空对象,并把【[OBJECT](代表一个对象)对象】【当作新对象的原型链指向】作为参数传入这个空对象
    • 如果传null进去,就会创建一个没有原型/原型链的空对象(不是任何类的实例)
    • 返回值是【创建的修改自身原型指向的空类】的实例(空对象)
        function Cat(name){
        this.name = name;
        }
        Cat.prototype.bark = function (){
            console.log('miaomiao');
        };
        Cat.prototype.sayName = function(){
            console.log('my name is'+this.name);
        };
    
        function _new(Func,...args){  
            //创建实例对象,并改变实例的原型链指向
            // let obj = {};
            // obj.__proto__ = Func.prototype;//修改原型链指向的方法1
            let obj = Object.create(Func.prototype);//修改原型链指向的方法2
            let result = Func.call(obj,...args);
            if(result!==null&&/^(object|function)$/.test(typeof result)){
                return result;
            }
            return obj;
        }
        let mimi = _new(Cat,'咪咪');
        mimi.bark();
        mimi.sayName();
        console.log(mimi instanceof Cat);
    

手动实现create方法【Object.carate([OBJECT])】

  • 不支持null
  • create方法是创建一个空对象,并把[OBJECT](代表一个对象)对象作为新对象的原型链指向
    • 所以,重写create的大致思路是:
      1. 首先create方法是将传入的对象,作为新建空对象的proto原型链指向,所以先判断传入的参数【是否为空】或【不是一个对象】,若是则报错
      2. 创建一个空类
      3. 让空类的prototype原型指向传入的对象(我们指定的prototype原型对象)
      4. 返回这个【被修改原型对象的空类】的实例,从而实现创建一个空对象,空对象的原型链指向我们指定的原型对象
    //1.首先create方法是将传入的对象,作为新建空对象所属类的原型链指向
    //2.所以先判断传入的参数【是否为空】或【不是一个对象】,若是则报错
    
    Object.create = function create(prototype){
        if(prototype === null || typeof prototype !== 'object'){
            throw new TypeError(`Object prototype may only be an Object:${prototype}`);
        }
        function Temp(){};//创建一个空类
        Temp.prototype = prototype;//让该类的prototype原型指向传入的对象(我们指定的prototype原型对象)
        return new Temp;
        // 返回这个被修改原型对象的类的实例(空对象),
        //从而实现创建一个空对象,空对象的原型链指向我们指定的原型对象
    }

向内置类的原型对象上扩展自定义的方法

  1. 为了防止自己设定的方法覆盖内置的方法,我们设置的方法名要加上自己的前缀
  • 【优势】:

  1. 使用起来方便,和内置方法类似,直接让实例调用即可,方法中的this【一般】就是当前操作的实例,(也就不需要基于形参传递实例进来了)
  2. 只要 保证方法的返回结果还是当前类的实例,那么我们就可以基于“链式方法”调用当前类中提供的其他方法 【而返回的结果要是其他类的实例,我们就可以继续调用其所属类的方法】
    //给数组类的原型上添加一个数组去重的方法【最懒方法】
    Array.prototype.myDistinct = function myDistinct(){
        //this->调用此方法的实例
        //使用ES6中的Set结构来实现数组去重(不重复数组)
        let newArr = [...new Set(this)];//返回的是一个实例,转换为数组【方法1】
        let newArr = Array.from(new Set(this));//返回的是一个实例,转换为数组,from方法能把一个类数组对象或者可遍历对象转换成一个真正的数组【方法2】
        return newArr;
    }

    let arr =  [1,2,3,4,2,3,3,4,6,2,1,2,3];
    arr.myDistinct();
    //把对象通过扩展运算符转换为数组
    arr.myDistinct().reverse().map(item => item*10).push('X');//最后返回的是数组的长度,不能再继续链式调用了

/* 在所属内置类的原型上实现【求和】plus和【减法】minus方法,实现要求的效果 */

//先判断一下参数的类型【要细致的考虑参数处理】
function initParams(num){
    num = Number(num);
    return isNaN(num)?0:num;
}
Number.prototype.plus = function plus(num){
    // This=>永远都是对象数据类型值【除null和undefined外】
    // 【对象+数字】:不一定都是字符串拼接!【基本数据类型的对象型的实例在进行运算时,是用原始值进行数学运算】
    // 对象本身是想转换为数字再进行运算的,
    //而【对象转换为数字】并不是先toString直接转换为字符串,
    //而是先.valueOf获取原始值[[PrimitiveValue]],
    //【如果有原始值(原始值都是基本数据类型值),直接基于原始值处理】,【没有原始值才去toString】

    //进来运算时先来调用initParams方法判断一下参数类型
    num = initParams(num);
    return this + num;
    // 通过实例调用时,直接将this代表的Number类的实例的原始值与参数进行运算,
    //而不是直接toString转为字符串【因为this永远是一个对象类型的值】是先对this进行valueof取原始值,
    //若为进本数据类型值,则会被由于this是对象类型被转为“包装类【new Number(10)】”
    //从而取到原始值【基本数据类型实例都有原始值属性】,
    //引用数据类型取不到才进行toString转换成字符串,在进行字符串拼接,
};
Number.prototype.minus = function minus(num){
    num = initParams(num);
    return this - num;
};

//在所属内置类的原型上实现plus和minus方法,实现要求的效果
    let n = 10;
    let m = n.plus(10).minus(5);//n.plus(10)返回的仍是Number类的实例,所以仍可继续链式调用
    console.log(m);//=>15(10+10-5)

    /*
     * 编写queryURLParams方法实现如下的效果(至少两种方案)
     */
    //url的值是字符串,是String类的实例,则需要给String的原型上扩展自定义方法
    //【方案一:字符串拆分解构赋值】
    String.prototype.queryURLParams = function queryURLParams(key){
        let obj = {};
        let {search,hash} = new URL(this);

        //处理hash
        if(hash){
            obj['_HASH'] = hash.substring(1);
        }
        
        //处理PARAMS
        if(search){
            search = search.substring(1).split('&');//此时的到一个数组//['lx=1','from=wx']
            search.forEach(item => {//[[lx,1],[from,wx]]
                let[key,value] = item.split('=');//接着解构赋值
                obj[key] = value;
            });
        }
        return key?obj[key]:obj;//如果传了key【想要的属性名】,就返回对应的属性值,否则返回obj
    }

    let url="http://www.zhuzaishanlizhenbuchuo.cn/?lx=1&from=wx#video";
    console.log(url.queryURLParams("from")); //=>"wx"
    console.log(url.queryURLParams("_HASH")); //=>"video"

    /*
     * 编写queryURLParams方法实现如下的效果(至少两种方案)
     */
    //url的值是字符串,是String类的实例,则需要给String的原型上扩展自定义方法
    //【方案二:正则】
    String.prototype.queryURLParams = function queryURLParams(key){
        let obj = {};
       this.replace(/([^?=&#]+)=([^?=&#]+)/g,(_,key,value) => obj[key] = value);
       this.replace(/#([^?=&#]+)/g,(_,hash)=>obj['_HASH'] = hash);
        return key?obj[key]:obj;//如果传了key【想要的属性名】,就返回对应的属性值,否则返回obj

    let url="http://www.zhuzaishanlizhenbuchuo.cn/?lx=1&from=wx#video";
    console.log(url.queryURLParams("from")); //=>"wx"
    console.log(url.queryURLParams("_HASH")); //=>"video"

  • 劣势:

    • 【for of】

      • 只对拥有Symbol.iterator属性的数据结构起作用【数组、new Set、类数组、字符串、、、】【而对象不具备Symbol.iterator属性】
    • 【for in】

      • 遍历的时候,所有 可以被枚举的属性都可以遍历到( ***【大部分私有属性】***和 【自己向内置类原型上扩展的属性】),所以如果要 【使用forin循环遍历所有私有属性方法】时,【所以我们要加入hasOwnProperty进行判断】
      • ***【优点:可以手动控制,兼容】***可有手动控制循环的次数和筛选的属性
    • 【Object.keys】将对象所有私有属性名得到形成一个数组 【不兼容】

      • 基于keys方法,配合着forEach可以做到 【只处理私有属性和方法】
      • ***【缺点:无法控制】【不兼容】***但是有多少属性就只能循环多少次
          let obj = {
              name:'xxx',
              age:20
          };
      
          Object.keys(obj).forEach(key => {
              console.log(key,obj[key]);
          });
      

【重点】Function 和 Object的特殊之处

  1. 所有的函数【包含内置类】都是Function类的实例,所以所有的函数【包含内置类】的【__proto__原型链】一定是指向【Function类的prototype原型对象】

    • Function类的__proto__原型链指向的是Function类的原型对象 【Function.__proto__=== Function.prototype =>true】
    • Function类 【Function instanceof Function => true】【因为Function类本身也是一个函数,就是Function类的一个实例】
  2. 所有的对象【包含原型对象】都是Object类的实例【如typeof Array.prototype=>'Object'】,除了Function类的原型对象(但实际操作上没有区别)所以最后都能通过原型链指向找到Object类的原型对象。

    • Function类的原型对象是一个【匿名空函数】【empty/anonymous】 但是相关的操作和其他原型对象没有任何区别(因为函数也是一个对象?)

    • Object的原型对象的原型链属性为null,因为 Object作为所有类的'基类',它的prototype原型对象上的__proto__属性理论上应该指向 【Object类的prototype原型对象】,也就是指向自己但是这样没有任何意义,所以指向为null。

  3. 【Object作为一个内置类(一个函数)是Function的一个实例】【Function是一个函数(内置类)也是【一个对象】,所以他既是【Function的一个实例】,也是【Object的一个实例】】

    • 所以就出现了 【Object.__proto__.__proto__ === Object.prototype】
      • Object.__proto__是指向【Function.prototype】
      • Object.proto.__proto__是【Function.prototype.proto】是指向【Object.prototype】
  4. 在JS中的任何实例,任何值(【除了基本数据类型的值(当然如果将其对象化之后也是如此)】)最后都可以基于自己的原型链,最终找到Object.prototype。

    • 从而所有值都是Object的实例======>也就是所说的万物皆对象


类的原型重定向问题

  • 当将一个类的原型对象的重新指向一个新的对象,则这个新的对象成为了这个类的原型对象。这种现象称为【类的原型重定向】

  • 一般想要给某一类的原型对象上批量添加属性、方法,都会基于重定向的方式。

  • 【!!!!内置类的原型是不能重定向的!!!,就算重定向也不会生效】

  • 重定向后的原型对象存几个严重的问题:

    1. 重定向后的原型对象当中 缺失了constructor
    2. 原始的原型对象上,存放的属性方法,不会放到新的重定向的原型对象上,导致实例无法再使用原始的原型对象上的那些方法。缺失了原始原型对象上的属性和方法
    3. 原始的原型对象不被占用后,会被释放掉。(丢失)
  • 解决constructor和原始属性方法的丢失的问题的几种方案

    1. 手动加上constuctor【只能解决constructor问题】
    2. 【Object.assign】 新旧两个原型对象合并,其中的属性方法,该替换的替换该保留的保留
      • 也存在一个问题,那就是如果新老原型中有个属性方法相同,则新值会替换老的值
          func.prototype = Object.assign(func.prototype,{新对象});
      
    3. 【重构原型指向】 把老的原型对象作为新原型对象的上级原型
    • 基于Object.assgin和Object.create
          function A(){};//创建构造函数A
      
          //为A的原型对象添加属性方法
          A.prototype.name = 'A类的原型对象的原有属性之一';
          A.prototype.height = 'A类的原型对象的原有属性之2';
          
          let a = Object.create(A.prototype);
          //创建了一个新的【空对象】a,
          //其__proto__原型链的指向【不是a的所属类的原型】而是被改为了【指定的A类的原型对象】
      
          //创建一个新对象b,并添加属性和方法
          let b = {
              age:'新的对象的属性1',
              area:'新的对象的属性2'
          };
      
          a = Object.assign(a,b);//合并新的对象b和空对象a
          A.prototype = a;//重定向【A类的原型对象】为【合并后的新对象a】
          console.log(A.prototype);
      
          /*查看A类的原型对象
            A.prototype{
                    age: "新的对象的属性1"
                    area: "新的对象的属性2"}
                    __proto__:
                            height: "A类的原型对象的原有属性之2"
                            name: "A类的原型对象的原有属性之一"
                            constructor: ƒ A()
                            __proto__: Object
          
           */
      
      
    • 奇怪的方法增加了
        A.prototype = new A;
        let a = new A;
        //也相当于实现重定向,并保留原始原型对象的效果,但是新原型对象会保留创建实例时的一个私有属性